Compare commits

..

8 Commits

Author SHA1 Message Date
PJB3005
0a47ecc5b5 Version: 260.2.4 2025-12-01 16:07:18 +01:00
PJB3005
f694f0e311 Backport BitArray .NET 10 serializer fix
83ad6042a7 & b267cd6fb4

Does not include test code to avoid risking merge conflicts.

(cherry picked from commit 415585a30d74fcae61f581808220a7aaeca3eaf5)
(cherry picked from commit e36628a6d436ea08d6d31441c101a88a5504c515)
2025-12-01 16:07:18 +01:00
PJB3005
cd90cc6e26 Version: 260.2.3 2025-09-26 13:40:44 +02:00
PJB3005
3923dd39ae Validate that content assemblies have a limited list of names.
Also, only read assemblies once from disk

(cherry picked from commit 443a8dfca65be7d60c4bd46181b4c749b4756114)
2025-09-26 13:40:44 +02:00
PJB3005
3caffa04da Version: 260.2.2 2025-09-19 09:17:28 +02:00
Skye
06b377d1d5 Fix resource loading on non-Windows platforms (#6201)
(cherry picked from commit 51bbc5dc45)
2025-09-19 09:17:28 +02:00
PJB3005
41fb191dda Version: 260.2.1 2025-09-14 14:55:52 +02:00
PJB3005
d4bcc1dc05 Squashed commit of the following:
commit d4f265c314
Author: PJB3005 <pieterjan.briers+git@gmail.com>
Date:   Sun Sep 14 14:32:44 2025 +0200

    Fix incorrect path combine in DirLoader and WritableDirProvider

    This (and the other couple past commits) reported by Elelzedel.

commit 7654d38612
Author: PJB3005 <pieterjan.briers+git@gmail.com>
Date:   Sat Sep 13 22:50:51 2025 +0200

    Move CEF cache out of data directory

    Don't want content messing with this...

commit cdcc255123
Author: PJB3005 <pieterjan.briers+git@gmail.com>
Date:   Sat Sep 13 19:11:16 2025 +0200

    Make Robust.Client.WebView.Cef.Program internal.

commit 2f56a6a110
Author: PJB3005 <pieterjan.briers+git@gmail.com>
Date:   Sat Sep 13 19:10:46 2025 +0200

    Update SpaceWizards.NFluidSynth to 0.2.2

commit 16fc48cef2
Author: PJB3005 <pieterjan.briers+git@gmail.com>
Date:   Sat Sep 13 19:09:43 2025 +0200

    Hide IWritableDirProvider.RootDir on client

    This shouldn't be exposed.

(cherry picked from commit 2f07159336bc640e41fbbccfdec4133a68c13bdb)
(cherry picked from commit d6c3212c74373ed2420cc4be2cf10fcd899c2106)
(cherry picked from commit bfa70d7e2ca6758901b680547fcfa9b24e0610b7)
(cherry picked from commit 06e52f5d58efc1491915822c2650f922673c82c6)
2025-09-14 14:55:51 +02:00
165 changed files with 1843 additions and 2260 deletions

View File

@@ -57,7 +57,7 @@
<PackageVersion Include="SharpZstd.Interop" Version="1.5.2-beta2" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.7" />
<PackageVersion Include="SpaceWizards.HttpListener" Version="0.1.1" />
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.1.1" />
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.2.2" />
<PackageVersion Include="SpaceWizards.SharpFont" Version="1.0.2" />
<PackageVersion Include="SpaceWizards.Sodium" Version="0.2.1" />
<PackageVersion Include="TerraFX.Interop.Windows" Version="10.0.26100.1" />

View File

@@ -1,4 +1,4 @@
<Project>
<!-- This file automatically reset by Tools/version.py -->
<PropertyGroup><Version>263.0.0</Version></PropertyGroup>
</Project>
<Project>
<!-- This file automatically reset by Tools/version.py -->

View File

@@ -54,144 +54,16 @@ END TEMPLATE-->
*None yet*
## 263.0.0
### Breaking changes
* Fully removed some non-`Entity<T>` container methods.
### New features
* `IMidiRenderer.LoadSoundfont` has been split into `LoadSoundfontResource` and `LoadSoundfontUser`, the original now being deprecated.
* Client command execution now properly catches errors instead of letting them bubble up through the input stack.
* Added `CompletionHelper.PrototypeIdsLimited` API to allow commands to autocomplete entity prototype IDs.
* Added `spawn:in` Toolshed command.
* Added `MapLoaderSystem.TryLoadGeneric` overload to load from a `Stream`.
* Added `OutputPanel.GetMessage()` and `OutputPanel.SetMessage()` to allow replacing individual messages.
### Bugfixes
* Fixed debug asserts when using MIDI on Windows.
* Fixed an error getting logged on startup on macOS related to window icons.
* `CC-BY-NC-ND-4.0` is now a valid license for the RGA validator.
* Fixed `TabContainer.CurrentTab` clamping against the wrong value.
* Fix culture-based parsing in `TimespanSerializer`.
* Fixed grid rendering blowing up on tile IDs that aren't registered.
* Fixed debug assert when loading MIDI soundfonts on Windows.
* Make `ColorSelectorSliders` properly update the dropdown when changing `SelectorType`.
* Fixed `tpto` allowing teleports to oneself, thereby causing them to be deleted.
* Fix OpenAL extensions being requested incorrectly, causing an error on macOS.
* Fixed horizontal measuring of markup controls in rich text.
### Other
* Improved logging for some audio entity errors.
* Avoided more server stutters when using `csci`.
* Improved physics performance.
* Made various localization functions like `GENDER()` not throw if passed a string instead of an `EntityUid`.
* The generic clause on `EntitySystem.AddComp<T>` has been changed to `IComponent` (from `Component`) for consistency with `IEntityManager.AddComponent<T>`.
* `DataDefinitionAnalyzer` has been optimized somewhat.
* Improved assert logging error message when static data fields are encountered.
### Internal
* Warning cleanup.
* Added more tests for `DataDefinitionAnalyzer`.
* Consistently use `EntitySystem` proxy methods in engine.
## 260.2.4
## 262.0.0
### Breaking changes
* Toolshed commands will now validate that each non-generic command argument is parseable (i.e., has a corresponding type parser). This check can be disabled by explicitly marking the argument as unparseable via `CommandArgumentAttribute.Unparseable`.
### New features
* `ToolshedManager.TryParse` now also supports nullable value types.
* Add an ignoredComponents arg to IsDefault.
### Bugfixes
* Fix `SpriteComponent.Layer.Visible` setter not marking a sprite's bounding box as dirty.
* The audio params in the passed SoundSpecifier for PlayStatic(SoundSpecifier, Filter, ...) will now be used as a default like other PlayStatic overrides.
* Fix windows not saving their positions correctly when their x position is <= 0.
* Fix transform state handling overriding PVS detachment.
## 260.2.3
## 261.2.0
### New features
* Implement IEquatable for ResolvedPathSpecifier & ResolvedCollectionSpecifier.
* Add NearestChunkEnumerator.
### Bugfixes
* Fix static entities not having the center of mass updated.
* Fix TryQueueDelete.
* Fix tpto potentially parenting grids to non-map entities.
### Other
* TileChangedEvent is now raised once in clientside grid state handling rather than per tile.
* Removed ITileDefinition.ID as it was redundant.
* Change the lifestage checks on predicted entity deletion to check for terminating.
### Internal
* Update some `GetComponentName<T>` uses to generic.
## 260.2.2
## 261.1.0
### New features
* Automatically create logger sawmills for `UIController`s similar to `EntitySystem`s.
### Bugfixes
* Fix physics forces not auto-clearing / respecting the cvar.
### Internal
* Cleanup more compiler warnings in unit tests.
## 261.0.0
### Breaking changes
* Remove unused TryGetContainingContainer override.
* Stop recursive FrameUpdates for controls that are not visible.
* Initialize LocMgr earlier in the callstack for GameController.
* Fix FastNoiseLise fractal bounding and remove its DataField property as it should be derived on other properties updating.
* Make RaiseMoveEvent internal.
* MovedGridsComponent and PhysicsMapComponent are now purged and properties on `SharedPhysicsSystem`. Additionally the TransformComponent for Awake entities is stored alongside the PhysicsComponent for them.
* TransformComponent is now stored on physics contacts.
* Gravity2DComponent and Gravity2DController were moved to SharedPhysicsSystem.
### New features
* `IFileDialogManager` now allows specifying `FileAccess` and `FileShare` modes.
* Add Intersects and Enlarged to Box2i in line with Box2.
* Make `KeyFrame`s on `AnimationTrackProperty` public settable.
* Add the spawned entities to a returned array from `SpawnEntitiesAttachedTo`.
### Bugfixes
* Fixed SDL3 file dialog implementation having a memory leak and not opening files read-write.
* Fix GetMapLinearVelocity.
### Other
* `uploadfile` and `loadprototype` commands now only open files with read access.
* Optimize `ToMapCoordinates`.
### Internal
* Cleanup on internals of `IFileDialogManager`, removing duplicate code.
* Fix Contacts not correctly being marked as `Touching` while contact is ongoing.
## 260.2.1
## 260.2.0

View File

@@ -195,8 +195,6 @@ command-description-spawn-at =
Spawns an entity at the given coordinates.
command-description-spawn-on =
Spawns an entity on the given entity, at it's coordinates.
command-description-spawn-in =
Spawns an entity in the given container on the given entity, dropping it at its coordinates if it doesn't fit
command-description-spawn-attached =
Spawns an entity attached to the given entity, at (0 0) relative to it.
command-description-mappos =

View File

@@ -55,7 +55,7 @@ public sealed class DataDefinitionAnalyzerTest
namespace Robust.Shared.Serialization.Manager.Attributes
{
public class DataFieldBaseAttribute : Attribute;
public class DataFieldAttribute(string? tag = null) : DataFieldBaseAttribute;
public class DataFieldAttribute : DataFieldBaseAttribute;
public sealed class DataDefinitionAttribute : Attribute;
public sealed class NotYamlSerializableAttribute : Attribute;
}
@@ -117,61 +117,6 @@ public sealed class DataDefinitionAnalyzerTest
);
}
[Test]
public async Task PartialDataDefinitionTest()
{
const string code = """
using Robust.Shared.Serialization.Manager.Attributes;
[DataDefinition]
public sealed class Foo { }
""";
await Verifier(code,
// /0/Test0.cs(4,15): error RA0017: Type Foo is a DataDefinition but is not partial
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataDefinitionPartialRule).WithSpan(4, 15, 4, 20).WithArguments("Foo")
);
}
[Test]
public async Task NestedPartialDataDefinitionTest()
{
const string code = """
using Robust.Shared.Serialization.Manager.Attributes;
public sealed class Foo
{
[DataDefinition]
public sealed partial class Nested { }
}
""";
await Verifier(code,
// /0/Test0.cs(3,15): error RA0018: Type Foo contains nested data definition Nested but is not partial
VerifyCS.Diagnostic(DataDefinitionAnalyzer.NestedDataDefinitionPartialRule).WithSpan(3, 15, 3, 20).WithArguments("Foo", "Nested")
);
}
[Test]
public async Task RedundantDataFieldTagTest()
{
const string code = """
using Robust.Shared.Serialization.Manager.Attributes;
[DataDefinition]
public sealed partial class Foo
{
[DataField("someValue")]
public int SomeValue;
}
""";
await Verifier(code,
// /0/Test0.cs(6,6): info RA0027: Data field SomeValue in data definition Foo has an explicitly set tag that matches autogenerated tag
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldRedundantTagRule).WithSpan(6, 6, 6, 28).WithArguments("SomeValue", "Foo")
);
}
[Test]
public async Task ReadOnlyPropertyTest()
{

View File

@@ -22,7 +22,7 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
private const string DataFieldAttributeName = "DataField";
private const string ViewVariablesAttributeName = "ViewVariables";
public static readonly DiagnosticDescriptor DataDefinitionPartialRule = new(
private static readonly DiagnosticDescriptor DataDefinitionPartialRule = new(
Diagnostics.IdDataDefinitionPartial,
"Type must be partial",
"Type {0} is a DataDefinition but is not partial",
@@ -32,7 +32,7 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
"Make sure to mark any type that is a data definition as partial."
);
public static readonly DiagnosticDescriptor NestedDataDefinitionPartialRule = new(
private static readonly DiagnosticDescriptor NestedDataDefinitionPartialRule = new(
Diagnostics.IdNestedDataDefinitionPartial,
"Type must be partial",
"Type {0} contains nested data definition {1} but is not partial",
@@ -62,7 +62,7 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
"Make sure to add a setter."
);
public static readonly DiagnosticDescriptor DataFieldRedundantTagRule = new(
private static readonly DiagnosticDescriptor DataFieldRedundantTagRule = new(
Diagnostics.IdDataFieldRedundantTag,
"Data field has redundant tag specified",
"Data field {0} in data definition {1} has an explicitly set tag that matches autogenerated tag",
@@ -102,23 +102,14 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSymbolStartAction(symbolContext =>
{
if (symbolContext.Symbol is not INamedTypeSymbol typeSymbol)
return;
context.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.ClassDeclaration);
context.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.StructDeclaration);
context.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.RecordDeclaration);
context.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.RecordStructDeclaration);
context.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.InterfaceDeclaration);
if (!IsDataDefinition(typeSymbol))
return;
symbolContext.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.ClassDeclaration);
symbolContext.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.StructDeclaration);
symbolContext.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.RecordDeclaration);
symbolContext.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.RecordStructDeclaration);
symbolContext.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.InterfaceDeclaration);
symbolContext.RegisterSyntaxNodeAction(AnalyzeDataField, SyntaxKind.FieldDeclaration);
symbolContext.RegisterSyntaxNodeAction(AnalyzeDataFieldProperty, SyntaxKind.PropertyDeclaration);
}, SymbolKind.NamedType);
context.RegisterSyntaxNodeAction(AnalyzeDataField, SyntaxKind.FieldDeclaration);
context.RegisterSyntaxNodeAction(AnalyzeDataFieldProperty, SyntaxKind.PropertyDeclaration);
}
private void AnalyzeDataDefinition(SyntaxNodeAnalysisContext context)
@@ -126,7 +117,8 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
if (context.Node is not TypeDeclarationSyntax declaration)
return;
if (context.ContainingSymbol is not INamedTypeSymbol type)
var type = context.SemanticModel.GetDeclaredSymbol(declaration)!;
if (!IsDataDefinition(type))
return;
if (!IsPartial(declaration))
@@ -137,7 +129,7 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
var containingType = type.ContainingType;
while (containingType != null)
{
var containingTypeDeclaration = (TypeDeclarationSyntax)containingType.DeclaringSyntaxReferences[0].GetSyntax();
var containingTypeDeclaration = (TypeDeclarationSyntax) containingType.DeclaringSyntaxReferences[0].GetSyntax();
if (!IsPartial(containingTypeDeclaration))
{
context.ReportDiagnostic(Diagnostic.Create(NestedDataDefinitionPartialRule, containingTypeDeclaration.Keyword.GetLocation(), containingType.Name, type.Name));
@@ -152,26 +144,27 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
if (context.Node is not FieldDeclarationSyntax field)
return;
if (context.ContainingSymbol?.ContainingType is not INamedTypeSymbol type)
var typeDeclaration = field.FirstAncestorOrSelf<TypeDeclarationSyntax>();
if (typeDeclaration == null)
return;
var type = context.SemanticModel.GetDeclaredSymbol(typeDeclaration)!;
if (!IsDataDefinition(type))
return;
foreach (var variable in field.Declaration.Variables)
{
var fieldSymbol = context.SemanticModel.GetDeclaredSymbol(variable);
if (fieldSymbol == null)
continue;
if (!IsDataField(fieldSymbol, out _, out var datafieldAttribute))
continue;
if (IsReadOnlyDataField(type, fieldSymbol))
{
TryGetModifierLocation(field, SyntaxKind.ReadOnlyKeyword, out var location);
context.ReportDiagnostic(Diagnostic.Create(DataFieldWritableRule, location, fieldSymbol.Name, type.Name));
}
if (HasRedundantTag(fieldSymbol, datafieldAttribute))
if (HasRedundantTag(fieldSymbol))
{
TryGetAttributeLocation(field, DataFieldAttributeName, out var location);
context.ReportDiagnostic(Diagnostic.Create(DataFieldRedundantTagRule, location, fieldSymbol.Name, type.Name));
@@ -203,28 +196,25 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
if (context.Node is not PropertyDeclarationSyntax property)
return;
if (context.ContainingSymbol is not IPropertySymbol propertySymbol)
var typeDeclaration = property.FirstAncestorOrSelf<TypeDeclarationSyntax>();
if (typeDeclaration == null)
return;
if (propertySymbol.ContainingType is not INamedTypeSymbol type)
return;
if (type.IsRecord || type.IsValueType)
var type = context.SemanticModel.GetDeclaredSymbol(typeDeclaration)!;
if (!IsDataDefinition(type) || type.IsRecord || type.IsValueType)
return;
var propertySymbol = context.SemanticModel.GetDeclaredSymbol(property);
if (propertySymbol == null)
return;
if (!IsDataField(propertySymbol, out _, out var datafieldAttribute))
return;
if (IsReadOnlyDataField(type, propertySymbol))
{
var location = property.AccessorList != null ? property.AccessorList.GetLocation() : property.GetLocation();
context.ReportDiagnostic(Diagnostic.Create(DataFieldPropertyWritableRule, location, propertySymbol.Name, type.Name));
}
if (HasRedundantTag(propertySymbol, datafieldAttribute))
if (HasRedundantTag(propertySymbol))
{
TryGetAttributeLocation(property, DataFieldAttributeName, out var location);
context.ReportDiagnostic(Diagnostic.Create(DataFieldRedundantTagRule, location, propertySymbol.Name, type.Name));
@@ -252,6 +242,9 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
private static bool IsReadOnlyDataField(ITypeSymbol type, ISymbol field)
{
if (!IsDataField(field, out _, out _))
return false;
return IsReadOnlyMember(type, field);
}
@@ -376,14 +369,17 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
return false;
}
private static bool HasRedundantTag(ISymbol symbol, AttributeData datafieldAttribute)
private static bool HasRedundantTag(ISymbol symbol)
{
if (!IsDataField(symbol, out var _, out var attribute))
return false;
// No args, no problem
if (datafieldAttribute.ConstructorArguments.Length == 0)
if (attribute.ConstructorArguments.Length == 0)
return false;
// If a tag is explicitly specified, it will be the first argument...
var tagArgument = datafieldAttribute.ConstructorArguments[0];
var tagArgument = attribute.ConstructorArguments[0];
// ...but the first arg could also something else, since tag is optional
// so we make sure that it's a string
if (tagArgument.Value is not string explicitName)
@@ -398,6 +394,9 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
private static bool HasVVReadWrite(ISymbol symbol)
{
if (!IsDataField(symbol, out _, out _))
return false;
// Make sure it has ViewVariablesAttribute
AttributeData? viewVariablesAttribute = null;
foreach (var attr in symbol.GetAttributes())
@@ -423,6 +422,9 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
private static bool IsNotYamlSerializable(ISymbol field, ITypeSymbol type)
{
if (!IsDataField(field, out _, out _))
return false;
return HasAttribute(type, NotYamlSerializableName);
}

View File

@@ -6,7 +6,7 @@ using Xilium.CefGlue;
namespace Robust.Client.WebView.Cef
{
public static class Program
internal static class Program
{
// This was supposed to be the main entry for the subprocess program... It doesn't work.
public static int Main(string[] args)

View File

@@ -5,6 +5,7 @@ using System.Net;
using System.Reflection;
using System.Text;
using Robust.Client.Console;
using Robust.Client.Utility;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.IoC;
@@ -24,6 +25,7 @@ namespace Robust.Client.WebView.Cef
[Dependency] private readonly IDependencyCollection _dependencyCollection = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IGameControllerInternal _gameController = default!;
[Dependency] private readonly IResourceManagerInternal _resourceManager = default!;
[Dependency] private readonly IClientConsoleHost _consoleHost = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
@@ -61,7 +63,10 @@ namespace Robust.Client.WebView.Cef
var cachePath = "";
if (_resourceManager.UserData is WritableDirProvider userData)
cachePath = userData.GetFullPath(new ResPath("/cef_cache"));
{
var rootDir = UserDataDir.GetRootUserDataDir(_gameController);
cachePath = Path.Combine(rootDir, "cef_cache", "0");
}
var settings = new CefSettings()
{

View File

@@ -13,7 +13,7 @@ namespace Robust.Client.Animations
/// </summary>
public abstract class AnimationTrackProperty : AnimationTrack
{
public List<KeyFrame> KeyFrames { get; set; } = new();
public List<KeyFrame> KeyFrames { get; protected set; } = new();
/// <summary>
/// How to interpolate values when between two keyframes.

View File

@@ -57,8 +57,8 @@ internal sealed partial class AudioManager : IAudioInternal
_checkAlError();
// Load up AL context extensions.
var s = ALC.GetString(_openALDevice, AlcGetString.Extensions) ?? "";
foreach (var extension in s.Split(' ', StringSplitOptions.RemoveEmptyEntries))
var s = ALC.GetString(ALDevice.Null, AlcGetString.Extensions) ?? "";
foreach (var extension in s.Split(' '))
{
_alContextExtensions.Add(extension);
}

View File

@@ -582,7 +582,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
{
if (TerminatingOrDeleted(entity))
{
LogAudioPlaybackOnInvalidEntity(specifier, entity);
Log.Error($"Tried to play coordinates audio on a terminating / deleted entity {ToPrettyString(entity)}");
return null;
}
@@ -626,7 +626,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
{
if (TerminatingOrDeleted(coordinates.EntityId))
{
LogAudioPlaybackOnInvalidEntity(specifier, coordinates.EntityId);
Log.Error($"Tried to play coordinates audio on a terminating / deleted entity {ToPrettyString(coordinates.EntityId)}");
return null;
}
@@ -753,12 +753,6 @@ public sealed partial class AudioSystem : SharedAudioSystem
return _resourceCache.GetResource<AudioResource>(filename).AudioStream.Length;
}
private void LogAudioPlaybackOnInvalidEntity(ResolvedSoundSpecifier? specifier, EntityUid entityId)
{
var soundInfo = specifier?.ToString() ?? "unknown sound";
Log.Error($"Tried to play coordinates audio on a terminating / deleted entity {ToPrettyString(entityId)}. Sound: {soundInfo}. Trace: {Environment.StackTrace}");
}
#region Jobs
private record struct UpdateAudioJob : IParallelRobustJob

View File

@@ -6,7 +6,6 @@ using Robust.Shared.Audio.Midi;
using Robust.Shared.Audio.Sources;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Utility;
namespace Robust.Client.Audio.Midi;
@@ -157,13 +156,8 @@ public interface IMidiRenderer : IDisposable
/// <summary>
/// Loads a new soundfont into the renderer.
/// </summary>
[Obsolete("Use LoadSoundfontResource or LoadSoundfontUser instead")]
void LoadSoundfont(string filename, bool resetPresets = false);
void LoadSoundfontResource(ResPath path, bool resetPresets = false);
void LoadSoundfontUser(ResPath path, bool resetPresets = false);
/// <summary>
/// Invoked whenever a new midi event is registered.
/// </summary>

View File

@@ -1,262 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using NFluidsynth;
using Robust.Shared.ContentPack;
using Robust.Shared.Utility;
namespace Robust.Client.Audio.Midi;
internal sealed partial class MidiManager
{
// For loading sound fonts, we have to use a callback model where we can only parse a string.
// This API, frankly, fucking sucks.
//
// These prefixes are used to separate the various places a file *can* be loaded from.
//
// We cannot prevent Fluidsynth from trying to load prefixed paths itself if they are invalid
// So if content specifies "/foobar.sf2" to be loaded and it doesn't exist,
// Fluidsynth *will* try to fopen("RES:/foobar.sf2"). For this reason I'm putting in some nonsense characters
// that will pass through Fluidsynth fine, but make sure the filename is *never* a practically valid OS path.
//
// NOTE: Raw disk paths *cannot* be prefixed as Fluidsynth needs to load those itself.
// Specifically, their .dls loader doesn't respect file callbacks.
// If you're curious why this is: it's two-fold:
// * The Fluidsynth C code for the .dls loader just doesn't use the file callbacks, period.
// * Even if it did, we're not specifying those file callbacks, as they're per loader,
// and we're only adding a *new* sound font loader with file callbacks, not modifying the existing ones.
// The loader for .sfX format and .dls format are different loader objects in Fluidsynth.
internal const string PrefixCommon = "!/ -?\x0001";
internal const string PrefixLegacy = PrefixCommon + "LEGACY";
internal const string PrefixUser = PrefixCommon + "USER";
internal const string PrefixResources = PrefixCommon + "RES";
private void LoadSoundFontSetup(MidiRenderer renderer)
{
_midiSawmill.Debug($"Loading fallback soundfont {FallbackSoundfont}");
// Since the last loaded soundfont takes priority, we load the fallback soundfont before the soundfont.
renderer.LoadSoundfontResource(FallbackSoundfont);
// Load system-specific soundfonts.
if (OperatingSystem.IsLinux())
{
foreach (var filepath in LinuxSoundfonts)
{
if (!File.Exists(filepath) || !SoundFont.IsSoundFont(filepath))
continue;
try
{
_midiSawmill.Debug($"Loading OS soundfont {filepath}");
renderer.LoadSoundfontDisk(filepath);
}
catch (Exception)
{
continue;
}
break;
}
}
else if (OperatingSystem.IsMacOS())
{
if (File.Exists(OsxSoundfont) && SoundFont.IsSoundFont(OsxSoundfont))
{
_midiSawmill.Debug($"Loading OS soundfont {OsxSoundfont}");
renderer.LoadSoundfontDisk(OsxSoundfont);
}
}
else if (OperatingSystem.IsWindows())
{
if (File.Exists(WindowsSoundfont) && SoundFont.IsSoundFont(WindowsSoundfont))
{
_midiSawmill.Debug($"Loading OS soundfont {WindowsSoundfont}");
renderer.LoadSoundfontDisk(WindowsSoundfont);
}
}
// Maybe load soundfont specified in environment variable.
// Load it here so it can override system soundfonts but not content or user data soundfonts.
if (Environment.GetEnvironmentVariable(SoundfontEnvironmentVariable) is { } soundfontOverride)
{
// Just to avoid funny shit: avoid people smuggling a prefix in here.
// I wish I could separate this properly...
var (prefix, _) = SplitPrefix(soundfontOverride);
if (IsValidPrefix(prefix))
{
_midiSawmill.Error($"Not respecting {SoundfontEnvironmentVariable} env variable: invalid file path");
}
else if (File.Exists(soundfontOverride) && SoundFont.IsSoundFont(soundfontOverride))
{
_midiSawmill.Debug($"Loading environment variable soundfont {soundfontOverride}");
renderer.LoadSoundfontDisk(soundfontOverride);
}
}
// Load content-specific custom soundfonts, which should override the system/fallback soundfont.
_midiSawmill.Debug($"Loading soundfonts from content directory {ContentCustomSoundfontDirectory}");
foreach (var file in _resourceManager.ContentFindFiles(ContentCustomSoundfontDirectory))
{
if (file.Extension != "sf2" && file.Extension != "dls" && file.Extension != "sf3") continue;
_midiSawmill.Debug($"Loading content soundfont {file}");
renderer.LoadSoundfontResource(file);
}
// Load every soundfont from the user data directory last, since those may override any other soundfont.
_midiSawmill.Debug($"Loading soundfonts from user data directory {CustomSoundfontDirectory}");
var enumerator = _resourceManager.UserData.Find($"{CustomSoundfontDirectory.ToRelativePath()}*").Item1;
foreach (var file in enumerator)
{
if (file.Extension != "sf2" && file.Extension != "dls" && file.Extension != "sf3") continue;
_midiSawmill.Debug($"Loading user soundfont {file}");
renderer.LoadSoundfontUser(file);
}
}
internal static string PrefixPath(string prefix, string value)
{
return $"{prefix}:{value}";
}
internal static (string prefix, string? value) SplitPrefix(string filename)
{
var filenameSplit = filename.Split(':', 2);
if (filenameSplit.Length == 1)
return (filenameSplit[0], null);
return (filenameSplit[0], filenameSplit[1]);
}
internal static bool IsValidPrefix(string prefix)
{
return prefix is PrefixLegacy or PrefixUser or PrefixResources;
}
/// <summary>
/// This class is used to load soundfonts.
/// </summary>
private sealed class ResourceLoaderCallbacks : SoundFontLoaderCallbacks
{
private readonly MidiManager _parent;
private readonly Dictionary<int, Stream> _openStreams = new();
private int _nextStreamId = 1;
public ResourceLoaderCallbacks(MidiManager parent)
{
_parent = parent;
}
public override IntPtr Open(string filename)
{
if (string.IsNullOrEmpty(filename))
{
return IntPtr.Zero;
}
Stream stream;
try
{
stream = OpenCore(filename);
}
catch (Exception e)
{
_parent._midiSawmill.Error($"Error while opening sound font: {e}");
return IntPtr.Zero;
}
var id = _nextStreamId++;
_openStreams.Add(id, stream);
return (IntPtr) id;
}
private Stream OpenCore(string filename)
{
var (prefix, value) = SplitPrefix(filename);
if (!IsValidPrefix(prefix) || value == null)
return File.OpenRead(filename);
var resourceCache = _parent._resourceManager;
var resourcePath = new ResPath(value);
switch (prefix)
{
case PrefixUser:
return resourceCache.UserData.OpenRead(resourcePath);
case PrefixResources:
return resourceCache.ContentFileRead(resourcePath);
case PrefixLegacy:
// Try resources first, then try user data.
if (resourceCache.TryContentFileRead(resourcePath, out var stream))
return stream;
return resourceCache.UserData.OpenRead(resourcePath);
default:
throw new UnreachableException("Invalid prefix specified!");
}
}
public override unsafe int Read(IntPtr buf, long count, IntPtr sfHandle)
{
var length = (int) count;
var span = new Span<byte>(buf.ToPointer(), length);
var stream = _openStreams[(int) sfHandle];
// Fluidsynth's docs state that this method should leave the buffer unmodified if it fails. (returns -1)
try
{
// Fluidsynth does a LOT of tiny allocations (frankly, way too much).
if (count < 1024)
{
// ReSharper disable once SuggestVarOrType_Elsewhere
Span<byte> buffer = stackalloc byte[(int)count];
stream.ReadExact(buffer);
buffer.CopyTo(span);
}
else
{
var buffer = stream.ReadExact(length);
buffer.CopyTo(span);
}
}
catch (EndOfStreamException)
{
return -1;
}
return 0;
}
public override int Seek(IntPtr sfHandle, long offset, SeekOrigin origin)
{
var stream = _openStreams[(int) sfHandle];
stream.Seek(offset, origin);
return 0;
}
public override long Tell(IntPtr sfHandle)
{
var stream = _openStreams[(int) sfHandle];
return (long) stream.Position;
}
public override int Close(IntPtr sfHandle)
{
if (!_openStreams.Remove((int) sfHandle, out var stream))
return -1;
stream.Dispose();
return 0;
}
}
}

View File

@@ -119,7 +119,7 @@ internal sealed partial class MidiManager : IMidiManager
private const string OsxSoundfont =
"/System/Library/Components/CoreAudio.component/Contents/Resources/gs_instruments.dls";
private static readonly ResPath FallbackSoundfont = new ResPath("/Midi/fallback.sf2");
private const string FallbackSoundfont = "/Midi/fallback.sf2";
private const string ContentCustomSoundfontDirectory = "/Audio/MidiCustom/";
@@ -265,7 +265,81 @@ internal sealed partial class MidiManager : IMidiManager
var renderer = new MidiRenderer(_settings!, soundfontLoader, mono, this, _audio, _taskManager, _midiSawmill);
LoadSoundFontSetup(renderer);
_midiSawmill.Debug($"Loading fallback soundfont {FallbackSoundfont}");
// Since the last loaded soundfont takes priority, we load the fallback soundfont before the soundfont.
renderer.LoadSoundfont(FallbackSoundfont);
// Load system-specific soundfonts.
if (OperatingSystem.IsLinux())
{
foreach (var filepath in LinuxSoundfonts)
{
if (!File.Exists(filepath) || !SoundFont.IsSoundFont(filepath))
continue;
try
{
_midiSawmill.Debug($"Loading OS soundfont {filepath}");
renderer.LoadSoundfont(filepath);
}
catch (Exception)
{
continue;
}
break;
}
}
else if (OperatingSystem.IsMacOS())
{
if (File.Exists(OsxSoundfont) && SoundFont.IsSoundFont(OsxSoundfont))
{
_midiSawmill.Debug($"Loading OS soundfont {OsxSoundfont}");
renderer.LoadSoundfont(OsxSoundfont);
}
}
else if (OperatingSystem.IsWindows())
{
if (File.Exists(WindowsSoundfont) && SoundFont.IsSoundFont(WindowsSoundfont))
{
_midiSawmill.Debug($"Loading OS soundfont {WindowsSoundfont}");
renderer.LoadSoundfont(WindowsSoundfont);
}
}
// Maybe load soundfont specified in environment variable.
// Load it here so it can override system soundfonts but not content or user data soundfonts.
if (Environment.GetEnvironmentVariable(SoundfontEnvironmentVariable) is {} soundfontOverride)
{
if (File.Exists(soundfontOverride) && SoundFont.IsSoundFont(soundfontOverride))
{
_midiSawmill.Debug($"Loading environment variable soundfont {soundfontOverride}");
renderer.LoadSoundfont(soundfontOverride);
}
}
// Load content-specific custom soundfonts, which should override the system/fallback soundfont.
_midiSawmill.Debug($"Loading soundfonts from content directory {ContentCustomSoundfontDirectory}");
foreach (var file in _resourceManager.ContentFindFiles(ContentCustomSoundfontDirectory))
{
if (file.Extension != "sf2" && file.Extension != "dls" && file.Extension != "sf3") continue;
_midiSawmill.Debug($"Loading content soundfont {file}");
renderer.LoadSoundfont(file.ToString());
}
var userDataPath = _resourceManager.UserData.RootDir == null
? CustomSoundfontDirectory
: new ResPath(_resourceManager.UserData.RootDir) / CustomSoundfontDirectory.ToRelativePath();
// Load every soundfont from the user data directory last, since those may override any other soundfont.
_midiSawmill.Debug($"Loading soundfonts from user data directory {userDataPath}");
var enumerator = _resourceManager.UserData.Find($"{CustomSoundfontDirectory.ToRelativePath()}*").Item1;
foreach (var file in enumerator)
{
if (file.Extension != "sf2" && file.Extension != "dls" && file.Extension != "sf3") continue;
_midiSawmill.Debug($"Loading user soundfont {file}");
renderer.LoadSoundfont(file.ToString());
}
renderer.Source.Gain = _gain;
@@ -498,6 +572,130 @@ internal sealed partial class MidiManager : IMidiManager
midiEvent.Velocity);
}
/// <summary>
/// This class is used to load soundfonts.
/// </summary>
private sealed class ResourceLoaderCallbacks : SoundFontLoaderCallbacks
{
private readonly MidiManager _parent;
private readonly Dictionary<int, Stream> _openStreams = new();
private int _nextStreamId = 1;
public ResourceLoaderCallbacks(MidiManager parent)
{
_parent = parent;
}
public override IntPtr Open(string filename)
{
if (string.IsNullOrEmpty(filename))
{
return IntPtr.Zero;
}
Stream? stream;
var resourceCache = _parent._resourceManager;
var resourcePath = new ResPath(filename);
if (resourcePath.IsRooted)
{
// is it in content?
if (resourceCache.ContentFileExists(filename))
{
if (!resourceCache.TryContentFileRead(filename, out stream))
return IntPtr.Zero;
}
// is it in userdata?
else if (resourceCache.UserData.Exists(resourcePath))
{
stream = resourceCache.UserData.OpenRead(resourcePath);
}
else if (File.Exists(filename))
{
stream = File.OpenRead(filename);
}
else
{
return IntPtr.Zero;
}
}
else if (File.Exists(filename))
{
stream = File.OpenRead(filename);
}
else
{
return IntPtr.Zero;
}
var id = _nextStreamId++;
_openStreams.Add(id, stream);
return (IntPtr) id;
}
public override unsafe int Read(IntPtr buf, long count, IntPtr sfHandle)
{
var length = (int) count;
var span = new Span<byte>(buf.ToPointer(), length);
var stream = _openStreams[(int) sfHandle];
// Fluidsynth's docs state that this method should leave the buffer unmodified if it fails. (returns -1)
try
{
// Fluidsynth does a LOT of tiny allocations (frankly, way too much).
if (count < 1024)
{
// ReSharper disable once SuggestVarOrType_Elsewhere
Span<byte> buffer = stackalloc byte[(int)count];
stream.ReadExact(buffer);
buffer.CopyTo(span);
}
else
{
var buffer = stream.ReadExact(length);
buffer.CopyTo(span);
}
}
catch (EndOfStreamException)
{
return -1;
}
return 0;
}
public override int Seek(IntPtr sfHandle, long offset, SeekOrigin origin)
{
var stream = _openStreams[(int) sfHandle];
stream.Seek(offset, origin);
return 0;
}
public override long Tell(IntPtr sfHandle)
{
var stream = _openStreams[(int) sfHandle];
return (long) stream.Position;
}
public override int Close(IntPtr sfHandle)
{
if (!_openStreams.Remove((int) sfHandle, out var stream))
return -1;
stream.Dispose();
return 0;
}
}
#region Jobs
private record struct MidiUpdateJob : IParallelRobustJob

View File

@@ -1,45 +0,0 @@
using System;
using Robust.Shared.Utility;
namespace Robust.Client.Audio.Midi;
internal sealed partial class MidiRenderer
{
[Obsolete("Use LoadSoundfontResource or LoadSoundfontUser instead")]
public void LoadSoundfont(string filename, bool resetPresets = true)
{
LoadSoundfontCore(
MidiManager.PrefixPath(MidiManager.PrefixLegacy, filename),
resetPresets);
}
public void LoadSoundfontResource(ResPath path, bool resetPresets = false)
{
LoadSoundfontCore(
MidiManager.PrefixPath(MidiManager.PrefixResources, path.ToString()),
resetPresets);
}
public void LoadSoundfontUser(ResPath path, bool resetPresets = false)
{
LoadSoundfontCore(
MidiManager.PrefixPath(MidiManager.PrefixUser, path.ToString()),
resetPresets);
}
internal void LoadSoundfontDisk(string path, bool resetPresets = false)
{
LoadSoundfontCore(
path,
resetPresets);
}
private void LoadSoundfontCore(string filenameString, bool resetPresets)
{
lock (_playerStateLock)
{
_synth.LoadSoundFont(filenameString, resetPresets);
MidiSoundfont = 1;
}
}
}

View File

@@ -16,7 +16,7 @@ using Robust.Shared.ViewVariables;
namespace Robust.Client.Audio.Midi;
internal sealed partial class MidiRenderer : IMidiRenderer
internal sealed class MidiRenderer : IMidiRenderer
{
private readonly IMidiManager _midiManager;
private readonly ITaskManager _taskManager;
@@ -435,6 +435,15 @@ internal sealed partial class MidiRenderer : IMidiRenderer
_sequencer.RemoveEvents(SequencerClientId.Wildcard, SequencerClientId.Wildcard, -1);
}
public void LoadSoundfont(string filename, bool resetPresets = true)
{
lock (_playerStateLock)
{
_synth.LoadSoundFont(filename, resetPresets);
MidiSoundfont = 1;
}
}
void IMidiRenderer.Render()
{
Render();

View File

@@ -21,7 +21,7 @@ public sealed class SpriteTreeSystem : ComponentTreeSystem<SpriteTreeComponent,
protected override Box2 ExtractAabb(in ComponentTreeEntry<SpriteComponent> entry, Vector2 pos, Angle rot)
{
// TODO SPRITE optimize this
// Because the just take the BB of the rotated BB, I'm pretty sure we do a lot of unnecessary maths.
// Because the just take the BB of the rotated BB, I'mt pretty sure we do a lot of unnecessary maths.
return _sprite.CalculateBounds((entry.Uid, entry.Component), pos, rot, default).CalcBoundingBox();
}

View File

@@ -191,16 +191,8 @@ namespace Robust.Client.Console
var shell = new ConsoleShell(this, session ?? _player.LocalSession, session == null);
var cmdArgs = args.ToArray();
try
{
AnyCommandExecuted?.Invoke(shell, commandName, command, cmdArgs);
cmd.Execute(shell, command, cmdArgs);
}
catch (Exception e)
{
_conLogger.Error($"ExecuteError - {command}:\n{e}");
shell.WriteError($"There was an error while executing the command: {e}");
}
AnyCommandExecuted?.Invoke(shell, commandName, command, cmdArgs);
cmd.Execute(shell, command, cmdArgs);
}
private bool CanExecute(string cmdName)

View File

@@ -82,7 +82,7 @@ namespace Robust.Client.Debugging
foreach (var ent in _mapSystem.GetAnchoredEntities(gridUid, grid, spot))
{
if (TryComp<MetaDataComponent>(ent, out var meta))
if (EntityManager.TryGetComponent<MetaDataComponent>(ent, out var meta))
{
text.AppendLine($"uid: {ent}, {meta.EntityName}");
}

View File

@@ -160,7 +160,6 @@ namespace Robust.Client
}
_serializationManager.Initialize();
_loc.Initialize();
// Call Init in game assemblies.
_modLoader.BroadcastRunLevel(ModRunLevel.PreInit);
@@ -183,6 +182,7 @@ namespace Robust.Client
_serializer.Initialize();
_inputManager.Initialize();
_console.Initialize();
_loc.Initialize();
// Make sure this is done before we try to load prototypes,
// avoid any possibility of race conditions causing the check to not finish
@@ -387,7 +387,7 @@ namespace Robust.Client
_prof.Initialize();
_resManager.Initialize(Options.LoadConfigAndUserData ? userDataDir : null);
_resManager.Initialize(Options.LoadConfigAndUserData ? userDataDir : null, hideUserDataDir: true);
var mountOptions = _commandLineArgs != null
? MountOptions.Merge(_commandLineArgs.MountOptions, Options.MountOptions)

View File

@@ -296,7 +296,7 @@ namespace Robust.Client.GameObjects
public override void PredictedDeleteEntity(Entity<MetaDataComponent?, TransformComponent?> ent)
{
if (!MetaQuery.Resolve(ent.Owner, ref ent.Comp1)
|| ent.Comp1.EntityLifeStage >= EntityLifeStage.Terminating
|| ent.Comp1.EntityDeleted
|| !TransformQuery.Resolve(ent.Owner, ref ent.Comp2))
{
return;
@@ -322,7 +322,7 @@ namespace Robust.Client.GameObjects
{
if (IsQueuedForDeletion(ent.Owner)
|| !MetaQuery.Resolve(ent.Owner, ref ent.Comp1)
|| ent.Comp1.EntityLifeStage >= EntityLifeStage.Terminating
|| ent.Comp1.EntityDeleted
|| !TransformQuery.Resolve(ent.Owner, ref ent.Comp2))
{
return;

View File

@@ -24,7 +24,9 @@ namespace Robust.Client.GameObjects
public sealed class SpriteBoundsSystem : EntitySystem
{
[Dependency] private readonly SharedTransformSystem _xformSystem = default!;
[Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly SpriteTreeSystem _spriteTree = default!;
private SpriteBoundsOverlay? _overlay;
@@ -40,7 +42,7 @@ namespace Robust.Client.GameObjects
if (_enabled)
{
DebugTools.AssertNull(_overlay);
_overlay = new SpriteBoundsOverlay(EntityManager);
_overlay = new SpriteBoundsOverlay(_spriteTree, _xformSystem);
_overlayManager.AddOverlay(_overlay);
}
else
@@ -55,13 +57,18 @@ namespace Robust.Client.GameObjects
private bool _enabled;
}
public sealed class SpriteBoundsOverlay(IEntityManager entMan) : Overlay
public sealed class SpriteBoundsOverlay : Overlay
{
public override OverlaySpace Space => OverlaySpace.WorldSpace;
private readonly SharedTransformSystem _xformSystem = entMan.System<SharedTransformSystem>();
private readonly SpriteSystem _spriteSystem = entMan.System<SpriteSystem>();
private readonly SpriteTreeSystem _renderTree = entMan.System<SpriteTreeSystem>();
private readonly SharedTransformSystem _xformSystem;
private SpriteTreeSystem _renderTree;
public SpriteBoundsOverlay(SpriteTreeSystem renderTree, SharedTransformSystem xformSystem)
{
_renderTree = renderTree;
_xformSystem = xformSystem;
}
protected internal override void Draw(in OverlayDrawArgs args)
{
@@ -69,11 +76,10 @@ namespace Robust.Client.GameObjects
var currentMap = args.MapId;
var viewport = args.WorldBounds;
foreach (var entry in _renderTree.QueryAabb(currentMap, viewport))
foreach (var (sprite, xform) in _renderTree.QueryAabb(currentMap, viewport))
{
var (sprite, xform) = entry;
var (worldPos, worldRot) = _xformSystem.GetWorldPositionRotation(xform);
var bounds = _spriteSystem.CalculateBounds((entry.Uid, sprite), worldPos, worldRot, args.Viewport.Eye?.Rotation ?? default);
var bounds = sprite.CalculateRotatedBoundingBox(worldPos, worldRot, args.Viewport.Eye?.Rotation ?? default);
// Get scaled down bounds used to indicate the "south" of a sprite.
var localBound = bounds.Box;

View File

@@ -1224,8 +1224,6 @@ namespace Robust.Client.GameObjects
return;
_visible = value;
Owner.Comp.BoundsDirty = true;
// ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract
if (_parent.Owner != EntityUid.Invalid)
Owner.Comp.Sys?.QueueUpdateIsInert(Owner);
@@ -1793,15 +1791,76 @@ namespace Robust.Client.GameObjects
[Obsolete("Use SpriteSystem.GetPrototypeTextures() instead")]
public static IEnumerable<IDirectionalTextureProvider> GetPrototypeTextures(EntityPrototype prototype, IResourceCache resourceCache, out bool noRot)
{
var sys = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<SpriteSystem>();
return sys.GetPrototypeTextures(prototype, out noRot);
var results = new List<IDirectionalTextureProvider>();
noRot = false;
// TODO when moving to a non-static method in a system, pass in IComponentFactory
if (prototype.TryGetComponent(out IconComponent? icon))
{
var sys = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<SpriteSystem>();
results.Add(sys.GetIcon(icon));
return results;
}
if (!prototype.Components.TryGetValue("Sprite", out _))
{
results.Add(resourceCache.GetFallback<TextureResource>().Texture);
return results;
}
var entityManager = IoCManager.Resolve<IEntityManager>();
var dummy = entityManager.SpawnEntity(prototype.ID, MapCoordinates.Nullspace);
var spriteComponent = entityManager.EnsureComponent<SpriteComponent>(dummy);
EntitySystem.Get<AppearanceSystem>().OnChangeData(dummy, spriteComponent);
foreach (var layer in spriteComponent.AllLayers)
{
if (!layer.Visible) continue;
if (layer.Texture != null)
{
results.Add(layer.Texture);
continue;
}
if (!layer.RsiState.IsValid) continue;
var rsi = layer.Rsi ?? spriteComponent.BaseRSI;
if (rsi == null ||
!rsi.TryGetState(layer.RsiState, out var state))
continue;
results.Add(state);
}
noRot = spriteComponent.NoRotation;
entityManager.DeleteEntity(dummy);
if (results.Count == 0)
results.Add(resourceCache.GetFallback<TextureResource>().Texture);
return results;
}
[Obsolete("Use SpriteSystem.GetPrototypeIcon() instead")]
public static IRsiStateLike GetPrototypeIcon(EntityPrototype prototype, IResourceCache resourceCache)
{
var sys = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<SpriteSystem>();
return sys.GetPrototypeIcon(prototype);
// TODO when moving to a non-static method in a system, pass in IComponentFactory
if (prototype.TryGetComponent(out IconComponent? icon))
return sys.GetIcon(icon);
if (!prototype.Components.ContainsKey("Sprite"))
return sys.GetFallbackState();
var entityManager = IoCManager.Resolve<IEntityManager>();
var dummy = entityManager.SpawnEntity(prototype.ID, MapCoordinates.Nullspace);
var spriteComponent = entityManager.EnsureComponent<SpriteComponent>(dummy);
var result = spriteComponent.Icon ?? sys.GetFallbackState();
entityManager.DeleteEntity(dummy);
return result;
}
}
}

View File

@@ -97,7 +97,7 @@ namespace Robust.Client.GameObjects
[Obsolete("Use Play(EntityUid<AnimationPlayerComponent> ent, Animation animation, string key) instead")]
public void Play(EntityUid uid, AnimationPlayerComponent? component, Animation animation, string key)
{
component ??= EnsureComp<AnimationPlayerComponent>(uid);
component ??= EntityManager.EnsureComponent<AnimationPlayerComponent>(uid);
Play(new Entity<AnimationPlayerComponent>(uid, component), animation, key);
}
@@ -158,7 +158,7 @@ namespace Robust.Client.GameObjects
public bool HasRunningAnimation(EntityUid uid, string key)
{
return TryComp(uid, out AnimationPlayerComponent? component) &&
return EntityManager.TryGetComponent(uid, out AnimationPlayerComponent? component) &&
component.PlayingAnimations.ContainsKey(key);
}

View File

@@ -223,12 +223,12 @@ namespace Robust.Client.GameObjects
private void SetEntityContextActive(IInputManager inputMan, EntityUid entity)
{
if(entity == default || !Exists(entity))
if(entity == default || !EntityManager.EntityExists(entity))
throw new ArgumentNullException(nameof(entity));
if (!TryComp(entity, out InputComponent? inputComp))
if (!EntityManager.TryGetComponent(entity, out InputComponent? inputComp))
{
_sawmillInputContext.Debug($"AttachedEnt has no InputComponent: entId={entity}, entProto={Comp<MetaDataComponent>(entity).EntityPrototype}. Setting default \"{InputContextContainer.DefaultContextName}\" context...");
_sawmillInputContext.Debug($"AttachedEnt has no InputComponent: entId={entity}, entProto={EntityManager.GetComponent<MetaDataComponent>(entity).EntityPrototype}. Setting default \"{InputContextContainer.DefaultContextName}\" context...");
inputMan.Contexts.SetActiveContext(InputContextContainer.DefaultContextName);
return;
}
@@ -239,7 +239,7 @@ namespace Robust.Client.GameObjects
}
else
{
_sawmillInputContext.Error($"Unknown context: entId={entity}, entProto={Comp<MetaDataComponent>(entity).EntityPrototype}, context={inputComp.ContextName}. . Setting default \"{InputContextContainer.DefaultContextName}\" context...");
_sawmillInputContext.Error($"Unknown context: entId={entity}, entProto={EntityManager.GetComponent<MetaDataComponent>(entity).EntityPrototype}, context={inputComp.ContextName}. . Setting default \"{InputContextContainer.DefaultContextName}\" context...");
inputMan.Contexts.SetActiveContext(InputContextContainer.DefaultContextName);
}
}

View File

@@ -51,7 +51,7 @@ public sealed class ShowPlayerVelocityDebugSystem : EntitySystem
var player = _playerManager.LocalEntity;
if (player == null || !TryComp(player.Value, out PhysicsComponent? body))
if (player == null || !EntityManager.TryGetComponent(player.Value, out PhysicsComponent? body))
{
_label.Visible = false;
return;

View File

@@ -56,6 +56,10 @@ public sealed partial class SpriteSystem
/// </summary>
public IRsiStateLike GetPrototypeIcon(string prototype)
{
// Check if this prototype has been cached before, and if so return the result.
if (_cachedPrototypeIcons.TryGetValue(prototype, out var cachedResult))
return cachedResult;
if (!_proto.TryIndex<EntityPrototype>(prototype, out var entityPrototype))
{
// The specified prototype doesn't exist, return the fallback "error" sprite.
@@ -63,7 +67,11 @@ public sealed partial class SpriteSystem
return GetFallbackState();
}
return GetPrototypeIcon(entityPrototype);
// Generate the icon and cache it in case it's ever needed again.
var result = GetPrototypeIcon(entityPrototype);
_cachedPrototypeIcons[prototype] = result;
return result;
}
/// <summary>
@@ -71,19 +79,13 @@ public sealed partial class SpriteSystem
/// This method does NOT cache the result.
/// </summary>
public IRsiStateLike GetPrototypeIcon(EntityPrototype prototype)
{
// This method may spawn & delete an entity to get an accruate RSI state, hence we cache the results
if (_cachedPrototypeIcons.TryGetValue(prototype.ID, out var cachedResult))
return cachedResult;
return _cachedPrototypeIcons[prototype.ID] = GetPrototypeIconInternal(prototype);
}
private IRsiStateLike GetPrototypeIconInternal(EntityPrototype prototype)
{
// IconComponent takes precedence. If it has a valid icon, return that. Otherwise, continue as normal.
if (prototype.TryGetComponent(out IconComponent? icon, _factory))
if (prototype.Components.TryGetValue("Icon", out var compData)
&& compData.Component is IconComponent icon)
{
return GetIcon(icon);
}
// If the prototype doesn't have a SpriteComponent, then there's nothing we can do but return the fallback.
if (!prototype.Components.ContainsKey("Sprite"))
@@ -100,63 +102,6 @@ public sealed partial class SpriteSystem
return result;
}
public IEnumerable<IDirectionalTextureProvider> GetPrototypeTextures(EntityPrototype proto) =>
GetPrototypeTextures(proto, out _);
public IEnumerable<IDirectionalTextureProvider> GetPrototypeTextures(EntityPrototype proto, out bool noRot)
{
var results = new List<IDirectionalTextureProvider>();
noRot = false;
if (proto.TryGetComponent(out IconComponent? icon, _factory))
{
results.Add(GetIcon(icon));
return results;
}
if (!proto.Components.ContainsKey("Sprite"))
{
results.Add(_resourceCache.GetFallback<TextureResource>().Texture);
return results;
}
var dummy = Spawn(proto.ID, MapCoordinates.Nullspace);
var spriteComponent = EnsureComp<SpriteComponent>(dummy);
// TODO SPRITE is this needed?
// And if it is, shouldn't GetPrototypeIconInternal also use this?
_appearance.OnChangeData(dummy, spriteComponent);
foreach (var layer in spriteComponent.AllLayers)
{
if (!layer.Visible)
continue;
if (layer.Texture != null)
{
results.Add(layer.Texture);
continue;
}
if (!layer.RsiState.IsValid)
continue;
var rsi = layer.Rsi ?? spriteComponent.BaseRSI;
if (rsi == null || !rsi.TryGetState(layer.RsiState, out var state))
continue;
results.Add(state);
}
noRot = spriteComponent.NoRotation;
Del(dummy);
if (results.Count == 0)
results.Add(_resourceCache.GetFallback<TextureResource>().Texture);
return results;
}
[Pure]
public RSI.State GetFallbackState()
{

View File

@@ -34,12 +34,8 @@ namespace Robust.Client.GameObjects
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IComponentFactory _factory = default!;
// Note that any new system dependencies have to be added to RobustUnitTest.BaseSetup()
[Dependency] private readonly SharedTransformSystem _xforms = default!;
[Dependency] private readonly SpriteTreeSystem _tree = default!;
[Dependency] private readonly AppearanceSystem _appearance = default!;
public static readonly ProtoId<ShaderPrototype> UnshadedId = "unshaded";
private readonly Queue<SpriteComponent> _inertUpdateQueue = new();

View File

@@ -631,7 +631,7 @@ namespace Robust.Client.GameStates
if (_sawmill.Level <= LogLevel.Debug)
_sawmill.Debug($" A component was dirtied: {comp.GetType()}");
if ((meta.Flags & MetaDataFlags.Detached) == 0 && compState != null)
if (compState != null)
{
var handleState = new ComponentHandleState(compState, null);
_entities.EventBus.RaiseComponentEvent(entity, comp, ref handleState);

View File

@@ -254,9 +254,9 @@ namespace Robust.Client.Graphics.Clyde
region = regionMaybe[tile.Variant];
}
var rotationMirroring = (_tileDefinitionManager.TryGetDefinition(tile.TypeId, out var tileDef) && tileDef.AllowRotationMirror) ?
tile.RotationMirroring
: 0;
var rotationMirroring = _tileDefinitionManager[tile.TypeId].AllowRotationMirror
? tile.RotationMirroring
: 0;
WriteTileToBuffers(i, gridX, gridY, vertexBuffer, indexBuffer, region, rotationMirroring);
i += 1;

View File

@@ -467,7 +467,7 @@ namespace Robust.Client.Graphics.Clyde
_windowing!.RunOnWindowThread(a);
}
public IFileDialogManagerImplementation? FileDialogImpl => _windowing as IFileDialogManagerImplementation;
public IFileDialogManager? FileDialogImpl => _windowing as IFileDialogManager;
private abstract class WindowReg
{

View File

@@ -307,7 +307,7 @@ namespace Robust.Client.Graphics.Clyde
action();
}
public IFileDialogManagerImplementation? FileDialogImpl => null;
public IFileDialogManager? FileDialogImpl => null;
private sealed class DummyCursor : ICursor
{

View File

@@ -662,10 +662,6 @@ namespace Robust.Client.Graphics.Clyde
{
var icons = _clyde.LoadWindowIcons().ToArray();
// Done if no icon (e.g., macOS)
if (icons.Length == 0)
return;
// Turn each image into a byte[] so we can actually pin their contents.
// Wish I knew a clean way to do this without allocations.
var images = icons

View File

@@ -1,4 +1,5 @@
using System;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -11,16 +12,31 @@ namespace Robust.Client.Graphics.Clyde;
internal partial class Clyde
{
private sealed partial class Sdl3WindowingImpl : IFileDialogManagerImplementation
private sealed partial class Sdl3WindowingImpl : IFileDialogManager
{
public async Task<string?> OpenFile(FileDialogFilters? filters)
public async Task<Stream?> OpenFile(FileDialogFilters? filters = null)
{
return await ShowFileDialogOfType(SDL.SDL_FILEDIALOG_OPENFILE, filters);
var fileName = await ShowFileDialogOfType(SDL.SDL_FILEDIALOG_OPENFILE, filters);
if (fileName == null)
return null;
return File.OpenRead(fileName);
}
public async Task<string?> SaveFile(FileDialogFilters? filters)
public async Task<(Stream fileStream, bool alreadyExisted)?> SaveFile(FileDialogFilters? filters = null, bool truncate = true)
{
return await ShowFileDialogOfType(SDL.SDL_FILEDIALOG_SAVEFILE, filters);
var fileName = await ShowFileDialogOfType(SDL.SDL_FILEDIALOG_SAVEFILE, filters);
if (fileName == null)
return null;
try
{
return (File.Open(fileName, truncate ? FileMode.Truncate : FileMode.Open), true);
}
catch (FileNotFoundException)
{
return (File.Open(fileName, FileMode.Create), false);
}
}
private unsafe Task<string?> ShowFileDialogOfType(int type, FileDialogFilters? filters)
@@ -58,8 +74,6 @@ internal partial class Clyde
NativeMemory.Free(filter.name);
NativeMemory.Free(filter.pattern);
}
NativeMemory.Free(filtersAlloc);
}
return task;

View File

@@ -71,6 +71,6 @@ namespace Robust.Client.Graphics
void RunOnWindowThread(Action action);
IFileDialogManagerImplementation? FileDialogImpl { get; }
IFileDialogManager? FileDialogImpl { get; }
}
}

View File

@@ -47,7 +47,7 @@ namespace Robust.Client.Physics
* This will draw above every body involved in a particular island solve.
*/
public readonly Queue<(TimeSpan Time, List<Entity<PhysicsComponent, TransformComponent>> Bodies)> IslandSolve = new();
public readonly Queue<(TimeSpan Time, List<PhysicsComponent> Bodies)> IslandSolve = new();
public const float SolveDuration = 0.1f;
public override void Initialize()

View File

@@ -67,7 +67,7 @@ namespace Robust.Client.Physics
// Add new joint (if possible).
// Need to wait for BOTH joint components to come in first before we can add it. Yay dependencies!
if (!HasComp<JointComponent>(other))
if (!EntityManager.HasComponent<JointComponent>(other))
continue;
// TODO: if (other entity is outside of PVS range) continue;

View File

@@ -90,10 +90,9 @@ public sealed partial class PhysicsSystem
// existing contacts for predicted entities before performing any actual prediction.
var contacts = new List<Contact>();
var maps = new HashSet<EntityUid>();
var enumerator = AllEntityQuery<PredictedPhysicsComponent, PhysicsComponent, TransformComponent>();
_broadphase.FindNewContacts();
while (enumerator.MoveNext(out _, out var physics, out var xform))
{
DebugTools.Assert(physics.Predict);
@@ -101,6 +100,10 @@ public sealed partial class PhysicsSystem
if (xform.MapUid is not { } map)
continue;
if (maps.Add(map) && PhysMapQuery.TryGetComponent(map, out var physMap) &&
MapQuery.TryGetComponent(map, out var mapComp))
_broadphase.FindNewContacts(physMap, mapComp.MapId);
contacts.AddRange(physics.Contacts);
}

View File

@@ -1,6 +1,5 @@
using System.Collections.Generic;
using JetBrains.Annotations;
using Robust.Shared.Collections;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Physics;
@@ -24,23 +23,21 @@ namespace Robust.Client.Physics
SimulateWorld(frameTime, _gameTiming.InPrediction);
}
protected override void Cleanup(float frameTime)
protected override void Cleanup(PhysicsMapComponent component, float frameTime)
{
var toRemove = new ValueList<Entity<PhysicsComponent, TransformComponent>>();
var toRemove = new List<Entity<PhysicsComponent>>();
// Because we're not predicting 99% of bodies its sleep timer never gets incremented so we'll just do it ourselves.
// (and serializing it over the network isn't necessary?)
// This is a client-only problem.
// Also need to suss out having the client build the island anyway and just... not solving it?
foreach (var ent in AwakeBodies)
foreach (var body in component.AwakeBodies)
{
var body = ent.Comp1;
if (!body.SleepingAllowed || body.LinearVelocity.Length() > LinearToleranceSqr / 2f || body.AngularVelocity * body.AngularVelocity > AngularToleranceSqr / 2f) continue;
body.SleepTime += frameTime;
if (body.SleepTime > TimeToSleep)
{
toRemove.Add(ent);
toRemove.Add(new Entity<PhysicsComponent>(body.Owner, body));
}
}
@@ -49,38 +46,37 @@ namespace Robust.Client.Physics
SetAwake(body, false);
}
base.Cleanup(frameTime);
base.Cleanup(component, frameTime);
}
protected override void UpdateLerpData(List<Entity<PhysicsComponent, TransformComponent>> bodies)
protected override void UpdateLerpData(PhysicsMapComponent component, List<PhysicsComponent> bodies, EntityQuery<TransformComponent> xformQuery)
{
foreach (var bodyEnt in bodies)
foreach (var body in bodies)
{
var body = bodyEnt.Comp1;
var xform = bodyEnt.Comp2;
if (body.BodyType == BodyType.Static ||
LerpData.TryGetValue(bodyEnt, out var lerpData) ||
lerpData == xform.ParentUid)
component.LerpData.TryGetValue(body.Owner, out var lerpData) ||
!xformQuery.TryGetComponent(body.Owner, out var xform) ||
lerpData.ParentUid == xform.ParentUid)
{
continue;
}
LerpData[bodyEnt.Owner] = xform.ParentUid;
component.LerpData[xform.Owner] = (xform.ParentUid, xform.LocalPosition, xform.LocalRotation);
}
}
/// <summary>
/// Flush all of our lerping data.
/// </summary>
protected override void FinalStep()
protected override void FinalStep(PhysicsMapComponent component)
{
base.FinalStep();
base.FinalStep(component);
var xformQuery = GetEntityQuery<TransformComponent>();
foreach (var (uid, parentUid) in LerpData)
foreach (var (uid, (parentUid, position, rotation)) in component.LerpData)
{
// Can't just re-use xform from before as movement events may cause event subs to fire.
if (!XformQuery.TryGetComponent(uid, out var xform) || !parentUid.IsValid())
if (!xformQuery.TryGetComponent(uid, out var xform) ||
!parentUid.IsValid())
{
continue;
}
@@ -89,7 +85,7 @@ namespace Robust.Client.Physics
_transform.SetLocalPositionRotation(uid, xform.LocalPosition, xform.LocalRotation, xform);
}
LerpData.Clear();
component.LerpData.Clear();
}
}
}

View File

@@ -22,7 +22,7 @@ public sealed class LoadPrototypeCommand : IConsoleCommand
var dialogManager = IoCManager.Resolve<IFileDialogManager>();
var loadManager = IoCManager.Resolve<IGamePrototypeLoadManager>();
var stream = await dialogManager.OpenFile(access: FileAccess.Read);
var stream = await dialogManager.OpenFile();
if (stream is null)
return;

View File

@@ -1,4 +1,3 @@
using System.IO;
using Robust.Client.UserInterface;
using Robust.Shared;
using Robust.Shared.Configuration;
@@ -37,7 +36,7 @@ public sealed class UploadFileCommand : IConsoleCommand
var path = new ResPath(args[0]).ToRelativePath();
var filters = new FileDialogFilters(new FileDialogFilters.Group(path.Extension));
await using var file = await _dialog.OpenFile(filters, FileAccess.Read);
await using var file = await _dialog.OpenFile(filters);
if (file == null)
{

View File

@@ -994,9 +994,6 @@ namespace Robust.Client.UserInterface
internal int DoFrameUpdateRecursive(FrameEventArgs args)
{
if (!Visible)
return 0;
var total = 1;
FrameUpdate(args);

View File

@@ -1,9 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Timing;
namespace Robust.Client.UserInterface.Controllers;
@@ -14,14 +11,11 @@ namespace Robust.Client.UserInterface.Controllers;
/// and <see cref="UISystemDependencyAttribute"/> to depend on <see cref="EntitySystem"/>s, which will be automatically
/// injected once they are created.
/// </summary>
public abstract partial class UIController : IPostInjectInit
public abstract partial class UIController
{
[Dependency] protected readonly IUserInterfaceManager UIManager = default!;
[Dependency] protected readonly IEntitySystemManager EntitySystemManager = default!;
[Dependency] protected readonly IEntityManager EntityManager = default!;
[Dependency] protected readonly ILogManager LogManager = default!;
public ISawmill Log { get; protected set; } = default!;
public virtual void Initialize()
{
@@ -30,39 +24,4 @@ public abstract partial class UIController : IPostInjectInit
public virtual void FrameUpdate(FrameEventArgs args)
{
}
protected virtual string SawmillName
{
get
{
var name = GetType().Name;
// Strip trailing "UIController"
if (name.EndsWith("UIController"))
name = name.Substring(0, name.Length - "UIController".Length);
// Convert CamelCase to snake_case
// Ignore if all uppercase, assume acronym (e.g. NPC or HTN)
if (name.All(char.IsUpper))
{
name = name.ToLower(CultureInfo.InvariantCulture);
}
else
{
name = string.Concat(name.Select(x => char.IsUpper(x) ? $"_{char.ToLower(x)}" : x.ToString()));
name = name.Trim('_');
}
return $"ui.{name}";
}
}
public void PostInject()
{
Log = LogManager.GetSawmill(SawmillName);
#if !DEBUG
Log.Level = LogLevel.Info;
#endif
}
}

View File

@@ -42,7 +42,6 @@ public sealed class ColorSelectorSliders : Control
break;
}
_currentType = value;
_typeSelector.Select(_types.IndexOf(value));
UpdateType();
Update();
}

View File

@@ -107,11 +107,6 @@ namespace Robust.Client.UserInterface.Controls
_scrollBar.Value = 0;
}
public FormattedMessage GetMessage(Index index)
{
return new FormattedMessage(_entries[index].Message);
}
public void RemoveEntry(Index index)
{
var entry = _entries[index];
@@ -143,31 +138,6 @@ namespace Robust.Client.UserInterface.Controls
_entries.Add(entry);
var font = _getFont();
AddNewItemHeight(font, entry);
_scrollBar.MaxValue = Math.Max(_scrollBar.Page, _totalContentHeight);
if (_isAtBottom && ScrollFollowing)
{
_scrollBar.MoveToEnd();
}
}
public void SetMessage(Index index, FormattedMessage message, Type[]? tagsAllowed = null, Color? defaultColor = null)
{
var oldEntry = _entries[index];
var font = _getFont();
_totalContentHeight -= oldEntry.Height + font.GetLineSeparation(UIScale);
_scrollBar.MaxValue = Math.Max(_scrollBar.Page, _totalContentHeight);
var entry = new RichTextEntry(message, this, _tagManager, tagsAllowed, defaultColor);
entry.Update(_tagManager, _getFont(), _getContentBox().Width, UIScale);
_entries[index] = entry;
AddNewItemHeight(font, in entry);
}
private void AddNewItemHeight(Font font, in RichTextEntry entry)
{
_totalContentHeight += entry.Height;
if (_firstLine)
{
@@ -177,6 +147,12 @@ namespace Robust.Client.UserInterface.Controls
{
_totalContentHeight += font.GetLineSeparation(UIScale);
}
_scrollBar.MaxValue = Math.Max(_scrollBar.Page, _totalContentHeight);
if (_isAtBottom && ScrollFollowing)
{
_scrollBar.MoveToEnd();
}
}
public void ScrollToBottom()

View File

@@ -30,12 +30,12 @@ namespace Robust.Client.UserInterface.Controls
get => _currentTab;
set
{
if (value < 0)
if (_currentTab < 0)
{
throw new ArgumentOutOfRangeException(nameof(value), value, "Current tab must be positive.");
}
if (value >= ChildCount)
if (_currentTab >= ChildCount)
{
throw new ArgumentOutOfRangeException(nameof(value), value,
"Current tab must less than the amount of tabs.");

View File

@@ -145,11 +145,6 @@ namespace Robust.Client.UserInterface.CustomControls
protected override void FrameUpdate(FrameEventArgs args)
{
// This is to avoid unnecessarily setting a position where our size isn't yet fully updated.
// This most commonly happens with saved window positions if your window position is <= 0.
if (!IsMeasureValid)
return;
var (spaceX, spaceY) = Parent!.Size;
var maxX = spaceX - ((AllowOffScreen & DirectionFlag.West) == 0 ? Size.X : WindowEdgeSeparation);

View File

@@ -8,19 +8,12 @@ namespace Robust.Client.UserInterface
/// </summary>
internal sealed class DummyFileDialogManager : IFileDialogManager
{
public Task<Stream?> OpenFile(
FileDialogFilters? filters = null,
FileAccess access = FileAccess.ReadWrite,
FileShare? share = null)
public Task<Stream?> OpenFile(FileDialogFilters? filters = null)
{
return Task.FromResult<Stream?>(null);
}
public Task<(Stream fileStream, bool alreadyExisted)?> SaveFile(
FileDialogFilters? filters = null,
bool truncate = true,
FileAccess access = FileAccess.ReadWrite,
FileShare share = FileShare.None)
public Task<(Stream fileStream, bool alreadyExisted)?> SaveFile(FileDialogFilters? filters = null, bool truncate = true)
{
return Task.FromResult<(Stream fileStream, bool alreadyExisted)?>(null);
}

View File

@@ -7,7 +7,9 @@ using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Robust.Client.Graphics;
using Robust.Shared;
using Robust.Shared.Console;
using Robust.Shared.Asynchronous;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Utility;
@@ -18,41 +20,28 @@ namespace Robust.Client.UserInterface
[SuppressMessage("ReSharper", "IdentifierTypo")]
[SuppressMessage("ReSharper", "CommentTypo")]
[SuppressMessage("ReSharper", "StringLiteralTypo")]
internal sealed class FileDialogManager : IFileDialogManager, IPostInjectInit
internal sealed class FileDialogManager : IFileDialogManager
{
// Uses nativefiledialog to open the file dialogs cross platform.
// On Linux, if the kdialog command is found, it will be used instead.
// TODO: Should we maybe try to avoid running kdialog if the DE isn't KDE?
[Dependency] private readonly IClydeInternal _clyde = default!;
[Dependency] private readonly ILogManager _log = default!;
private ISawmill _sawmill = default!;
private bool _kDialogAvailable;
private bool _checkedKDialogAvailable;
public async Task<Stream?> OpenFile(
FileDialogFilters? filters = null,
FileAccess access = FileAccess.ReadWrite,
FileShare? share = null)
public async Task<Stream?> OpenFile(FileDialogFilters? filters = null)
{
if ((access & FileAccess.ReadWrite) != access)
throw new ArgumentException("Invalid file access specified");
var realShare = share ?? (access == FileAccess.Read ? FileShare.Read : FileShare.None);
if ((realShare & (FileShare.ReadWrite | FileShare.Delete)) != realShare)
throw new ArgumentException("Invalid file share specified");
string? name;
if (_clyde.FileDialogImpl is { } clydeImpl)
name = await clydeImpl.OpenFile(filters);
else
name = await GetOpenFileName(filters);
return await clydeImpl.OpenFile(filters);
var name = await GetOpenFileName(filters);
if (name == null)
{
return null;
}
return File.Open(name, FileMode.Open, access, realShare);
return File.Open(name, FileMode.Open);
}
private async Task<string?> GetOpenFileName(FileDialogFilters? filters)
@@ -65,34 +54,24 @@ namespace Robust.Client.UserInterface
return await OpenFileNfd(filters);
}
public async Task<(Stream, bool)?> SaveFile(
FileDialogFilters? filters,
bool truncate = true,
FileAccess access = FileAccess.ReadWrite,
FileShare share = FileShare.None)
public async Task<(Stream, bool)?> SaveFile(FileDialogFilters? filters, bool truncate = true)
{
if ((access & FileAccess.ReadWrite) != access)
throw new ArgumentException("Invalid file access specified");
if ((share & (FileShare.ReadWrite | FileShare.Delete)) != share)
throw new ArgumentException("Invalid file share specified");
string? name;
if (_clyde.FileDialogImpl is { } clydeImpl)
name = await clydeImpl.SaveFile(filters);
else
name = await GetSaveFileName(filters);
return await clydeImpl.SaveFile(filters);
var name = await GetSaveFileName(filters);
if (name == null)
{
return null;
}
try
{
return (File.Open(name, truncate ? FileMode.Truncate : FileMode.Open, access, share), true);
return (File.Open(name, truncate ? FileMode.Truncate : FileMode.Open), true);
}
catch (FileNotFoundException)
{
return (File.Open(name, FileMode.Create, access, share), false);
return (File.Open(name, FileMode.Create), false);
}
}
@@ -270,7 +249,7 @@ namespace Robust.Client.UserInterface
if (_kDialogAvailable)
{
_sawmill.Debug("kdialog available.");
Logger.DebugS("filedialog", "kdialog available.");
}
}
catch
@@ -407,11 +386,6 @@ namespace Robust.Client.UserInterface
[DllImport("swnfd.dll")]
private static extern unsafe void sw_NFD_Free(void* ptr);
public void PostInject()
{
_sawmill = _log.GetSawmill("filedialog");
}
private enum sw_nfdresult
{
SW_NFD_ERROR,

View File

@@ -1,6 +1,5 @@
using System.IO;
using System.Threading.Tasks;
using Robust.Client.Graphics;
namespace Robust.Client.UserInterface
{
@@ -20,17 +19,7 @@ namespace Robust.Client.UserInterface
/// The file stream for the file the user opened.
/// <see langword="null" /> if the user cancelled the action.
/// </returns>
/// <param name="filters">Filters for file types that the user can select.</param>
/// <param name="access">What access is desired from the file operation.</param>
/// <param name="share">
/// What sharing mode is desired from the file operation.
/// If null is provided and <paramref name="access"/> is <see cref="FileAccess.Read"/>,
/// <see cref="FileShare.Read"/> is selected, otherwise <see cref="FileShare.None"/>.
/// </param>
Task<Stream?> OpenFile(
FileDialogFilters? filters = null,
FileAccess access = FileAccess.ReadWrite,
FileShare? share = null);
Task<Stream?> OpenFile(FileDialogFilters? filters = null);
/// <summary>
/// Open a file dialog used for saving a single file.
@@ -40,21 +29,6 @@ namespace Robust.Client.UserInterface
/// Null if the user cancelled the action.
/// </returns>
/// <param name="truncate">Should we truncate an existing file to 0-size then write or append.</param>
/// <param name="access">What access is desired from the file operation.</param>
/// <param name="share">Sharing mode for the opened file.</param>
Task<(Stream fileStream, bool alreadyExisted)?> SaveFile(
FileDialogFilters? filters = null,
bool truncate = true,
FileAccess access = FileAccess.ReadWrite,
FileShare share = FileShare.None);
}
/// <summary>
/// Internal implementation interface used to connect <see cref="FileDialogManager"/> and <see cref="IClydeInternal"/>.
/// </summary>
internal interface IFileDialogManagerImplementation
{
Task<string?> OpenFile(FileDialogFilters? filters);
Task<string?> SaveFile(FileDialogFilters? filters);
Task<(Stream fileStream, bool alreadyExisted)?> SaveFile(FileDialogFilters? filters = null, bool truncate = true);
}
}

View File

@@ -141,7 +141,7 @@ namespace Robust.Client.UserInterface
if (Controls == null || !Controls.TryGetValue(nodeIndex, out var control))
continue;
control.Measure(new Vector2(maxSizeX, Height));
control.Measure(new Vector2(Width, Height));
var desiredSize = control.DesiredPixelSize;
var controlMetrics = new CharMetrics(

View File

@@ -137,7 +137,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
if (TerminatingOrDeleted(coordinates.EntityId))
{
LogAudioPlaybackOnInvalidEntity(specifier, coordinates.EntityId);
Log.Error($"Tried to play coordinates audio on a terminating / deleted entity {ToPrettyString(coordinates.EntityId)}. Trace: {Environment.StackTrace}");
return null;
}
@@ -160,7 +160,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
if (TerminatingOrDeleted(coordinates.EntityId))
{
LogAudioPlaybackOnInvalidEntity(specifier, coordinates.EntityId);
Log.Error($"Tried to play coordinates audio on a terminating / deleted entity {ToPrettyString(coordinates.EntityId)}. Trace: {Environment.StackTrace}");
return null;
}
@@ -281,10 +281,4 @@ public sealed partial class AudioSystem : SharedAudioSystem
{
// TODO: Yeah remove this...
}
private void LogAudioPlaybackOnInvalidEntity(ResolvedSoundSpecifier? specifier, EntityUid entityId)
{
var soundInfo = specifier?.ToString() ?? "unknown sound";
Log.Error($"Tried to play coordinates audio on a terminating / deleted entity {ToPrettyString(entityId)}. Sound: {soundInfo}. Trace: {Environment.StackTrace}");
}
}

View File

@@ -297,7 +297,7 @@ namespace Robust.Server
: null;
// Set up the VFS
_resources.Initialize(dataDir);
_resources.Initialize(dataDir, hideUserDataDir: false);
var mountOptions = _commandLineArgs != null
? MountOptions.Merge(_commandLineArgs.MountOptions, Options.MountOptions) : Options.MountOptions;

View File

@@ -60,7 +60,7 @@ namespace Robust.Server.GameObjects
foreach (var uid in toDelete)
{
Del(uid);
EntityManager.DeleteEntity(uid);
}
}
}
@@ -75,7 +75,7 @@ namespace Robust.Server.GameObjects
if (!_deleteEmptyGrids || TerminatingOrDeleted(uid) || HasComp<MapComponent>(uid))
return;
Del(args.GridId);
EntityManager.DeleteEntity(args.GridId);
}
}
}

View File

@@ -20,7 +20,7 @@ public sealed class ViewSubscriberSystem : SharedViewSubscriberSystem
public override void AddViewSubscriber(EntityUid uid, ICommonSession session)
{
// If the entity doesn't have the component, it will be added.
var viewSubscriber = EnsureComp<Shared.GameObjects.ViewSubscriberComponent>(uid);
var viewSubscriber = EntityManager.EnsureComponent<Shared.GameObjects.ViewSubscriberComponent>(uid);
if (viewSubscriber.SubscribedSessions.Contains(session))
return; // Already subscribed, do nothing else.
@@ -36,7 +36,7 @@ public sealed class ViewSubscriberSystem : SharedViewSubscriberSystem
/// </summary>
public override void RemoveViewSubscriber(EntityUid uid, ICommonSession session)
{
if(!TryComp(uid, out Shared.GameObjects.ViewSubscriberComponent? viewSubscriber))
if(!EntityManager.TryGetComponent(uid, out Shared.GameObjects.ViewSubscriberComponent? viewSubscriber))
return; // Entity didn't have any subscriptions, do nothing.
if (!viewSubscriber.SubscribedSessions.Remove(session))

View File

@@ -132,7 +132,7 @@ internal sealed partial class PvsSystem
if (enumerateAll)
{
var query = AllEntityQuery<MetaDataComponent>();
var query = EntityManager.AllEntityQueryEnumerator<MetaDataComponent>();
while (query.MoveNext(out var uid, out var md))
{
DebugTools.Assert(md.EntityLifeStage >= EntityLifeStage.Initialized, $"Entity {ToPrettyString(uid)} has not been initialized");

View File

@@ -199,7 +199,7 @@ internal sealed partial class PvsSystem : EntitySystem
foreach (var uid in _toDelete)
{
QueueDel(uid);
EntityManager.QueueDeleteEntity(uid);
}
_toDelete.Clear();

View File

@@ -1,82 +0,0 @@
using System.Collections.Generic;
using System.Numerics;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Controllers;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Systems;
namespace Robust.Server.Physics.Commands;
public sealed class BoxStackCommand : IConsoleCommand
{
[Dependency] private readonly IEntityManager _entManager = default!;
public string Command => "boxstack";
public string Description => string.Empty;
public string Help => "boxstack [mapid] [columns] [rows]";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 3 ||
!int.TryParse(args[0], out var mapInt) ||
!int.TryParse(args[1], out var columns) ||
!int.TryParse(args[2], out var rows))
{
return;
}
var mapId = new MapId(mapInt);
var fixtureSystem = _entManager.System<FixtureSystem>();
var physSystem = _entManager.System<SharedPhysicsSystem>();
physSystem.SetGravity(new Vector2(0f, -9.8f));
var groundUid = _entManager.SpawnEntity(null, new MapCoordinates(0, 0, mapId));
var ground = _entManager.AddComponent<PhysicsComponent>(groundUid);
var groundManager = _entManager.EnsureComponent<FixturesComponent>(groundUid);
var horizontal = new EdgeShape(new Vector2(-40, 0), new Vector2(40, 0));
fixtureSystem.CreateFixture(groundUid, "fix1", new Fixture(horizontal, 1, 1, true), manager: groundManager, body: ground);
var vertical = new EdgeShape(new Vector2(10, 0), new Vector2(10, 10));
fixtureSystem.CreateFixture(groundUid, "fix2", new Fixture(vertical, 1, 1, true), manager: groundManager, body: ground);
physSystem.WakeBody(groundUid, manager: groundManager, body: ground);
var xs = new[]
{
0.0f, -10.0f, -5.0f, 5.0f, 10.0f
};
for (var j = 0; j < columns; j++)
{
for (var i = 0; i < rows; i++)
{
var x = 0.0f;
var boxUid = _entManager.SpawnEntity(null,
new MapCoordinates(new Vector2(xs[j] + x, 0.55f + 2.1f * i), mapId));
var box = _entManager.AddComponent<PhysicsComponent>(boxUid);
var manager = _entManager.EnsureComponent<FixturesComponent>(boxUid);
physSystem.SetBodyType(boxUid, BodyType.Dynamic, manager: manager, body: box);
var poly = new PolygonShape(0.001f);
poly.Set(new List<Vector2>()
{
new(0.5f, -0.5f),
new(0.5f, 0.5f),
new(-0.5f, 0.5f),
new(-0.5f, -0.5f),
});
fixtureSystem.CreateFixture(boxUid, "fix1", new Fixture(poly, 1, 1, true), manager: manager, body: box);
physSystem.WakeBody(boxUid, manager: manager, body: box);
}
}
}
}

View File

@@ -140,7 +140,7 @@ namespace Robust.Server.Placement
{
// Replace existing entities if relevant.
if (msg.Replacement && _prototype.Index<EntityPrototype>(entityTemplateName).Components.TryGetValue(
_factory.GetComponentName<PlacementReplacementComponent>(), out var compRegistry))
_factory.GetComponentName(typeof(PlacementReplacementComponent)), out var compRegistry))
{
var key = ((PlacementReplacementComponent)compRegistry.Component).Key;
var gridUid = _xformSystem.GetGrid(coordinates);

View File

@@ -10,7 +10,7 @@ namespace Robust.Server.Player
{
foreach (var uid in entities)
{
if (TryComp(uid, out ActorComponent? actor))
if (EntityManager.TryGetComponent(uid, out ActorComponent? actor))
filter.AddPlayer(actor.PlayerSession);
}

View File

@@ -1,4 +1,3 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
@@ -189,20 +188,13 @@ namespace Robust.Server.Scripting
}
// Compile ahead of time so that we can do syntax highlighting correctly for the echo.
await Task.Run(() =>
{
newScript.Compile();
newScript.Compile();
// Echo entered script.
var echoMessage = new FormattedMessage();
ScriptInstanceShared.AddWithSyntaxHighlighting(
newScript,
echoMessage,
code,
instance.HighlightWorkspace.Value);
// Echo entered script.
var echoMessage = new FormattedMessage();
ScriptInstanceShared.AddWithSyntaxHighlighting(newScript, echoMessage, code, instance.HighlightWorkspace);
replyMessage.Echo = echoMessage;
});
replyMessage.Echo = echoMessage;
var msg = new FormattedMessage();
@@ -340,7 +332,7 @@ namespace Robust.Server.Scripting
private sealed class ScriptInstance
{
public Lazy<Workspace> HighlightWorkspace { get; } = new(() => new AdhocWorkspace());
public Workspace HighlightWorkspace { get; } = new AdhocWorkspace();
public StringBuilder InputBuffer { get; } = new();
public FormattedMessage OutputBuffer { get; } = new();
public bool RunningScript { get; set; }
@@ -381,7 +373,7 @@ namespace Robust.Server.Scripting
script.Compile();
var syntax = new FormattedMessage();
ScriptInstanceShared.AddWithSyntaxHighlighting(script, syntax, code, _scriptInstance.HighlightWorkspace.Value);
ScriptInstanceShared.AddWithSyntaxHighlighting(script, syntax, code, _scriptInstance.HighlightWorkspace);
_scriptInstance.OutputBuffer.AddMessage(syntax);
}

View File

@@ -1,5 +1,4 @@
using System;
using System.Diagnostics.Contracts;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -102,7 +101,6 @@ namespace Robust.Shared.Maths
/// <summary>
/// Returns the smallest rectangle that contains both of the rectangles.
/// </summary>
[Pure]
public readonly Box2i Union(in Box2i other)
{
var botLeft = Vector2i.ComponentMin(BottomLeft, other.BottomLeft);
@@ -209,7 +207,6 @@ namespace Robust.Shared.Maths
/// <summary>
/// Multiplies each side of the box by the scalar.
/// </summary>
[Pure]
public Box2i Scale(int scalar)
{
return new Box2i(
@@ -218,21 +215,6 @@ namespace Robust.Shared.Maths
Right * scalar,
Top * scalar);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[Pure]
public bool Intersects(in Box2i other)
{
return other.Bottom <= this.Top && other.Top >= this.Bottom && other.Right >= this.Left &&
other.Left <= this.Right;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[Pure]
public readonly Box2i Enlarged(int size)
{
return new(Left - size, Bottom - size, Right + size, Top + size);
}
}
/// <summary>

View File

@@ -6,4 +6,3 @@
[assembly: InternalsVisibleTo("Robust.Client")]
[assembly: InternalsVisibleTo("Robust.UnitTesting")]
[assembly: InternalsVisibleTo("Content.Benchmarks")]

View File

@@ -13,8 +13,7 @@ namespace Robust.Shared.Audio;
/// <seealso cref="ResolvedPathSpecifier"/>
/// <seealso cref="ResolvedCollectionSpecifier"/>
[Serializable, NetSerializable]
public abstract partial class ResolvedSoundSpecifier
{
public abstract partial class ResolvedSoundSpecifier {
[Obsolete("String literals for sounds are deprecated, use a SoundSpecifier or ResolvedSoundSpecifier as appropriate instead")]
public static implicit operator ResolvedSoundSpecifier(string s) => new ResolvedPathSpecifier(s);
[Obsolete("String literals for sounds are deprecated, use a SoundSpecifier or ResolvedSoundSpecifier as appropriate instead")]
@@ -23,10 +22,8 @@ public abstract partial class ResolvedSoundSpecifier
/// <summary>
/// Returns whether <c>s</c> is null, or if it contains an empty path/collection ID.
/// </summary>
public static bool IsNullOrEmpty(ResolvedSoundSpecifier? s)
{
return s switch
{
public static bool IsNullOrEmpty(ResolvedSoundSpecifier? s) {
return s switch {
null => true,
ResolvedPathSpecifier path => path.Path.ToString() == "",
ResolvedCollectionSpecifier collection => string.IsNullOrEmpty(collection.Collection),
@@ -40,8 +37,7 @@ public abstract partial class ResolvedSoundSpecifier
/// </summary>
/// <seealso cref="ResolvedCollectionSpecifier"/>
[Serializable, NetSerializable]
public sealed partial class ResolvedPathSpecifier : ResolvedSoundSpecifier, IEquatable<ResolvedPathSpecifier>
{
public sealed partial class ResolvedPathSpecifier : ResolvedSoundSpecifier {
/// <summary>
/// The resource path of the sound.
/// </summary>
@@ -61,21 +57,6 @@ public sealed partial class ResolvedPathSpecifier : ResolvedSoundSpecifier, IEqu
public ResolvedPathSpecifier(string path) : this(new ResPath(path))
{
}
public bool Equals(ResolvedPathSpecifier? other)
{
return Path.Equals(other?.Path);
}
public override bool Equals(object? obj)
{
return Equals(obj as ResolvedPathSpecifier);
}
public override int GetHashCode()
{
return Path.GetHashCode();
}
}
/// <summary>
@@ -83,9 +64,7 @@ public sealed partial class ResolvedPathSpecifier : ResolvedSoundSpecifier, IEqu
/// </summary>
/// <seealso cref="ResolvedPathSpecifier"/>
[Serializable, NetSerializable]
public sealed partial class ResolvedCollectionSpecifier : ResolvedSoundSpecifier, IEquatable<ResolvedCollectionSpecifier>
{
public sealed partial class ResolvedCollectionSpecifier : ResolvedSoundSpecifier {
/// <summary>
/// The ID of the <see cref="SoundCollectionPrototype">sound collection</see> to look up.
/// </summary>
@@ -108,19 +87,4 @@ public sealed partial class ResolvedCollectionSpecifier : ResolvedSoundSpecifier
Collection = collection;
Index = index;
}
public bool Equals(ResolvedCollectionSpecifier? other)
{
return Collection.Equals(other?.Collection) && Index.Equals(other?.Index);
}
public override bool Equals(object? obj)
{
return Equals(obj as ResolvedCollectionSpecifier);
}
public override int GetHashCode()
{
return HashCode.Combine(Collection, Index);
}
}

View File

@@ -665,7 +665,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
/// <param name="coordinates">The coordinates at which to play the audio.</param>
public (EntityUid Entity, Components.AudioComponent Component)? PlayStatic(SoundSpecifier? sound, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null)
{
return sound == null ? null : PlayStatic(ResolveSound(sound), playerFilter, coordinates, recordReplay, audioParams ?? sound.Params);
return sound == null ? null : PlayStatic(ResolveSound(sound), playerFilter, coordinates, recordReplay, audioParams);
}
/// <summary>

View File

@@ -127,20 +127,19 @@ public sealed class TeleportToCommand : LocalizedEntityCommands
{
foreach (var victim in args)
{
if (!TryGetTransformFromUidOrUsername(victim, shell, out var uid, out var victimTransform))
if (victim == target)
continue;
if (uid == targetUid)
if (!TryGetTransformFromUidOrUsername(victim, shell, out var uid, out var victimTransform))
continue;
victims.Add((uid.Value, victimTransform));
}
}
var targetMapCoords = _transform.ToMapCoordinates(targetCoords);
foreach (var victim in victims)
{
_transform.SetMapCoordinates(victim.Entity, targetMapCoords);
_transform.SetCoordinates(victim.Entity, targetCoords);
_transform.AttachToGridOrMap(victim.Entity, victim.Transform);
}
}

View File

@@ -1,4 +1,3 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -156,7 +155,7 @@ public static class CompletionHelper
/// Returns a completion list for all prototype IDs of the given type.
/// </summary>
/// <remarks>
/// Don't use this for prototypes types that likely have a large number of entries, like <see cref="EntityPrototype"/>, use <see cref="PrototypeIdsLimited{T}"/> instead.
/// Don't use this for prototypes types that likely have a large number of entries, like <see cref="EntityPrototype"/>.
/// </remarks>
public static IEnumerable<CompletionOption> PrototypeIDs<T>(bool sorted = true, IPrototypeManager? proto = null)
where T: class, IPrototype
@@ -167,36 +166,6 @@ public static class CompletionHelper
return sorted ? protoOptions.OrderBy(o => o.Value) : protoOptions;
}
/// <summary>
/// Returns a completion list for all prototype IDs of the given type, limited to avoid performance problems.
/// </summary>
/// <remarks>
/// This is a limited alternative to <see cref="PrototypeIDs{T}"/>.
/// The limit is applied before sorting of results, so the unfiltered results are somewhat arbitrary.
/// </remarks>
/// <param name="currentArgument">The argument being currently typed for the completion.</param>
/// <param name="proto">The <see cref="IPrototypeManager"/>.</param>
/// <param name="sorted">Whether to sort the results or not.</param>
/// <param name="maxCount">The maximum amount of results to return at once.</param>
/// <typeparam name="T">The type of prototype to search through.</typeparam>
/// <returns></returns>
public static IEnumerable<CompletionOption> PrototypeIdsLimited<T>(
string currentArgument,
IPrototypeManager proto,
bool sorted = true,
int maxCount = 30) where T : class, IPrototype
{
var protoOptions = proto.EnumeratePrototypes<T>()
.Where(p => p.ID.StartsWith(currentArgument, StringComparison.OrdinalIgnoreCase))
.Take(maxCount)
.Select(p => new CompletionOption(p.ID));
if (sorted)
protoOptions = protoOptions.OrderBy(o => o.Value);
return protoOptions;
}
/// <summary>
/// Returns a list of connected session names.
/// </summary>

View File

@@ -279,6 +279,16 @@ namespace Robust.Shared.Containers
#region Container Helpers
[Obsolete("Use Entity<T> variant")]
public bool TryGetContainingContainer(
EntityUid uid,
[NotNullWhen(true)] out BaseContainer? container,
MetaDataComponent? meta = null,
TransformComponent? transform = null)
{
return TryGetContainingContainer((uid, transform, meta), out container);
}
public bool TryGetContainingContainer(
Entity<TransformComponent?, MetaDataComponent?> ent,
[NotNullWhen(true)] out BaseContainer? container)
@@ -391,6 +401,13 @@ namespace Robust.Shared.Containers
return TryFindComponentsOnEntityContainerOrParent(xform.ParentUid, entityQuery, foundComponents);
}
[Obsolete("Use Entity<T> variant")]
public bool IsInSameOrNoContainer(EntityUid user, EntityUid other)
{
return IsInSameOrNoContainer((user, null, null), (other, null, null));
}
/// <summary>
/// Returns true if the two entities are not contained, or are contained in the same container.
/// </summary>
@@ -411,6 +428,13 @@ namespace Robust.Shared.Containers
return userContainer == otherContainer;
}
[Obsolete("Use Entity<T> variant")]
public bool IsInSameOrParentContainer(EntityUid user, EntityUid other)
{
return IsInSameOrParentContainer((user, null), other);
}
/// <summary>
/// Returns true if the two entities are not contained, or are contained in the same container, or if one
/// entity contains the other (i.e., is the parent).
@@ -445,6 +469,21 @@ namespace Robust.Shared.Containers
return userContainer == otherContainer;
}
[Obsolete("Use Entity<T> variant")]
public bool IsInSameOrTransparentContainer(
EntityUid user,
EntityUid other,
BaseContainer? userContainer = null,
BaseContainer? otherContainer = null,
bool userSeeInsideSelf = false)
{
return IsInSameOrTransparentContainer((user, null),
other,
userContainer,
otherContainer,
userSeeInsideSelf);
}
/// <summary>
/// Check whether a given entity can see another entity despite whatever containers they may be in.
/// </summary>
@@ -567,7 +606,7 @@ namespace Robust.Shared.Containers
/// <param name="force">Whether to forcibly remove the entity from the container.</param>
/// <param name="wasInContainer">Whether the entity was actually inside a container or not.</param>
/// <returns>If the entity could be removed. Also returns false if it wasn't inside a container.</returns>
public bool TryRemoveFromContainer(Entity<TransformComponent?, MetaDataComponent?> entity, bool force, out bool wasInContainer)
public bool TryRemoveFromContainer(EntityUid entity, bool force, out bool wasInContainer)
{
DebugTools.Assert(Exists(entity));
@@ -592,7 +631,7 @@ namespace Robust.Shared.Containers
/// <param name="entity">Entity that might be inside a container.</param>
/// <param name="force">Whether to forcibly remove the entity from the container.</param>
/// <returns>If the entity could be removed. Also returns false if it wasn't inside a container.</returns>
public bool TryRemoveFromContainer(Entity<TransformComponent?, MetaDataComponent?> entity, bool force = false)
public bool TryRemoveFromContainer(EntityUid entity, bool force = false)
{
return TryRemoveFromContainer(entity, force, out _);
}
@@ -639,8 +678,9 @@ namespace Robust.Shared.Containers
{
// TODO make this check upwards for any container, and parent to that.
// Currently this just checks the direct parent, so entities will still teleport through containers.
if (!transform.Comp.ParentUid.IsValid()
|| !TryGetContainingContainer((transform.Comp.ParentUid, Transform(transform.Comp.ParentUid)), out var container)
|| !TryGetContainingContainer(transform.Comp.ParentUid, out var container)
|| !TryInsertIntoContainer(transform, container))
{
_transform.AttachToGridOrMap(transform, transform.Comp);
@@ -652,9 +692,8 @@ namespace Robust.Shared.Containers
if (Insert((transform.Owner, transform.Comp, null, null), container))
return true;
var ownerXform = Transform(container.Owner);
if (ownerXform.ParentUid.IsValid()
&& TryGetContainingContainer((container.Owner, ownerXform), out var newContainer))
if (Transform(container.Owner).ParentUid.IsValid()
&& TryGetContainingContainer(container.Owner, out var newContainer))
return TryInsertIntoContainer(transform, newContainer);
return false;

View File

@@ -88,6 +88,7 @@ namespace Robust.Shared.ContentPack
public string SystemAssemblyName = default!;
public HashSet<VerifierError> AllowedVerifierErrors = default!;
public List<string> WhitelistedNamespaces = default!;
public List<string> AllowedAssemblyPrefixes = default!;
public Dictionary<string, Dictionary<string, TypeConfig>> Types = default!;
}

View File

@@ -131,6 +131,16 @@ namespace Robust.Shared.ContentPack
return false;
}
#pragma warning disable RA0004
var loadedConfig = _config.Result;
#pragma warning restore RA0004
if (!loadedConfig.AllowedAssemblyPrefixes.Any(allowedNamePrefix => asmName.StartsWith(allowedNamePrefix)))
{
_sawmill.Error($"Assembly name '{asmName}' is not allowed for a content assembly");
return false;
}
if (VerifyIL)
{
if (!DoVerifyIL(asmName, resolver, peReader, reader))
@@ -179,10 +189,6 @@ namespace Robust.Shared.ContentPack
return true;
}
#pragma warning disable RA0004
var loadedConfig = _config.Result;
#pragma warning restore RA0004
var badRefs = new ConcurrentBag<EntityHandle>();
// We still do explicit type reference scanning, even though the actual whitelists work with raw members.

View File

@@ -60,9 +60,7 @@ namespace Robust.Shared.ContentPack
internal string GetPath(ResPath relPath)
{
return Path.GetFullPath(Path.Combine(_directory.FullName, relPath.ToRelativeSystemPath()))
// Sanitise platform-specific path and standardize it for engine use.
.Replace(Path.DirectorySeparatorChar, '/');
return PathHelpers.SafeGetResourcePath(_directory.FullName, relPath);
}
/// <inheritdoc />

View File

@@ -14,7 +14,11 @@ namespace Robust.Shared.ContentPack
/// The directory to use for user data.
/// If null, a virtual temporary file system is used instead.
/// </param>
void Initialize(string? userData);
/// <param name="hideUserDataDir">
/// If true, <see cref="IWritableDirProvider.RootDir"/> will be hidden on
/// <see cref="IResourceManager.UserData"/>.
/// </param>
void Initialize(string? userData, bool hideUserDataDir);
/// <summary>
/// Mounts a single stream as a content file. Useful for unit testing.

View File

@@ -13,7 +13,7 @@ namespace Robust.Shared.ContentPack
{
/// <summary>
/// The root path of this provider.
/// Can be null if it's a virtual provider.
/// Can be null if it's a virtual provider or the path is protected (e.g. on the client).
/// </summary>
string? RootDir { get; }

View File

@@ -93,19 +93,23 @@ namespace Robust.Shared.ContentPack
{
var sw = Stopwatch.StartNew();
Sawmill.Debug("LOADING modules");
var files = new Dictionary<string, (ResPath Path, string[] references)>();
var files = new Dictionary<string, (ResPath Path, MemoryStream data, string[] references)>();
// Find all modules we want to load.
foreach (var fullPath in paths)
{
using var asmFile = _res.ContentFileRead(fullPath);
var refData = GetAssemblyReferenceData(asmFile);
var ms = new MemoryStream();
asmFile.CopyTo(ms);
ms.Position = 0;
var refData = GetAssemblyReferenceData(ms);
if (refData == null)
continue;
var (asmRefs, asmName) = refData.Value;
if (!files.TryAdd(asmName, (fullPath, asmRefs)))
if (!files.TryAdd(asmName, (fullPath, ms, asmRefs)))
{
Sawmill.Error("Found multiple modules with the same assembly name " +
$"'{asmName}', A: {files[asmName].Path}, B: {fullPath}.");
@@ -122,10 +126,10 @@ namespace Robust.Shared.ContentPack
Parallel.ForEach(files, pair =>
{
var (name, (path, _)) = pair;
var (name, (_, data, _)) = pair;
using var stream = _res.ContentFileRead(path);
if (!typeChecker.CheckAssembly(stream, resolver))
data.Position = 0;
if (!typeChecker.CheckAssembly(data, resolver))
{
throw new TypeCheckFailedException($"Assembly {name} failed type checks.");
}
@@ -137,14 +141,15 @@ namespace Robust.Shared.ContentPack
var nodes = TopologicalSort.FromBeforeAfter(
files,
kv => kv.Key,
kv => kv.Value.Path,
kv => kv.Value,
_ => Array.Empty<string>(),
kv => kv.Value.references,
allowMissing: true); // missing refs would be non-content assemblies so allow that.
// Actually load them in the order they depend on each other.
foreach (var path in TopologicalSort.Sort(nodes))
foreach (var item in TopologicalSort.Sort(nodes))
{
var (path, memory, _) = item;
Sawmill.Debug($"Loading module: '{path}'");
try
{
@@ -156,9 +161,9 @@ namespace Robust.Shared.ContentPack
}
else
{
using var assemblyStream = _res.ContentFileRead(path);
memory.Position = 0;
using var symbolsStream = _res.ContentFileReadOrNull(path.WithExtension("pdb"));
LoadGameAssembly(assemblyStream, symbolsStream, skipVerify: true);
LoadGameAssembly(memory, symbolsStream, skipVerify: true);
}
}
catch (Exception e)
@@ -174,7 +179,7 @@ namespace Robust.Shared.ContentPack
private (string[] refs, string name)? GetAssemblyReferenceData(Stream stream)
{
using var reader = ModLoader.MakePEReader(stream);
using var reader = ModLoader.MakePEReader(stream, leaveOpen: true);
var metaReader = reader.GetMetadataReader();
var name = metaReader.GetString(metaReader.GetAssemblyDefinition().Name);

View File

@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using Robust.Shared.Utility;
namespace Robust.Shared.ContentPack
{
@@ -63,5 +64,27 @@ namespace Robust.Shared.ContentPack
!OperatingSystem.IsWindows()
&& !OperatingSystem.IsMacOS();
internal static string SafeGetResourcePath(string baseDir, ResPath path)
{
var relSysPath = path.ToRelativeSystemPath();
if (relSysPath.Contains("\\..") || relSysPath.Contains("/.."))
{
// Hard cap on any exploit smuggling a .. in there.
// Since that could allow leaving sandbox.
throw new InvalidOperationException($"This branch should never be reached. Path: {path}");
}
var retPath = Path.GetFullPath(Path.Join(baseDir, relSysPath));
// better safe than sorry check
if (!retPath.StartsWith(baseDir))
{
// Allow path to match if it's just missing the directory separator at the end.
if (retPath != baseDir.TrimEnd(Path.DirectorySeparatorChar))
throw new InvalidOperationException($"This branch should never be reached. Path: {path}");
}
return retPath;
}
}
}

View File

@@ -41,13 +41,13 @@ namespace Robust.Shared.ContentPack
public IWritableDirProvider UserData { get; private set; } = default!;
/// <inheritdoc />
public virtual void Initialize(string? userData)
public virtual void Initialize(string? userData, bool hideRootDir)
{
Sawmill = _logManager.GetSawmill("res");
if (userData != null)
{
UserData = new WritableDirProvider(Directory.CreateDirectory(userData));
UserData = new WritableDirProvider(Directory.CreateDirectory(userData), hideRootDir);
}
else
{
@@ -379,6 +379,10 @@ namespace Robust.Shared.ContentPack
{
var rootDir = loader.GetPath(new ResPath(@"/"));
// TODO: GET RID OF THIS.
// This code shouldn't be passing OS disk paths through ResPath.
rootDir = rootDir.Replace(Path.DirectorySeparatorChar, '/');
yield return new ResPath(rootDir);
}
}

View File

@@ -17,6 +17,10 @@ WhitelistedNamespaces:
- Content
- OpenDreamShared
AllowedAssemblyPrefixes:
- OpenDream
- Content
# The type whitelist does NOT care about which assembly types come from.
# This is because types switch assembly all the time.
# Just look up stuff like StreamReader on https://apisof.net.

View File

@@ -10,17 +10,22 @@ namespace Robust.Shared.ContentPack
/// <inheritdoc />
internal sealed class WritableDirProvider : IWritableDirProvider
{
/// <inheritdoc />
private readonly bool _hideRootDir;
public string RootDir { get; }
string? IWritableDirProvider.RootDir => _hideRootDir ? null : RootDir;
/// <summary>
/// Constructs an instance of <see cref="WritableDirProvider"/>.
/// </summary>
/// <param name="rootDir">Root file system directory to allow writing.</param>
public WritableDirProvider(DirectoryInfo rootDir)
/// <param name="hideRootDir">If true, <see cref="IWritableDirProvider.RootDir"/> is reported as null.</param>
public WritableDirProvider(DirectoryInfo rootDir, bool hideRootDir)
{
// FullName does not have a trailing separator, and we MUST have a separator.
RootDir = rootDir.FullName + Path.DirectorySeparatorChar.ToString();
_hideRootDir = hideRootDir;
}
#region File Access
@@ -119,7 +124,7 @@ namespace Robust.Shared.ContentPack
throw new FileNotFoundException();
var dirInfo = new DirectoryInfo(GetFullPath(path));
return new WritableDirProvider(dirInfo);
return new WritableDirProvider(dirInfo, _hideRootDir);
}
/// <inheritdoc />
@@ -180,20 +185,7 @@ namespace Robust.Shared.ContentPack
path = path.Clean();
return GetFullPath(RootDir, path);
}
private static string GetFullPath(string root, ResPath path)
{
var relPath = path.ToRelativeSystemPath();
if (relPath.Contains("\\..") || relPath.Contains("/.."))
{
// Hard cap on any exploit smuggling a .. in there.
// Since that could allow leaving sandbox.
throw new InvalidOperationException($"This branch should never be reached. Path: {path}");
}
return Path.GetFullPath(Path.Combine(root, relPath));
return PathHelpers.SafeGetResourcePath(RootDir, path);
}
}
}

View File

@@ -547,20 +547,19 @@ public sealed class EntityDeserializer :
_stopwatch.Restart();
foreach (var (entity, data) in Entities)
{
#if EXCEPTION_TOLERANCE
try
{
#endif
CurrentReadingEntity = data;
LoadEntity(entity, _metaQuery.Comp(entity), data.Components, data.MissingComponents);
#if EXCEPTION_TOLERANCE
}
catch (Exception e)
{
#if !EXCEPTION_TOLERANCE
throw;
#endif
ToDelete.Add(entity);
_log.Error($"Encountered error while loading entity. Yaml uid: {data.YamlId}. Loaded loaded entity: {EntMan.ToPrettyString(entity)}. Error:\n{e}.");
}
#endif
}
CurrentReadingEntity = null;

View File

@@ -162,8 +162,8 @@ public sealed class EntitySerializer : ISerializationContext,
_log = _logMan.GetSawmill("entity_serializer");
SerializerProvider.RegisterSerializer(this);
_metaName = _factory.GetComponentName<MetaDataComponent>();
_xformName = _factory.GetComponentName<TransformComponent>();
_metaName = _factory.GetComponentName(typeof(MetaDataComponent));
_xformName = _factory.GetComponentName(typeof(TransformComponent));
_emptyMetaNode = _serialization.WriteValueAs<MappingDataNode>(typeof(MetaDataComponent), new MetaDataComponent(), alwaysWrite: true, context: this);
CurrentComponent = _xformName;

View File

@@ -41,31 +41,6 @@ public sealed partial class MapLoaderSystem
return true;
}
/// <summary>
/// Tries to load entities from a YAML file, taking in a raw byte stream.
/// </summary>
/// <param name="file">The file contents to load from.</param>
/// <param name="fileName">
/// The name of the file being loaded. This is used purely for logging/informational purposes.
/// </param>
/// <param name="result">The result of the load operation.</param>
/// <param name="options">Options for the load operation.</param>
/// <returns>True if the load succeeded, false otherwise.</returns>
/// <seealso cref="M:Robust.Shared.EntitySerialization.Systems.MapLoaderSystem.TryLoadGeneric(Robust.Shared.Utility.ResPath,Robust.Shared.EntitySerialization.LoadResult@,System.Nullable{Robust.Shared.EntitySerialization.MapLoadOptions})"/>
public bool TryLoadGeneric(
Stream file,
string fileName,
[NotNullWhen(true)] out LoadResult? result,
MapLoadOptions? options = null)
{
result = null;
if (!TryReadFile(new StreamReader(file), out var data))
return false;
return TryLoadGeneric(data, fileName, out result, options);
}
/// <summary>
/// Tries to load entities from a yaml file. Whenever possible, you should try to use <see cref="TryLoadMap"/>,
/// <see cref="TryLoadGrid"/>, or <see cref="TryLoadEntity"/> instead.
@@ -80,17 +55,6 @@ public sealed partial class MapLoaderSystem
if (!TryReadFile(file, out var data))
return false;
return TryLoadGeneric(data, file.ToString(), out result, options);
}
private bool TryLoadGeneric(
MappingDataNode data,
string fileName,
[NotNullWhen(true)] out LoadResult? result,
MapLoadOptions? options = null)
{
result = null;
_stopwatch.Restart();
var ev = new BeforeEntityReadEvent();
RaiseLocalEvent(ev);
@@ -121,7 +85,7 @@ public sealed partial class MapLoaderSystem
if (!deserializer.TryProcessData())
{
Log.Debug($"Failed to process entity data in {fileName}");
Log.Debug($"Failed to process entity data in {file}");
return false;
}
@@ -131,7 +95,7 @@ public sealed partial class MapLoaderSystem
}
catch (Exception e)
{
Log.Error($"Caught exception while creating entities for map {fileName}: {e}");
Log.Error($"Caught exception while creating entities for map {file}: {e}");
Delete(deserializer.Result);
throw;
}
@@ -139,7 +103,7 @@ public sealed partial class MapLoaderSystem
if (opts.ExpectedCategory is { } exp && exp != deserializer.Result.Category)
{
// Did someone try to load a map file as a grid or vice versa?
Log.Error($"Map {fileName} does not contain the expected data. Expected {exp} but got {deserializer.Result.Category}");
Log.Error($"Map {file} does not contain the expected data. Expected {exp} but got {deserializer.Result.Category}");
Delete(deserializer.Result);
return false;
}

View File

@@ -64,13 +64,6 @@ public sealed partial class MapLoaderSystem : EntitySystem
return false;
Log.Info($"Loading file: {resPath}");
return TryReadFile(reader, out data);
}
private bool TryReadFile(TextReader reader, [NotNullWhen(true)] out MappingDataNode? data)
{
data = null;
_stopwatch.Restart();
using var textReader = reader;

View File

@@ -633,7 +633,7 @@ namespace Robust.Shared.GameObjects
/// An invalid entity UID indicates that this entity has intentionally been removed from broadphases and should
/// not automatically be re-added by movement events.
/// </remarks>
internal record struct BroadphaseData(EntityUid Uid, bool CanCollide, bool Static)
internal record struct BroadphaseData(EntityUid Uid, EntityUid PhysicsMap, bool CanCollide, bool Static)
{
public bool IsValid() => Uid.IsValid();
public bool Valid => IsValid();

View File

@@ -4,7 +4,6 @@ using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using Robust.Shared.Collections;
using Robust.Shared.Containers;
using Robust.Shared.Maths;
using Robust.Shared.Serialization;
@@ -85,17 +84,12 @@ public partial class EntityManager
return ents;
}
public EntityUid[] SpawnEntitiesAttachedTo(EntityCoordinates coordinates, IEnumerable<EntProtoId> protoNames)
public void SpawnEntitiesAttachedTo(EntityCoordinates coordinates, IEnumerable<EntProtoId> protoNames)
{
var ents = new ValueList<EntityUid>();
foreach (var protoName in protoNames)
{
var uid = SpawnAttachedTo(protoName, coordinates);
ents.Add(uid);
SpawnAttachedTo(protoName, coordinates);
}
return ents.ToArray();
}
public virtual EntityUid SpawnAttachedTo(string? protoName, EntityCoordinates coordinates, ComponentRegistry? overrides = null, Angle rotation = default)

View File

@@ -161,7 +161,7 @@ namespace Robust.Shared.GameObjects
/// <summary>
/// Returns true if the entity's data (apart from transform) is default.
/// </summary>
public bool IsDefault(EntityUid uid, ICollection<string>? ignoredComps = null)
public bool IsDefault(EntityUid uid)
{
if (!MetaQuery.TryGetComponent(uid, out var metadata) || metadata.EntityPrototype == null)
return false;
@@ -195,9 +195,6 @@ namespace Robust.Shared.GameObjects
var compName = _componentFactory.GetComponentName(compType);
if (ignoredComps?.Contains(compName) == true)
continue;
// If the component isn't on the prototype then it's custom.
if (!protoData.TryGetValue(compName, out var protoMapping))
return false;
@@ -501,7 +498,7 @@ namespace Robust.Shared.GameObjects
if (Deleted(uid.Value))
return false;
if (QueuedDeletionsSet.Contains(uid.Value))
if (!QueuedDeletionsSet.Add(uid.Value))
return false;
QueueDeleteEntity(uid);

View File

@@ -665,7 +665,7 @@ public partial class EntitySystem
/// <inheritdoc cref="IEntityManager.AddComponent&lt;T&gt;(EntityUid)"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected T AddComp<T>(EntityUid uid) where T : IComponent, new()
protected T AddComp<T>(EntityUid uid) where T : Component, new()
{
return EntityManager.AddComponent<T>(uid);
}

View File

@@ -1,6 +1,5 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.Collections;
using Robust.Shared.Containers;
using Robust.Shared.Map;
using Robust.Shared.Maths;
@@ -23,7 +22,7 @@ public partial interface IEntityManager
EntityUid[] SpawnEntities(MapCoordinates coordinates, params string?[] protoNames);
EntityUid[] SpawnEntities(MapCoordinates coordinates, string? prototype, int count);
EntityUid[] SpawnEntities(MapCoordinates coordinates, List<string?> protoNames);
EntityUid[] SpawnEntitiesAttachedTo(EntityCoordinates coordinates, IEnumerable<EntProtoId> protoNames);
void SpawnEntitiesAttachedTo(EntityCoordinates coordinates, IEnumerable<EntProtoId> protoNames);
EntityUid[] SpawnEntitiesAttachedTo(EntityCoordinates coordinates, params EntProtoId[] protoNames);
EntityUid[] SpawnEntitiesAttachedTo(EntityCoordinates coordinates, List<string?> protoNames);
EntityUid[] SpawnEntitiesAttachedTo(EntityCoordinates coordinates, params string?[] protoNames);

View File

@@ -18,7 +18,7 @@ namespace Robust.Shared.GameObjects
private void OnStartup(EntityUid uid, CollideOnAnchorComponent component, ComponentStartup args)
{
if (!TryComp(uid, out TransformComponent? transformComponent)) return;
if (!EntityManager.TryGetComponent(uid, out TransformComponent? transformComponent)) return;
SetCollide(uid, component, transformComponent.Anchored);
}
@@ -31,7 +31,7 @@ namespace Robust.Shared.GameObjects
private void SetCollide(EntityUid uid, CollideOnAnchorComponent component, bool anchored)
{
if (!TryComp(uid, out PhysicsComponent? body)) return;
if (!EntityManager.TryGetComponent(uid, out PhysicsComponent? body)) return;
var enabled = component.Enable;

View File

@@ -89,6 +89,7 @@ public sealed partial class EntityLookupSystem : EntitySystem
private EntityQuery<MapGridComponent> _gridQuery;
private EntityQuery<MetaDataComponent> _metaQuery;
private EntityQuery<PhysicsComponent> _physicsQuery;
private EntityQuery<PhysicsMapComponent> _physMapQuery;
private EntityQuery<TransformComponent> _xformQuery;
/// <summary>
@@ -118,6 +119,7 @@ public sealed partial class EntityLookupSystem : EntitySystem
_gridQuery = GetEntityQuery<MapGridComponent>();
_metaQuery = GetEntityQuery<MetaDataComponent>();
_physicsQuery = GetEntityQuery<PhysicsComponent>();
_physMapQuery = GetEntityQuery<PhysicsMapComponent>();
_xformQuery = GetEntityQuery<TransformComponent>();
SubscribeLocalEvent<BroadphaseComponent, EntityTerminatingEvent>(OnBroadphaseTerminating);
@@ -151,12 +153,15 @@ public sealed partial class EntityLookupSystem : EntitySystem
private void OnBroadphaseTerminating(EntityUid uid, BroadphaseComponent component, ref EntityTerminatingEvent args)
{
var xform = _xformQuery.GetComponent(uid);
RemoveChildrenFromTerminatingBroadphase(xform, component);
var map = xform.MapUid;
_physMapQuery.TryGetComponent(map, out var physMap);
RemoveChildrenFromTerminatingBroadphase(xform, component, physMap);
RemComp(uid, component);
}
private void RemoveChildrenFromTerminatingBroadphase(TransformComponent xform,
BroadphaseComponent component)
BroadphaseComponent component,
PhysicsMapComponent? map)
{
foreach (var child in xform._children)
{
@@ -174,15 +179,19 @@ public sealed partial class EntityLookupSystem : EntitySystem
if (childXform.Broadphase.Value.CanCollide && _fixturesQuery.TryGetComponent(child, out var fixtures))
{
if (map == null)
_physMapQuery.TryGetComponent(childXform.Broadphase.Value.PhysicsMap, out map);
DebugTools.Assert(map == null || childXform.Broadphase.Value.PhysicsMap == map.Owner);
var tree = childXform.Broadphase.Value.Static ? component.StaticTree : component.DynamicTree;
foreach (var fixture in fixtures.Fixtures.Values)
{
DestroyProxies(fixture, tree);
DestroyProxies(fixture, tree, map);
}
}
childXform.Broadphase = null;
RemoveChildrenFromTerminatingBroadphase(childXform, component);
RemoveChildrenFromTerminatingBroadphase(childXform, component, map);
}
}
@@ -218,18 +227,26 @@ public sealed partial class EntityLookupSystem : EntitySystem
if (xform.MapUid == null)
return;
if (!_physMapQuery.TryGetComponent(xform.MapUid, out var physMap))
{
throw new InvalidOperationException(
$"Broadphase's map is missing a physics map comp. Broadphase: {ToPrettyString(broadphase.Owner)}");
}
var ent = new Entity<TransformComponent, BroadphaseComponent>(broadphase, xform, broadphase);
var map = new Entity<PhysicsMapComponent>(xform.MapUid.Value, physMap);
var enumerator = xform.ChildEnumerator;
while (enumerator.MoveNext(out var child))
{
if (!_broadQuery.HasComp(child))
InitializeChild(child, ent);
InitializeChild(child, ent, map);
}
}
private void InitializeChild(
EntityUid child,
Entity<TransformComponent, BroadphaseComponent> broadphase)
Entity<TransformComponent, BroadphaseComponent> broadphase,
Entity<PhysicsMapComponent> map)
{
if (LifeStage(child) <= EntityLifeStage.PreInit)
return;
@@ -241,6 +258,7 @@ public sealed partial class EntityLookupSystem : EntitySystem
if (!xform.Broadphase.Value.IsValid())
return; // Entity is intentionally not on a broadphase (deferred updating?).
_physMapQuery.TryGetComponent(xform.Broadphase.Value.PhysicsMap, out var oldPhysMap);
if (!_broadQuery.TryGetComponent(xform.Broadphase.Value.Uid, out var oldBroadphase))
{
DebugTools.Assert("Encountered deleted broadphase.");
@@ -257,15 +275,16 @@ public sealed partial class EntityLookupSystem : EntitySystem
}
else if (oldBroadphase != broadphase.Comp2)
{
RemoveFromEntityTree(xform.Broadphase.Value.Uid, oldBroadphase,child, xform);
RemoveFromEntityTree(xform.Broadphase.Value.Uid, oldBroadphase, ref oldPhysMap, child, xform);
}
}
DebugTools.Assert(xform.Broadphase is not {} x || x.Uid == broadphase.Owner && !x.CanCollide);
DebugTools.Assert(xform.Broadphase is not {} x || x.Uid == broadphase.Owner && (!x.CanCollide || x.PhysicsMap == map.Owner));
AddOrUpdateEntityTree(
broadphase.Owner,
broadphase.Comp2,
broadphase.Comp1,
map.Comp,
child,
xform);
}
@@ -296,6 +315,9 @@ public sealed partial class EntityLookupSystem : EntitySystem
if (!TryGetCurrentBroadphase(xform, out var broadphase))
return;
if (!_physMapQuery.TryGetComponent(xform.MapUid, out var physMap))
throw new InvalidOperationException();
var (worldPos, worldRot) = _transform.GetWorldPositionRotation(xform);
var mapTransform = new Transform(worldPos, worldRot);
@@ -304,10 +326,10 @@ public sealed partial class EntityLookupSystem : EntitySystem
var tree = body.BodyType == BodyType.Static ? broadphase.StaticTree : broadphase.DynamicTree;
DebugTools.Assert(fixture.ProxyCount == 0);
AddOrMoveProxies((uid, body, xform), fixtureId, fixture, tree, broadphaseTransform);
AddOrMoveProxies(uid, fixtureId, fixture, body, tree, broadphaseTransform, mapTransform, physMap.MoveBuffer);
}
internal void DestroyProxies(EntityUid uid, string fixtureId, Fixture fixture, TransformComponent xform, BroadphaseComponent broadphase)
internal void DestroyProxies(EntityUid uid, string fixtureId, Fixture fixture, TransformComponent xform, BroadphaseComponent broadphase, PhysicsMapComponent? physicsMap)
{
DebugTools.AssertNotNull(xform.Broadphase);
DebugTools.Assert(xform.Broadphase!.Value.Uid == broadphase.Owner);
@@ -322,7 +344,7 @@ public sealed partial class EntityLookupSystem : EntitySystem
}
var tree = xform.Broadphase.Value.Static ? broadphase.StaticTree : broadphase.DynamicTree;
DestroyProxies(fixture, tree);
DestroyProxies(fixture, tree, physicsMap);
}
#endregion
@@ -373,7 +395,8 @@ public sealed partial class EntityLookupSystem : EntitySystem
var fixtures = Comp<FixturesComponent>(uid);
if (old.CanCollide)
{
RemoveBroadTree(broadphase, fixtures, old.Static);
_physMapQuery.TryGetComponent(old.PhysicsMap, out var physicsMap);
RemoveBroadTree(broadphase, fixtures, old.Static, physicsMap);
}
else
(old.Static ? broadphase.StaticSundriesTree : broadphase.SundriesTree).Remove(uid);
@@ -385,23 +408,23 @@ public sealed partial class EntityLookupSystem : EntitySystem
AddOrUpdateSundriesTree(old.Uid, broadphase, uid, xform, body.BodyType == BodyType.Static);
}
private void RemoveBroadTree(BroadphaseComponent lookup, FixturesComponent manager, bool staticBody)
private void RemoveBroadTree(BroadphaseComponent lookup, FixturesComponent manager, bool staticBody, PhysicsMapComponent? map)
{
var tree = staticBody ? lookup.StaticTree : lookup.DynamicTree;
foreach (var fixture in manager.Fixtures.Values)
{
DestroyProxies(fixture, tree);
DestroyProxies(fixture, tree, map);
}
}
internal void DestroyProxies(Fixture fixture, IBroadPhase tree)
internal void DestroyProxies(Fixture fixture, IBroadPhase tree, PhysicsMapComponent? map)
{
var buffer = _physics.MoveBuffer;
var buffer = map?.MoveBuffer;
for (var i = 0; i < fixture.ProxyCount; i++)
{
var proxy = fixture.Proxies[i];
tree.RemoveProxy(proxy.ProxyId);
buffer.Remove(proxy);
buffer?.Remove(proxy);
}
fixture.ProxyCount = 0;
@@ -415,7 +438,10 @@ public sealed partial class EntityLookupSystem : EntitySystem
if (broadphaseXform.MapID == MapId.Nullspace)
return;
AddOrUpdatePhysicsTree(uid, broadUid, broadphase, broadphaseXform, xform, body, fixtures);
if (!_physMapQuery.TryGetComponent(broadphaseXform.MapUid, out var physMap))
throw new InvalidOperationException($"Physics Broadphase is missing physics map. {ToPrettyString(broadUid)}");
AddOrUpdatePhysicsTree(uid, broadUid, broadphase, broadphaseXform, physMap, xform, body, fixtures);
}
private void AddOrUpdatePhysicsTree(
@@ -423,15 +449,16 @@ public sealed partial class EntityLookupSystem : EntitySystem
EntityUid broadUid,
BroadphaseComponent broadphase,
TransformComponent broadphaseXform,
PhysicsMapComponent physicsMap,
TransformComponent xform,
PhysicsComponent body,
FixturesComponent manager)
{
DebugTools.Assert(!_container.IsEntityOrParentInContainer(body.Owner, null, xform));
DebugTools.Assert(xform.Broadphase == null || xform.Broadphase == new BroadphaseData(broadphase.Owner, body.CanCollide, body.BodyType == BodyType.Static));
DebugTools.Assert(xform.Broadphase == null || xform.Broadphase == new BroadphaseData(broadphase.Owner, physicsMap.Owner, body.CanCollide, body.BodyType == BodyType.Static));
DebugTools.Assert(broadphase.Owner == broadUid);
xform.Broadphase ??= new(broadUid, body.CanCollide, body.BodyType == BodyType.Static);
xform.Broadphase ??= new(broadUid, physicsMap.Owner, body.CanCollide, body.BodyType == BodyType.Static);
var tree = body.BodyType == BodyType.Static ? broadphase.StaticTree : broadphase.DynamicTree;
// TOOD optimize this. This function iterates UP through parents, while we are currently iterating down.
@@ -443,19 +470,20 @@ public sealed partial class EntityLookupSystem : EntitySystem
foreach (var (id, fixture) in manager.Fixtures)
{
AddOrMoveProxies((uid, body, xform), id, fixture, tree, broadphaseTransform);
AddOrMoveProxies(uid, id, fixture, body, tree, broadphaseTransform, mapTransform, physicsMap.MoveBuffer);
}
}
private void AddOrMoveProxies(
Entity<PhysicsComponent, TransformComponent> ent,
EntityUid uid,
string fixtureId,
Fixture fixture,
PhysicsComponent body,
IBroadPhase tree,
Transform broadphaseTransform)
Transform broadphaseTransform,
Transform mapTransform,
Dictionary<FixtureProxy, Box2> moveBuffer)
{
var moveBuffer = _physics.MoveBuffer;
// Moving
if (fixture.ProxyCount > 0)
{
@@ -465,7 +493,7 @@ public sealed partial class EntityLookupSystem : EntitySystem
var proxy = fixture.Proxies[i];
tree.MoveProxy(proxy.ProxyId, bounds);
proxy.AABB = bounds;
moveBuffer.Add(proxy);
moveBuffer[proxy] = fixture.Shape.ComputeAABB(mapTransform, i);
}
return;
@@ -477,11 +505,11 @@ public sealed partial class EntityLookupSystem : EntitySystem
for (var i = 0; i < count; i++)
{
var bounds = fixture.Shape.ComputeAABB(broadphaseTransform, i);
var proxy = new FixtureProxy(ent.Owner, ent.Comp1, ent.Comp2, bounds, fixtureId, fixture, i);
var proxy = new FixtureProxy(uid, body, bounds, fixtureId, fixture, i);
proxy.ProxyId = tree.AddProxy(ref proxy);
proxy.AABB = bounds;
proxies[i] = proxy;
moveBuffer.Add(proxy);
moveBuffer[proxy] = fixture.Shape.ComputeAABB(mapTransform, i);
}
fixture.Proxies = proxies;
@@ -491,8 +519,8 @@ public sealed partial class EntityLookupSystem : EntitySystem
private void AddOrUpdateSundriesTree(EntityUid broadUid, BroadphaseComponent broadphase, EntityUid uid, TransformComponent xform, bool staticBody, Box2? aabb = null)
{
DebugTools.Assert(!_container.IsEntityOrParentInContainer(uid));
DebugTools.Assert(xform.Broadphase == null || xform.Broadphase == new BroadphaseData(broadUid, false, staticBody));
xform.Broadphase ??= new(broadUid, false, staticBody);
DebugTools.Assert(xform.Broadphase == null || xform.Broadphase == new BroadphaseData(broadUid, default, false, staticBody));
xform.Broadphase ??= new(broadUid, default, false, staticBody);
(staticBody ? broadphase.StaticSundriesTree : broadphase.SundriesTree).AddOrUpdate(uid, aabb);
}
@@ -510,12 +538,8 @@ public sealed partial class EntityLookupSystem : EntitySystem
{
if (args.Component.GridUid == args.Sender)
{
// If grid changes map MoveBuffer will have incorrect worldpositions for all children.
if (args.ParentChanged)
{
if (args.ParentChanged) // grid changed maps, need to update children and clear the move buffer.
OnGridChangedMap(args);
}
return;
}
DebugTools.Assert(!_gridQuery.HasComp(args.Sender));
@@ -539,21 +563,84 @@ public sealed partial class EntityLookupSystem : EntitySystem
return;
// We need to recursively update the cached data and remove children from the move buffer
DebugTools.Assert(HasComp<MapGridComponent>(args.Sender));
DebugTools.Assert(!newMap.IsValid() || HasComp<MapComponent>(newMap));
DebugTools.Assert(!oldMap.IsValid() || HasComp<MapComponent>(oldMap));
DebugTools.Assert(_gridQuery.HasComp(args.Sender));
DebugTools.Assert(!newMap.IsValid() || _mapQuery.HasComp(newMap));
DebugTools.Assert(!oldMap.IsValid() || _mapQuery.HasComp(oldMap));
var oldBuffer = _physMapQuery.CompOrNull(oldMap)?.MoveBuffer;
var newBuffer = _physMapQuery.CompOrNull(newMap)?.MoveBuffer;
foreach (var child in args.Component._children)
{
RecursiveOnGridChangedMap(child, oldMap, newMap, oldBuffer, newBuffer);
}
}
private void RecursiveOnGridChangedMap(
EntityUid uid,
EntityUid oldMap,
EntityUid newMap,
Dictionary<FixtureProxy, Box2>? oldBuffer,
Dictionary<FixtureProxy, Box2>? newBuffer)
{
if (!_xformQuery.TryGetComponent(uid, out var xform))
return;
foreach (var child in xform._children)
{
RecursiveOnGridChangedMap(child, oldMap, newMap, oldBuffer, newBuffer);
}
if (xform.Broadphase == null || !xform.Broadphase.Value.CanCollide)
return;
DebugTools.Assert(_netMan.IsClient || !xform.Broadphase.Value.PhysicsMap.IsValid() || xform.Broadphase.Value.PhysicsMap == oldMap);
xform.Broadphase = xform.Broadphase.Value with { PhysicsMap = newMap };
if (!_fixturesQuery.TryGetComponent(uid, out var fixtures))
return;
if (oldBuffer != null)
{
foreach (var fix in fixtures.Fixtures.Values)
foreach (var prox in fix.Proxies)
{
oldBuffer.Remove(prox);
}
}
if (newBuffer == null)
return;
// TODO PERFORMANCE
// track world position while recursively iterating down through children.
var (worldPos, worldRot) = _transform.GetWorldPositionRotation(xform);
var mapTransform = new Transform(worldPos, worldRot);
foreach (var fixture in fixtures.Fixtures.Values)
{
for (var i = 0; i < fixture.ProxyCount; i++)
{
var proxy = fixture.Proxies[i];
newBuffer[proxy] = fixture.Shape.ComputeAABB(mapTransform, i);
}
}
}
private void UpdateParent(EntityUid uid, TransformComponent xform)
{
BroadphaseComponent? oldBroadphase = null;
PhysicsMapComponent? oldPhysMap = null;
if (xform.Broadphase != null)
{
if (!xform.Broadphase.Value.IsValid())
return; // Entity is intentionally not on a broadphase (deferred updating?).
_physMapQuery.TryGetComponent(xform.Broadphase.Value.PhysicsMap, out oldPhysMap);
if (!_broadQuery.TryGetComponent(xform.Broadphase.Value.Uid, out oldBroadphase))
{
DebugTools.Assert("Encountered deleted broadphase.");
// broadphase was probably deleted.
@@ -574,18 +661,24 @@ public sealed partial class EntityLookupSystem : EntitySystem
if (oldBroadphase != null && oldBroadphase != newBroadphase)
{
RemoveFromEntityTree(oldBroadphase.Owner, oldBroadphase, uid, xform);
RemoveFromEntityTree(oldBroadphase.Owner, oldBroadphase, ref oldPhysMap, uid, xform);
}
if (newBroadphase == null)
return;
var newBroadphaseXform = _xformQuery.GetComponent(newBroadphase.Owner);
if (!_physMapQuery.TryGetComponent(newBroadphaseXform.MapUid, out var physMap))
{
throw new InvalidOperationException(
$"Broadphase's map is missing a physics map comp. Broadphase: {ToPrettyString(newBroadphase.Owner)}");
}
AddOrUpdateEntityTree(
newBroadphase.Owner,
newBroadphase,
newBroadphaseXform,
physMap,
uid,
xform);
}
@@ -620,11 +713,17 @@ public sealed partial class EntityLookupSystem : EntitySystem
bool recursive = true)
{
var broadphaseXform = _xformQuery.GetComponent(broadphase.Owner);
if (!_physMapQuery.TryGetComponent(broadphaseXform.MapUid, out var physMap))
{
throw new InvalidOperationException(
$"Broadphase's map is missing a physics map comp. Broadphase: {ToPrettyString(broadphase.Owner)}");
}
AddOrUpdateEntityTree(
broadUid,
broadphase,
broadphaseXform,
physMap,
uid,
xform,
recursive);
@@ -634,6 +733,7 @@ public sealed partial class EntityLookupSystem : EntitySystem
EntityUid broadUid,
BroadphaseComponent broadphase,
TransformComponent broadphaseXform,
PhysicsMapComponent physicsMap,
EntityUid uid,
TransformComponent xform,
bool recursive = true)
@@ -658,7 +758,7 @@ public sealed partial class EntityLookupSystem : EntitySystem
}
else
{
AddOrUpdatePhysicsTree(uid, broadUid, broadphase, broadphaseXform, xform, body, _fixturesQuery.GetComponent(uid));
AddOrUpdatePhysicsTree(uid, broadUid, broadphase, broadphaseXform, physicsMap, xform, body, _fixturesQuery.GetComponent(uid));
}
if (xform.ChildCount == 0 || !recursive)
@@ -671,7 +771,7 @@ public sealed partial class EntityLookupSystem : EntitySystem
foreach (var child in xform._children)
{
var childXform = _xformQuery.GetComponent(child);
AddOrUpdateEntityTree(broadUid, broadphase, broadphaseXform, child, childXform, recursive);
AddOrUpdateEntityTree(broadUid, broadphase, broadphaseXform, physicsMap, child, childXform, recursive);
}
return;
}
@@ -682,7 +782,7 @@ public sealed partial class EntityLookupSystem : EntitySystem
continue;
var childXform = _xformQuery.GetComponent(child);
AddOrUpdateEntityTree(broadUid, broadphase, broadphaseXform, child, childXform, recursive);
AddOrUpdateEntityTree(broadUid, broadphase, broadphaseXform, physicsMap, child, childXform, recursive);
}
}
@@ -694,10 +794,16 @@ public sealed partial class EntityLookupSystem : EntitySystem
if (!TryGetCurrentBroadphase(xform, out var broadphase))
return;
DebugTools.Assert(!HasComp<MapGridComponent>(uid));
DebugTools.Assert(!HasComp<MapComponent>(uid));
DebugTools.Assert(!_gridQuery.HasComp(uid));
DebugTools.Assert(!_mapQuery.HasComp(uid));
PhysicsMapComponent? physMap = null;
if (xform.Broadphase!.Value.PhysicsMap is { Valid: true } map && !_physMapQuery.TryGetComponent(map, out physMap))
{
throw new InvalidOperationException(
$"Broadphase's map is missing a physics map comp. Broadphase: {ToPrettyString(broadphase.Owner)}");
}
RemoveFromEntityTree(broadphase.Owner, broadphase, uid, xform);
RemoveFromEntityTree(broadphase.Owner, broadphase, ref physMap, uid, xform);
}
/// <summary>
@@ -706,6 +812,7 @@ public sealed partial class EntityLookupSystem : EntitySystem
private void RemoveFromEntityTree(
EntityUid broadUid,
BroadphaseComponent broadphase,
ref PhysicsMapComponent? physicsMap,
EntityUid uid,
TransformComponent xform,
bool recursive = true)
@@ -726,9 +833,16 @@ public sealed partial class EntityLookupSystem : EntitySystem
broadUid = old.Uid;
}
if (old.PhysicsMap.IsValid() && physicsMap?.Owner != old.PhysicsMap)
{
if (!_physMapQuery.TryGetComponent(old.PhysicsMap, out physicsMap))
Log.Error($"Entity {ToPrettyString(uid)} has missing physics map?");
}
if (old.CanCollide)
{
RemoveBroadTree(broadphase, _fixturesQuery.GetComponent(uid), old.Static);
DebugTools.Assert(old.PhysicsMap == (physicsMap?.Owner ?? default));
RemoveBroadTree(broadphase, _fixturesQuery.GetComponent(uid), old.Static, physicsMap);
}
else if (old.Static)
broadphase.StaticSundriesTree.Remove(uid);
@@ -744,6 +858,7 @@ public sealed partial class EntityLookupSystem : EntitySystem
RemoveFromEntityTree(
broadUid,
broadphase,
ref physicsMap,
child,
_xformQuery.GetComponent(child));
}

View File

@@ -73,8 +73,8 @@ internal sealed class PrototypeReloadSystem : EntitySystem
var data = newPrototype.Components[name];
var component = _componentFactory.GetComponent(name);
if (!HasComp(entity, component.GetType()))
AddComp(entity, component);
if (!EntityManager.HasComponent(entity, component.GetType()))
EntityManager.AddComponent(entity, component);
}
// Update entity metadata

View File

@@ -69,19 +69,19 @@ namespace Robust.Shared.GameObjects
if (!_enabled)
return;
if (!TryComp(uid, out PhysicsComponent? body))
if (!EntityManager.TryGetComponent(uid, out PhysicsComponent? body))
{
Log.Error($"Trying to regenerate collision for {uid} that doesn't have {nameof(body)}");
return;
}
if (!TryComp(uid, out FixturesComponent? manager))
if (!EntityManager.TryGetComponent(uid, out FixturesComponent? manager))
{
Log.Error($"Trying to regenerate collision for {uid} that doesn't have {nameof(manager)}");
return;
}
if (!TryComp(uid, out TransformComponent? xform))
if (!EntityManager.TryGetComponent(uid, out TransformComponent? xform))
{
Log.Error($"Trying to regenerate collision for {uid} that doesn't have {nameof(TransformComponent)}");
return;

View File

@@ -158,7 +158,7 @@ public abstract partial class SharedMapSystem
// Just MapLoader things.
if (component.MapProxy == DynamicTree.Proxy.Free) return;
var xform = Comp<TransformComponent>(uid);
var xform = EntityManager.GetComponent<TransformComponent>(uid);
var aabb = GetWorldAABB(uid, component, xform);
if (TryComp<GridTreeComponent>(xform.MapUid, out var gridTree))
@@ -166,7 +166,10 @@ public abstract partial class SharedMapSystem
gridTree.Tree.MoveProxy(component.MapProxy, in aabb);
}
_physics.MovedGrids.Add(uid);
if (TryComp<MovedGridsComponent>(xform.MapUid, out var movedGrids))
{
movedGrids.MovedGrids.Add(uid);
}
}
private void OnGridMove(EntityUid uid, MapGridComponent component, ref MoveEvent args)
@@ -188,7 +191,10 @@ public abstract partial class SharedMapSystem
gridTree.Tree.MoveProxy(component.MapProxy, in aabb);
}
_physics.MovedGrids.Add(uid);
if (TryComp<MovedGridsComponent>(xform.MapUid, out var movedGrids))
{
movedGrids.MovedGrids.Add(uid);
}
}
private void OnParentChange(EntityUid uid, MapGridComponent component, ref MoveEvent args)
@@ -213,23 +219,23 @@ public abstract partial class SharedMapSystem
if (xform.ParentUid != xform.MapUid && meta.EntityLifeStage < EntityLifeStage.Terminating && _netManager.IsServer)
{
Log.Error($"Grid {ToPrettyString(uid, meta)} is parented to {ToPrettyString(xform._parent)} which is not a map. y'all need jesus. {Environment.StackTrace}");
Log.Error($"Grid {ToPrettyString(uid, meta)} is not parented to {ToPrettyString(xform._parent)} which is not a map. y'all need jesus. {Environment.StackTrace}");
return;
}
// Make sure we cleanup old map for moved grid stuff.
var oldMap = _transform.ToMapCoordinates(args.OldPosition);
var oldMapUid = GetMapOrInvalid(oldMap.MapId);
if (component.MapProxy != DynamicTree.Proxy.Free)
if (component.MapProxy != DynamicTree.Proxy.Free && TryComp<MovedGridsComponent>(oldMapUid, out var oldMovedGrids))
{
_physics.MovedGrids.Remove(uid);
oldMovedGrids.MovedGrids.Remove(uid);
RemoveGrid(uid, component, oldMapUid);
}
DebugTools.Assert(component.MapProxy == DynamicTree.Proxy.Free);
if (xform.MapUid != null)
if (TryComp<MovedGridsComponent>(xform.MapUid, out var newMovedGrids))
{
_physics.MovedGrids.Add(uid);
newMovedGrids.MovedGrids.Add(uid);
AddGrid(uid, component);
}
}
@@ -338,8 +344,6 @@ public abstract partial class SharedMapSystem
chunk.SuppressCollisionRegeneration = true;
DebugTools.Assert(data.TileData.Any(x => !x.IsEmpty));
DebugTools.Assert(data.TileData.Length == component.ChunkSize * component.ChunkSize);
var changedEntry = new ValueList<TileChangedEntry>();
for (ushort x = 0; x < component.ChunkSize; x++)
{
for (ushort y = 0; y < component.ChunkSize; y++)
@@ -348,15 +352,12 @@ public abstract partial class SharedMapSystem
if (!chunk.TrySetTile(x, y, tile, out var oldTile, out _))
continue;
var chunkIndex = new Vector2i(x, y);
var gridIndices = chunk.ChunkTileToGridTile(chunkIndex);
changedEntry.Add(new TileChangedEntry(tile, oldTile, chunk.Indices, gridIndices));
var gridIndices = chunk.ChunkTileToGridTile((x, y));
var newTileRef = new TileRef(uid, gridIndices, tile);
_mapInternal.RaiseOnTileChanged(gridEnt, newTileRef, oldTile, index);
}
}
var ev = new TileChangedEvent(gridEnt, changedEntry.ToArray());
EntityManager.EventBus.RaiseLocalEvent(gridEnt.Owner, ref ev, true);
DebugTools.Assert(chunk.Fixtures.SetEquals(data.Fixtures));
// These should never refer to the same object
@@ -518,9 +519,9 @@ public abstract partial class SharedMapSystem
component.MapProxy = proxy;
}
if (xform.MapUid != null)
if (TryComp<MovedGridsComponent>(xform.MapUid, out var movedGrids))
{
_physics.MovedGrids.Add(uid);
movedGrids.MovedGrids.Add(uid);
}
}
@@ -572,9 +573,9 @@ public abstract partial class SharedMapSystem
grid.MapProxy = proxy;
}
if (xform.MapUid != null)
if (TryComp<MovedGridsComponent>(xform.MapUid, out var movedGrids))
{
_physics.MovedGrids.Add(uid);
movedGrids.MovedGrids.Add(uid);
}
}
@@ -587,9 +588,9 @@ public abstract partial class SharedMapSystem
grid.MapProxy = DynamicTree.Proxy.Free;
if (mapUid.IsValid())
if (TryComp<MovedGridsComponent>(mapUid, out var movedGrids))
{
_physics.MovedGrids.Remove(uid);
movedGrids.MovedGrids.Remove(uid);
}
}

View File

@@ -116,7 +116,9 @@ public abstract partial class SharedMapSystem
private void OnComponentAdd(EntityUid uid, MapComponent component, ComponentAdd args)
{
// ordered startups when
EnsureComp<PhysicsMapComponent>(uid);
EnsureComp<GridTreeComponent>(uid);
EnsureComp<MovedGridsComponent>(uid);
}
internal void AssignMapId(Entity<MapComponent> map, MapId? id = null)

View File

@@ -1,16 +1,16 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
using System.Runtime.CompilerServices;
using JetBrains.Annotations;
using Robust.Shared.Containers;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Utility;
using System;
using System.Linq;
using System.Runtime.CompilerServices;
using Robust.Shared.Map.Components;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using Robust.Shared.Containers;
namespace Robust.Shared.GameObjects;
@@ -217,10 +217,9 @@ public abstract partial class SharedTransformSystem
{
#if !EXCEPTION_TOLERANCE
throw new Exception("Transform is initialising before map ids have been assigned?");
#else
#endif
Log.Error($"Transform is initialising before map ids have been assigned?");
_map.AssignMapId((uid, mapComp));
#endif
}
xform.MapUid = uid;

View File

@@ -53,14 +53,8 @@ public abstract partial class SharedTransformSystem
return MapCoordinates.Nullspace;
}
Vector2 pos = xform._localRotation.RotateVec(coordinates.Position) + xform._localPosition;
while (xform.ParentUid != xform.MapUid && xform.ParentUid.IsValid())
{
xform = XformQuery.GetComponent(xform.ParentUid);
pos = xform._localRotation.RotateVec(pos) + xform._localPosition;
}
return new MapCoordinates(pos, xform.MapID);
var worldPos = Vector2.Transform(coordinates.Position, GetWorldMatrix(xform));
return new MapCoordinates(worldPos, xform.MapID);
}
/// <summary>
@@ -73,41 +67,6 @@ public abstract partial class SharedTransformSystem
return ToMapCoordinates(eCoords);
}
/// <summary>
/// Converts entity-local coordinates into map terms.
/// The same as ToMapCoordinates(coordinates, logError).Position, but doesn't have to construct the MapCoordinates first.
/// </summary>
public Vector2 ToWorldPosition(EntityCoordinates coordinates, bool logError = true)
{
if (!TryComp(coordinates.EntityId, out TransformComponent? xform))
{
if (logError)
Log.Error($"Attempted to convert coordinates with invalid entity: {coordinates}. Trace: {Environment.StackTrace}");
return Vector2.Zero;
}
Vector2 pos = xform._localRotation.RotateVec(coordinates.Position) + xform._localPosition;
while (xform.ParentUid != xform.MapUid && xform.ParentUid.IsValid())
{
xform = XformQuery.GetComponent(xform.ParentUid);
pos = xform._localRotation.RotateVec(pos) + xform._localPosition;
}
return pos;
}
/// <summary>
/// Converts entity-local coordinates into map terms.
/// The same as ToMapCoordinates(coordinates).Position, but doesn't have to construct the MapCoordinates first.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector2 ToWorldPosition(NetCoordinates coordinates)
{
var eCoords = GetCoordinates(coordinates);
return ToWorldPosition(eCoords);
}
/// <summary>
/// Creates EntityCoordinates given an entity and some MapCoordinates.
/// </summary>

View File

@@ -263,7 +263,7 @@ namespace Robust.Shared.GameObjects
return true;
}
internal void RaiseMoveEvent(
public void RaiseMoveEvent(
Entity<TransformComponent, MetaDataComponent> ent,
EntityUid oldParent,
Vector2 oldPosition,

View File

@@ -636,7 +636,7 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
if (!_timing.IsFirstTimePredicted)
return;
RaisePredictiveEvent(new BoundUIWrapMessage(GetNetEntity(entity.Owner), new CloseBoundInterfaceMessage(), key));
EntityManager.RaisePredictiveEvent(new BoundUIWrapMessage(GetNetEntity(entity.Owner), new CloseBoundInterfaceMessage(), key));
}
/// <summary>
@@ -685,7 +685,7 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
{
// Not guaranteed to open so rely upon the event handling it.
// Also lets client request it to be opened remotely too.
RaisePredictiveEvent(new BoundUIWrapMessage(GetNetEntity(entity.Owner), new OpenBoundInterfaceMessage(), key));
EntityManager.RaisePredictiveEvent(new BoundUIWrapMessage(GetNetEntity(entity.Owner), new OpenBoundInterfaceMessage(), key));
}
}
else

View File

@@ -12,7 +12,7 @@ namespace Robust.Shared.GameObjects
base.Update(frameTime);
// Avoid a collection was modified while enumerating.
var timers = EntityQueryEnumerator<TimerComponent>();
var timers = EntityManager.EntityQueryEnumerator<TimerComponent>();
var timersList = new ValueList<(EntityUid, TimerComponent)>();
while (timers.MoveNext(out var uid, out var timer))
{
@@ -28,7 +28,7 @@ namespace Robust.Shared.GameObjects
{
if (!timer.Deleted && !EntityManager.Deleted(uid) && timer.RemoveOnEmpty && timer.TimerCount == 0)
{
RemComp<TimerComponent>(uid);
EntityManager.RemoveComponent<TimerComponent>(uid);
}
}
}

View File

@@ -170,8 +170,10 @@ namespace Robust.Shared.Localization
if (args.Args.Count < 1) return new LocValueString(nameof(Gender.Neuter));
ILocValue entity0 = args.Args[0];
if (entity0.Value is EntityUid entity)
if (entity0.Value != null)
{
EntityUid entity = (EntityUid)entity0.Value;
if (_entMan.TryGetComponent(entity, out GrammarComponent? grammar) && grammar.Gender.HasValue)
{
return new LocValueString(grammar.Gender.Value.ToString().ToLowerInvariant());
@@ -244,8 +246,10 @@ namespace Robust.Shared.Localization
if (args.Args.Count < 1) return new LocValueString(GetString("zzzz-counter-default"));
ILocValue entity0 = args.Args[0];
if (entity0.Value is EntityUid entity)
if (entity0.Value != null)
{
EntityUid entity = (EntityUid)entity0.Value;
if (TryGetEntityLocAttrib(entity, "counter", out var counter))
{
return new LocValueString(counter);
@@ -288,8 +292,9 @@ namespace Robust.Shared.Localization
if (args.Args.Count < 2) return new LocValueString("other");
ILocValue entity0 = args.Args[0];
if (entity0.Value is EntityUid entity)
if (entity0.Value != null)
{
EntityUid entity = (EntityUid)entity0.Value;
ILocValue attrib0 = args.Args[1];
if (TryGetEntityLocAttrib(entity, attrib0.Format(new LocContext(bundle)), out var attrib))
{
@@ -308,8 +313,10 @@ namespace Robust.Shared.Localization
if (args.Args.Count < 1) return new LocValueString("false");
ILocValue entity0 = args.Args[0];
if (entity0.Value is EntityUid entity)
if (entity0.Value != null)
{
EntityUid entity = (EntityUid)entity0.Value;
if (_entMan.TryGetComponent(entity, out GrammarComponent? grammar) && grammar.ProperNoun.HasValue)
{
return new LocValueString(grammar.ProperNoun.Value.ToString().ToLowerInvariant());

View File

@@ -0,0 +1,16 @@
using System.Collections.Generic;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.ViewVariables;
namespace Robust.Shared.Map.Components;
/// <summary>
/// Stores what grids moved in a tick.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class MovedGridsComponent : Component
{
[ViewVariables]
public HashSet<EntityUid> MovedGrids = new();
}

Some files were not shown because too many files have changed in this diff Show More