diff --git a/Directory.Packages.props b/Directory.Packages.props index 98cdd7ae0c2..f8bf89b7a7c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -43,7 +43,7 @@ - + @@ -56,7 +56,7 @@ - + @@ -71,4 +71,4 @@ - \ No newline at end of file + diff --git a/MSBuild/Robust.Engine.Version.props b/MSBuild/Robust.Engine.Version.props index 69965a2c72e..a26d04e5b89 100644 --- a/MSBuild/Robust.Engine.Version.props +++ b/MSBuild/Robust.Engine.Version.props @@ -1,4 +1,4 @@ - 224.1.1 + 226.3.0 diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 6afc2154b7a..1a5a43272f7 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -39,7 +39,10 @@ END TEMPLATE--> ### New features -*None yet* +* Added `LocalizedEntityCommands`, which are console commands that have the ability to take entity system dependencies. +* Added `BeginRegistrationRegion` to `IConsoleHost` to allow efficient bulk-registration of console commands. +* Added `IConsoleHost.RegisterCommand` overload that takes an `IConsoleCommand`. +* Added a `Finished` boolean to `AnimationCompletedEvent` which allows distinguishing if an animation was removed prematurely or completed naturally. ### Bugfixes @@ -47,13 +50,105 @@ END TEMPLATE--> ### Other -*None yet* +* Tab completions containing spaces are now properly quoted, so the command will actually work properly once entered. ### Internal *None yet* +## 226.3.0 + +### New features + +* `System.Collections.IList` and `System.Collections.ICollection` are now sandbox safe, this fixes some collection expression cases. +* The sandboxing system will now report the methods responsible for references to illegal items. + + +## 226.2.0 + +### New features + +* `Control.VisibilityChanged()` virtual function. +* Add some System.Random methods for NextFloat and NextPolarVector2. + +### Bugfixes + +* Fixes ContainerSystem failing client-side debug asserts when an entity gets unanchored & inserted into a container on the same tick. +* Remove potential race condition on server startup from invoking ThreadPool.SetMinThreads. + +### Other + +* Increase default value of res.rsi_atlas_size. +* Fix internal networking logic. +* Updates of `OutputPanel` contents caused by change in UI scale are now deferred until visible. Especially important to avoid updates from debug console. +* Debug console is now limited to only keep `con.max_entries` entries. +* Non-existent resources are cached by `IResourceCache.TryGetResource`. This avoids the game constantly trying to re-load non-existent resources in common patterns such as UI theme texture fallbacks. +* Default IPv4 MTU has been lowered to 700. +* Update Robust.LoaderApi. + +### Internal + +* Split out PVS serialization from compression and sending game states. +* Turn broadphase contacts into an IParallelRobustJob and remove unnecessary GetMapEntityIds for every contact. + + +## 226.1.0 + +### New features + +* Add some GetLocalEntitiesIntersecting methods for `Entity`. + +### Other + +* Fix internal networking logic + + +## 226.0.0 + +### Breaking changes + +* `IEventBus.RaiseComponentEvent` now requires an EntityUid argument. +* The `AddedComponentEventArgs` and `RemovedComponentEventArgs` constructors are now internal + +### New features + +* Allow RequestScreenTexture to be set in overlays. + +### Bugfixes + +* Fix AnimationCompletedEvent not always going out. + + +## 225.0.0 + +### Breaking changes + +* `NetEntity.Parse` and `TryParse` will now fail to parse empty strings. +* Try to prevent EventBus looping. This also caps the amount of directed component subscriptions for a particular component to 256. + +### New features + +* `IPrototypeManager.TryIndex` will now default to logging errors if passed an invalid prototype id struct (i,e., `EntProtoId` or `ProtoId`). There is a new optional bool argument to disable logging errors. +* `Eye` now allows its `Position` to be set directly. Please only do this with the `FixedEye` child type constructed manually. +* Engine now respects the hub's `can_skip_build` parameter on info query, fixing an issue where the first hub advertisement fails due to ACZ taking too long. +* Add GetSession & TryGetSession to ActorSystem. +* Raise an event when an entity's name is changed. + +### Bugfixes + +* The `ent` toolshed command now takes `NetEntity` values, fixing parsing in practical uses. +* Fix ComponentFactory test mocks. +* Fix LookupFlags missing from a couple of EntityLookupSystem methods. + +### Other + +* Improved engine's Happy Eyeballs implementation, should result in more usage of IPv6 for HTTP APIs when available. +* Remove CompIdx locks to improve performance inside Pvs at higher player counts. +* Avoid a read lock in GetEntityQuery to also improve performance. +* Mark `EntityManager.System` as Pure. + + ## 224.1.1 ### Bugfixes diff --git a/Resources/Locale/en-US/commands.ftl b/Resources/Locale/en-US/commands.ftl index 2e4b3196297..4d0c0cf848f 100644 --- a/Resources/Locale/en-US/commands.ftl +++ b/Resources/Locale/en-US/commands.ftl @@ -382,9 +382,9 @@ cmd-tp-desc = Teleports a player to any location in the round. cmd-tp-help = tp [] cmd-tpto-desc = Teleports the current player or the specified players/entities to the location of the first player/entity. -cmd-tpto-help = tpto [username|uid]... -cmd-tpto-destination-hint = destination (uid or username) -cmd-tpto-victim-hint = entity to teleport (uid or username) +cmd-tpto-help = tpto [username|NetEntity]... +cmd-tpto-destination-hint = destination (NetEntity or username) +cmd-tpto-victim-hint = entity to teleport (NetEntity or username) cmd-tpto-parse-error = Cant resolve entity or player: {$str} cmd-listplayers-desc = Lists all players currently connected. diff --git a/Robust.Analyzers.Tests/MustCallBaseAnalyzerTest.cs b/Robust.Analyzers.Tests/MustCallBaseAnalyzerTest.cs new file mode 100644 index 00000000000..96e6a5567f7 --- /dev/null +++ b/Robust.Analyzers.Tests/MustCallBaseAnalyzerTest.cs @@ -0,0 +1,92 @@ +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis.Testing.Verifiers; +using NUnit.Framework; +using VerifyCS = + Microsoft.CodeAnalysis.CSharp.Testing.NUnit.AnalyzerVerifier; + +namespace Robust.Analyzers.Tests; + +[Parallelizable(ParallelScope.All | ParallelScope.Fixtures)] +[TestFixture] +public sealed class MustCallBaseAnalyzerTest +{ + private static Task Verifier(string code, params DiagnosticResult[] expected) + { + var test = new CSharpAnalyzerTest() + { + TestState = + { + Sources = { code } + }, + }; + + TestHelper.AddEmbeddedSources( + test.TestState, + "Robust.Shared.IoC.MustCallBaseAttribute.cs" + ); + + // ExpectedDiagnostics cannot be set, so we need to AddRange here... + test.TestState.ExpectedDiagnostics.AddRange(expected); + + return test.RunAsync(); + } + + [Test] + public async Task Test() + { + const string code = """ + using Robust.Shared.Analyzers; + + public class Foo + { + [MustCallBase] + public virtual void Function() + { + + } + + [MustCallBase(true)] + public virtual void Function2() + { + + } + } + + public class Bar : Foo + { + public override void Function() + { + + } + + public override void Function2() + { + + } + } + + public class Baz : Foo + { + public override void Function() + { + base.Function(); + } + } + + public class Bal : Bar + { + public override void Function2() + { + } + } + """; + + await Verifier(code, + // /0/Test0.cs(20,26): warning RA0028: Overriders of this function must always call the base function + VerifyCS.Diagnostic().WithSpan(20, 26, 20, 34), + // /0/Test0.cs(41,26): warning RA0028: Overriders of this function must always call the base function + VerifyCS.Diagnostic().WithSpan(41, 26, 41, 35)); + } +} diff --git a/Robust.Analyzers.Tests/Robust.Analyzers.Tests.csproj b/Robust.Analyzers.Tests/Robust.Analyzers.Tests.csproj index 513bdebfbfa..bcc0264bca4 100644 --- a/Robust.Analyzers.Tests/Robust.Analyzers.Tests.csproj +++ b/Robust.Analyzers.Tests/Robust.Analyzers.Tests.csproj @@ -10,6 +10,7 @@ + diff --git a/Robust.Analyzers/MustCallBaseAnalyzer.cs b/Robust.Analyzers/MustCallBaseAnalyzer.cs new file mode 100644 index 00000000000..46966347cfc --- /dev/null +++ b/Robust.Analyzers/MustCallBaseAnalyzer.cs @@ -0,0 +1,111 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Robust.Roslyn.Shared; + +namespace Robust.Analyzers; + +#nullable enable + +/// +/// Enforces MustCallBaseAttribute. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class MustCallBaseAnalyzer : DiagnosticAnalyzer +{ + private const string Attribute = "Robust.Shared.Analyzers.MustCallBaseAttribute"; + + private static readonly DiagnosticDescriptor Rule = new( + Diagnostics.IdMustCallBase, + "No base call in overriden function", + "Overriders of this function must always call the base function", + "Usage", + DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.Method); + } + + private static void AnalyzeSymbol(SymbolAnalysisContext context) + { + if (context.Symbol is not IMethodSymbol { IsOverride: true } method) + return; + + var attrSymbol = context.Compilation.GetTypeByMetadataName(Attribute); + if (attrSymbol == null) + return; + + if (DoesMethodOverriderHaveAttribute(method, attrSymbol) is not { } data) + return; + + if (data is { onlyOverrides: true, depth: < 2 }) + return; + + var syntax = (MethodDeclarationSyntax) method.DeclaringSyntaxReferences[0].GetSyntax(); + if (HasBaseCall(syntax)) + return; + + var diag = Diagnostic.Create(Rule, syntax.Identifier.GetLocation()); + context.ReportDiagnostic(diag); + } + + private static (int depth, bool onlyOverrides)? DoesMethodOverriderHaveAttribute( + IMethodSymbol method, + INamedTypeSymbol attributeSymbol) + { + var depth = 0; + while (method.OverriddenMethod != null) + { + depth += 1; + method = method.OverriddenMethod; + if (GetAttribute(method, attributeSymbol) is not { } attribute) + continue; + + var onlyOverrides = attribute.ConstructorArguments is [{Kind: TypedConstantKind.Primitive, Value: true}]; + return (depth, onlyOverrides); + } + + return null; + } + + private static bool HasBaseCall(MethodDeclarationSyntax syntax) + { + return syntax.Accept(new BaseCallLocator()); + } + + private static AttributeData? GetAttribute(ISymbol namedTypeSymbol, INamedTypeSymbol attrSymbol) + { + return namedTypeSymbol.GetAttributes() + .SingleOrDefault(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, attrSymbol)); + } + + private sealed class BaseCallLocator : CSharpSyntaxVisitor + { + public override bool VisitBaseExpression(BaseExpressionSyntax node) + { + return true; + } + + public override bool DefaultVisit(SyntaxNode node) + { + foreach (var childNode in node.ChildNodes()) + { + if (childNode is not CSharpSyntaxNode cSharpSyntax) + continue; + + if (cSharpSyntax.Accept(this)) + return true; + } + + return false; + } + } +} diff --git a/Robust.Benchmarks/Configs/DefaultSQLConfig.cs b/Robust.Benchmarks/Configs/DefaultSQLConfig.cs index 566276210ec..2b6eb552be3 100644 --- a/Robust.Benchmarks/Configs/DefaultSQLConfig.cs +++ b/Robust.Benchmarks/Configs/DefaultSQLConfig.cs @@ -26,7 +26,8 @@ private DefaultSQLConfig(){} public IEnumerable GetExporters() { - yield return SQLExporter.Default; + //yield return SQLExporter.Default; + yield break; } public IEnumerable GetColumnProviders() => DefaultConfig.Instance.GetColumnProviders(); diff --git a/Robust.Benchmarks/Exporters/SQLExporter.cs b/Robust.Benchmarks/Exporters/SQLExporter.cs index cdb1f3c87e5..a8552e9c6aa 100644 --- a/Robust.Benchmarks/Exporters/SQLExporter.cs +++ b/Robust.Benchmarks/Exporters/SQLExporter.cs @@ -15,11 +15,10 @@ using Microsoft.EntityFrameworkCore.Design; using Npgsql; using Npgsql.Internal; -using Npgsql.Internal.TypeHandlers; -using Npgsql.Internal.TypeHandling; namespace Robust.Benchmarks.Exporters; +/* public sealed class SQLExporter : IExporter { private static readonly JsonSerializerOptions JsonSerializerOptions = new JsonSerializerOptions @@ -98,7 +97,9 @@ private void Export(Summary summary, ILogger logger) public string Name => "sql"; } +*/ +/* // https://github.com/npgsql/efcore.pg/issues/1107#issuecomment-945126627 class JsonOverrideTypeHandlerResolverFactory : TypeHandlerResolverFactory { @@ -138,6 +139,7 @@ internal JsonOverrideTypeHandlerResolver(NpgsqlConnector connector, JsonSerializ => null; // Let the built-in resolver do this } } +*/ public sealed class DesignTimeContextFactoryPostgres : IDesignTimeDbContextFactory { diff --git a/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs b/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs index a666e735eef..a4bdcae91b7 100644 --- a/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs +++ b/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs @@ -1379,7 +1379,7 @@ private void QueueUpdateRenderTree() // TODO whenever sprite comp gets ECS'd , just make this a direct method call. var ev = new QueueSpriteTreeUpdateEvent(entities.GetComponent(Owner)); - entities.EventBus.RaiseComponentEvent(this, ref ev); + entities.EventBus.RaiseComponentEvent(Owner, this, ref ev); } private void QueueUpdateIsInert() @@ -1389,7 +1389,7 @@ private void QueueUpdateIsInert() // TODO whenever sprite comp gets ECS'd , just make this a direct method call. var ev = new SpriteUpdateInertEvent(); - entities.EventBus.RaiseComponentEvent(this, ref ev); + entities.EventBus.RaiseComponentEvent(Owner, this, ref ev); } [Obsolete("Use SpriteSystem instead.")] diff --git a/Robust.Client/GameObjects/EntitySystems/AnimationPlayerSystem.cs b/Robust.Client/GameObjects/EntitySystems/AnimationPlayerSystem.cs index 46aec1e9978..ed760390419 100644 --- a/Robust.Client/GameObjects/EntitySystems/AnimationPlayerSystem.cs +++ b/Robust.Client/GameObjects/EntitySystems/AnimationPlayerSystem.cs @@ -11,6 +11,7 @@ public sealed class AnimationPlayerSystem : EntitySystem { private readonly List> _activeAnimations = new(); + private EntityQuery _playerQuery; private EntityQuery _metaQuery; [Dependency] private readonly IComponentFactory _compFact = default!; @@ -18,6 +19,7 @@ public sealed class AnimationPlayerSystem : EntitySystem public override void Initialize() { base.Initialize(); + _playerQuery = GetEntityQuery(); _metaQuery = GetEntityQuery(); } @@ -74,7 +76,8 @@ private bool Update(EntityUid uid, AnimationPlayerComponent component, float fra foreach (var key in remie) { component.PlayingAnimations.Remove(key); - EntityManager.EventBus.RaiseLocalEvent(uid, new AnimationCompletedEvent {Uid = uid, Key = key}, true); + var completedEvent = new AnimationCompletedEvent {Uid = uid, Key = key, Finished = true}; + EntityManager.EventBus.RaiseLocalEvent(uid, completedEvent, true); } return false; @@ -171,31 +174,42 @@ public bool HasRunningAnimation(AnimationPlayerComponent component, string key) return component.PlayingAnimations.ContainsKey(key); } + [Obsolete] public void Stop(AnimationPlayerComponent component, string key) { - component.PlayingAnimations.Remove(key); + Stop((component.Owner, component), key); } - public void Stop(EntityUid uid, string key) + public void Stop(Entity entity, string key) { - if (!TryComp(uid, out var player)) + if (!_playerQuery.Resolve(entity.Owner, ref entity.Comp, false) || + !entity.Comp.PlayingAnimations.Remove(key)) + { return; + } - player.PlayingAnimations.Remove(key); + var completedEvent = new AnimationCompletedEvent {Uid = entity.Owner, Key = key, Finished = false}; + EntityManager.EventBus.RaiseLocalEvent(entity.Owner, completedEvent, true); } public void Stop(EntityUid uid, AnimationPlayerComponent? component, string key) { - if (!Resolve(uid, ref component, false)) - return; - - component.PlayingAnimations.Remove(key); + Stop((uid, component), key); } } + /// + /// Raised whenever an animation stops, either due to running its course or being stopped manually. + /// public sealed class AnimationCompletedEvent : EntityEventArgs { public EntityUid Uid { get; init; } public string Key { get; init; } = string.Empty; + + /// + /// If true, the animation finished by getting to its natural end. + /// If false, it was removed prematurely via or similar overloads. + /// + public bool Finished { get; init; } } } diff --git a/Robust.Client/GameStates/ClientGameStateManager.cs b/Robust.Client/GameStates/ClientGameStateManager.cs index 5bea213d925..f1d01566878 100644 --- a/Robust.Client/GameStates/ClientGameStateManager.cs +++ b/Robust.Client/GameStates/ClientGameStateManager.cs @@ -603,7 +603,7 @@ public void ResetPredictedEntities() if (compState != null) { var handleState = new ComponentHandleState(compState, null); - _entities.EventBus.RaiseComponentEvent(comp, ref handleState); + _entities.EventBus.RaiseComponentEvent(entity, comp, ref handleState); } comp.LastModifiedTick = _timing.LastRealTick; @@ -640,7 +640,7 @@ public void ResetPredictedEntities() if (state != null) { var stateEv = new ComponentHandleState(state, null); - _entities.EventBus.RaiseComponentEvent(comp, ref stateEv); + _entities.EventBus.RaiseComponentEvent(entity, comp, ref stateEv); } comp.ClearCreationTick(); // don't undo the re-adding. @@ -1361,7 +1361,7 @@ private void HandleEntityState(EntityUid uid, NetEntity netEntity, MetaDataCompo continue; var handleState = new ComponentHandleState(cur, next); - bus.RaiseComponentEvent(comp, ref handleState); + bus.RaiseComponentEvent(uid, comp, ref handleState); } } @@ -1516,7 +1516,7 @@ private void ResetEnt(EntityUid uid, MetaDataComponent meta, bool skipDetached = continue; var handleState = new ComponentHandleState(state, null); - _entityManager.EventBus.RaiseComponentEvent(comp, ref handleState); + _entityManager.EventBus.RaiseComponentEvent(uid, comp, ref handleState); } // ensure we don't have any extra components diff --git a/Robust.Client/Graphics/Overlays/Overlay.cs b/Robust.Client/Graphics/Overlays/Overlay.cs index b381dcb71b8..a2bfcdcaf90 100644 --- a/Robust.Client/Graphics/Overlays/Overlay.cs +++ b/Robust.Client/Graphics/Overlays/Overlay.cs @@ -22,7 +22,7 @@ public abstract class Overlay /// If set to true, will be set to the current frame (at the moment before the overlay is rendered). This can be costly to performance, but /// some shaders will require it as a passed in uniform to operate. /// - public virtual bool RequestScreenTexture => false; + public virtual bool RequestScreenTexture { get; set; } = false; /// /// If is true, then this will be set to the texture corresponding to the current frame. If false, it will always be null. diff --git a/Robust.Client/Replays/Loading/ReplayLoadManager.Read.cs b/Robust.Client/Replays/Loading/ReplayLoadManager.Read.cs index 023f2aab7ba..3d6dd0dfafd 100644 --- a/Robust.Client/Replays/Loading/ReplayLoadManager.Read.cs +++ b/Robust.Client/Replays/Loading/ReplayLoadManager.Read.cs @@ -162,9 +162,7 @@ public async Task LoadReplayAsync(IReplayFileReader fileReader, Load } using var stringFile = fileReader.Open(FileStrings); - var stringData = new byte[stringFile.Length]; - stringFile.ReadExactly(stringData); - _serializer.SetStringSerializerPackage(stringHash, stringData); + _serializer.SetStringSerializerPackage(stringHash, stringFile.CopyToArray()); using var cvarsFile = fileReader.Open(FileCvars); // Note, this does not invoke the received-initial-cvars event. But at least currently, that doesn't matter diff --git a/Robust.Client/ResourceManagement/ResourceCache.Preload.cs b/Robust.Client/ResourceManagement/ResourceCache.Preload.cs index cc567565ef5..da768589fcf 100644 --- a/Robust.Client/ResourceManagement/ResourceCache.Preload.cs +++ b/Robust.Client/ResourceManagement/ResourceCache.Preload.cs @@ -47,7 +47,7 @@ private void PreloadTextures(ISawmill sawmill) { sawmill.Debug("Preloading textures..."); var sw = Stopwatch.StartNew(); - var resList = GetTypeDict(); + var resList = GetTypeData().Resources; var texList = _manager.ContentFindFiles("/Textures/") // Skip PNG files inside RSIs. @@ -119,7 +119,7 @@ private void PreloadTextures(ISawmill sawmill) private void PreloadRsis(ISawmill sawmill) { var sw = Stopwatch.StartNew(); - var resList = GetTypeDict(); + var resList = GetTypeData().Resources; var rsiList = _manager.ContentFindFiles("/Textures/") .Where(p => p.ToString().EndsWith(".rsi/meta.json")) diff --git a/Robust.Client/ResourceManagement/ResourceCache.cs b/Robust.Client/ResourceManagement/ResourceCache.cs index 9d48643cd83..cdcea4fccfe 100644 --- a/Robust.Client/ResourceManagement/ResourceCache.cs +++ b/Robust.Client/ResourceManagement/ResourceCache.cs @@ -17,9 +17,7 @@ namespace Robust.Client.ResourceManagement; /// internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInternal, IDisposable { - private readonly Dictionary> _cachedResources = - new(); - + private readonly Dictionary _cachedResources = new(); private readonly Dictionary _fallbacks = new(); public T GetResource(string path, bool useFallback = true) where T : BaseResource, new() @@ -29,8 +27,8 @@ internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInt public T GetResource(ResPath path, bool useFallback = true) where T : BaseResource, new() { - var cache = GetTypeDict(); - if (cache.TryGetValue(path, out var cached)) + var cache = GetTypeData(); + if (cache.Resources.TryGetValue(path, out var cached)) { return (T) cached; } @@ -40,7 +38,7 @@ internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInt { var dependencies = IoCManager.Instance!; resource.Load(dependencies, path); - cache[path] = resource; + cache.Resources[path] = resource; return resource; } catch (Exception e) @@ -67,24 +65,31 @@ internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInt public bool TryGetResource(ResPath path, [NotNullWhen(true)] out T? resource) where T : BaseResource, new() { - var cache = GetTypeDict(); - if (cache.TryGetValue(path, out var cached)) + var cache = GetTypeData(); + if (cache.Resources.TryGetValue(path, out var cached)) { resource = (T) cached; return true; } + if (cache.NonExistent.Contains(path)) + { + resource = null; + return false; + } + var _resource = new T(); try { var dependencies = IoCManager.Instance!; _resource.Load(dependencies, path); resource = _resource; - cache[path] = resource; + cache.Resources[path] = resource; return true; } catch (FileNotFoundException) { + cache.NonExistent.Add(path); resource = null; return false; } @@ -109,9 +114,9 @@ public bool TryGetResource(AudioStream stream, [NotNullWhen(true)] out AudioReso public void ReloadResource(ResPath path) where T : BaseResource, new() { - var cache = GetTypeDict(); + var cache = GetTypeData(); - if (!cache.TryGetValue(path, out var res)) + if (!cache.Resources.TryGetValue(path, out var res)) { return; } @@ -145,7 +150,7 @@ public bool TryGetResource(AudioStream stream, [NotNullWhen(true)] out AudioReso public void CacheResource(ResPath path, T resource) where T : BaseResource, new() { - GetTypeDict()[path] = resource; + GetTypeData().Resources[path] = resource; } public T GetFallback() where T : BaseResource, new() @@ -168,7 +173,7 @@ public bool TryGetResource(AudioStream stream, [NotNullWhen(true)] out AudioReso public IEnumerable> GetAllResources() where T : BaseResource, new() { - return GetTypeDict().Select(p => new KeyValuePair(p.Key, (T) p.Value)); + return GetTypeData().Resources.Select(p => new KeyValuePair(p.Key, (T) p.Value)); } public event Action? OnRawTextureLoaded; @@ -193,7 +198,7 @@ private void Dispose(bool disposing) if (disposing) { - foreach (var res in _cachedResources.Values.SelectMany(dict => dict.Values)) + foreach (var res in _cachedResources.Values.SelectMany(dict => dict.Resources.Values)) { res.Dispose(); } @@ -210,15 +215,9 @@ private void Dispose(bool disposing) #endregion IDisposable Members [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected Dictionary GetTypeDict() + private TypeData GetTypeData() { - if (!_cachedResources.TryGetValue(typeof(T), out var ret)) - { - ret = new Dictionary(); - _cachedResources.Add(typeof(T), ret); - } - - return ret; + return _cachedResources.GetOrNew(typeof(T)); } public void TextureLoaded(TextureLoadedEventArgs eventArgs) @@ -230,4 +229,13 @@ public void RsiLoaded(RsiLoadedEventArgs eventArgs) { OnRsiLoaded?.Invoke(eventArgs); } + + private sealed class TypeData + { + public readonly Dictionary Resources = new(); + + // List of resources which DON'T exist. + // Needed to avoid innocuous TryGet calls repeatedly trying to re-load non-existent resources from disk. + public readonly HashSet NonExistent = new(); + } } diff --git a/Robust.Client/UserInterface/Control.cs b/Robust.Client/UserInterface/Control.cs index e69607990fb..dce6df5c270 100644 --- a/Robust.Client/UserInterface/Control.cs +++ b/Robust.Client/UserInterface/Control.cs @@ -212,9 +212,18 @@ public bool Visible } } + /// + /// Called when this control's visibility in the control tree changed. + /// + protected virtual void VisibilityChanged(bool newVisible) + { + } + private void _propagateVisibilityChanged(bool newVisible) { + VisibilityChanged(newVisible); OnVisibilityChanged?.Invoke(this); + if (!VisibleInTree) { UserInterfaceManagerInternal.ControlHidden(this); diff --git a/Robust.Client/UserInterface/Controls/CheckBox.cs b/Robust.Client/UserInterface/Controls/CheckBox.cs index 4f12ae0c0b9..aabb879187e 100644 --- a/Robust.Client/UserInterface/Controls/CheckBox.cs +++ b/Robust.Client/UserInterface/Controls/CheckBox.cs @@ -15,6 +15,36 @@ public class CheckBox : ContainerButton public Label Label { get; } public TextureRect TextureRect { get; } + /// + /// Should the checkbox be to the left or the right of the label. + /// + public bool LeftAlign + { + get => _leftAlign; + set + { + if (_leftAlign == value) + return; + + _leftAlign = value; + + if (value) + { + Label.HorizontalExpand = false; + TextureRect.SetPositionFirst(); + Label.SetPositionInParent(1); + } + else + { + Label.HorizontalExpand = true; + Label.SetPositionFirst(); + TextureRect.SetPositionInParent(1); + } + } + } + + private bool _leftAlign = true; + public CheckBox() { ToggleMode = true; @@ -31,10 +61,21 @@ public CheckBox() StyleClasses = { StyleClassCheckBox }, VerticalAlignment = VAlignment.Center, }; - hBox.AddChild(TextureRect); Label = new Label(); - hBox.AddChild(Label); + + if (LeftAlign) + { + Label.HorizontalExpand = false; + hBox.AddChild(TextureRect); + hBox.AddChild(Label); + } + else + { + Label.HorizontalExpand = true; + hBox.AddChild(Label); + hBox.AddChild(TextureRect); + } } protected override void DrawModeChanged() diff --git a/Robust.Client/UserInterface/Controls/ContainerButton.cs b/Robust.Client/UserInterface/Controls/ContainerButton.cs index f3ecffb8d14..f981e9f766c 100644 --- a/Robust.Client/UserInterface/Controls/ContainerButton.cs +++ b/Robust.Client/UserInterface/Controls/ContainerButton.cs @@ -15,6 +15,8 @@ public class ContainerButton : BaseButton public const string StylePseudoClassHover = "hover"; public const string StylePseudoClassDisabled = "disabled"; + public StyleBox? StyleBoxOverride { get; set; } + public ContainerButton() { DrawModeChanged(); @@ -24,6 +26,11 @@ private StyleBox ActualStyleBox { get { + if (StyleBoxOverride != null) + { + return StyleBoxOverride; + } + if (TryGetStyleProperty(StylePropertyStyleBox, out var box)) { return box; diff --git a/Robust.Client/UserInterface/Controls/OutputPanel.cs b/Robust.Client/UserInterface/Controls/OutputPanel.cs index 1dd52e0b005..c23f4ceaa9e 100644 --- a/Robust.Client/UserInterface/Controls/OutputPanel.cs +++ b/Robust.Client/UserInterface/Controls/OutputPanel.cs @@ -1,9 +1,8 @@ using System; -using System.Collections.Generic; using System.Numerics; -using System.Runtime.InteropServices; using Robust.Client.Graphics; using Robust.Client.UserInterface.RichText; +using Robust.Shared.Collections; using Robust.Shared.IoC; using Robust.Shared.Maths; using Robust.Shared.Utility; @@ -20,7 +19,7 @@ public class OutputPanel : Control public const string StylePropertyStyleBox = "stylebox"; - private readonly List _entries = new(); + private readonly RingBufferList _entries = new(); private bool _isAtBottom = true; private int _totalContentHeight; @@ -30,6 +29,8 @@ public class OutputPanel : Control public bool ScrollFollowing { get; set; } = true; + private bool _invalidOnVisible; + public OutputPanel() { IoCManager.InjectDependencies(this); @@ -45,6 +46,8 @@ public OutputPanel() _scrollBar.OnValueChanged += _ => _isAtBottom = _scrollBar.IsAtEnd; } + public int EntryCount => _entries.Count; + public StyleBox? StyleBoxOverride { get => _styleBoxOverride; @@ -91,7 +94,7 @@ public void AddMessage(FormattedMessage message) { var entry = new RichTextEntry(message, this, _tagManager, null); - entry.Update(_getFont(), _getContentBox().Width, UIScale); + entry.Update(_tagManager, _getFont(), _getContentBox().Width, UIScale); _entries.Add(entry); var font = _getFont(); @@ -134,7 +137,7 @@ protected internal override void Draw(DrawingHandleScreen handle) // So when a new color tag gets hit this stack gets the previous color pushed on. var context = new MarkupDrawingContext(2); - foreach (ref var entry in CollectionsMarshal.AsSpan(_entries)) + foreach (ref var entry in _entries) { if (entryOffset + entry.Height < 0) { @@ -147,7 +150,7 @@ protected internal override void Draw(DrawingHandleScreen handle) break; } - entry.Draw(handle, font, contentBox, entryOffset, context, UIScale); + entry.Draw(_tagManager, handle, font, contentBox, entryOffset, context, UIScale); entryOffset += entry.Height + font.GetLineSeparation(UIScale); } @@ -185,9 +188,9 @@ private void _invalidateEntries() _totalContentHeight = 0; var font = _getFont(); var sizeX = _getContentBox().Width; - foreach (ref var entry in CollectionsMarshal.AsSpan(_entries)) + foreach (ref var entry in _entries) { - entry.Update(font, sizeX, UIScale); + entry.Update(_tagManager, font, sizeX, UIScale); _totalContentHeight += entry.Height + font.GetLineSeparation(UIScale); } @@ -239,7 +242,13 @@ private UIBox2 _getContentBox() protected internal override void UIScaleChanged() { - _invalidateEntries(); + // If this control isn't visible, don't invalidate entries immediately. + // This saves invalidating the debug console if it's hidden, + // which is a huge boon as auto-scaling changes UI scale a lot in that scenario. + if (!VisibleInTree) + _invalidOnVisible = true; + else + _invalidateEntries(); base.UIScaleChanged(); } @@ -257,5 +266,14 @@ protected override void EnteredTree() // existing ones were valid when the UI scale was set. _invalidateEntries(); } + + protected override void VisibilityChanged(bool newVisible) + { + if (newVisible && _invalidOnVisible) + { + _invalidateEntries(); + _invalidOnVisible = false; + } + } } } diff --git a/Robust.Client/UserInterface/Controls/RichTextLabel.cs b/Robust.Client/UserInterface/Controls/RichTextLabel.cs index 8e4f2e8c967..b1776b97a97 100644 --- a/Robust.Client/UserInterface/Controls/RichTextLabel.cs +++ b/Robust.Client/UserInterface/Controls/RichTextLabel.cs @@ -68,7 +68,7 @@ protected override Vector2 MeasureOverride(Vector2 availableSize) } var font = _getFont(); - _entry.Update(font, availableSize.X * UIScale, UIScale, LineHeightScale); + _entry.Update(_tagManager, font, availableSize.X * UIScale, UIScale, LineHeightScale); return new Vector2(_entry.Width / UIScale, _entry.Height / UIScale); } @@ -82,7 +82,7 @@ protected internal override void Draw(DrawingHandleScreen handle) return; } - _entry.Draw(handle, _getFont(), SizeBox, 0, new MarkupDrawingContext(), UIScale, LineHeightScale); + _entry.Draw(_tagManager, handle, _getFont(), SizeBox, 0, new MarkupDrawingContext(), UIScale, LineHeightScale); } [Pure] diff --git a/Robust.Client/UserInterface/Controls/TabContainer.cs b/Robust.Client/UserInterface/Controls/TabContainer.cs index 33114d66a7c..623ac18ac1a 100644 --- a/Robust.Client/UserInterface/Controls/TabContainer.cs +++ b/Robust.Client/UserInterface/Controls/TabContainer.cs @@ -65,6 +65,10 @@ public bool TabsVisible } } + public StyleBox? PanelStyleBoxOverride { get; set; } + public Color? TabFontColorOverride { get; set; } + public Color? TabFontColorInactiveOverride { get; set; } + public event Action? OnTabChanged; public TabContainer() @@ -361,6 +365,9 @@ private int _getHeaderSize() [System.Diagnostics.Contracts.Pure] private Color _getTabFontColorActive() { + if (TabFontColorOverride != null) + return TabFontColorOverride.Value; + if (TryGetStyleProperty(stylePropertyTabFontColor, out Color color)) { return color; @@ -371,6 +378,9 @@ private Color _getTabFontColorActive() [System.Diagnostics.Contracts.Pure] private Color _getTabFontColorInactive() { + if (TabFontColorInactiveOverride != null) + return TabFontColorInactiveOverride.Value; + if (TryGetStyleProperty(StylePropertyTabFontColorInactive, out Color color)) { return color; @@ -381,6 +391,9 @@ private Color _getTabFontColorInactive() [System.Diagnostics.Contracts.Pure] private StyleBox? _getPanel() { + if (PanelStyleBoxOverride != null) + return PanelStyleBoxOverride; + TryGetStyleProperty(StylePropertyPanelStyleBox, out var box); return box; } diff --git a/Robust.Client/UserInterface/Controls/WindowRoot.cs b/Robust.Client/UserInterface/Controls/WindowRoot.cs index b85b524ce93..33066247930 100644 --- a/Robust.Client/UserInterface/Controls/WindowRoot.cs +++ b/Robust.Client/UserInterface/Controls/WindowRoot.cs @@ -13,6 +13,12 @@ internal WindowRoot(IClydeWindow window) } public override float UIScale => UIScaleSet; internal float UIScaleSet { get; set; } + + /// + /// Set after the window is resized, to batch up UI scale updates on window resizes. + /// + internal bool UIScaleUpdateNeeded { get; set; } + public override IClydeWindow Window { get; } /// diff --git a/Robust.Client/UserInterface/CustomControls/DebugConsole.xaml.Completions.cs b/Robust.Client/UserInterface/CustomControls/DebugConsole.xaml.Completions.cs index e135181dd38..0bfd2c43254 100644 --- a/Robust.Client/UserInterface/CustomControls/DebugConsole.xaml.Completions.cs +++ b/Robust.Client/UserInterface/CustomControls/DebugConsole.xaml.Completions.cs @@ -277,8 +277,21 @@ private void CompletionKeyDown(GUIBoundKeyEventArgs args) CommandBar.CursorPosition = lastRange.end; CommandBar.SelectionStart = lastRange.start; var insertValue = CommandParsing.Escape(completion); + + // If the replacement contains a space, we must quote it to treat it as a single argument. + var mustQuote = insertValue.Contains(' '); if ((completionFlags & CompletionOptionFlags.PartialCompletion) == 0) + { + if (mustQuote) + insertValue = $"\"{insertValue}\""; + insertValue += " "; + } + else if (mustQuote) + { + // If it's a partial completion, only quote the start. + insertValue = '"' + insertValue; + } CommandBar.InsertAtCursor(insertValue); diff --git a/Robust.Client/UserInterface/CustomControls/DebugConsole.xaml.cs b/Robust.Client/UserInterface/CustomControls/DebugConsole.xaml.cs index 59104cc0aca..b012319663e 100644 --- a/Robust.Client/UserInterface/CustomControls/DebugConsole.xaml.cs +++ b/Robust.Client/UserInterface/CustomControls/DebugConsole.xaml.cs @@ -7,6 +7,7 @@ using Robust.Client.Console; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; +using Robust.Shared; using Robust.Shared.Configuration; using Robust.Shared.ContentPack; using Robust.Shared.Input; @@ -51,6 +52,8 @@ public sealed partial class DebugConsole : Control, IDebugConsoleView private readonly ConcurrentQueue _messageQueue = new(); private readonly ISawmill _logger; + private int _maxEntries; + public DebugConsole() { RobustXamlLoader.Load(this); @@ -78,6 +81,7 @@ protected override void EnteredTree() _consoleHost.AddString += OnAddString; _consoleHost.AddFormatted += OnAddFormatted; _consoleHost.ClearText += OnClearText; + _cfg.OnValueChanged(CVars.ConMaxEntries, MaxEntriesChanged, true); UserInterfaceManager.ModalRoot.AddChild(_compPopup); } @@ -89,10 +93,17 @@ protected override void ExitedTree() _consoleHost.AddString -= OnAddString; _consoleHost.AddFormatted -= OnAddFormatted; _consoleHost.ClearText -= OnClearText; + _cfg.UnsubValueChanged(CVars.ConMaxEntries, MaxEntriesChanged); UserInterfaceManager.ModalRoot.RemoveChild(_compPopup); } + private void MaxEntriesChanged(int value) + { + _maxEntries = value; + TrimExtraOutputEntries(); + } + private void OnClearText(object? _, EventArgs args) { Clear(); @@ -165,6 +176,15 @@ public void Clear() private void _addFormattedLineInternal(FormattedMessage message) { Output.AddMessage(message); + TrimExtraOutputEntries(); + } + + private void TrimExtraOutputEntries() + { + while (Output.EntryCount > _maxEntries) + { + Output.RemoveEntry(0); + } } private void _flushQueue() diff --git a/Robust.Client/UserInterface/RichTextEntry.cs b/Robust.Client/UserInterface/RichTextEntry.cs index 4c42084a59c..432280bb06a 100644 --- a/Robust.Client/UserInterface/RichTextEntry.cs +++ b/Robust.Client/UserInterface/RichTextEntry.cs @@ -17,7 +17,6 @@ namespace Robust.Client.UserInterface internal struct RichTextEntry { private readonly Color _defaultColor; - private readonly MarkupTagManager _tagManager; private readonly Type[]? _tagsAllowed; public readonly FormattedMessage Message; @@ -37,7 +36,7 @@ internal struct RichTextEntry /// public ValueList LineBreaks; - private readonly Dictionary _tagControls = new(); + private readonly Dictionary? _tagControls; public RichTextEntry(FormattedMessage message, Control parent, MarkupTagManager tagManager, Type[]? tagsAllowed = null, Color? defaultColor = null) { @@ -46,23 +45,26 @@ public RichTextEntry(FormattedMessage message, Control parent, MarkupTagManager Width = 0; LineBreaks = default; _defaultColor = defaultColor ?? new(200, 200, 200); - _tagManager = tagManager; _tagsAllowed = tagsAllowed; + Dictionary? tagControls = null; var nodeIndex = -1; - foreach (var node in Message.Nodes) + foreach (var node in Message) { nodeIndex++; if (node.Name == null) continue; - if (!_tagManager.TryGetMarkupTag(node.Name, _tagsAllowed, out var tag) || !tag.TryGetControl(node, out var control)) + if (!tagManager.TryGetMarkupTag(node.Name, _tagsAllowed, out var tag) || !tag.TryGetControl(node, out var control)) continue; parent.Children.Add(control); - _tagControls.Add(nodeIndex, control); + tagControls ??= new Dictionary(); + tagControls.Add(nodeIndex, control); } + + _tagControls = tagControls; } /// @@ -72,7 +74,7 @@ public RichTextEntry(FormattedMessage message, Control parent, MarkupTagManager /// The maximum horizontal size of the container of this entry. /// /// - public void Update(Font defaultFont, float maxSizeX, float uiScale, float lineHeightScale = 1) + public void Update(MarkupTagManager tagManager, Font defaultFont, float maxSizeX, float uiScale, float lineHeightScale = 1) { // This method is gonna suck due to complexity. // Bear with me here. @@ -91,10 +93,10 @@ public void Update(Font defaultFont, float maxSizeX, float uiScale, float lineHe // Nodes can change the markup drawing context and return additional text. // It's also possible for nodes to return inline controls. They get treated as one large rune. var nodeIndex = -1; - foreach (var node in Message.Nodes) + foreach (var node in Message) { nodeIndex++; - var text = ProcessNode(node, context); + var text = ProcessNode(tagManager, node, context); if (!context.Font.TryPeek(out var font)) font = defaultFont; @@ -113,7 +115,7 @@ public void Update(Font defaultFont, float maxSizeX, float uiScale, float lineHe return; } - if (!_tagControls.TryGetValue(nodeIndex, out var control)) + if (_tagControls == null || !_tagControls.TryGetValue(nodeIndex, out var control)) continue; if (ProcessRune(ref this, new Rune(' '), out breakLine)) @@ -166,6 +168,7 @@ void CheckLineBreak(ref RichTextEntry src, int? line) } public readonly void Draw( + MarkupTagManager tagManager, DrawingHandleScreen handle, Font defaultFont, UIBox2 drawBox, @@ -184,10 +187,10 @@ public readonly void Draw( var controlYAdvance = 0f; var nodeIndex = -1; - foreach (var node in Message.Nodes) + foreach (var node in Message) { nodeIndex++; - var text = ProcessNode(node, context); + var text = ProcessNode(tagManager, node, context); if (!context.Color.TryPeek(out var color) || !context.Font.TryPeek(out var font)) { color = _defaultColor; @@ -210,7 +213,7 @@ public readonly void Draw( globalBreakCounter += 1; } - if (!_tagControls.TryGetValue(nodeIndex, out var control)) + if (_tagControls == null || !_tagControls.TryGetValue(nodeIndex, out var control)) continue; var invertedScale = 1f / uiScale; @@ -223,24 +226,22 @@ public readonly void Draw( } } - private readonly string ProcessNode(MarkupNode node, MarkupDrawingContext context) + private readonly string ProcessNode(MarkupTagManager tagManager, MarkupNode node, MarkupDrawingContext context) { // If a nodes name is null it's a text node. if (node.Name == null) return node.Value.StringValue ?? ""; //Skip the node if there is no markup tag for it. - if (!_tagManager.TryGetMarkupTag(node.Name, _tagsAllowed, out var tag)) + if (!tagManager.TryGetMarkupTag(node.Name, _tagsAllowed, out var tag)) return ""; if (!node.Closing) { - context.Tags.Add(tag); tag.PushDrawContext(node, context); return tag.TextBefore(node); } - context.Tags.Remove(tag); tag.PopDrawContext(node, context); return tag.TextAfter(node); } diff --git a/Robust.Client/UserInterface/UserInterfaceManager.Scaling.cs b/Robust.Client/UserInterface/UserInterfaceManager.Scaling.cs index c96e4cc38f0..7e76ecc94b2 100644 --- a/Robust.Client/UserInterface/UserInterfaceManager.Scaling.cs +++ b/Robust.Client/UserInterface/UserInterfaceManager.Scaling.cs @@ -123,7 +123,12 @@ private float CalculateAutoScale(WindowRoot root) private void UpdateUIScale(WindowRoot root) { - root.UIScaleSet = CalculateAutoScale(root); + var newScale = CalculateAutoScale(root); + // ReSharper disable once CompareOfFloatsByEqualityOperator + if (newScale == root.UIScaleSet) + return; + + root.UIScaleSet = newScale; _propagateUIScaleChanged(root); root.InvalidateMeasure(); } @@ -142,7 +147,21 @@ private void WindowSizeChanged(WindowResizedEventArgs windowResizedEventArgs) { if (!_windowsToRoot.TryGetValue(windowResizedEventArgs.Window.Id, out var root)) return; - UpdateUIScale(root); + + root.UIScaleUpdateNeeded = true; root.InvalidateMeasure(); } + + private void CheckRootUIScaleUpdate(WindowRoot root) + { + if (!root.UIScaleUpdateNeeded) + return; + + using (_prof.Group("UIScaleUpdate")) + { + UpdateUIScale(root); + } + + root.UIScaleUpdateNeeded = false; + } } diff --git a/Robust.Client/UserInterface/UserInterfaceManager.cs b/Robust.Client/UserInterface/UserInterfaceManager.cs index 5e9e247d54f..bb988f0aca2 100644 --- a/Robust.Client/UserInterface/UserInterfaceManager.cs +++ b/Robust.Client/UserInterface/UserInterfaceManager.cs @@ -216,6 +216,8 @@ public void FrameUpdate(FrameEventArgs args) { foreach (var root in _roots) { + CheckRootUIScaleUpdate(root); + using (_prof.Group("Root")) { var totalUpdated = root.DoFrameUpdateRecursive(args); diff --git a/Robust.LoaderApi b/Robust.LoaderApi index 99a2f4b8807..86a02eef163 160000 --- a/Robust.LoaderApi +++ b/Robust.LoaderApi @@ -1 +1 @@ -Subproject commit 99a2f4b88077629f69fb66f74f50e88dbe43e0e8 +Subproject commit 86a02eef163156fe899eb498acd488e8d7063a0e diff --git a/Robust.Roslyn.Shared/Diagnostics.cs b/Robust.Roslyn.Shared/Diagnostics.cs index 03b20162cf5..0971717682d 100644 --- a/Robust.Roslyn.Shared/Diagnostics.cs +++ b/Robust.Roslyn.Shared/Diagnostics.cs @@ -31,6 +31,7 @@ public static class Diagnostics public const string IdDependencyFieldAssigned = "RA0025"; public const string IdUncachedRegex = "RA0026"; public const string IdDataFieldRedundantTag = "RA0027"; + public const string IdMustCallBase = "RA0028"; public static SuppressionDescriptor MeansImplicitAssignment => new SuppressionDescriptor("RADC1000", "CS0649", "Marked as implicitly assigned."); diff --git a/Robust.Server/GameObjects/EntitySystems/VisibilitySystem.cs b/Robust.Server/GameObjects/EntitySystems/VisibilitySystem.cs index 786df007b38..8f6e31d3e88 100644 --- a/Robust.Server/GameObjects/EntitySystems/VisibilitySystem.cs +++ b/Robust.Server/GameObjects/EntitySystems/VisibilitySystem.cs @@ -40,12 +40,6 @@ public override void Shutdown() EntityManager.EntityInitialized -= OnEntityInit; } - [Obsolete("Use Entity variant")] - public void AddLayer(EntityUid uid, VisibilityComponent component, int layer, bool refresh = true) - { - AddLayer((uid, component), (ushort)layer, refresh); - } - public void AddLayer(Entity ent, ushort layer, bool refresh = true) { ent.Comp ??= _visibilityQuery.CompOrNull(ent.Owner) ?? AddComp(ent.Owner); @@ -59,13 +53,6 @@ public void AddLayer(Entity ent, ushort layer, bool refres RefreshVisibility(ent); } - - [Obsolete("Use Entity variant")] - public void RemoveLayer(EntityUid uid, VisibilityComponent component, int layer, bool refresh = true) - { - RemoveLayer((uid, component), (ushort)layer, refresh); - } - public void RemoveLayer(Entity ent, ushort layer, bool refresh = true) { if (!_visibilityQuery.Resolve(ent.Owner, ref ent.Comp, false)) @@ -80,12 +67,6 @@ public void RemoveLayer(Entity ent, ushort layer, bool ref RefreshVisibility(ent); } - [Obsolete("Use Entity variant")] - public void SetLayer(EntityUid uid, VisibilityComponent component, int layer, bool refresh = true) - { - SetLayer((uid, component), (ushort)layer, refresh); - } - public void SetLayer(Entity ent, ushort layer, bool refresh = true) { ent.Comp ??= _visibilityQuery.CompOrNull(ent.Owner) ?? AddComp(ent.Owner); diff --git a/Robust.Server/GameObjects/ServerEntityManager.cs b/Robust.Server/GameObjects/ServerEntityManager.cs index 7596e65e94b..000cbc6a2e9 100644 --- a/Robust.Server/GameObjects/ServerEntityManager.cs +++ b/Robust.Server/GameObjects/ServerEntityManager.cs @@ -229,21 +229,6 @@ public void SendSystemNetworkMessage(EntityEventArgs message, INetChannel target private void HandleEntityNetworkMessage(MsgEntity message) { - var msgT = message.SourceTick; - var cT = _gameTiming.CurTick; - - if (msgT <= cT) - { - if (msgT < cT && _logLateMsgs) - { - _netEntSawmill.Warning("Got late MsgEntity! Diff: {0}, msgT: {2}, cT: {3}, player: {1}", - (int) msgT.Value - (int) cT.Value, message.MsgChannel.UserName, msgT, cT); - } - - DispatchEntityNetworkMessage(message); - return; - } - _queue.Add(message); } diff --git a/Robust.Server/GameStates/PvsData.cs b/Robust.Server/GameStates/PvsData.cs index 9f222460b02..f6778464a93 100644 --- a/Robust.Server/GameStates/PvsData.cs +++ b/Robust.Server/GameStates/PvsData.cs @@ -1,11 +1,13 @@ using System; using System.Collections.Generic; +using System.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Robust.Shared.Collections; using Robust.Shared.GameObjects; using Robust.Shared.GameStates; using Robust.Shared.Network; +using Robust.Shared.Network.Messages; using Robust.Shared.Player; using Robust.Shared.Timing; using Robust.Shared.Utility; @@ -115,6 +117,16 @@ internal sealed class PvsSession(ICommonSession session, ResizableMemoryRegion

public GameState? State; + ///

+ /// The serialized object. + /// + public MemoryStream? StateStream; + + /// + /// Whether we should force reliable sending of the . + /// + public bool ForceSendReliably { get; set; } + /// /// Clears all stored game state data. This should only be used after the game state has been serialized. /// diff --git a/Robust.Server/GameStates/PvsSystem.Dirty.cs b/Robust.Server/GameStates/PvsSystem.Dirty.cs index 64d6f1150ed..98bab6daf07 100644 --- a/Robust.Server/GameStates/PvsSystem.Dirty.cs +++ b/Robust.Server/GameStates/PvsSystem.Dirty.cs @@ -75,15 +75,15 @@ private bool TryGetDirtyEntities(GameTick tick, [NotNullWhen(true)] out HashSet< return true; } - private void CleanupDirty(ICommonSession[] sessions) + private void CleanupDirty() { using var _ = Histogram.WithLabels("Clean Dirty").NewTimer(); if (!CullingEnabled) { _seenAllEnts.Clear(); - foreach (var player in sessions) + foreach (var player in _sessions) { - _seenAllEnts.Add(player); + _seenAllEnts.Add(player.Session); } } diff --git a/Robust.Server/GameStates/PvsSystem.Leave.cs b/Robust.Server/GameStates/PvsSystem.Leave.cs index cae9c9cf551..bfb0041e5f7 100644 --- a/Robust.Server/GameStates/PvsSystem.Leave.cs +++ b/Robust.Server/GameStates/PvsSystem.Leave.cs @@ -17,13 +17,12 @@ internal sealed partial class PvsSystem { private WaitHandle? _leaveTask; - private void ProcessLeavePvs(ICommonSession[] sessions) + private void ProcessLeavePvs() { - if (!CullingEnabled || sessions.Length == 0) + if (!CullingEnabled || _sessions.Length == 0) return; DebugTools.AssertNull(_leaveTask); - _leaveJob.Setup(sessions); if (_async) { @@ -76,29 +75,19 @@ private record struct PvsLeaveJob(PvsSystem _pvs) : IParallelRobustJob { public int BatchSize => 2; private PvsSystem _pvs = _pvs; - public int Count => _sessions.Length; - private PvsSession[] _sessions; + public int Count => _pvs._sessions.Length; + public void Execute(int index) { try { - _pvs.ProcessLeavePvs(_sessions[index]); + _pvs.ProcessLeavePvs(_pvs._sessions[index]); } catch (Exception e) { _pvs.Log.Log(LogLevel.Error, e, $"Caught exception while processing pvs-leave messages."); } } - - public void Setup(ICommonSession[] sessions) - { - // Copy references to PvsSession, in case players disconnect while the job is running. - Array.Resize(ref _sessions, sessions.Length); - for (var i = 0; i < sessions.Length; i++) - { - _sessions[i] = _pvs.PlayerData[sessions[i]]; - } - } } } diff --git a/Robust.Server/GameStates/PvsSystem.Send.cs b/Robust.Server/GameStates/PvsSystem.Send.cs new file mode 100644 index 00000000000..382c4a7bc57 --- /dev/null +++ b/Robust.Server/GameStates/PvsSystem.Send.cs @@ -0,0 +1,85 @@ +using System; +using System.Threading.Tasks; +using Prometheus; +using Robust.Shared.Log; +using Robust.Shared.Network.Messages; +using Robust.Shared.Player; +using Robust.Shared.Utility; + +namespace Robust.Server.GameStates; + +internal sealed partial class PvsSystem +{ + /// + /// Compress and send game states to connected clients. + /// + private void SendStates() + { + // TODO PVS make this async + // AFAICT ForEachAsync doesn't support using a threadlocal PvsThreadResources. + // Though if it is getting pooled, does it really matter? + + // If this does get run async, then ProcessDisconnections() has to ensure that the job has finished before modifying + // the sessions array + + using var _ = Histogram.WithLabels("Send States").NewTimer(); + var opts = new ParallelOptions {MaxDegreeOfParallelism = _parallelMgr.ParallelProcessCount}; + Parallel.ForEach(_sessions, opts, _threadResourcesPool.Get, SendSessionState, _threadResourcesPool.Return); + } + + private PvsThreadResources SendSessionState(PvsSession data, ParallelLoopState state, PvsThreadResources resource) + { + try + { + SendSessionState(data, resource.CompressionContext); + } + catch (Exception e) + { + Log.Log(LogLevel.Error, e, $"Caught exception while sending mail for {data.Session}."); + } + + return resource; + } + + private void SendSessionState(PvsSession data, ZStdCompressionContext ctx) + { + DebugTools.AssertEqual(data.State, null); + + // PVS benchmarks use dummy sessions. + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + if (data.Session.Channel is not DummyChannel) + { + DebugTools.AssertNotEqual(data.StateStream, null); + var msg = new MsgState + { + StateStream = data.StateStream, + ForceSendReliably = data.ForceSendReliably, + CompressionContext = ctx + }; + + _netMan.ServerSendMessage(msg, data.Session.Channel); + if (msg.ShouldSendReliably()) + { + data.RequestedFull = false; + data.LastReceivedAck = _gameTiming.CurTick; + lock (PendingAcks) + { + PendingAcks.Add(data.Session); + } + } + } + else + { + // Always "ack" dummy sessions. + data.LastReceivedAck = _gameTiming.CurTick; + data.RequestedFull = false; + lock (PendingAcks) + { + PendingAcks.Add(data.Session); + } + } + + data.StateStream?.Dispose(); + data.StateStream = null; + } +} diff --git a/Robust.Server/GameStates/PvsSystem.Serialize.cs b/Robust.Server/GameStates/PvsSystem.Serialize.cs new file mode 100644 index 00000000000..ff3fd0812fc --- /dev/null +++ b/Robust.Server/GameStates/PvsSystem.Serialize.cs @@ -0,0 +1,73 @@ +using System; +using System.Threading.Tasks; +using Prometheus; +using Robust.Shared.GameObjects; +using Robust.Shared.GameStates; +using Robust.Shared.IoC; +using Robust.Shared.Log; +using Robust.Shared.Player; +using Robust.Shared.Serialization; +using Robust.Shared.Timing; +using Robust.Shared.Utility; + +namespace Robust.Server.GameStates; + +internal sealed partial class PvsSystem +{ + [Dependency] private readonly IRobustSerializer _serializer = default!; + + /// + /// Get and serialize objects for each player. Compressing & sending the states is done later. + /// + private void SerializeStates() + { + using var _ = Histogram.WithLabels("Serialize States").NewTimer(); + var opts = new ParallelOptions {MaxDegreeOfParallelism = _parallelMgr.ParallelProcessCount}; + _oldestAck = GameTick.MaxValue.Value; + Parallel.For(-1, _sessions.Length, opts, SerializeState); + } + + /// + /// Get and serialize a for a single session (or the current replay). + /// + private void SerializeState(int i) + { + try + { + var guid = i >= 0 ? _sessions[i].Session.UserId.UserId : default; + ServerGameStateManager.PvsEventSource.Log.WorkStart(_gameTiming.CurTick.Value, i, guid); + + if (i >= 0) + SerializeSessionState(_sessions[i]); + else + _replay.Update(); + + ServerGameStateManager.PvsEventSource.Log.WorkStop(_gameTiming.CurTick.Value, i, guid); + } + catch (Exception e) // Catch EVERY exception + { + var source = i >= 0 ? _sessions[i].Session.ToString() : "replays"; + Log.Log(LogLevel.Error, e, $"Caught exception while serializing game state for {source}."); + } + } + + /// + /// Get and serialize a for a single session. + /// + private void SerializeSessionState(PvsSession data) + { + ComputeSessionState(data); + InterlockedHelper.Min(ref _oldestAck, data.FromTick.Value); + DebugTools.AssertEqual(data.StateStream, null); + + // PVS benchmarks use dummy sessions. + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + if (data.Session.Channel is not DummyChannel) + { + data.StateStream = RobustMemoryManager.GetMemoryStream(); + _serializer.SerializeDirect(data.StateStream, data.State); + } + + data.ClearState(); + } +} diff --git a/Robust.Server/GameStates/PvsSystem.Session.cs b/Robust.Server/GameStates/PvsSystem.Session.cs index 264cd7341cf..9e3a87dee40 100644 --- a/Robust.Server/GameStates/PvsSystem.Session.cs +++ b/Robust.Server/GameStates/PvsSystem.Session.cs @@ -7,8 +7,6 @@ using Robust.Shared.GameObjects; using Robust.Shared.GameStates; using Robust.Shared.Map; -using Robust.Shared.Network; -using Robust.Shared.Network.Messages; using Robust.Shared.Player; using Robust.Shared.Timing; using Robust.Shared.Utility; @@ -27,49 +25,6 @@ internal sealed partial class PvsSystem private List _disconnected = new(); - private void SendStateUpdate(ICommonSession session, PvsThreadResources resources) - { - var data = GetOrNewPvsSession(session); - ComputeSessionState(data); - - InterlockedHelper.Min(ref _oldestAck, data.FromTick.Value); - - // actually send the state - var msg = new MsgState - { - State = data.State, - CompressionContext = resources.CompressionContext - }; - - // PVS benchmarks use dummy sessions. - // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract - if (session.Channel is not DummyChannel) - { - _netMan.ServerSendMessage(msg, session.Channel); - if (msg.ShouldSendReliably()) - { - data.RequestedFull = false; - data.LastReceivedAck = _gameTiming.CurTick; - lock (PendingAcks) - { - PendingAcks.Add(session); - } - } - } - else - { - // Always "ack" dummy sessions. - data.LastReceivedAck = _gameTiming.CurTick; - data.RequestedFull = false; - lock (PendingAcks) - { - PendingAcks.Add(session); - } - } - - data.ClearState(); - } - private PvsSession GetOrNewPvsSession(ICommonSession session) { if (!PlayerData.TryGetValue(session, out var pvsSession)) @@ -104,7 +59,7 @@ internal void ComputeSessionState(PvsSession session) session.PlayerStates, _deletedEntities); - session.State.ForceSendReliably = session.RequestedFull + session.ForceSendReliably = session.RequestedFull || _gameTiming.CurTick > session.LastReceivedAck + (uint) ForceAckThreshold; } @@ -125,14 +80,17 @@ private void UpdateSession(PvsSession session) // Update visibility masks & viewer positions // TODO PVS do this before sending state. // I,e, we already enumerate over all eyes when computing visible chunks. - Span positions = stackalloc MapCoordinates[session.Viewers.Length]; + Span<(MapCoordinates pos, float scale)> positions = stackalloc (MapCoordinates, float)[session.Viewers.Length]; int i = 0; foreach (var viewer in session.Viewers) { if (viewer.Comp2 != null) session.VisMask |= viewer.Comp2.VisibilityMask; - positions[i++] = _transform.GetMapCoordinates(viewer.Owner, viewer.Comp1); + var mapCoordinates = _transform.GetMapCoordinates(viewer.Owner, viewer.Comp1); + mapCoordinates = mapCoordinates.Offset(viewer.Comp2?.Offset ?? Vector2.Zero); + var scale = MathF.Max((viewer.Comp2?.PvsScale ?? 1), 0.1f); + positions[i++] = (mapCoordinates, scale); } if (!CullingEnabled || session.DisableCulling) @@ -157,7 +115,7 @@ private void UpdateSession(PvsSession session) DebugTools.Assert(!chunk.UpdateQueued); DebugTools.Assert(!chunk.Dirty); - foreach (var pos in positions) + foreach (var (pos, scale) in positions) { if (pos.MapId != chunk.Position.MapId) continue; @@ -165,8 +123,9 @@ private void UpdateSession(PvsSession session) dist = Math.Min(dist, (pos.Position - chunk.Position.Position).LengthSquared()); var relative = Vector2.Transform(pos.Position, chunk.InvWorldMatrix) - chunk.Centre; + relative = Vector2.Abs(relative); - chebDist = Math.Min(chebDist, Math.Max(relative.X, relative.Y)); + chebDist = Math.Min(chebDist, Math.Max(relative.X, relative.Y) / scale); } distances.Add(dist); diff --git a/Robust.Server/GameStates/PvsSystem.cs b/Robust.Server/GameStates/PvsSystem.cs index b3a051c09b4..7a53079a01e 100644 --- a/Robust.Server/GameStates/PvsSystem.cs +++ b/Robust.Server/GameStates/PvsSystem.cs @@ -3,10 +3,8 @@ using System.Diagnostics; using System.Linq; using System.Numerics; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; -using System.Threading.Tasks; using Microsoft.Extensions.ObjectPool; using Prometheus; using Robust.Server.Configuration; @@ -16,9 +14,6 @@ using Robust.Shared; using Robust.Shared.Configuration; using Robust.Shared.GameObjects; -using Robust.Shared.GameStates; -using Robust.Shared.IoC; -using Robust.Shared.Log; using Robust.Shared.Map; using Robust.Shared.Network; using Robust.Shared.Player; @@ -99,6 +94,10 @@ internal sealed partial class PvsSystem : EntitySystem ///
private readonly List _deletedTick = new(); + /// + /// The sessions that are currently being processed. Note that this is in general used by parallel & async tasks. + /// Hence player disconnection processing is deferred and only run via . + /// private PvsSession[] _sessions = default!; private bool _async; @@ -183,52 +182,25 @@ public override void Update(float frameTime) /// internal void SendGameStates(ICommonSession[] players) { + // Wait for pending jobs and process disconnected players + ProcessDisconnections(); + // Ensure each session has a PvsSession entry before starting any parallel jobs. CacheSessionData(players); // Get visible chunks, and update any dirty chunks. - BeforeSendState(); - - // Construct & send the game state to each player. - SendStates(players); - - // Cull deletion history - AfterSendState(players); + BeforeSerializeStates(); - ProcessLeavePvs(players); - } - - private void SendStates(ICommonSession[] players) - { - using var _ = Histogram.WithLabels("Send States").NewTimer(); + // Construct & serialize the game state for each player (and for the replay). + SerializeStates(); - var opts = new ParallelOptions {MaxDegreeOfParallelism = _parallelMgr.ParallelProcessCount}; - _oldestAck = GameTick.MaxValue.Value; - - // Replays process game states in parallel with players - Parallel.For(-1, players.Length, opts, _threadResourcesPool.Get, SendPlayer, _threadResourcesPool.Return); - - PvsThreadResources SendPlayer(int i, ParallelLoopState state, PvsThreadResources resource) - { - try - { - var guid = i >= 0 ? players[i].UserId.UserId : default; - ServerGameStateManager.PvsEventSource.Log.WorkStart(_gameTiming.CurTick.Value, i, guid); + // Compress & send the states. + SendStates(); - if (i >= 0) - SendStateUpdate(players[i], resource); - else - _replay.Update(); + // Cull deletion history + AfterSerializeStates(); - ServerGameStateManager.PvsEventSource.Log.WorkStop(_gameTiming.CurTick.Value, i, guid); - } - catch (Exception e) // Catch EVERY exception - { - var source = i >= 0 ? players[i].ToString() : "replays"; - Log.Log(LogLevel.Error, e, $"Caught exception while generating mail for {source}."); - } - return resource; - } + ProcessLeavePvs(); } private void ResetParallelism(int _) => ResetParallelism(); @@ -390,8 +362,19 @@ private void VerifySessionData(PvsSession pvsSession) private (Vector2 worldPos, float range, EntityUid? map) CalcViewBounds(Entity eye) { - var size = Math.Max(eye.Comp2?.PvsSize ?? _priorityViewSize, 1); - return (_transform.GetWorldPosition(eye.Comp1), size / 2f, eye.Comp1.MapUid); + var size = _priorityViewSize; + var worldPos = _transform.GetWorldPosition(eye.Comp1); + + if (eye.Comp2 is not null) + { + // not using EyeComponent.Eye.Position, because it's updated only on the client's side + worldPos += eye.Comp2.Offset; + size *= eye.Comp2.PvsScale; + } + + size = Math.Max(size, 1); + + return (worldPos, size / 2f, eye.Comp1.MapUid); } private void CullDeletionHistoryUntil(GameTick tick) @@ -414,11 +397,25 @@ private void CullDeletionHistoryUntil(GameTick tick) } } - private void BeforeSendState() + private void BeforeSerializeStates() { DebugTools.Assert(_chunks.Values.All(x => Exists(x.Map) && Exists(x.Root))); DebugTools.Assert(_chunkSets.Keys.All(Exists)); + var ackJob = ProcessQueuedAcks(); + + // Figure out what chunks players can see and cache some chunk data. + if (CullingEnabled) + { + GetVisibleChunks(); + ProcessVisibleChunks(); + } + + ackJob?.WaitOne(); + } + + internal void ProcessDisconnections() + { _leaveTask?.WaitOne(); _leaveTask = null; @@ -430,17 +427,6 @@ private void BeforeSendState() FreeSessionDataMemory(pvsSession); } } - - var ackJob = ProcessQueuedAcks(); - - // Figure out what chunks players can see and cache some chunk data. - if (CullingEnabled) - { - GetVisibleChunks(); - ProcessVisibleChunks(); - } - - ackJob?.WaitOne(); } internal void CacheSessionData(ICommonSession[] players) @@ -452,9 +438,9 @@ internal void CacheSessionData(ICommonSession[] players) } } - private void AfterSendState(ICommonSession[] players) + private void AfterSerializeStates() { - CleanupDirty(players); + CleanupDirty(); if (_oldestAck == GameTick.MaxValue.Value) { diff --git a/Robust.Server/Player/PlayerManager.cs b/Robust.Server/Player/PlayerManager.cs index f1673941bd8..33288620630 100644 --- a/Robust.Server/Player/PlayerManager.cs +++ b/Robust.Server/Player/PlayerManager.cs @@ -143,7 +143,6 @@ private void HandlePlayerListReq(MsgPlayerListReq message) list.Add(info); } netMsg.Plyrs = list; - netMsg.PlyCount = (byte)list.Count; channel.SendMessage(netMsg); } diff --git a/Robust.Server/Program.cs b/Robust.Server/Program.cs index e25bd548c53..52c2067165b 100644 --- a/Robust.Server/Program.cs +++ b/Robust.Server/Program.cs @@ -39,8 +39,6 @@ internal static void Start(string[] args, ServerOptions options, bool contentSta return; } - ThreadPool.SetMinThreads(Environment.ProcessorCount * 2, Environment.ProcessorCount); - ParsedMain(parsed, contentStart, options); } diff --git a/Robust.Server/ServerHub/HubManager.cs b/Robust.Server/ServerHub/HubManager.cs index 7f30cff9474..5bd07ad8509 100644 --- a/Robust.Server/ServerHub/HubManager.cs +++ b/Robust.Server/ServerHub/HubManager.cs @@ -116,7 +116,7 @@ private async void SendPing() if (!response.IsSuccessStatusCode) { var errorText = await response.Content.ReadAsStringAsync(); - _sawmill.Error("Error status while advertising server: [{StatusCode}] {ErrorText}, to {HubUrl}", + _sawmill.Error("Error status while advertising server: [{StatusCode}] {ErrorText}, from {HubUrl}", response.StatusCode, errorText, hubUrl); diff --git a/Robust.Server/ServerStatus/StatusHost.Acz.cs b/Robust.Server/ServerStatus/StatusHost.Acz.cs index dfdaf9fc1e5..e04d0fd7f14 100644 --- a/Robust.Server/ServerStatus/StatusHost.Acz.cs +++ b/Robust.Server/ServerStatus/StatusHost.Acz.cs @@ -312,14 +312,14 @@ private static bool RequestWantsZStd(IStatusHandlerContext context) } // Only call this if the download URL is not available! - private async Task PrepareAcz() + private async Task PrepareAcz(bool optional = false) { // Take the ACZ lock asynchronously await _aczLock.WaitAsync(); try { // Setting this now ensures that it won't fail repeatedly on exceptions/etc. - if (_aczPrepareAttempted) + if (_aczPrepareAttempted || optional) return _aczPrepared; _aczPrepareAttempted = true; diff --git a/Robust.Server/ServerStatus/StatusHost.Handlers.cs b/Robust.Server/ServerStatus/StatusHost.Handlers.cs index e4ef00fa279..5ab29864313 100644 --- a/Robust.Server/ServerStatus/StatusHost.Handlers.cs +++ b/Robust.Server/ServerStatus/StatusHost.Handlers.cs @@ -80,8 +80,7 @@ private async Task HandleInfo(IStatusHandlerContext context) if (string.IsNullOrEmpty(downloadUrl)) { var query = HttpUtility.ParseQueryString(context.Url.Query); - var optional = query.Keys; - buildInfo = await PrepareACZBuildInfo(); + buildInfo = await PrepareACZBuildInfo(optional: query.Get("can_skip_build") == "1"); } else { @@ -129,9 +128,9 @@ private JsonObject GetExternalBuildInfo() }; } - private async Task PrepareACZBuildInfo() + private async Task PrepareACZBuildInfo(bool optional) { - var acm = await PrepareAcz(); + var acm = await PrepareAcz(optional); if (acm == null) return null; // Fork ID is an interesting case, we don't want to cause too many redownloads but we also don't want to pollute disk. diff --git a/Robust.Shared.CompNetworkGenerator/ComponentNetworkGenerator.cs b/Robust.Shared.CompNetworkGenerator/ComponentNetworkGenerator.cs index 894e9369a84..69cd47bedd4 100644 --- a/Robust.Shared.CompNetworkGenerator/ComponentNetworkGenerator.cs +++ b/Robust.Shared.CompNetworkGenerator/ComponentNetworkGenerator.cs @@ -275,7 +275,7 @@ public class ComponentNetworkGenerator : ISourceGenerator { eventRaise = @" var ev = new AfterAutoHandleStateEvent(args.Current); - EntityManager.EventBus.RaiseComponentEvent(component, ref ev);"; + EntityManager.EventBus.RaiseComponentEvent(uid, component, ref ev);"; } return $@"// diff --git a/Robust.Shared.Maths/Color.cs b/Robust.Shared.Maths/Color.cs index 4e56b73cb9e..419cb888dc9 100644 --- a/Robust.Shared.Maths/Color.cs +++ b/Robust.Shared.Maths/Color.cs @@ -26,6 +26,7 @@ using System; using System.Collections.Frozen; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Runtime.CompilerServices; @@ -35,11 +36,6 @@ using SysVector3 = System.Numerics.Vector3; using SysVector4 = System.Numerics.Vector4; -#if NETCOREAPP -using System.Runtime.Intrinsics; -using System.Runtime.Intrinsics.X86; -#endif - namespace Robust.Shared.Maths { /// @@ -50,37 +46,43 @@ namespace Robust.Shared.Maths public struct Color : IEquatable, ISpanFormattable { /// - /// The red component of this Color4 structure. + /// The red component of this Color structure. /// public float R; /// - /// The green component of this Color4 structure. + /// The green component of this Color structure. /// public float G; /// - /// The blue component of this Color4 structure. + /// The blue component of this Color structure. /// public float B; /// - /// The alpha component of this Color4 structure. + /// The alpha component of this Color structure. /// public float A; + /// + /// Vector representation, for easy SIMD operations. + /// + // ReSharper disable once InconsistentNaming + public readonly SysVector4 RGBA => Unsafe.BitCast(this); + public readonly byte RByte => (byte) (R * byte.MaxValue); public readonly byte GByte => (byte) (G * byte.MaxValue); public readonly byte BByte => (byte) (B * byte.MaxValue); public readonly byte AByte => (byte) (A * byte.MaxValue); /// - /// Constructs a new Color4 structure from the specified components. + /// Constructs a new structure from the specified components. /// - /// The red component of the new Color4 structure. - /// The green component of the new Color4 structure. - /// The blue component of the new Color4 structure. - /// The alpha component of the new Color4 structure. + /// The red component of the new Color structure. + /// The green component of the new Color structure. + /// The blue component of the new Color structure. + /// The alpha component of the new Color structure. public Color(float r, float g, float b, float a = 1) { R = r; @@ -90,14 +92,23 @@ public Color(float r, float g, float b, float a = 1) } /// - /// Constructs a new Color4 structure from the specified components. + /// Constructs a new Color structure from the components in a . + /// + public Color(in SysVector4 vec) + { + this = Unsafe.BitCast(vec); + } + + /// + /// Constructs a new Color structure from the specified components. /// - /// The red component of the new Color4 structure. - /// The green component of the new Color4 structure. - /// The blue component of the new Color4 structure. - /// The alpha component of the new Color4 structure. + /// The red component of the new Color structure. + /// The green component of the new Color structure. + /// The blue component of the new Color structure. + /// The alpha component of the new Color structure. public Color(byte r, byte g, byte b, byte a = 255) { + Unsafe.SkipInit(out this); R = r / (float) byte.MaxValue; G = g / (float) byte.MaxValue; B = b / (float) byte.MaxValue; @@ -124,7 +135,7 @@ public readonly int ToArgb() } /// - /// Compares the specified Color4 structures for equality. + /// Compares the specified Color structures for equality. /// /// The left-hand side of the comparison. /// The right-hand side of the comparison. @@ -135,7 +146,7 @@ public readonly int ToArgb() } /// - /// Compares the specified Color4 structures for inequality. + /// Compares the specified Color structures for inequality. /// /// The left-hand side of the comparison. /// The right-hand side of the comparison. @@ -146,10 +157,10 @@ public readonly int ToArgb() } /// - /// Converts the specified System.Drawing.Color to a Color4 structure. + /// Converts the specified System.Drawing.Color to a Color structure. /// /// The System.Drawing.Color to convert. - /// A new Color4 structure containing the converted components. + /// A new Color structure containing the converted components. public static implicit operator Color(System.Drawing.Color color) { return new(color.R, color.G, color.B, color.A); @@ -181,9 +192,9 @@ public readonly void Deconstruct(out float r, out float g, out float b) } /// - /// Converts the specified Color4 to a System.Drawing.Color structure. + /// Converts the specified Color to a System.Drawing.Color structure. /// - /// The Color4 to convert. + /// The Color to convert. /// A new System.Drawing.Color structure containing the converted components. public static explicit operator System.Drawing.Color(Color color) { @@ -210,11 +221,11 @@ public static IEnumerable> GetAllDefaultColors() } /// - /// Compares whether this Color4 structure is equal to the specified object. + /// Compares whether this Color structure is equal to the specified object. /// /// An object to compare to. - /// True obj is a Color4 structure with the same components as this Color4; false otherwise. - public override readonly bool Equals(object? obj) + /// True obj is a Color structure with the same components as this Color; false otherwise. + public readonly override bool Equals(object? obj) { if (!(obj is Color)) return false; @@ -223,19 +234,19 @@ public override readonly bool Equals(object? obj) } /// - /// Calculates the hash code for this Color4 structure. + /// Calculates the hash code for this Color structure. /// - /// A System.Int32 containing the hash code of this Color4 structure. - public override readonly int GetHashCode() + /// A System.Int32 containing the hash code of this Color structure. + public readonly override int GetHashCode() { return ToArgb(); } /// - /// Creates a System.String that describes this Color4 structure. + /// Creates a System.String that describes this Color structure. /// - /// A System.String that describes this Color4 structure. - public override readonly string ToString() + /// A System.String that describes this Color structure. + public readonly override string ToString() { return $"{{(R, G, B, A) = ({R}, {G}, {B}, {A})}}"; } @@ -309,7 +320,6 @@ public readonly Color WithAlpha(byte newA) public static Color FromSrgb(Color srgb) { float r, g, b; -#if NETCOREAPP if (srgb.R <= 0.04045f) r = srgb.R / 12.92f; else @@ -324,22 +334,6 @@ public static Color FromSrgb(Color srgb) b = srgb.B / 12.92f; else b = MathF.Pow((srgb.B + 0.055f) / (1.0f + 0.055f), 2.4f); -#else - if (srgb.R <= 0.04045f) - r = srgb.R / 12.92f; - else - r = (float) Math.Pow((srgb.R + 0.055f) / (1.0f + 0.055f), 2.4f); - - if (srgb.G <= 0.04045f) - g = srgb.G / 12.92f; - else - g = (float) Math.Pow((srgb.G + 0.055f) / (1.0f + 0.055f), 2.4f); - - if (srgb.B <= 0.04045f) - b = srgb.B / 12.92f; - else - b = (float) Math.Pow((srgb.B + 0.055f) / (1.0f + 0.055f), 2.4f); -#endif return new Color(r, g, b, srgb.A); } @@ -355,7 +349,6 @@ public static Color ToSrgb(Color rgb) { float r, g, b; -#if NETCOREAPP if (rgb.R <= 0.0031308) r = 12.92f * rgb.R; else @@ -370,22 +363,6 @@ public static Color ToSrgb(Color rgb) b = 12.92f * rgb.B; else b = (1.0f + 0.055f) * MathF.Pow(rgb.B, 1.0f / 2.4f) - 0.055f; -#else - if (rgb.R <= 0.0031308) - r = 12.92f * rgb.R; - else - r = (1.0f + 0.055f) * (float) Math.Pow(rgb.R, 1.0f / 2.4f) - 0.055f; - - if (rgb.G <= 0.0031308) - g = 12.92f * rgb.G; - else - g = (1.0f + 0.055f) * (float) Math.Pow(rgb.G, 1.0f / 2.4f) - 0.055f; - - if (rgb.B <= 0.0031308) - b = 12.92f * rgb.B; - else - b = (1.0f + 0.055f) * (float) Math.Pow(rgb.B, 1.0f / 2.4f) - 0.055f; -#endif return new Color(r, g, b, rgb.A); } @@ -471,6 +448,7 @@ public static Color FromHsl(Vector4 hsl) /// Each has a range of 0.0 to 1.0. /// /// Color value to convert. + [SuppressMessage("ReSharper", "CompareOfFloatsByEqualityOperator")] public static Vector4 ToHsl(Color rgb) { var max = MathF.Max(rgb.R, MathF.Max(rgb.G, rgb.B)); @@ -582,6 +560,7 @@ public static Color FromHsv(Vector4 hsv) /// Each has a range of 0.0 to 1.0. /// /// Color value to convert. + [SuppressMessage("ReSharper", "CompareOfFloatsByEqualityOperator")] public static Vector4 ToHsv(Color rgb) { var max = MathF.Max(rgb.R, MathF.Max(rgb.G, rgb.B)); @@ -770,6 +749,7 @@ public static Color FromHcy(Vector4 hcy) /// Each has a range of 0.0 to 1.0. /// /// Color value to convert. + [SuppressMessage("ReSharper", "CompareOfFloatsByEqualityOperator")] public static Vector4 ToHcy(Color rgb) { var max = MathF.Max(rgb.R, MathF.Max(rgb.G, rgb.B)); @@ -828,23 +808,10 @@ public static Color FromCmyk(Vector4 cmyk) /// with 0.5 being 50% of both colors, 0.25 being 25% of and 75% /// . /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Color InterpolateBetween(Color α, Color β, float λ) { - if (Sse.IsSupported && Fma.IsSupported) - { - var vecA = Unsafe.As>(ref α); - var vecB = Unsafe.As>(ref β); - - vecB = Fma.MultiplyAdd(Sse.Subtract(vecB, vecA), Vector128.Create(λ), vecA); - - return Unsafe.As, Color>(ref vecB); - } - ref var svA = ref Unsafe.As(ref α); - ref var svB = ref Unsafe.As(ref β); - - var res = SysVector4.Lerp(svA, svB, λ); - - return Unsafe.As(ref res); + return new(SysVector4.Lerp(α.RGBA, β.RGBA, λ)); } public static Color? TryFromHex(ReadOnlySpan hexColor) @@ -1000,13 +967,8 @@ public static Color Blend(Color dstColor, Color srcColor, BlendFactor dstFactor, /// /// Component wise multiplication of two colors. /// - /// - /// - /// - public static Color operator *(Color a, Color b) - { - return new(a.R * b.R, a.G * b.G, a.B * b.B, a.A * b.A); - } + public static Color operator *(in Color a, in Color b) + => new(a.RGBA * b.RGBA); public readonly string ToHex() { @@ -1030,17 +992,16 @@ public readonly string ToHexNoAlpha() } /// - /// Compares whether this Color4 structure is equal to the specified Color4. + /// Compares whether this Color structure is equal to the specified Color. /// - /// The Color4 structure to compare to. - /// True if both Color4 structures contain the same components; false otherwise. + /// The Color structure to compare to. + /// True if both Color structures contain the same components; false otherwise. public readonly bool Equals(Color other) { - return - MathHelper.CloseToPercent(R, other.R) && - MathHelper.CloseToPercent(G, other.G) && - MathHelper.CloseToPercent(B, other.B) && - MathHelper.CloseToPercent(A, other.A); + // TODO COLOR why is this approximate + // This method literally doesn't do what its docstring says it does. + // If people wanted approximate equality, they can check that manually. + return MathHelper.CloseToPercent(this, other); } [PublicAPI] @@ -1942,7 +1903,7 @@ public enum BlendFactor : byte public readonly string? Name() { - return DefaultColorsInverted.TryGetValue(this, out var name) ? name : null; + return DefaultColorsInverted.GetValueOrDefault(this); } public static bool TryParse(string input, out Color color) diff --git a/Robust.Shared.Maths/MathHelper.cs b/Robust.Shared.Maths/MathHelper.cs index ed5417cb0de..43d04a6f9de 100644 --- a/Robust.Shared.Maths/MathHelper.cs +++ b/Robust.Shared.Maths/MathHelper.cs @@ -15,6 +15,7 @@ using System.Diagnostics.CodeAnalysis; using System.Numerics; using System.Runtime.CompilerServices; +using Vec4 = System.Numerics.Vector4; namespace Robust.Shared.Maths { @@ -525,6 +526,27 @@ public static bool CloseToPercent(float a, float b, double percentage = .00001) return Math.Abs(a - b) <= epsilon; } + /// + /// Returns whether two vectors are within of each other + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool CloseToPercent(Vec4 a, Vec4 b, float percentage = .00001f) + { + a = Vec4.Abs(a); + b = Vec4.Abs(b); + var p = new Vec4(percentage); + var epsilon = Vec4.Max(Vec4.Max(a, b) * p, p); + var delta = Vec4.Abs(a - b); + return delta.X <= epsilon.X && delta.Y <= epsilon.Y && delta.Z <= epsilon.Z && delta.W <= epsilon.W; + } + + /// + /// Returns whether two colours are within of each other + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool CloseToPercent(Color a, Color b, float percentage = .00001f) + => CloseToPercent(a.RGBA, b.RGBA, percentage); + /// /// Returns whether two floating point numbers are within of eachother /// diff --git a/Robust.Shared/Analyzers/ComponentNetworkGeneratorAuxiliary.cs b/Robust.Shared/Analyzers/ComponentNetworkGeneratorAuxiliary.cs index b1b2e65566e..dc08dc3b73b 100644 --- a/Robust.Shared/Analyzers/ComponentNetworkGeneratorAuxiliary.cs +++ b/Robust.Shared/Analyzers/ComponentNetworkGeneratorAuxiliary.cs @@ -39,5 +39,5 @@ public sealed class AutoNetworkedFieldAttribute : Attribute /// is true, so that other systems /// can have effects after handling state without having to redefine all replication. /// -[ByRefEvent] +[ByRefEvent, ComponentEvent] public record struct AfterAutoHandleStateEvent(IComponentState State); diff --git a/Robust.Shared/Analyzers/MustCallBaseAttribute.cs b/Robust.Shared/Analyzers/MustCallBaseAttribute.cs new file mode 100644 index 00000000000..df0ab665db7 --- /dev/null +++ b/Robust.Shared/Analyzers/MustCallBaseAttribute.cs @@ -0,0 +1,17 @@ +using System; + +namespace Robust.Shared.Analyzers; + +/// +/// Indicates that overriders of this method must always call the base function. +/// +/// +/// If true, only base calls to *overrides* are necessary. +/// This is intended for base classes where the base function is always empty, +/// so a base call from the first override may be ommitted. +/// +[AttributeUsage(AttributeTargets.Method)] +public sealed class MustCallBaseAttribute(bool onlyOverrides = false) : Attribute +{ + public bool OnlyOverrides { get; } = onlyOverrides; +} diff --git a/Robust.Shared/CVars.cs b/Robust.Shared/CVars.cs index 099d642c88e..a10fd601567 100644 --- a/Robust.Shared/CVars.cs +++ b/Robust.Shared/CVars.cs @@ -70,7 +70,7 @@ protected CVars() /// /// public static readonly CVarDef NetMtu = - CVarDef.Create("net.mtu", 900, CVar.ARCHIVE); + CVarDef.Create("net.mtu", 700, CVar.ARCHIVE); /// /// Maximum UDP payload size to send by default, for IPv6. @@ -1374,7 +1374,7 @@ protected CVars() /// the purpose of using an atlas if it gets too small. /// public static readonly CVarDef ResRSIAtlasSize = - CVarDef.Create("res.rsi_atlas_size", 8192, CVar.CLIENTONLY); + CVarDef.Create("res.rsi_atlas_size", 12288, CVar.CLIENTONLY); // TODO: Currently unimplemented. /// @@ -1560,6 +1560,12 @@ protected CVars() public static readonly CVarDef ConCompletionMargin = CVarDef.Create("con.completion_margin", 3, CVar.CLIENTONLY); + /// + /// Maximum amount of entries stored by the debug console. + /// + public static readonly CVarDef ConMaxEntries = + CVarDef.Create("con.max_entries", 3_000, CVar.CLIENTONLY); + /* * THREAD */ diff --git a/Robust.Shared/Collections/RingBufferList.cs b/Robust.Shared/Collections/RingBufferList.cs new file mode 100644 index 00000000000..2a309808449 --- /dev/null +++ b/Robust.Shared/Collections/RingBufferList.cs @@ -0,0 +1,304 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using Robust.Shared.Utility; +using ArgumentNullException = System.ArgumentNullException; + +namespace Robust.Shared.Collections; + +/// +/// Datastructure that acts like a , but is actually stored as a ring buffer internally. +/// This facilitates efficient removal from the start. +/// +/// Type of item contained in the collection. +internal sealed class RingBufferList : IList +{ + private T[] _items; + private int _read; + private int _write; + + public RingBufferList(int capacity) + { + _items = new T[capacity]; + } + + public RingBufferList() + { + _items = []; + } + + public int Capacity => _items.Length; + + private bool IsFull => _items.Length == 0 || NextIndex(_write) == _read; + + public void Add(T item) + { + if (IsFull) + Expand(); + + DebugTools.Assert(!IsFull); + + _items[_write] = item; + _write = NextIndex(_write); + } + + public void Clear() + { + _read = 0; + _write = 0; + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + Array.Clear(_items); + } + + public bool Contains(T item) + { + return IndexOf(item) >= 0; + } + + public void CopyTo(T[] array, int arrayIndex) + { + ArgumentNullException.ThrowIfNull(array); + ArgumentOutOfRangeException.ThrowIfNegative(arrayIndex); + + CopyTo(array.AsSpan(arrayIndex)); + } + + private void CopyTo(Span dest) + { + if (dest.Length < Count) + throw new ArgumentException("Not enough elements in destination!"); + + var i = 0; + foreach (var item in this) + { + dest[i++] = item; + } + } + + public bool Remove(T item) + { + var index = IndexOf(item); + if (index < 0) + return false; + + RemoveAt(index); + return true; + } + + public int Count + { + get + { + var length = _write - _read; + if (length >= 0) + return length; + + return length + _items.Length; + } + } + + public bool IsReadOnly => false; + + public int IndexOf(T item) + { + var i = 0; + foreach (var containedItem in this) + { + if (EqualityComparer.Default.Equals(item, containedItem)) + return i; + + i += 1; + } + + return -1; + } + + public void Insert(int index, T item) + { + throw new NotSupportedException(); + } + + public void RemoveAt(int index) + { + var length = Count; + ArgumentOutOfRangeException.ThrowIfNegative(index); + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, length); + + if (index == 0) + { + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + _items[_read] = default!; + + _read = NextIndex(_read); + } + else if (index == length - 1) + { + _write = WrapInv(_write - 1); + + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + _items[_write] = default!; + } + else + { + // If past me had better foresight I wouldn't be spending so much effort writing this right now. + + var realIdx = RealIndex(index); + var origValue = _items[realIdx]; + T result; + + if (realIdx < _read) + { + // Scenario one: to-remove index is after break. + // One shift is needed. + // v + // X X X O X X + // W R + DebugTools.Assert(_write < _read); + + result = ShiftDown(_items.AsSpan()[realIdx.._write], default!); + } + else if (_write < _read) + { + // Scenario two: to-remove index is before break, but write is after. + // Two shifts are needed. + // v + // X O X X X X + // W R + + var fromEnd = ShiftDown(_items.AsSpan(0, _write), default!); + result = ShiftDown(_items.AsSpan(realIdx), fromEnd); + } + else + { + // Scenario two: array is contiguous. + // One shift is needed. + // v + // X X X X O O + // R W + + result = ShiftDown(_items.AsSpan()[realIdx.._write], default!); + } + + // Just make sure we didn't bulldozer something. + DebugTools.Assert(EqualityComparer.Default.Equals(origValue, result)); + + _write = WrapInv(_write - 1); + } + } + + private static T ShiftDown(Span span, T substitution) + { + if (span.Length == 0) + return substitution; + + var first = span[0]; + span[1..].CopyTo(span[..^1]); + span[^1] = substitution!; + return first; + } + + public T this[int index] + { + get => GetSlot(index); + set => GetSlot(index) = value; + } + + private ref T GetSlot(int index) + { + ArgumentOutOfRangeException.ThrowIfNegative(index); + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, Count); + + return ref _items[RealIndex(index)]; + } + + private int RealIndex(int index) + { + return Wrap(index + _read); + } + + private int NextIndex(int index) => Wrap(index + 1); + + private int Wrap(int index) + { + if (index >= _items.Length) + index -= _items.Length; + + return index; + } + + private int WrapInv(int index) + { + if (index < 0) + index = _items.Length - 1; + + return index; + } + + private void Expand() + { + var prevSize = _items.Length; + var newSize = Math.Max(4, prevSize * 2); + Array.Resize(ref _items, newSize); + + if (_write >= _read) + return; + + // Write is behind read pointer, so we need to copy the items to be after the read pointer. + var toCopy = _items.AsSpan(0, _write); + var copyDest = _items.AsSpan(prevSize); + toCopy.CopyTo(copyDest); + + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + toCopy.Clear(); + + _write += prevSize; + } + + public Enumerator GetEnumerator() + { + return new Enumerator(this); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public struct Enumerator : IEnumerator + { + private readonly RingBufferList _ringBufferList; + private int _readPos; + + internal Enumerator(RingBufferList ringBufferList) + { + _ringBufferList = ringBufferList; + _readPos = _ringBufferList._read - 1; + } + + public bool MoveNext() + { + _readPos = _ringBufferList.NextIndex(_readPos); + return _readPos != _ringBufferList._write; + } + + public void Reset() + { + this = new Enumerator(_ringBufferList); + } + + public ref T Current => ref _ringBufferList._items[_readPos]; + + T IEnumerator.Current => Current; + object? IEnumerator.Current => Current; + + void IDisposable.Dispose() + { + } + } +} diff --git a/Robust.Shared/Console/Commands/DumpEventTablesCommand.cs b/Robust.Shared/Console/Commands/DumpEventTablesCommand.cs index aa185506abc..3d9f2a1fee3 100644 --- a/Robust.Shared/Console/Commands/DumpEventTablesCommand.cs +++ b/Robust.Shared/Console/Commands/DumpEventTablesCommand.cs @@ -34,7 +34,7 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args) { shell.WriteLine($"{evType}:"); - var idx = comps; + var idx = comps.Start; while (idx != -1) { ref var entry = ref table.ComponentLists[idx]; diff --git a/Robust.Shared/Console/Commands/TeleportCommands.cs b/Robust.Shared/Console/Commands/TeleportCommands.cs index a98a37a116f..24ec316510b 100644 --- a/Robust.Shared/Console/Commands/TeleportCommands.cs +++ b/Robust.Shared/Console/Commands/TeleportCommands.cs @@ -9,18 +9,17 @@ using Robust.Shared.Localization; using Robust.Shared.Map; using Robust.Shared.Map.Components; -using Robust.Shared.Maths; using Robust.Shared.Physics.Components; using Robust.Shared.Player; using Robust.Shared.Utility; namespace Robust.Shared.Console.Commands; -internal sealed class TeleportCommand : LocalizedCommands +internal sealed class TeleportCommand : LocalizedEntityCommands { [Dependency] private readonly IMapManager _map = default!; - [Dependency] private readonly IEntitySystemManager _entitySystem = default!; [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; public override string Command => "tp"; public override bool RequireServerOrSingleplayer => true; @@ -36,11 +35,10 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args) return; } - var xformSystem = _entitySystem.GetEntitySystem(); var transform = _entityManager.GetComponent(entity); var position = new Vector2(posX, posY); - xformSystem.AttachToGridOrMap(entity, transform); + _transform.AttachToGridOrMap(entity, transform); MapId mapId; if (args.Length == 3 && int.TryParse(args[2], out var intMapId)) @@ -56,25 +54,26 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args) if (_map.TryFindGridAt(mapId, position, out var gridUid, out var grid)) { - var gridPos = Vector2.Transform(position, xformSystem.GetInvWorldMatrix(gridUid)); + var gridPos = Vector2.Transform(position, _transform.GetInvWorldMatrix(gridUid)); - xformSystem.SetCoordinates(entity, transform, new EntityCoordinates(gridUid, gridPos)); + _transform.SetCoordinates(entity, transform, new EntityCoordinates(gridUid, gridPos)); } else { var mapEnt = _map.GetMapEntityIdOrThrow(mapId); - xformSystem.SetWorldPosition(transform, position); - xformSystem.SetParent(entity, transform, mapEnt); + _transform.SetWorldPosition(transform, position); + _transform.SetParent(entity, transform, mapEnt); } shell.WriteLine($"Teleported {shell.Player} to {mapId}:{posX},{posY}."); } } -public sealed class TeleportToCommand : LocalizedCommands +public sealed class TeleportToCommand : LocalizedEntityCommands { [Dependency] private readonly ISharedPlayerManager _players = default!; [Dependency] private readonly IEntityManager _entities = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; public override string Command => "tpto"; public override bool RequireServerOrSingleplayer => true; @@ -89,7 +88,6 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args) if (!TryGetTransformFromUidOrUsername(target, shell, out var targetUid, out _)) return; - var transformSystem = _entities.System(); var targetCoords = new EntityCoordinates(targetUid.Value, Vector2.Zero); if (_entities.TryGetComponent(targetUid, out PhysicsComponent? targetPhysics)) @@ -127,8 +125,8 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args) foreach (var victim in victims) { - transformSystem.SetCoordinates(victim.Entity, targetCoords); - transformSystem.AttachToGridOrMap(victim.Entity, victim.Transform); + _transform.SetCoordinates(victim.Entity, targetCoords); + _transform.AttachToGridOrMap(victim.Entity, victim.Transform); } } @@ -174,18 +172,14 @@ public override CompletionResult GetCompletion(IConsoleShell shell, string[] arg var hint = args.Length == 1 ? "cmd-tpto-destination-hint" : "cmd-tpto-victim-hint"; hint = Loc.GetString(hint); - - var opts = CompletionResult.FromHintOptions(users, hint); - if (last != string.Empty && !NetEntity.TryParse(last, out _)) - return opts; - - return CompletionResult.FromHintOptions(opts.Options.Concat(CompletionHelper.NetEntities(last, _entities)), hint); + return CompletionResult.FromHintOptions(users, hint); } } -sealed class LocationCommand : LocalizedCommands +sealed class LocationCommand : LocalizedEntityCommands { [Dependency] private readonly IEntityManager _ent = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; public override string Command => "loc"; @@ -197,18 +191,19 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args) var pt = _ent.GetComponent(entity); var pos = pt.Coordinates; - shell.WriteLine($"MapID:{pos.GetMapId(_ent)} GridUid:{pos.GetGridUid(_ent)} X:{pos.X:N2} Y:{pos.Y:N2}"); + var mapId = _transform.GetMapId(pos); + var gridUid = _transform.GetGrid(pos); + + shell.WriteLine($"MapID:{mapId} GridUid:{gridUid} X:{pos.X:N2} Y:{pos.Y:N2}"); } } -sealed class TpGridCommand : LocalizedCommands +sealed class TpGridCommand : LocalizedEntityCommands { [Dependency] private readonly IEntityManager _ent = default!; - [Dependency] private readonly IMapManager _map = default!; + [Dependency] private readonly SharedMapSystem _map = default!; public override string Command => "tpgrid"; - public override string Description => Loc.GetString("cmd-tpgrid-desc"); - public override string Help => Loc.GetString("cmd-tpgrid-help"); public override bool RequireServerOrSingleplayer => true; public override void Execute(IConsoleShell shell, string argStr, string[] args) @@ -251,14 +246,14 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args) mapId = new MapId(map); } - var id = _map.GetMapEntityId(mapId); + var id = _map.GetMap(mapId); if (id == EntityUid.Invalid) { shell.WriteError(Loc.GetString("cmd-parse-failure-mapid", ("arg", mapId.Value))); return; } - var pos = new EntityCoordinates(_map.GetMapEntityId(mapId), new Vector2(xPos, yPos)); + var pos = new EntityCoordinates(id, new Vector2(xPos, yPos)); _ent.System().SetCoordinates(uid.Value, pos); } diff --git a/Robust.Shared/Console/CompletionHelper.cs b/Robust.Shared/Console/CompletionHelper.cs index ff0b483993b..b804cd3ac39 100644 --- a/Robust.Shared/Console/CompletionHelper.cs +++ b/Robust.Shared/Console/CompletionHelper.cs @@ -189,27 +189,45 @@ public static IEnumerable MapUids(IEntityManager? entManager = return Components(string.Empty, entManager); } - public static IEnumerable NetEntities(string text, IEntityManager? entManager = null) + /// + /// Return all existing entities as possible completions. You should generally avoid using this unless you need to. + /// + public static IEnumerable NetEntities(string text, IEntityManager? entManager = null, int limit = 20) { - return Components(text, entManager); - } + if (!NetEntity.TryParse(text, out _)) + yield break; - public static IEnumerable Components(string text, IEntityManager? entManager = null) where T : IComponent - { IoCManager.Resolve(ref entManager); + var query = entManager.AllEntityQueryEnumerator(); - var query = entManager.AllEntityQueryEnumerator(); - - while (query.MoveNext(out var uid, out _, out var metadata)) + var i = 0; + while (i < limit && query.MoveNext(out var metadata)) { - if (!entManager.TryGetNetEntity(uid, out var netEntity, metadata: metadata)) + var netString = metadata.NetEntity.ToString(); + if (!netString.StartsWith(text)) continue; - var netString = netEntity.Value.ToString(); + i++; + yield return new CompletionOption(netString, metadata.EntityName); + } + } + + public static IEnumerable Components(string text, IEntityManager? entManager = null, int limit = 20) where T : IComponent + { + if (!NetEntity.TryParse(text, out _)) + yield break; + + IoCManager.Resolve(ref entManager); + var query = entManager.AllEntityQueryEnumerator(); + var i = 0; + while (i < limit && query.MoveNext(out _, out var metadata)) + { + var netString = metadata.NetEntity.ToString(); if (!netString.StartsWith(text)) continue; + i++; yield return new CompletionOption(netString, metadata.EntityName); } } diff --git a/Robust.Shared/Console/ConsoleHost.cs b/Robust.Shared/Console/ConsoleHost.cs index 82d68c8307f..b26f246604a 100644 --- a/Robust.Shared/Console/ConsoleHost.cs +++ b/Robust.Shared/Console/ConsoleHost.cs @@ -29,6 +29,9 @@ public abstract class ConsoleHost : IConsoleHost [Dependency] protected readonly ILocalizationManager LocalizationManager = default!; [ViewVariables] protected readonly Dictionary RegisteredCommands = new(); + [ViewVariables] private readonly HashSet _autoRegisteredCommands = []; + + private bool _isInRegistrationRegion; private readonly CommandBuffer _commandBuffer = new CommandBuffer(); @@ -61,6 +64,11 @@ public void LoadConsoleCommands() // search for all client commands in all assemblies, and register them foreach (var type in ReflectionManager.GetAllChildren()) { + // This sucks but I can't come up with anything better + // that won't just be 10x worse complexity for no gain. + if (type.IsAssignableTo(typeof(IEntityConsoleCommand))) + continue; + var instance = (IConsoleCommand)_typeFactory.CreateInstanceUnchecked(type, true); if (AvailableCommands.TryGetValue(instance.Command, out var duplicate)) { @@ -69,6 +77,7 @@ public void LoadConsoleCommands() } RegisteredCommands[instance.Command] = instance; + _autoRegisteredCommands.Add(instance.Command); } } @@ -76,6 +85,23 @@ protected virtual void UpdateAvailableCommands() { } + public void BeginRegistrationRegion() + { + if (_isInRegistrationRegion) + throw new InvalidOperationException("Cannot enter registration region twice!"); + + _isInRegistrationRegion = true; + } + + public void EndRegistrationRegion() + { + if (!_isInRegistrationRegion) + throw new InvalidOperationException("Was not in registration region."); + + _isInRegistrationRegion = false; + UpdateAvailableCommands(); + } + #region RegisterCommand public void RegisterCommand( string command, @@ -88,8 +114,7 @@ public void RegisterCommand( throw new InvalidOperationException($"Command already registered: {command}"); var newCmd = new RegisteredCommand(command, description, help, callback, requireServerOrSingleplayer); - RegisteredCommands.Add(command, newCmd); - UpdateAvailableCommands(); + RegisterCommand(newCmd); } public void RegisterCommand( @@ -104,8 +129,7 @@ public void RegisterCommand( throw new InvalidOperationException($"Command already registered: {command}"); var newCmd = new RegisteredCommand(command, description, help, callback, completionCallback, requireServerOrSingleplayer); - RegisteredCommands.Add(command, newCmd); - UpdateAvailableCommands(); + RegisterCommand(newCmd); } public void RegisterCommand( @@ -120,8 +144,7 @@ public void RegisterCommand( throw new InvalidOperationException($"Command already registered: {command}"); var newCmd = new RegisteredCommand(command, description, help, callback, completionCallback, requireServerOrSingleplayer); - RegisteredCommands.Add(command, newCmd); - UpdateAvailableCommands(); + RegisterCommand(newCmd); } public void RegisterCommand(string command, ConCommandCallback callback, @@ -153,6 +176,15 @@ public void RegisterCommand( var help = LocalizationManager.TryGetString($"cmd-{command}-help", out var val) ? val : ""; RegisterCommand(command, description, help, callback, completionCallback, requireServerOrSingleplayer); } + + public void RegisterCommand(IConsoleCommand command) + { + RegisteredCommands.Add(command.Command, command); + + if (!_isInRegistrationRegion) + UpdateAvailableCommands(); + } + #endregion /// @@ -161,12 +193,14 @@ public void UnregisterCommand(string command) if (!RegisteredCommands.TryGetValue(command, out var cmd)) throw new KeyNotFoundException($"Command {command} is not registered."); - if (cmd is not RegisteredCommand) + if (_autoRegisteredCommands.Contains(command)) throw new InvalidOperationException( "You cannot unregister commands that have been registered automatically."); RegisteredCommands.Remove(command); - UpdateAvailableCommands(); + + if (!_isInRegistrationRegion) + UpdateAvailableCommands(); } //TODO: Pull up diff --git a/Robust.Shared/Console/EntityConsoleHost.cs b/Robust.Shared/Console/EntityConsoleHost.cs new file mode 100644 index 00000000000..ede5fd364a2 --- /dev/null +++ b/Robust.Shared/Console/EntityConsoleHost.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Reflection; +using Robust.Shared.Utility; + +namespace Robust.Shared.Console; + +/// +/// Manages registration for "entity" console commands. +/// +/// +/// See for details on what "entity" console commands are. +/// +internal sealed class EntityConsoleHost +{ + [Dependency] private readonly IConsoleHost _consoleHost = default!; + [Dependency] private readonly IReflectionManager _reflectionManager = default!; + [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; + + private readonly HashSet _entityCommands = []; + + public void Startup() + { + DebugTools.Assert(_entityCommands.Count == 0); + + var deps = ((EntitySystemManager)_entitySystemManager).SystemDependencyCollection; + + _consoleHost.BeginRegistrationRegion(); + + // search for all client commands in all assemblies, and register them + foreach (var type in _reflectionManager.GetAllChildren()) + { + var instance = (IConsoleCommand)Activator.CreateInstance(type)!; + deps.InjectDependencies(instance, oneOff: true); + + _entityCommands.Add(instance.Command); + _consoleHost.RegisterCommand(instance); + } + + _consoleHost.EndRegistrationRegion(); + } + + public void Shutdown() + { + foreach (var command in _entityCommands) + { + _consoleHost.UnregisterCommand(command); + } + + _entityCommands.Clear(); + } +} diff --git a/Robust.Shared/Console/IConsoleCommand.cs b/Robust.Shared/Console/IConsoleCommand.cs index 46c705acaaf..1ab9785bd9b 100644 --- a/Robust.Shared/Console/IConsoleCommand.cs +++ b/Robust.Shared/Console/IConsoleCommand.cs @@ -83,4 +83,11 @@ ValueTask GetCompletionAsync(IConsoleShell shell, string[] arg return ValueTask.FromResult(GetCompletion(shell, args)); } } + + /// + /// Special marker interface used to indicate "entity" commands. + /// See for an overview. + /// + /// + internal interface IEntityConsoleCommand : IConsoleCommand; } diff --git a/Robust.Shared/Console/IConsoleHost.cs b/Robust.Shared/Console/IConsoleHost.cs index 223b8b51dc3..de28453aa76 100644 --- a/Robust.Shared/Console/IConsoleHost.cs +++ b/Robust.Shared/Console/IConsoleHost.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using Robust.Shared.Player; +using Robust.Shared.Reflection; using Robust.Shared.Utility; namespace Robust.Shared.Console @@ -173,6 +174,33 @@ void RegisterCommand( ConCommandCallback callback, ConCommandCompletionAsyncCallback completionCallback, bool requireServerOrSingleplayer = false); + + /// + /// Register an existing console command instance directly. + /// + /// + /// For this to be useful, the command has to be somehow excluded from automatic registration, + /// such as by using the . + /// + /// The command to register. + /// + void RegisterCommand(IConsoleCommand command); + + /// + /// Begin a region for registering many console commands in one go. + /// The region can be ended with . + /// + /// + /// Commands registered inside this region temporarily suppress some updating + /// logic that would cause significant wasted work. This logic runs when the region is ended instead. + /// + void BeginRegistrationRegion(); + + /// + /// End a registration region started with . + /// + void EndRegistrationRegion(); + #endregion /// diff --git a/Robust.Shared/Console/LocalizedCommands.cs b/Robust.Shared/Console/LocalizedCommands.cs index f8266971fee..13672f3572a 100644 --- a/Robust.Shared/Console/LocalizedCommands.cs +++ b/Robust.Shared/Console/LocalizedCommands.cs @@ -34,3 +34,17 @@ public virtual ValueTask GetCompletionAsync(IConsoleShell shel return ValueTask.FromResult(GetCompletion(shell, args)); } } + +/// +/// Base class for localized console commands that run in "entity space". +/// +/// +/// +/// This type of command is registered only while the entity system is active. +/// On the client this means that the commands are only available while connected to a server or in single player. +/// +/// +/// These commands are allowed to take dependencies on entity systems, reducing boilerplate for many usages. +/// +/// +public abstract class LocalizedEntityCommands : LocalizedCommands, IEntityConsoleCommand; diff --git a/Robust.Shared/Containers/SharedContainerSystem.Insert.cs b/Robust.Shared/Containers/SharedContainerSystem.Insert.cs index 207df1d64d7..8e7dab6feb5 100644 --- a/Robust.Shared/Containers/SharedContainerSystem.Insert.cs +++ b/Robust.Shared/Containers/SharedContainerSystem.Insert.cs @@ -40,8 +40,8 @@ public bool Insert(Entity reference) + { + _sawmill.Info("Started search for originator of bad references..."); + + var refs = reference.ToHashSet(); + ExpandReferences(reader, refs); + + foreach (var methodDefHandle in reader.MethodDefinitions) + { + var methodDef = reader.GetMethodDefinition(methodDefHandle); + if (methodDef.RelativeVirtualAddress == 0) + continue; + + var methodName = reader.GetString(methodDef.Name); + + var body = peReader.GetMethodBody(methodDef.RelativeVirtualAddress); + var bytes = body.GetILBytes()!; + + var ilReader = new ILReader(bytes); + var prefPosition = 0; + while (ilReader.MoveNext(out var instruction)) + { + if (instruction.TryGetEntityHandle(out var handle)) + { + if (refs.Contains(handle)) + { + var type = GetTypeFromDefinition(reader, methodDef.GetDeclaringType()); + _sawmill.Error( + $"Found reference to {DisplayHandle(reader, handle)} in method {type}.{methodName} at IL 0x{prefPosition:X4}"); + } + } + + prefPosition = ilReader.Position; + } + } + } + + private static string DisplayHandle(MetadataReader reader, EntityHandle handle) + { + switch (handle.Kind) + { + case HandleKind.MemberReference: + var memberRef = reader.GetMemberReference((MemberReferenceHandle)handle); + var name = reader.GetString(memberRef.Name); + var parent = DisplayHandle(reader, memberRef.Parent); + return $"{parent}.{name}"; + + case HandleKind.TypeReference: + return $"{ParseTypeReference(reader, (TypeReferenceHandle)handle)}"; + + case HandleKind.TypeSpecification: + var typeSpec = reader.GetTypeSpecification((TypeSpecificationHandle)handle); + var provider = new TypeProvider(); + var type = typeSpec.DecodeSignature(provider, 0); + return $"{type}"; + + default: + return $"({handle.Kind} handle)"; + } + } + + private static void ExpandReferences(MetadataReader reader, HashSet handles) + { + var toAdd = new List(); + + foreach (var memberRefHandle in reader.MemberReferences) + { + var memberRef = reader.GetMemberReference(memberRefHandle); + if (handles.Contains(memberRef.Parent)) + { + toAdd.Add(memberRefHandle); + } + } + + handles.UnionWith(toAdd); + } + + private readonly struct ILInstruction + { + public readonly ILOpCode OpCode; + public readonly long Argument; + public readonly int[]? SwitchTargets; + + public ILInstruction(ILOpCode opCode) + { + OpCode = opCode; + } + + public ILInstruction(ILOpCode opCode, long argument) + { + OpCode = opCode; + Argument = argument; + } + + public ILInstruction(ILOpCode opCode, long argument, int[] switchTargets) + { + OpCode = opCode; + Argument = argument; + SwitchTargets = switchTargets; + } + + public bool TryGetEntityHandle(out EntityHandle handle) + { + switch (OpCode) + { + case ILOpCode.Call: + case ILOpCode.Callvirt: + case ILOpCode.Newobj: + case ILOpCode.Jmp: + case ILOpCode.Box: + case ILOpCode.Castclass: + case ILOpCode.Cpobj: + case ILOpCode.Initobj: + case ILOpCode.Isinst: + case ILOpCode.Ldelem: + case ILOpCode.Ldelema: + case ILOpCode.Ldfld: + case ILOpCode.Ldflda: + case ILOpCode.Ldobj: + case ILOpCode.Ldstr: + case ILOpCode.Ldtoken: + case ILOpCode.Ldvirtftn: + case ILOpCode.Mkrefany: + case ILOpCode.Newarr: + case ILOpCode.Refanyval: + case ILOpCode.Sizeof: + case ILOpCode.Stelem: + case ILOpCode.Stfld: + case ILOpCode.Stobj: + case ILOpCode.Stsfld: + case ILOpCode.Throw: + case ILOpCode.Unbox_any: + handle = Unsafe.BitCast((int)Argument); + return true; + + default: + handle = default; + return false; + } + } + } + + private sealed class ILReader(byte[] body) + { + public int Position; + + public bool MoveNext(out ILInstruction instruction) + { + if (Position >= body.Length) + { + instruction = default; + return false; + } + + var firstByte = body[Position++]; + var opCode = (ILOpCode)firstByte; + if (firstByte == 0xFE) + opCode = 0xFE00 + (ILOpCode)body[Position++]; + + switch (opCode) + { + // no args. + case ILOpCode.Readonly: + case ILOpCode.Tail: + case ILOpCode.Volatile: + case ILOpCode.Add: + case ILOpCode.Add_ovf: + case ILOpCode.Add_ovf_un: + case ILOpCode.And: + case ILOpCode.Arglist: + case ILOpCode.Break: + case ILOpCode.Ceq: + case ILOpCode.Cgt: + case ILOpCode.Cgt_un: + case ILOpCode.Ckfinite: + case ILOpCode.Clt: + case ILOpCode.Clt_un: + case ILOpCode.Conv_i1: + case ILOpCode.Conv_i2: + case ILOpCode.Conv_i4: + case ILOpCode.Conv_i8: + case ILOpCode.Conv_r4: + case ILOpCode.Conv_r8: + case ILOpCode.Conv_u1: + case ILOpCode.Conv_u2: + case ILOpCode.Conv_u4: + case ILOpCode.Conv_u8: + case ILOpCode.Conv_i: + case ILOpCode.Conv_u: + case ILOpCode.Conv_r_un: + case ILOpCode.Conv_ovf_i1: + case ILOpCode.Conv_ovf_i2: + case ILOpCode.Conv_ovf_i4: + case ILOpCode.Conv_ovf_i8: + case ILOpCode.Conv_ovf_u4: + case ILOpCode.Conv_ovf_u8: + case ILOpCode.Conv_ovf_i: + case ILOpCode.Conv_ovf_u: + case ILOpCode.Conv_ovf_i1_un: + case ILOpCode.Conv_ovf_i2_un: + case ILOpCode.Conv_ovf_i4_un: + case ILOpCode.Conv_ovf_i8_un: + case ILOpCode.Conv_ovf_u4_un: + case ILOpCode.Conv_ovf_u8_un: + case ILOpCode.Conv_ovf_i_un: + case ILOpCode.Conv_ovf_u_un: + case ILOpCode.Cpblk: + case ILOpCode.Div: + case ILOpCode.Div_un: + case ILOpCode.Dup: + case ILOpCode.Endfilter: + case ILOpCode.Endfinally: + case ILOpCode.Initblk: + case ILOpCode.Ldarg_0: + case ILOpCode.Ldarg_1: + case ILOpCode.Ldarg_2: + case ILOpCode.Ldarg_3: + case ILOpCode.Ldc_i4_0: + case ILOpCode.Ldc_i4_1: + case ILOpCode.Ldc_i4_2: + case ILOpCode.Ldc_i4_3: + case ILOpCode.Ldc_i4_4: + case ILOpCode.Ldc_i4_5: + case ILOpCode.Ldc_i4_6: + case ILOpCode.Ldc_i4_7: + case ILOpCode.Ldc_i4_8: + case ILOpCode.Ldc_i4_m1: + case ILOpCode.Ldind_i1: + case ILOpCode.Ldind_u1: + case ILOpCode.Ldind_i2: + case ILOpCode.Ldind_u2: + case ILOpCode.Ldind_i4: + case ILOpCode.Ldind_u4: + case ILOpCode.Ldind_i8: + case ILOpCode.Ldind_i: + case ILOpCode.Ldind_r4: + case ILOpCode.Ldind_r8: + case ILOpCode.Ldind_ref: + case ILOpCode.Ldloc_0: + case ILOpCode.Ldloc_1: + case ILOpCode.Ldloc_2: + case ILOpCode.Ldloc_3: + case ILOpCode.Ldnull: + case ILOpCode.Localloc: + case ILOpCode.Mul: + case ILOpCode.Mul_ovf: + case ILOpCode.Mul_ovf_un: + case ILOpCode.Neg: + case ILOpCode.Nop: + case ILOpCode.Not: + case ILOpCode.Or: + case ILOpCode.Pop: + case ILOpCode.Rem: + case ILOpCode.Rem_un: + case ILOpCode.Ret: + case ILOpCode.Shl: + case ILOpCode.Shr: + case ILOpCode.Shr_un: + case ILOpCode.Stind_i1: + case ILOpCode.Stind_i2: + case ILOpCode.Stind_i4: + case ILOpCode.Stind_i8: + case ILOpCode.Stind_r4: + case ILOpCode.Stind_r8: + case ILOpCode.Stind_i: + case ILOpCode.Stind_ref: + case ILOpCode.Stloc_0: + case ILOpCode.Stloc_1: + case ILOpCode.Stloc_2: + case ILOpCode.Stloc_3: + case ILOpCode.Sub: + case ILOpCode.Sub_ovf: + case ILOpCode.Sub_ovf_un: + case ILOpCode.Xor: + case ILOpCode.Ldelem_i1: + case ILOpCode.Ldelem_u1: + case ILOpCode.Ldelem_i2: + case ILOpCode.Ldelem_u2: + case ILOpCode.Ldelem_i4: + case ILOpCode.Ldelem_u4: + case ILOpCode.Ldelem_i8: + case ILOpCode.Ldelem_i: + case ILOpCode.Ldelem_r4: + case ILOpCode.Ldelem_r8: + case ILOpCode.Ldelem_ref: + case ILOpCode.Ldlen: + case ILOpCode.Refanytype: + case ILOpCode.Rethrow: + case ILOpCode.Stelem_i1: + case ILOpCode.Stelem_i2: + case ILOpCode.Stelem_i4: + case ILOpCode.Stelem_i8: + case ILOpCode.Stelem_i: + case ILOpCode.Stelem_r4: + case ILOpCode.Stelem_r8: + case ILOpCode.Stelem_ref: + case ILOpCode.Throw: + instruction = new ILInstruction(opCode); + break; + + // 1-byte arg. + case ILOpCode.Unaligned: + case ILOpCode.Beq_s: + case ILOpCode.Bge_s: + case ILOpCode.Bge_un_s: + case ILOpCode.Bgt_s: + case ILOpCode.Bgt_un_s: + case ILOpCode.Ble_s: + case ILOpCode.Ble_un_s: + case ILOpCode.Blt_s: + case ILOpCode.Blt_un_s: + case ILOpCode.Bne_un_s: + case ILOpCode.Br_s: + case ILOpCode.Brfalse_s: + case ILOpCode.Brtrue_s: + case ILOpCode.Ldarg_s: + case ILOpCode.Ldarga_s: + case ILOpCode.Ldc_i4_s: + case ILOpCode.Ldloc_s: + case ILOpCode.Ldloca_s: + case ILOpCode.Leave_s: + case ILOpCode.Starg_s: + case ILOpCode.Stloc_s: + instruction = new ILInstruction(opCode, body[Position]); + Position += 1; + break; + + // 2-byte value + case ILOpCode.Ldarg: + case ILOpCode.Ldarga: + case ILOpCode.Ldloc: + case ILOpCode.Ldloca: + case ILOpCode.Starg: + case ILOpCode.Stloc: + var shortValue = BinaryPrimitives.ReadInt16LittleEndian(body.AsSpan(Position, 2)); + Position += 2; + instruction = new ILInstruction(opCode, shortValue); + break; + + // 4-byte value + case ILOpCode.Constrained: + case ILOpCode.Beq: + case ILOpCode.Bge: + case ILOpCode.Bge_un: + case ILOpCode.Bgt: + case ILOpCode.Bgt_un: + case ILOpCode.Ble: + case ILOpCode.Ble_un: + case ILOpCode.Blt: + case ILOpCode.Blt_un: + case ILOpCode.Bne_un: + case ILOpCode.Br: + case ILOpCode.Brfalse: + case ILOpCode.Brtrue: + case ILOpCode.Call: + case ILOpCode.Calli: + case ILOpCode.Jmp: + case ILOpCode.Ldc_i4: + case ILOpCode.Ldc_r4: + case ILOpCode.Ldftn: + case ILOpCode.Leave: + case ILOpCode.Box: + case ILOpCode.Callvirt: + case ILOpCode.Castclass: + case ILOpCode.Cpobj: + case ILOpCode.Initobj: + case ILOpCode.Isinst: + case ILOpCode.Ldelem: + case ILOpCode.Ldelema: + case ILOpCode.Ldfld: + case ILOpCode.Ldflda: + case ILOpCode.Ldobj: + case ILOpCode.Ldsfld: + case ILOpCode.Ldsflda: + case ILOpCode.Ldstr: + case ILOpCode.Ldtoken: + case ILOpCode.Ldvirtftn: + case ILOpCode.Mkrefany: + case ILOpCode.Newarr: + case ILOpCode.Newobj: + case ILOpCode.Refanyval: + case ILOpCode.Sizeof: + case ILOpCode.Stelem: + case ILOpCode.Stfld: + case ILOpCode.Stobj: + case ILOpCode.Stsfld: + case ILOpCode.Unbox: + case ILOpCode.Unbox_any: + var intValue = BinaryPrimitives.ReadInt32LittleEndian(body.AsSpan(Position, 4)); + Position += 4; + instruction = new ILInstruction(opCode, intValue); + break; + + // 8-byte value + case ILOpCode.Ldc_i8: + case ILOpCode.Ldc_r8: + var longValue = BinaryPrimitives.ReadInt64LittleEndian(body.AsSpan(Position, 8)); + Position += 8; + instruction = new ILInstruction(opCode, longValue); + break; + + // Switch + case ILOpCode.Switch: + var switchLength = BinaryPrimitives.ReadInt32LittleEndian(body.AsSpan(Position, 4)); + Position += 4; + var switchArgs = new int[switchLength]; + for (var i = 0; i < switchLength; i++) + { + switchArgs[i] = BinaryPrimitives.ReadInt32LittleEndian(body.AsSpan(Position, 4)); + Position += 4; + } + + instruction = new ILInstruction(opCode, switchLength, switchArgs); + break; + + default: + throw new InvalidDataException($"Unknown opcode: {opCode}"); + } + + return true; + } + } +} + +#endif diff --git a/Robust.Shared/ContentPack/AssemblyTypeChecker.cs b/Robust.Shared/ContentPack/AssemblyTypeChecker.cs index 5abc486cfc5..e00ba9f9c69 100644 --- a/Robust.Shared/ContentPack/AssemblyTypeChecker.cs +++ b/Robust.Shared/ContentPack/AssemblyTypeChecker.cs @@ -148,7 +148,7 @@ public bool CheckAssembly(Stream assembly, Resolver resolver) if ((Dump & DumpFlags.Types) != 0) { - foreach (var mType in types) + foreach (var (_, mType) in types) { _sawmill.Debug($"RefType: {mType}"); } @@ -156,7 +156,7 @@ public bool CheckAssembly(Stream assembly, Resolver resolver) if ((Dump & DumpFlags.Members) != 0) { - foreach (var memberRef in members) + foreach (var (_, memberRef) in members) { _sawmill.Debug($"RefMember: {memberRef}"); } @@ -183,14 +183,17 @@ public bool CheckAssembly(Stream assembly, Resolver resolver) var loadedConfig = _config.Result; #pragma warning restore RA0004 + var badRefs = new ConcurrentBag(); + // We still do explicit type reference scanning, even though the actual whitelists work with raw members. // This is so that we can simplify handling of generic type specifications during member checking: // we won't have to check that any types in their type arguments are whitelisted. - foreach (var type in types) + foreach (var (handle, type) in types) { if (!IsTypeAccessAllowed(loadedConfig, type, out _)) { errors.Add(new SandboxError($"Access to type not allowed: {type}")); + badRefs.Add(handle); } } @@ -208,13 +211,20 @@ public bool CheckAssembly(Stream assembly, Resolver resolver) _sawmill.Debug($"Type abuse... {fullStopwatch.ElapsedMilliseconds}ms"); - CheckMemberReferences(loadedConfig, members, errors); + CheckMemberReferences(loadedConfig, members, errors, badRefs); foreach (var error in errors) { _sawmill.Error($"Sandbox violation: {error.Message}"); } +#if TOOLS + if (!badRefs.IsEmpty) + { + ReportBadReferences(peReader, reader, badRefs); + } +#endif + _sawmill.Debug($"Checked assembly in {fullStopwatch.ElapsedMilliseconds}ms"); return errors.IsEmpty; @@ -351,11 +361,13 @@ private static void CheckNoTypeAbuse(MetadataReader reader, ConcurrentBag members, - ConcurrentBag errors) + List<(MemberReferenceHandle handle, MMemberRef parsed)> members, + ConcurrentBag errors, + ConcurrentBag badReferences) { - Parallel.ForEach(members, memberRef => + Parallel.ForEach(members, entry => { + var (handle, memberRef) = entry; MType baseType = memberRef.ParentType; while (!(baseType is MTypeReferenced)) { @@ -416,6 +428,7 @@ private void CheckMemberReferences( } errors.Add(new SandboxError($"Access to field not allowed: {mMemberRefField}")); + badReferences.Add(handle); break; } case MMemberRefMethod mMemberRefMethod: @@ -444,6 +457,7 @@ private void CheckMemberReferences( } errors.Add(new SandboxError($"Access to method not allowed: {mMemberRefMethod}")); + badReferences.Add(handle); break; default: throw new ArgumentOutOfRangeException(nameof(memberRef)); @@ -458,18 +472,18 @@ private void CheckInheritance( { // This inheritance whitelisting primarily serves to avoid content doing funny stuff // by e.g. inheriting Type. - foreach (var (_, baseType, interfaces) in inherited) + foreach (var (type, baseType, interfaces) in inherited) { if (!CanInherit(baseType)) { - errors.Add(new SandboxError($"Inheriting of type not allowed: {baseType}")); + errors.Add(new SandboxError($"Inheriting of type not allowed: {baseType} (by {type})")); } foreach (var @interface in interfaces) { if (!CanInherit(@interface)) { - errors.Add(new SandboxError($"Implementing of interface not allowed: {@interface}")); + errors.Add(new SandboxError($"Implementing of interface not allowed: {@interface} (by {type})")); } } @@ -547,25 +561,25 @@ private bool IsTypeAccessAllowed(SandboxConfig sandboxConfig, MTypeReferenced ty return nsDict.TryGetValue(type.Name, out cfg); } - private List GetReferencedTypes(MetadataReader reader, ConcurrentBag errors) + private List<(TypeReferenceHandle handle, MTypeReferenced parsed)> GetReferencedTypes(MetadataReader reader, ConcurrentBag errors) { return reader.TypeReferences.Select(typeRefHandle => { try { - return ParseTypeReference(reader, typeRefHandle); + return (typeRefHandle, ParseTypeReference(reader, typeRefHandle)); } catch (UnsupportedMetadataException e) { errors.Add(new SandboxError(e)); - return null; + return default; } }) - .Where(p => p != null) + .Where(p => p.Item2 != null) .ToList()!; } - private List GetReferencedMembers(MetadataReader reader, ConcurrentBag errors) + private List<(MemberReferenceHandle handle, MMemberRef parsed)> GetReferencedMembers(MetadataReader reader, ConcurrentBag errors) { return reader.MemberReferences.AsParallel() .Select(memRefHandle => @@ -586,7 +600,7 @@ private List GetReferencedMembers(MetadataReader reader, ConcurrentB catch (UnsupportedMetadataException u) { errors.Add(new SandboxError(u)); - return null; + return default; } break; @@ -600,7 +614,7 @@ private List GetReferencedMembers(MetadataReader reader, ConcurrentB catch (UnsupportedMetadataException u) { errors.Add(new SandboxError(u)); - return null; + return default; } break; @@ -616,7 +630,7 @@ private List GetReferencedMembers(MetadataReader reader, ConcurrentB { // Ensure this isn't a self-defined type. // This can happen due to generics since MethodSpec needs to point to MemberRef. - return null; + return default; } break; @@ -625,18 +639,18 @@ private List GetReferencedMembers(MetadataReader reader, ConcurrentB { errors.Add(new SandboxError( $"Module global variables and methods are unsupported. Name: {memName}")); - return null; + return default; } case HandleKind.MethodDefinition: { errors.Add(new SandboxError($"Vararg calls are unsupported. Name: {memName}")); - return null; + return default; } default: { errors.Add(new SandboxError( $"Unsupported member ref parent type: {memRef.Parent.Kind}. Name: {memName}")); - return null; + return default; } } @@ -667,9 +681,9 @@ private List GetReferencedMembers(MetadataReader reader, ConcurrentB throw new ArgumentOutOfRangeException(); } - return memberRef; + return (memRefHandle, memberRef); }) - .Where(p => p != null) + .Where(p => p.memberRef != null) .ToList()!; } @@ -780,7 +794,6 @@ public SandboxError(UnsupportedMetadataException ume) : this($"Unsupported metad } } - /// /// Thrown if the metadata does something funny we don't "support" like type forwarding. /// diff --git a/Robust.Shared/ContentPack/Sandbox.yml b/Robust.Shared/ContentPack/Sandbox.yml index deef0ebc481..61063a11fc0 100644 --- a/Robust.Shared/ContentPack/Sandbox.yml +++ b/Robust.Shared/ContentPack/Sandbox.yml @@ -320,9 +320,10 @@ Types: ConcurrentStack`1: { All: True } System.Collections: BitArray: { All: True } + ICollection: { All: True } IEnumerable: { All: True } IEnumerator: { All: True } - IReadOnlyList`1: { All: True } + IList: { All: True } System.ComponentModel: CancelEventArgs: { All: True } PropertyDescriptor: { } diff --git a/Robust.Shared/GameObjects/CompIdx.cs b/Robust.Shared/GameObjects/CompIdx.cs index 73a5f8f0ebe..136f45bb2b7 100644 --- a/Robust.Shared/GameObjects/CompIdx.cs +++ b/Robust.Shared/GameObjects/CompIdx.cs @@ -9,37 +9,20 @@ namespace Robust.Shared.GameObjects; public readonly struct CompIdx : IEquatable { - private static readonly ReaderWriterLockSlim SlowStoreLock = new(); - private static readonly Dictionary SlowStore = new(); - internal readonly int Value; internal static CompIdx Index() => Store.Index; - internal static CompIdx Index(Type t) - { - using (SlowStoreLock.ReadGuard()) - { - if (SlowStore.TryGetValue(t, out var idx)) - return idx; - } - - // Doesn't exist in the store, get a write lock and add it. - using (SlowStoreLock.WriteGuard()) - { - var idx = (CompIdx)typeof(Store<>) - .MakeGenericType(t) - .GetField(nameof(Store.Index), BindingFlags.Static | BindingFlags.Public)! - .GetValue(null)!; + internal static int ArrayIndex() => Index().Value; - SlowStore[t] = idx; - return idx; - } + internal static CompIdx GetIndex(Type type) + { + return (CompIdx)typeof(Store<>) + .MakeGenericType(type) + .GetField(nameof(Store.Index), BindingFlags.Static | BindingFlags.Public)! + .GetValue(null)!; } - internal static int ArrayIndex() => Index().Value; - internal static int ArrayIndex(Type type) => Index(type).Value; - internal static void AssignArray(ref T[] array, CompIdx idx, T value) { RefArray(ref array, idx) = value; diff --git a/Robust.Shared/GameObjects/ComponentEventArgs.cs b/Robust.Shared/GameObjects/ComponentEventArgs.cs index 367cb2570e8..9e97dc0b43e 100644 --- a/Robust.Shared/GameObjects/ComponentEventArgs.cs +++ b/Robust.Shared/GameObjects/ComponentEventArgs.cs @@ -33,7 +33,7 @@ public readonly struct AddedComponentEventArgs public readonly ComponentEventArgs BaseArgs; public readonly ComponentRegistration ComponentType; - public AddedComponentEventArgs(ComponentEventArgs baseArgs, ComponentRegistration componentType) + internal AddedComponentEventArgs(ComponentEventArgs baseArgs, ComponentRegistration componentType) { BaseArgs = baseArgs; ComponentType = componentType; @@ -48,11 +48,14 @@ public readonly struct RemovedComponentEventArgs public readonly MetaDataComponent Meta; - public RemovedComponentEventArgs(ComponentEventArgs baseArgs, bool terminating, MetaDataComponent meta) + public readonly CompIdx Idx; + + internal RemovedComponentEventArgs(ComponentEventArgs baseArgs, bool terminating, MetaDataComponent meta, CompIdx idx) { BaseArgs = baseArgs; Terminating = terminating; Meta = meta; + Idx = idx; } } } diff --git a/Robust.Shared/GameObjects/ComponentFactory.cs b/Robust.Shared/GameObjects/ComponentFactory.cs index 735ca26ed42..d0161db08e5 100644 --- a/Robust.Shared/GameObjects/ComponentFactory.cs +++ b/Robust.Shared/GameObjects/ComponentFactory.cs @@ -24,7 +24,6 @@ internal class ComponentFactory( // Bunch of dictionaries to allow lookups in all directions. /// - /// /// Mapping of component name to type. /// private FrozenDictionary _names @@ -63,6 +62,11 @@ private FrozenDictionary _types private FrozenDictionary _idxToType = FrozenDictionary.Empty; + /// + /// Slow-path for Type -> CompIdx mapping without generics. + /// + private FrozenDictionary _typeToIdx = FrozenDictionary.Empty; + /// public event Action? ComponentsAdded; @@ -78,6 +82,7 @@ private FrozenDictionary _idxToType private IEnumerable AllRegistrations => _types.Values; private ComponentRegistration Register(Type type, + CompIdx idx, Dictionary names, Dictionary lowerCaseNames, Dictionary types, @@ -123,8 +128,6 @@ private ComponentRegistration Register(Type type, var unsaved = type.HasCustomAttribute(); - var idx = CompIdx.Index(type); - var registration = new ComponentRegistration(name, type, idx, unsaved); idxToType[idx] = type; @@ -399,6 +402,20 @@ public void DoAutoRegistrations() RegisterTypesInternal(types, false); } + /// + [Pure] + public CompIdx GetIndex(Type type) + { + return _typeToIdx[type]; + } + + /// + [Pure] + public int GetArrayIndex(Type type) + { + return _typeToIdx[type].Value; + } + private void RegisterTypesInternal(Type[] types, bool overwrite) { var names = _names.ToDictionary(); @@ -408,12 +425,19 @@ private void RegisterTypesInternal(Type[] types, bool overwrite) var ignored = _ignored.ToHashSet(); var added = new ComponentRegistration[types.Length]; + var typeToidx = _typeToIdx.ToDictionary(); + for (int i = 0; i < types.Length; i++) { - added[i] = Register(types[i], names, lowerCaseNames, typesDict, idxToType, ignored, overwrite); + var type = types[i]; + var idx = CompIdx.GetIndex(type); + typeToidx[type] = idx; + + added[i] = Register(type, idx, names, lowerCaseNames, typesDict, idxToType, ignored, overwrite); } var st = RStopwatch.StartNew(); + _typeToIdx = typeToidx.ToFrozenDictionary(); _names = names.ToFrozenDictionary(); _lowerCaseNames = lowerCaseNames.ToFrozenDictionary(); _types = typesDict.ToFrozenDictionary(); diff --git a/Robust.Shared/GameObjects/Components/Eye/EyeComponent.cs b/Robust.Shared/GameObjects/Components/Eye/EyeComponent.cs index 1f3e8478aee..b4870fa04ba 100644 --- a/Robust.Shared/GameObjects/Components/Eye/EyeComponent.cs +++ b/Robust.Shared/GameObjects/Components/Eye/EyeComponent.cs @@ -1,7 +1,6 @@ using System.Numerics; using Robust.Shared.GameStates; using Robust.Shared.Graphics; -using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; @@ -41,6 +40,9 @@ public sealed partial class EyeComponent : Component [ViewVariables(VVAccess.ReadWrite), DataField("zoom")] public Vector2 Zoom = Vector2.One; + /// + /// Eye offset, relative to the map, and not affected by + /// [ViewVariables(VVAccess.ReadWrite), DataField("offset"), AutoNetworkedField] public Vector2 Offset; @@ -52,9 +54,13 @@ public sealed partial class EyeComponent : Component public int VisibilityMask = DefaultVisibilityMask; /// - /// Overrides the PVS view range of this eye, Effectively a per-eye cvar. + /// Scaling factor for the PVS view range of this eye. This effectively allows the + /// and cvars to be configured per + /// eye. /// - [DataField] public float? PvsSize; + [Access(typeof(SharedEyeSystem))] + [DataField] + public float PvsScale = 1; } /// diff --git a/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs b/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs index 36927867667..eb14a575407 100644 --- a/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs +++ b/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs @@ -23,7 +23,6 @@ namespace Robust.Shared.GameObjects public sealed partial class TransformComponent : Component, IComponentDebug { [Dependency] private readonly IEntityManager _entMan = default!; - [Dependency] private readonly IGameTiming _gameTiming = default!; // Currently this field just exists for VV. In future, it might become a real field [ViewVariables, PublicAPI] diff --git a/Robust.Shared/GameObjects/Entity.cs b/Robust.Shared/GameObjects/Entity.cs index f016d9544c7..9774d8c948a 100644 --- a/Robust.Shared/GameObjects/Entity.cs +++ b/Robust.Shared/GameObjects/Entity.cs @@ -1,12 +1,14 @@ -using Robust.Shared.Utility; +using Robust.Shared.Localization; +using Robust.Shared.Utility; namespace Robust.Shared.GameObjects; -public record struct Entity +public record struct Entity : IFluentEntityUid where T : IComponent? { public EntityUid Owner; public T Comp; + EntityUid IFluentEntityUid.FluentOwner => Owner; public Entity(EntityUid owner, T comp) { @@ -45,12 +47,13 @@ public readonly void Deconstruct(out EntityUid owner, out T comp) public override int GetHashCode() => Owner.GetHashCode(); } -public record struct Entity +public record struct Entity : IFluentEntityUid where T1 : IComponent? where T2 : IComponent? { public EntityUid Owner; public T1 Comp1; public T2 Comp2; + EntityUid IFluentEntityUid.FluentOwner => Owner; public Entity(EntityUid owner, T1 comp1, T2 comp2) { @@ -110,13 +113,14 @@ public static implicit operator Entity(Entity ent) } } -public record struct Entity +public record struct Entity : IFluentEntityUid where T1 : IComponent? where T2 : IComponent? where T3 : IComponent? { public EntityUid Owner; public T1 Comp1; public T2 Comp2; public T3 Comp3; + EntityUid IFluentEntityUid.FluentOwner => Owner; public Entity(EntityUid owner, T1 comp1, T2 comp2, T3 comp3) { @@ -211,7 +215,7 @@ public static implicit operator Entity(Entity ent) #endregion } -public record struct Entity +public record struct Entity : IFluentEntityUid where T1 : IComponent? where T2 : IComponent? where T3 : IComponent? where T4 : IComponent? { public EntityUid Owner; @@ -219,6 +223,7 @@ public record struct Entity public T2 Comp2; public T3 Comp3; public T4 Comp4; + EntityUid IFluentEntityUid.FluentOwner => Owner; public Entity(EntityUid owner, T1 comp1, T2 comp2, T3 comp3, T4 comp4) { @@ -336,7 +341,7 @@ public static implicit operator Entity(Entity ent) #endregion } -public record struct Entity +public record struct Entity : IFluentEntityUid where T1 : IComponent? where T2 : IComponent? where T3 : IComponent? where T4 : IComponent? where T5 : IComponent? { public EntityUid Owner; @@ -345,6 +350,7 @@ public record struct Entity public T3 Comp3; public T4 Comp4; public T5 Comp5; + EntityUid IFluentEntityUid.FluentOwner => Owner; public Entity(EntityUid owner, T1 comp1, T2 comp2, T3 comp3, T4 comp4, T5 comp5) { @@ -485,7 +491,7 @@ public static implicit operator Entity(Entity +public record struct Entity : IFluentEntityUid where T1 : IComponent? where T2 : IComponent? where T3 : IComponent? where T4 : IComponent? where T5 : IComponent? where T6 : IComponent? { public EntityUid Owner; @@ -495,6 +501,7 @@ public record struct Entity public T4 Comp4; public T5 Comp5; public T6 Comp6; + EntityUid IFluentEntityUid.FluentOwner => Owner; public Entity(EntityUid owner, T1 comp1, T2 comp2, T3 comp3, T4 comp4, T5 comp5, T6 comp6) { @@ -658,7 +665,7 @@ public static implicit operator Entity(Entity +public record struct Entity : IFluentEntityUid where T1 : IComponent? where T2 : IComponent? where T3 : IComponent? where T4 : IComponent? where T5 : IComponent? where T6 : IComponent? where T7 : IComponent? { public EntityUid Owner; @@ -669,6 +676,7 @@ public record struct Entity public T5 Comp5; public T6 Comp6; public T7 Comp7; + EntityUid IFluentEntityUid.FluentOwner => Owner; public Entity(EntityUid owner, T1 comp1, T2 comp2, T3 comp3, T4 comp4, T5 comp5, T6 comp6, T7 comp7) { @@ -855,7 +863,7 @@ public static implicit operator Entity(Entity +public record struct Entity : IFluentEntityUid where T1 : IComponent? where T2 : IComponent? where T3 : IComponent? where T4 : IComponent? where T5 : IComponent? where T6 : IComponent? where T7 : IComponent? where T8 : IComponent? { public EntityUid Owner; @@ -867,6 +875,7 @@ public record struct Entity public T6 Comp6; public T7 Comp7; public T8 Comp8; + EntityUid IFluentEntityUid.FluentOwner => Owner; public Entity(EntityUid owner, T1 comp1, T2 comp2, T3 comp3, T4 comp4, T5 comp5, T6 comp6, T7 comp7, T8 comp8) { diff --git a/Robust.Shared/GameObjects/EntityEventBus.Directed.cs b/Robust.Shared/GameObjects/EntityEventBus.Directed.cs index 80291a6cc31..e20e7260318 100644 --- a/Robust.Shared/GameObjects/EntityEventBus.Directed.cs +++ b/Robust.Shared/GameObjects/EntityEventBus.Directed.cs @@ -69,39 +69,29 @@ void UnsubscribeLocalEvent() /// DO NOT USE THIS IN CONTENT UNLESS YOU KNOW WHAT YOU'RE DOING, the only reason it's not internal /// is because of the component network source generator. /// - /// Event to dispatch. - /// Component receiving the event. - /// Event arguments for the event. - public void RaiseComponentEvent(IComponent component, TEvent args) + public void RaiseComponentEvent(EntityUid uid, TComponent component, TEvent args) + where TEvent : notnull + where TComponent : IComponent; + + /// + public void RaiseComponentEvent(EntityUid uid, IComponent component, TEvent args) where TEvent : notnull; - /// - /// Dispatches an event directly to a specific component. - /// - /// - /// This has a very specific purpose, and has massive potential to be abused. - /// DO NOT USE THIS IN CONTENT UNLESS YOU KNOW WHAT YOU'RE DOING, the only reason it's not internal - /// is because of the component network source generator. - /// - /// Event to dispatch. - /// Component receiving the event. - /// Type of the component, for faster lookups. - /// Event arguments for the event. - public void RaiseComponentEvent(IComponent component, CompIdx idx, TEvent args) + /// + public void RaiseComponentEvent(EntityUid uid, IComponent component, CompIdx idx, TEvent args) where TEvent : notnull; - /// - /// Dispatches an event directly to a specific component, by-ref. - /// - /// - /// This has a very specific purpose, and has massive potential to be abused. - /// DO NOT USE THIS IN CONTENT UNLESS YOU KNOW WHAT YOU'RE DOING, the only reason it's not internal - /// is because of the component network source generator. - /// - /// Event to dispatch. - /// Component receiving the event. - /// Event arguments for the event. - public void RaiseComponentEvent(IComponent component, ref TEvent args) + /// + public void RaiseComponentEvent(EntityUid uid, IComponent component, ref TEvent args) + where TEvent : notnull; + + /// + public void RaiseComponentEvent(EntityUid uid, TComponent component, ref TEvent args) + where TEvent : notnull + where TComponent : IComponent; + + /// + public void RaiseComponentEvent(EntityUid uid, IComponent component, CompIdx idx, ref TEvent args) where TEvent : notnull; public void OnlyCallOnRobustUnitTestISwearToGodPleaseSomebodyKillThisNightmare(); @@ -114,6 +104,15 @@ internal partial class EntityEventBus : IDisposable private delegate void DirectedEventHandler(EntityUid uid, IComponent comp, ref TEvent args) where TEvent : notnull; + /// + /// Max size of a components event subscription linked list. + /// Used to limit the stackalloc in + /// + /// + /// SS14 currently requires only 18, I doubt it will ever need to exceed 256. + /// + private const int MaxEventLinkedListSize = 256; + /// /// Constructs a new instance of . /// @@ -131,37 +130,58 @@ public EntityEventBus(IEntityManager entMan, IReflectionManager reflection) } /// - void IDirectedEventBus.RaiseComponentEvent(IComponent component, TEvent args) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void RaiseComponentEvent(EntityUid uid, IComponent component, TEvent args) + where TEvent : notnull + { + RaiseComponentEvent(uid, component, _comFac.GetIndex(component.GetType()), ref args); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void RaiseComponentEvent(EntityUid uid, TComponent component, TEvent args) + where TEvent : notnull + where TComponent : IComponent { - ref var unitRef = ref Unsafe.As(ref args); + RaiseComponentEvent(uid, component, CompIdx.Index(), ref args); + } - DispatchComponent( - component.Owner, - component, - CompIdx.Index(component.GetType()), - ref unitRef); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void RaiseComponentEvent(EntityUid uid, IComponent component, CompIdx type, TEvent args) + where TEvent : notnull + { + RaiseComponentEvent(uid, component, type, ref args); } - void IDirectedEventBus.RaiseComponentEvent(IComponent component, CompIdx type, TEvent args) + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void RaiseComponentEvent(EntityUid uid, IComponent component, ref TEvent args) + where TEvent : notnull { - ref var unitRef = ref Unsafe.As(ref args); + RaiseComponentEvent(uid, component, _comFac.GetIndex(component.GetType()), ref args); + } - DispatchComponent( - component.Owner, - component, - type, - ref unitRef); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void RaiseComponentEvent(EntityUid uid, TComponent component, ref TEvent args) + where TEvent : notnull + where TComponent : IComponent + { + RaiseComponentEvent(uid, component, CompIdx.Index(), ref args); } /// - void IDirectedEventBus.RaiseComponentEvent(IComponent component, ref TEvent args) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void RaiseComponentEvent(EntityUid uid, IComponent component, CompIdx type, ref TEvent args) + where TEvent : notnull { ref var unitRef = ref Unsafe.As(ref args); DispatchComponent( - component.Owner, + uid, component, - CompIdx.Index(component.GetType()), + type, ref unitRef); } @@ -390,7 +410,7 @@ private bool IsComponentEvent(Type t) public void OnComponentRemoved(in RemovedComponentEventArgs e) { - EntRemoveComponent(e.BaseArgs.Owner, CompIdx.Index(e.BaseArgs.Component.GetType())); + EntRemoveComponent(e.BaseArgs.Owner, e.Idx); } private void EntAddSubscription( @@ -488,7 +508,7 @@ private void EntAddComponent(EntityUid euid, CompIdx compType) DebugTools.Assert(eventTable.Free >= 0); - ref var eventStartIdx = ref CollectionsMarshal.GetValueRefOrAddDefault( + ref var indices = ref CollectionsMarshal.GetValueRefOrAddDefault( eventTable.EventIndices, evType, out var exists); @@ -500,10 +520,13 @@ private void EntAddComponent(EntityUid euid, CompIdx compType) // Set it up entry.Component = compType; - entry.Next = exists ? eventStartIdx : -1; + entry.Next = exists ? indices.Start : -1; // Assign new list entry to EventIndices dictionary. - eventStartIdx = entryIdx; + indices.Start = entryIdx; + indices.Count++; + if (indices.Count > MaxEventLinkedListSize) + throw new NotSupportedException($"Exceeded maximum event linked list size. Need to implement stackalloc fallback."); } } @@ -541,40 +564,37 @@ private void EntRemoveComponent(EntityUid euid, CompIdx compType) foreach (var evType in compSubs.Keys) { DebugTools.Assert(!_eventData[evType].ComponentEvent); - ref var dictIdx = ref CollectionsMarshal.GetValueRefOrNullRef(eventTable.EventIndices, evType); - if (Unsafe.IsNullRef(ref dictIdx)) + ref var indices = ref CollectionsMarshal.GetValueRefOrNullRef(eventTable.EventIndices, evType); + if (Unsafe.IsNullRef(ref indices)) { DebugTools.Assert("This should not be possible. Were the events for this component never added?"); continue; } - ref var updateNext = ref dictIdx; - - // Go over linked list to find index of component. - var entryIdx = dictIdx; - ref var entry = ref Unsafe.NullRef(); - while (true) - { - entry = ref eventTable.ComponentLists[entryIdx]; - if (entry.Component == compType) - { - // Found - break; - } - - entryIdx = entry.Next; - updateNext = ref entry.Next; - } + var entryIdx = indices.Start; + ref var entry = ref eventTable.ComponentLists[entryIdx]; - if (entry.Next == -1 && Unsafe.AreSame(ref dictIdx, ref updateNext)) + if (indices.Count == 1) { // Last entry for this event type, remove from dict. + DebugTools.AssertEqual(entry.Next, -1); eventTable.EventIndices.Remove(evType); } else { + ref var updateNext = ref indices.Start; + + // Go over linked list to find index of component. + while (entry.Component != compType) + { + updateNext = ref entry.Next; + entryIdx = entry.Next; + entry = ref eventTable.ComponentLists[entryIdx]; + } + // Rewrite previous index to point to next in chain. updateNext = entry.Next; + indices.Count--; } // Push entry back onto free list. @@ -585,15 +605,33 @@ private void EntRemoveComponent(EntityUid euid, CompIdx compType) private void EntDispatch(EntityUid euid, Type eventType, ref Unit args) { - if (!EntTryGetSubscriptions(eventType, euid, out var enumerator)) + if (!_entEventTables.TryGetValue(euid, out var eventTable)) return; - while (enumerator.MoveNext(out var component, out var reg)) + if (!eventTable.EventIndices.TryGetValue(eventType, out var indices)) + return; + + DebugTools.Assert(indices.Count > 0); + DebugTools.Assert(indices.Start >= 0); + + // First, collect all subscribing components. + // This is to avoid infinite loops over the linked list if subscription handlers add or remove components. + Span compIds = stackalloc CompIdx[indices.Count]; + var idx = indices.Start; + for (var index = 0; index < compIds.Length; index++) { - if (component.Deleted) - continue; + DebugTools.Assert(idx >= 0); + ref var entry = ref eventTable.ComponentLists[idx]; + idx = entry.Next; + compIds[index] = entry.Component; + } - reg.Handler(euid, component, ref args); + foreach (var compIdx in compIds) + { + if (!_entMan.TryGetComponent(euid, compIdx, out var comp)) + continue; + var compSubs = _eventSubs[compIdx.Value]; + compSubs[eventType].Handler(euid, comp, ref args); } } @@ -602,16 +640,30 @@ private void EntCollectOrdered( Type eventType, ref ValueList found) { - if (!EntTryGetSubscriptions(eventType, euid, out var enumerator)) + if (!_entEventTables.TryGetValue(euid, out var eventTable)) return; - while (enumerator.MoveNext(out var component, out var reg)) + if (!eventTable.EventIndices.TryGetValue(eventType, out var indices)) + return; + + DebugTools.Assert(indices.Count > 0); + DebugTools.Assert(indices.Start >= 0); + var idx = indices.Start; + while (idx != -1) { - found.Add(new OrderedEventDispatch((ref Unit ev) => - { - if (!component.Deleted) - reg.Handler(euid, component, ref ev); - }, reg.Order)); + ref var entry = ref eventTable.ComponentLists[idx]; + idx = entry.Next; + var comp = _entMan.GetComponentInternal(euid, entry.Component); + var compSubs = _eventSubs[entry.Component.Value]; + var reg = compSubs[eventType]; + + found.Add(new OrderedEventDispatch( + (ref Unit ev) => + { + if (!comp.Deleted) + reg.Handler(euid, comp, ref ev); + }, + reg.Order)); } } @@ -626,28 +678,6 @@ private void DispatchComponent( reg.Handler(euid, component, ref args); } - /// - /// Enumerates all subscriptions for an event on a specific entity, returning the component instances and registrations. - /// - private bool EntTryGetSubscriptions(Type eventType, EntityUid euid, out SubscriptionsEnumerator enumerator) - { - if (!_entEventTables.TryGetValue(euid, out var eventTable)) - { - enumerator = default!; - return false; - } - - // No subscriptions to this event type, return null. - if (!eventTable.EventIndices.TryGetValue(eventType, out var startEntry)) - { - enumerator = default; - return false; - } - - enumerator = new(eventType, startEntry, eventTable.ComponentLists, _eventSubs, euid, _entMan); - return true; - } - public void ClearSubscriptions() { _subscriptionLock = false; @@ -678,59 +708,6 @@ public void Dispose() _eventSubsInv = null!; } - private struct SubscriptionsEnumerator - { - private readonly Type _eventType; - private readonly EntityUid _uid; - private readonly FrozenDictionary[] _subscriptions; - private readonly IEntityManager _entityManager; - private readonly EventTableListEntry[] _list; - private int _idx; - - public SubscriptionsEnumerator( - Type eventType, - int startEntry, - EventTableListEntry[] list, - FrozenDictionary[] subscriptions, - EntityUid uid, - IEntityManager entityManager) - { - _eventType = eventType; - _list = list; - _subscriptions = subscriptions; - _idx = startEntry; - _entityManager = entityManager; - _uid = uid; - } - - public bool MoveNext( - [NotNullWhen(true)] out IComponent? component, - [NotNullWhen(true)] out DirectedRegistration? registration) - { - if (_idx == -1) - { - component = null; - registration = null; - return false; - } - - ref var entry = ref _list[_idx]; - _idx = entry.Next; - - var compType = entry.Component; - var compSubs = _subscriptions[compType.Value]; - - if (!compSubs.TryGetValue(_eventType, out registration)) - { - component = default; - return false; - } - - component = _entityManager.GetComponentInternal(_uid, compType); - return true; - } - } - internal sealed class DirectedRegistration : OrderedRegistration { public readonly Delegate Original; @@ -760,7 +737,7 @@ internal sealed class EventTable // Free contains the first free linked list node, or -1 if there is none. // Free nodes form their own linked list. // ComponentList is the actual region of memory containing linked list nodes. - public readonly Dictionary EventIndices = new(); + public readonly Dictionary EventIndices = new(); public int Free; public EventTableListEntry[] ComponentLists = new EventTableListEntry[InitialListSize]; diff --git a/Robust.Shared/GameObjects/EntityEventBus.Ordering.cs b/Robust.Shared/GameObjects/EntityEventBus.Ordering.cs index 3ae3a702ef9..13f89675638 100644 --- a/Robust.Shared/GameObjects/EntityEventBus.Ordering.cs +++ b/Robust.Shared/GameObjects/EntityEventBus.Ordering.cs @@ -35,6 +35,9 @@ private void RaiseLocalOrdered( if (broadcast) CollectBroadcastOrdered(EventSource.Local, subs, ref found); + // TODO PERF + // consider ordering the linked list itself? + // Then make broadcast events always a lower priority and replace the valuelist with stackalloc? EntCollectOrdered(uid, eventType, ref found); DispatchOrderedEvents(ref unitRef, ref found); diff --git a/Robust.Shared/GameObjects/EntityManager.Components.cs b/Robust.Shared/GameObjects/EntityManager.Components.cs index 9dc006f3f37..35d911c0c43 100644 --- a/Robust.Shared/GameObjects/EntityManager.Components.cs +++ b/Robust.Shared/GameObjects/EntityManager.Components.cs @@ -125,8 +125,8 @@ public void InitializeComponents(EntityUid uid, MetaDataComponent? metadata = nu foreach (var comp in comps) { - if (comp is { LifeStage: ComponentLifeStage.Added }) - LifeInitialize(comp, CompIdx.Index(comp.GetType())); + if (comp is {LifeStage: ComponentLifeStage.Added}) + LifeInitialize(uid, comp, _componentFactory.GetIndex(comp.GetType())); } #if DEBUG @@ -159,19 +159,19 @@ public void StartComponents(EntityUid uid) // Init transform first, we always have it. var transform = TransformQuery.GetComponent(uid); if (transform.LifeStage == ComponentLifeStage.Initialized) - LifeStartup(transform); + LifeStartup(uid, transform, CompIdx.Index()); // Init physics second if it exists. if (_physicsQuery.TryComp(uid, out var phys) && phys.LifeStage == ComponentLifeStage.Initialized) { - LifeStartup(phys); + LifeStartup(uid, phys, CompIdx.Index()); } // Do rest of components. foreach (var comp in comps) { if (comp is { LifeStage: ComponentLifeStage.Initialized }) - LifeStartup(comp); + LifeStartup(uid, comp, _componentFactory.GetIndex(comp.GetType())); } } @@ -267,10 +267,10 @@ public void Dispose() return; if (!Comp.Initialized) - ((EntityManager) _entMan).LifeInitialize(Comp, CompType); + ((EntityManager) _entMan).LifeInitialize(_owner, Comp, CompType); if (metadata.EntityInitialized && !Comp.Running) - ((EntityManager) _entMan).LifeStartup(Comp); + ((EntityManager) _entMan).LifeStartup(_owner, Comp, CompType); } public static implicit operator T(CompInitializeHandle handle) @@ -355,7 +355,7 @@ private void AddComponentInternal(EntityUid uid, T component, ComponentRegist // This will invalidate the comp ref as it removes the key from the dictionary. // This is inefficient, but component overriding rarely ever happens. - RemoveComponentImmediate(comp!, uid, false, metadata); + RemoveComponentImmediate(uid, comp!, type, false, metadata); dict.Add(uid, component); } else @@ -382,7 +382,7 @@ private void AddComponentInternal(EntityUid uid, T component, ComponentRegist ComponentAdded?.Invoke(eventArgs); _eventBus.OnComponentAdded(eventArgs); - LifeAddToEntity(component, reg.Idx); + LifeAddToEntity(uid, component, reg.Idx); if (skipInit) return; @@ -395,20 +395,24 @@ private void AddComponentInternal(EntityUid uid, T component, ComponentRegist if (component.Networked) DirtyEntity(uid, metadata); - LifeInitialize(component, reg.Idx); + LifeInitialize(uid, component, reg.Idx); if (metadata.EntityInitialized) - LifeStartup(component); + LifeStartup(uid, component, reg.Idx); if (metadata.EntityLifeStage >= EntityLifeStage.MapInitialized) - EventBus.RaiseComponentEvent(component, MapInitEventInstance); + EventBus.RaiseComponentEvent(uid, component, reg.Idx, MapInitEventInstance); } /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool RemoveComponent(EntityUid uid, MetaDataComponent? meta = null) + public bool RemoveComponent(EntityUid uid, MetaDataComponent? meta = null) where T : IComponent { - return RemoveComponent(uid, typeof(T), meta); + if (!TryGetComponent(uid, out T? comp)) + return false; + + RemoveComponentImmediate(uid, comp, CompIdx.Index(), false, meta); + return true; } /// @@ -418,7 +422,7 @@ public bool RemoveComponent(EntityUid uid, Type type, MetaDataComponent? meta = if (!TryGetComponent(uid, type, out var comp)) return false; - RemoveComponentImmediate(comp, uid, false, meta); + RemoveComponentImmediate(uid, comp, _componentFactory.GetIndex(type), false, meta); return true; } @@ -432,7 +436,8 @@ public bool RemoveComponent(EntityUid uid, ushort netId, MetaDataComponent? meta if (!TryGetComponent(uid, netId, out var comp, meta)) return false; - RemoveComponentImmediate(comp, uid, false, meta); + var idx = _componentFactory.GetIndex(comp.GetType()); + RemoveComponentImmediate(uid, comp, idx, false, meta); return true; } @@ -440,7 +445,8 @@ public bool RemoveComponent(EntityUid uid, ushort netId, MetaDataComponent? meta [MethodImpl(MethodImplOptions.AggressiveInlining)] public void RemoveComponent(EntityUid uid, IComponent component, MetaDataComponent? meta = null) { - RemoveComponentImmediate(component, uid, false, meta); + var idx = _componentFactory.GetIndex(component.GetType()); + RemoveComponentImmediate(uid, component, idx, false, meta); } /// @@ -510,7 +516,8 @@ public void RemoveComponents(EntityUid uid, MetaDataComponent? meta = null) foreach (var comp in InSafeOrder(_entCompIndex[uid])) { - RemoveComponentImmediate(comp, uid, false, meta); + var idx = _componentFactory.GetIndex(comp.GetType()); + RemoveComponentImmediate(uid, comp, idx, false, meta); } } @@ -524,7 +531,8 @@ public void DisposeComponents(EntityUid uid, MetaDataComponent? meta = null) { try { - RemoveComponentImmediate(comp, uid, true, meta); + var idx = _componentFactory.GetIndex(comp.GetType()); + RemoveComponentImmediate(uid, comp, idx, true, meta); } catch (Exception) { @@ -537,14 +545,16 @@ public void DisposeComponents(EntityUid uid, MetaDataComponent? meta = null) private void RemoveComponentDeferred(IComponent component, EntityUid uid, bool terminating) { - if (component == null) throw new ArgumentNullException(nameof(component)); + if (component == null) + throw new ArgumentNullException(nameof(component)); #pragma warning disable CS0618 // Type or member is obsolete if (component.Owner != uid) #pragma warning restore CS0618 // Type or member is obsolete throw new InvalidOperationException("Component is not owned by entity."); - if (component.Deleted) return; + if (component.Deleted) + return; #if EXCEPTION_TOLERANCE try @@ -564,7 +574,7 @@ private void RemoveComponentDeferred(IComponent component, EntityUid uid, bool t } if (component.LifeStage >= ComponentLifeStage.Initialized && component.LifeStage <= ComponentLifeStage.Running) - LifeShutdown(component); + LifeShutdown(uid, component, _componentFactory.GetIndex(component.GetType())); #if EXCEPTION_TOLERANCE } catch (Exception e) @@ -575,7 +585,11 @@ private void RemoveComponentDeferred(IComponent component, EntityUid uid, bool t #endif } - private void RemoveComponentImmediate(IComponent component, EntityUid uid, bool terminating, + private void RemoveComponentImmediate( + EntityUid uid, + IComponent component, + CompIdx idx, + bool terminating, MetaDataComponent? meta) { if (component.Deleted) @@ -596,10 +610,10 @@ private void RemoveComponentImmediate(IComponent component, EntityUid uid, bool } if (component.Running) - LifeShutdown(component); + LifeShutdown(uid, component, idx); if (component.LifeStage != ComponentLifeStage.PreAdd) - LifeRemoveFromEntity(component); // Sets delete + LifeRemoveFromEntity(uid, component, idx); // Sets delete #if EXCEPTION_TOLERANCE } @@ -609,7 +623,7 @@ private void RemoveComponentImmediate(IComponent component, EntityUid uid, bool _runtimeLog.LogException(e, nameof(RemoveComponentImmediate)); } #endif - DeleteComponent(uid, component, terminating, meta); + DeleteComponent(uid, component, idx, terminating, meta); } /// @@ -620,6 +634,7 @@ public void CullRemovedComponents() if (component.Deleted) continue; var uid = component.Owner; + var idx = _componentFactory.GetIndex(component.GetType()); #if EXCEPTION_TOLERANCE try @@ -630,11 +645,11 @@ public void CullRemovedComponents() { // TODO add options to cancel deferred deletion? _sawmill.Warning($"Found a running component while culling deferred deletions, owner={ToPrettyString(uid)}, type={component.GetType()}"); - LifeShutdown(component); + LifeShutdown(uid, component, idx); } if (component.LifeStage != ComponentLifeStage.PreAdd) - LifeRemoveFromEntity(component); + LifeRemoveFromEntity(uid, component, idx); #if EXCEPTION_TOLERANCE } @@ -645,43 +660,49 @@ public void CullRemovedComponents() } #endif var meta = MetaQuery.GetComponent(uid); - DeleteComponent(uid, component, false, meta); + DeleteComponent(uid, component, idx, false, meta); } _deleteSet.Clear(); } - private void DeleteComponent(EntityUid entityUid, IComponent component, bool terminating, MetaDataComponent? metadata) + private void DeleteComponent( + EntityUid entityUid, + IComponent component, + CompIdx idx, + bool terminating, + MetaDataComponent? metadata) { if (!MetaQuery.ResolveInternal(entityUid, ref metadata)) return; - var eventArgs = new RemovedComponentEventArgs(new ComponentEventArgs(component, entityUid), false, metadata); + var eventArgs = new RemovedComponentEventArgs(new ComponentEventArgs(component, entityUid), false, metadata, idx); ComponentRemoved?.Invoke(eventArgs); _eventBus.OnComponentRemoved(eventArgs); - var reg = _componentFactory.GetRegistration(component); - DebugTools.Assert(component.Networked == (reg.NetID != null)); - - if (!terminating && reg.NetID != null) + if (!terminating) { - if (!metadata.NetComponents.Remove(reg.NetID.Value)) - _sawmill.Error($"Entity {ToPrettyString(entityUid, metadata)} did not have {component.GetType().Name} in its networked component dictionary during component deletion."); - - if (component.NetSyncEnabled) + var reg = _componentFactory.GetRegistration(component); + DebugTools.Assert(component.Networked == (reg.NetID != null)); + if (reg.NetID != null) { - DirtyEntity(entityUid, metadata); - metadata.LastComponentRemoved = _gameTiming.CurTick; + if (!metadata.NetComponents.Remove(reg.NetID.Value)) + _sawmill.Error($"Entity {ToPrettyString(entityUid, metadata)} did not have {component.GetType().Name} in its networked component dictionary during component deletion."); + + if (component.NetSyncEnabled) + { + DirtyEntity(entityUid, metadata); + metadata.LastComponentRemoved = _gameTiming.CurTick; + } } } - _entTraitArray[reg.Idx.Value].Remove(entityUid); + _entTraitArray[idx.Value].Remove(entityUid); // TODO if terminating the entity, maybe defer this? // _entCompIndex.Remove(uid) gets called later on anyways. _entCompIndex.Remove(entityUid, component); - DebugTools.Assert(_netMan.IsClient // Client side prediction can set LastComponentRemoved to some future tick, || metadata.EntityLastModifiedTick >= metadata.LastComponentRemoved); } @@ -999,7 +1020,7 @@ public EntityQuery GetEntityQuery() where TComp1 : IComponent public EntityQuery GetEntityQuery(Type type) { - var comps = _entTraitArray[CompIdx.ArrayIndex(type)]; + var comps = _entTraitDict[type]; DebugTools.Assert(comps != null, $"Unknown component: {type.Name}"); return new EntityQuery(comps, _resolveSawmill); } @@ -1407,7 +1428,7 @@ public IEnumerable EntityQuery(bool includePaused = false) where T : IComp { DebugTools.Assert(component.NetSyncEnabled, $"Attempting to get component state for an un-synced component: {component.GetType()}"); var getState = new ComponentGetState(session, fromTick); - eventBus.RaiseComponentEvent(component, ref getState); + eventBus.RaiseComponentEvent(component.Owner, component, ref getState); return getState.State; } @@ -1415,7 +1436,7 @@ public IEnumerable EntityQuery(bool includePaused = false) where T : IComp public bool CanGetComponentState(IEventBus eventBus, IComponent component, ICommonSession player) { var attempt = new ComponentGetStateAttemptEvent(player); - eventBus.RaiseComponentEvent(component, ref attempt); + eventBus.RaiseComponentEvent(component.Owner, component, ref attempt); return !attempt.Cancelled; } diff --git a/Robust.Shared/GameObjects/EntityManager.LifeCycle.cs b/Robust.Shared/GameObjects/EntityManager.LifeCycle.cs index 226be0279fb..8119109640c 100644 --- a/Robust.Shared/GameObjects/EntityManager.LifeCycle.cs +++ b/Robust.Shared/GameObjects/EntityManager.LifeCycle.cs @@ -14,7 +14,7 @@ public partial class EntityManager /// Increases the life stage from to , /// after raising a event. /// - internal void LifeAddToEntity(IComponent component, CompIdx type) + internal void LifeAddToEntity(EntityUid uid, IComponent component, CompIdx idx) { DebugTools.Assert(component.LifeStage == ComponentLifeStage.PreAdd); @@ -23,7 +23,7 @@ internal void LifeAddToEntity(IComponent component, CompIdx type) component.CreationTick = CurrentTick; // networked components are assumed to be dirty when added to entities. See also: ClearTicks() component.LastModifiedTick = CurrentTick; - EventBus.RaiseComponentEvent(component, type, CompAddInstance); + EventBus.RaiseComponentEvent(uid, component, idx, CompAddInstance); component.LifeStage = ComponentLifeStage.Added; #pragma warning restore CS0618 // Type or member is obsolete } @@ -32,12 +32,12 @@ internal void LifeAddToEntity(IComponent component, CompIdx type) /// Increases the life stage from to , /// calling . /// - internal void LifeInitialize(IComponent component, CompIdx type) + internal void LifeInitialize(EntityUid uid, IComponent component, CompIdx idx) { DebugTools.Assert(component.LifeStage == ComponentLifeStage.Added); component.LifeStage = ComponentLifeStage.Initializing; - EventBus.RaiseComponentEvent(component, type, CompInitInstance); + EventBus.RaiseComponentEvent(uid, component, idx, CompInitInstance); component.LifeStage = ComponentLifeStage.Initialized; } @@ -45,12 +45,12 @@ internal void LifeInitialize(IComponent component, CompIdx type) /// Increases the life stage from to /// , calling . /// - internal void LifeStartup(IComponent component) + internal void LifeStartup(EntityUid uid, IComponent component, CompIdx idx) { DebugTools.Assert(component.LifeStage == ComponentLifeStage.Initialized); component.LifeStage = ComponentLifeStage.Starting; - EventBus.RaiseComponentEvent(component, CompStartupInstance); + EventBus.RaiseComponentEvent(uid, component, idx, CompStartupInstance); component.LifeStage = ComponentLifeStage.Running; } @@ -61,7 +61,7 @@ internal void LifeStartup(IComponent component) /// /// Components are allowed to remove themselves in their own Startup function. /// - internal void LifeShutdown(IComponent component) + internal void LifeShutdown(EntityUid uid, IComponent component, CompIdx idx) { DebugTools.Assert(component.LifeStage is >= ComponentLifeStage.Initializing and < ComponentLifeStage.Stopping); @@ -73,7 +73,7 @@ internal void LifeShutdown(IComponent component) } component.LifeStage = ComponentLifeStage.Stopping; - EventBus.RaiseComponentEvent(component, CompShutdownInstance); + EventBus.RaiseComponentEvent(uid, component, idx, CompShutdownInstance); component.LifeStage = ComponentLifeStage.Stopped; } @@ -81,13 +81,13 @@ internal void LifeShutdown(IComponent component) /// Increases the life stage from to , /// calling . /// - internal void LifeRemoveFromEntity(IComponent component) + internal void LifeRemoveFromEntity(EntityUid uid, IComponent component, CompIdx idx) { // can be called at any time after PreAdd, including inside other life stage events. DebugTools.Assert(component.LifeStage != ComponentLifeStage.PreAdd); component.LifeStage = ComponentLifeStage.Removing; - EventBus.RaiseComponentEvent(component, CompRemoveInstance); + EventBus.RaiseComponentEvent(uid, component, idx, CompRemoveInstance); component.LifeStage = ComponentLifeStage.Deleted; } diff --git a/Robust.Shared/GameObjects/EntityManager.Systems.cs b/Robust.Shared/GameObjects/EntityManager.Systems.cs index 05f4c42c865..7ead67a3b1b 100644 --- a/Robust.Shared/GameObjects/EntityManager.Systems.cs +++ b/Robust.Shared/GameObjects/EntityManager.Systems.cs @@ -1,14 +1,17 @@ using System.Diagnostics.CodeAnalysis; +using JetBrains.Annotations; namespace Robust.Shared.GameObjects; public partial class EntityManager { + [Pure] public T System() where T : IEntitySystem { return _entitySystemManager.GetEntitySystem(); } + [Pure] public T? SystemOrNull() where T : IEntitySystem { return _entitySystemManager.GetEntitySystemOrNull(); diff --git a/Robust.Shared/GameObjects/EntityManager.cs b/Robust.Shared/GameObjects/EntityManager.cs index 6146f94abe6..0b8e7d224ef 100644 --- a/Robust.Shared/GameObjects/EntityManager.cs +++ b/Robust.Shared/GameObjects/EntityManager.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Runtime.CompilerServices; using Prometheus; +using Robust.Shared.Console; using Robust.Shared.Containers; using Robust.Shared.GameStates; using Robust.Shared.Log; @@ -42,6 +43,7 @@ public abstract partial class EntityManager : IEntityManager [IoC.Dependency] private readonly ProfManager _prof = default!; [IoC.Dependency] private readonly INetManager _netMan = default!; [IoC.Dependency] private readonly IReflectionManager _reflection = default!; + [IoC.Dependency] private readonly EntityConsoleHost _entityConsoleHost = default!; // I feel like PJB might shed me for putting a system dependency here, but its required for setting entity // positions on spawn.... @@ -216,6 +218,7 @@ public virtual void Startup() TransformQuery = GetEntityQuery(); _physicsQuery = GetEntityQuery(); _actorQuery = GetEntityQuery(); + _entityConsoleHost.Startup(); } public virtual void Shutdown() @@ -227,6 +230,7 @@ public virtual void Shutdown() ClearComponents(); ShuttingDown = false; Started = false; + _entityConsoleHost.Shutdown(); } public virtual void Cleanup() @@ -586,7 +590,7 @@ private void RecursiveDeleteEntity( { try { - LifeShutdown(component); + LifeShutdown(uid, component, _componentFactory.GetIndex(component.GetType())); } catch (Exception e) { diff --git a/Robust.Shared/GameObjects/EntitySystem.Proxy.cs b/Robust.Shared/GameObjects/EntitySystem.Proxy.cs index c70bb05a362..6e0148be980 100644 --- a/Robust.Shared/GameObjects/EntitySystem.Proxy.cs +++ b/Robust.Shared/GameObjects/EntitySystem.Proxy.cs @@ -8,7 +8,6 @@ using Robust.Shared.Maths; using Robust.Shared.Prototypes; using Robust.Shared.Timing; -using TerraFX.Interop.Windows; namespace Robust.Shared.GameObjects; @@ -76,6 +75,15 @@ protected bool TerminatingOrDeleted(EntityUid uid, MetaDataComponent? metaData = return LifeStage(uid, metaData) >= EntityLifeStage.Terminating; } + /// + /// Checks whether the entity is being or has been deleted (or never existed in the first place). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected bool TerminatingOrDeleted(EntityUid? uid, MetaDataComponent? metaData = null) + { + return !uid.HasValue || TerminatingOrDeleted(uid.Value, metaData); + } + [Obsolete("Use override without the EntityQuery")] protected bool Deleted(EntityUid uid, EntityQuery metaQuery) => Deleted(uid); diff --git a/Robust.Shared/GameObjects/EntitySystem.cs b/Robust.Shared/GameObjects/EntitySystem.cs index e907a9c8c0a..af1f7321481 100644 --- a/Robust.Shared/GameObjects/EntitySystem.cs +++ b/Robust.Shared/GameObjects/EntitySystem.cs @@ -74,6 +74,7 @@ protected EntitySystem(IEntityManager entityManager) } /// + [MustCallBase(true)] public virtual void Initialize() { } /// @@ -81,12 +82,15 @@ public virtual void Initialize() { } /// Not ran on the client if prediction is disabled and /// is false (the default). /// + [MustCallBase(true)] public virtual void Update(float frameTime) { } /// + [MustCallBase(true)] public virtual void FrameUpdate(float frameTime) { } /// + [MustCallBase(true)] public virtual void Shutdown() { ShutdownSubscriptions(); diff --git a/Robust.Shared/GameObjects/EntitySystemMessages/EntityRenamedEvent.cs b/Robust.Shared/GameObjects/EntitySystemMessages/EntityRenamedEvent.cs new file mode 100644 index 00000000000..798359b6b35 --- /dev/null +++ b/Robust.Shared/GameObjects/EntitySystemMessages/EntityRenamedEvent.cs @@ -0,0 +1,7 @@ +namespace Robust.Shared.GameObjects; + +/// +/// Raised directed on an entity when its name is changed. +/// +[ByRefEvent] +public readonly record struct EntityRenamedEvent(string NewName); diff --git a/Robust.Shared/GameObjects/IComponentFactory.cs b/Robust.Shared/GameObjects/IComponentFactory.cs index d77f335bc0e..8f22cb4c374 100644 --- a/Robust.Shared/GameObjects/IComponentFactory.cs +++ b/Robust.Shared/GameObjects/IComponentFactory.cs @@ -77,6 +77,18 @@ public interface IComponentFactory /// The availability of the component. ComponentAvailability GetComponentAvailability(string componentName, bool ignoreCase = false); + /// + /// Slow-path for Type -> CompIdx mapping without generics. + /// + [Pure] + CompIdx GetIndex(Type type); + + /// + /// Slow-path to get the component index for a specified type. + /// + [Pure] + int GetArrayIndex(Type type); + /// /// Registers a component class with the factory. /// diff --git a/Robust.Shared/GameObjects/IEntityManager.Components.cs b/Robust.Shared/GameObjects/IEntityManager.Components.cs index 5fc1e6616d6..8e23583c23d 100644 --- a/Robust.Shared/GameObjects/IEntityManager.Components.cs +++ b/Robust.Shared/GameObjects/IEntityManager.Components.cs @@ -100,7 +100,7 @@ public partial interface IEntityManager /// /// The component reference type to remove. /// Entity UID to modify. - bool RemoveComponent(EntityUid uid, MetaDataComponent? meta = null); + bool RemoveComponent(EntityUid uid, MetaDataComponent? meta = null) where T : IComponent; /// /// Removes the component with a specified type. @@ -294,7 +294,7 @@ public partial interface IEntityManager /// Entity UID to check. /// Component of the specified type (if exists). /// If the component existed in the entity. - bool TryGetComponent(EntityUid uid, [NotNullWhen(true)] out T? component) where T : IComponent?; + bool TryGetComponent(EntityUid uid, [NotNullWhen(true)] out T? component) where T : IComponent?; /// /// Returns the component of a specific type. diff --git a/Robust.Shared/GameObjects/IEntityManager.Systems.cs b/Robust.Shared/GameObjects/IEntityManager.Systems.cs index fcbcd93848c..4a2ac46c671 100644 --- a/Robust.Shared/GameObjects/IEntityManager.Systems.cs +++ b/Robust.Shared/GameObjects/IEntityManager.Systems.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using JetBrains.Annotations; namespace Robust.Shared.GameObjects; @@ -9,6 +10,7 @@ public partial interface IEntityManager /// /// The type of entity system to find. /// The instance matching the specified type. + [Pure] T System() where T : IEntitySystem; /// @@ -16,6 +18,7 @@ public partial interface IEntityManager /// /// The type of entity system to find. /// The instance matching the specified type, or null. + [Pure] T? SystemOrNull() where T : IEntitySystem; /// diff --git a/Robust.Shared/GameObjects/NetEntity.cs b/Robust.Shared/GameObjects/NetEntity.cs index 857fd0878fa..ffe6ff5222d 100644 --- a/Robust.Shared/GameObjects/NetEntity.cs +++ b/Robust.Shared/GameObjects/NetEntity.cs @@ -49,7 +49,7 @@ public NetEntity(int id) public static NetEntity Parse(ReadOnlySpan uid) { if (uid.Length == 0) - return default; + throw new FormatException($"An empty string is not a valid NetEntity"); if (uid[0] != 'c') return new NetEntity(int.Parse(uid)); diff --git a/Robust.Shared/GameObjects/Systems/EntityLookupSystem.ComponentQueries.cs b/Robust.Shared/GameObjects/Systems/EntityLookupSystem.ComponentQueries.cs index 48274004012..04e81f54e62 100644 --- a/Robust.Shared/GameObjects/Systems/EntityLookupSystem.ComponentQueries.cs +++ b/Robust.Shared/GameObjects/Systems/EntityLookupSystem.ComponentQueries.cs @@ -614,16 +614,16 @@ public void GetEntitiesIntersecting(MapId mapId, IPhysShape shape, Transform #region EntityCoordinates - public void GetEntitiesInRange(EntityCoordinates coordinates, float range, HashSet> entities) where T : IComponent + public void GetEntitiesInRange(EntityCoordinates coordinates, float range, HashSet> entities, LookupFlags flags = DefaultFlags) where T : IComponent { var mapPos = coordinates.ToMap(EntityManager, _transform); - GetEntitiesInRange(mapPos, range, entities); + GetEntitiesInRange(mapPos, range, entities, flags); } - public HashSet> GetEntitiesInRange(EntityCoordinates coordinates, float range) where T : IComponent + public HashSet> GetEntitiesInRange(EntityCoordinates coordinates, float range, LookupFlags flags = DefaultFlags) where T : IComponent { var entities = new HashSet>(); - GetEntitiesInRange(coordinates, range, entities); + GetEntitiesInRange(coordinates, range, entities, flags); return entities; } @@ -739,6 +739,47 @@ public void GetEntitiesOnMap(MapId mapId, HashSet + /// Gets the entities intersecting the specified broadphase entity using a local AABB. + /// + public void GetLocalEntitiesIntersecting( + EntityUid gridUid, + Vector2i localTile, + HashSet> intersecting, + float enlargement = TileEnlargementRadius, + LookupFlags flags = DefaultFlags, + MapGridComponent? gridComp = null) where T : IComponent + { + ushort tileSize = 1; + + if (_gridQuery.Resolve(gridUid, ref gridComp)) + { + tileSize = gridComp.TileSize; + } + + var localAABB = GetLocalBounds(localTile, tileSize); + localAABB = localAABB.Enlarged(TileEnlargementRadius); + GetLocalEntitiesIntersecting(gridUid, localAABB, intersecting, flags); + } + + /// + /// Gets the entities intersecting the specified broadphase entity using a local AABB. + /// + public void GetLocalEntitiesIntersecting( + EntityUid gridUid, + Box2 localAABB, + HashSet> intersecting, + LookupFlags flags = DefaultFlags) where T : IComponent + { + var query = GetEntityQuery(); + AddLocalEntitiesIntersecting(gridUid, intersecting, localAABB, flags, query); + AddContained(intersecting, flags, query); + } + + #endregion + /// /// Gets entities with the specified component with the specified parent. /// diff --git a/Robust.Shared/GameObjects/Systems/MetaDataSystem.cs b/Robust.Shared/GameObjects/Systems/MetaDataSystem.cs index f6ba9c41a1b..a67ba3d2589 100644 --- a/Robust.Shared/GameObjects/Systems/MetaDataSystem.cs +++ b/Robust.Shared/GameObjects/Systems/MetaDataSystem.cs @@ -42,12 +42,19 @@ private void OnMetaDataHandle(EntityUid uid, MetaDataComponent component, ref Co component.PauseTime = state.PauseTime; } - public void SetEntityName(EntityUid uid, string value, MetaDataComponent? metadata = null) + public void SetEntityName(EntityUid uid, string value, MetaDataComponent? metadata = null, bool raiseEvents = true) { if (!_metaQuery.Resolve(uid, ref metadata) || value.Equals(metadata.EntityName)) return; metadata._entityName = value; + + if (raiseEvents) + { + var ev = new EntityRenamedEvent(value); + RaiseLocalEvent(uid, ref ev); + } + Dirty(uid, metadata, metadata); } diff --git a/Robust.Shared/GameObjects/Systems/SharedEyeSystem.cs b/Robust.Shared/GameObjects/Systems/SharedEyeSystem.cs index 41473b22c61..039201ca8dc 100644 --- a/Robust.Shared/GameObjects/Systems/SharedEyeSystem.cs +++ b/Robust.Shared/GameObjects/Systems/SharedEyeSystem.cs @@ -1,3 +1,4 @@ +using System; using System.Numerics; using Robust.Shared.IoC; using Robust.Shared.Maths; @@ -97,6 +98,23 @@ public void SetZoom(EntityUid uid, Vector2 value, EyeComponent? eyeComponent = n eyeComponent.Eye.Zoom = value; } + public void SetPvsScale(Entity eye, float scale) + { + if (!Resolve(eye.Owner, ref eye.Comp, false)) + return; + + // Prevent a admin or some other fuck-up from causing exception spam in PVS system due to divide-by-zero or + // other such issues + if (!float.IsFinite(scale)) + { + Log.Error($"Attempted to set pvs scale to invalid value: {scale}. Eye: {ToPrettyString(eye)}"); + SetPvsScale(eye, 1); + return; + } + + eye.Comp.PvsScale = Math.Clamp(scale, 0.1f, 100f); + } + public void SetVisibilityMask(EntityUid uid, int value, EyeComponent? eyeComponent = null) { if (!Resolve(uid, ref eyeComponent)) diff --git a/Robust.Shared/Graphics/Eye.cs b/Robust.Shared/Graphics/Eye.cs index 1069e1441e6..461fbcaecc5 100644 --- a/Robust.Shared/Graphics/Eye.cs +++ b/Robust.Shared/Graphics/Eye.cs @@ -28,9 +28,12 @@ public class Eye : IEye public virtual MapCoordinates Position { get => _coords; - internal set => _coords = value; + set => _coords = value; } + /// + /// Eye offset, relative to the map, and not affected by + /// [ViewVariables(VVAccess.ReadWrite)] public Vector2 Offset { get; set; } diff --git a/Robust.Shared/Localization/LocalizationManager.Functions.cs b/Robust.Shared/Localization/LocalizationManager.Functions.cs index 356460a3e3f..0004ea66a32 100644 --- a/Robust.Shared/Localization/LocalizationManager.Functions.cs +++ b/Robust.Shared/Localization/LocalizationManager.Functions.cs @@ -425,6 +425,7 @@ public static IFluentType FluentFromObject(this object obj, LocContext context) { ILocValue wrap => new FluentLocWrapperType(wrap, context), EntityUid entity => new FluentLocWrapperType(new LocValueEntity(entity), context), + IFluentEntityUid entity => new FluentLocWrapperType(new LocValueEntity(entity.FluentOwner), context), DateTime dateTime => new FluentLocWrapperType(new LocValueDateTime(dateTime), context), TimeSpan timeSpan => new FluentLocWrapperType(new LocValueTimeSpan(timeSpan), context), Color color => (FluentString)color.ToHex(), @@ -455,4 +456,9 @@ public static IFluentType FluentFromVal(this ILocValue locValue, LocContext cont }; } } + + internal interface IFluentEntityUid + { + internal EntityUid FluentOwner { get; } + }; } diff --git a/Robust.Shared/Map/EntityCoordinates.cs b/Robust.Shared/Map/EntityCoordinates.cs index f3d8ddcd2e1..f029d8f2c53 100644 --- a/Robust.Shared/Map/EntityCoordinates.cs +++ b/Robust.Shared/Map/EntityCoordinates.cs @@ -248,6 +248,7 @@ public MapId GetMapId(IEntityManager entityManager) /// /// The vector to offset by local to the entity. /// Newly offset coordinates. + [Pure] public EntityCoordinates Offset(Vector2 position) { return new(EntityId, Position + position); diff --git a/Robust.Shared/Network/HappyEyeballsHttp.cs b/Robust.Shared/Network/HappyEyeballsHttp.cs index 62bf8bea19b..56622fc73ef 100644 --- a/Robust.Shared/Network/HappyEyeballsHttp.cs +++ b/Robust.Shared/Network/HappyEyeballsHttp.cs @@ -1,4 +1,9 @@ -using System.IO; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; using System.Net; using System.Net.Http; using System.Net.Sockets; @@ -9,79 +14,87 @@ namespace Robust.Shared.Network; internal static class HappyEyeballsHttp { + private const int ConnectionAttemptDelay = 250; + +#if DEBUG + + private const int SlowIpv6 = 0; + private const bool BrokenIpv6 = false; + +#endif + // .NET does not implement Happy Eyeballs at the time of writing. // https://github.com/space-wizards/SS14.Launcher/issues/38 // This is the workaround. // - // Implementation taken from https://github.com/ppy/osu-framework/pull/4191/files - - public static SocketsHttpHandler CreateHttpHandler() + // What's Happy Eyeballs? It makes the launcher try both IPv6 and IPv4, + // the former with priority, so that if IPv6 is broken your launcher still works. + // + // Implementation originally based on, + // rewritten as to be nigh-impossible to recognize https://github.com/ppy/osu-framework/pull/4191/files + // + // This is a simple implementation. It does not fully implement RFC 8305: + // * We do not separately handle parallel A and AAAA DNS requests as optimization. + // * We don't sort IPs as specified in RFC 6724. I can't tell if GetHostEntryAsync does. + // * Look I wanted to keep this simple OK? + // We don't do any fancy shit like statefulness or incremental sorting + // or incremental DNS updates who cares about that. + public static SocketsHttpHandler CreateHttpHandler(bool autoRedirect = true) { return new SocketsHttpHandler { ConnectCallback = OnConnect, AutomaticDecompression = DecompressionMethods.All, + AllowAutoRedirect = autoRedirect, + // PooledConnectionLifetime = TimeSpan.FromSeconds(1) }; } - /// - /// Whether IPv6 should be preferred. Value may change based on runtime failures. - /// - private static bool _useIPv6 = Socket.OSSupportsIPv6; - - /// - /// Whether the initial IPv6 check has been performed (to determine whether v6 is available or not). - /// - private static bool _hasResolvedIPv6Availability; - - private const int FirstTryTimeout = 2000; - private static async ValueTask OnConnect( SocketsHttpConnectionContext context, CancellationToken cancellationToken) { - if (_useIPv6) - { - try - { - var localToken = cancellationToken; + // Get IPs via DNS. + // Note that we do not attempt to exclude IPv6 if the user doesn't have IPv6. + // According to the docs, GetHostEntryAsync will not return them if there's no address. + // BUT! I tested and that's a lie at least on Linux. + // Regardless, if you don't have IPv6, + // an attempt to connect to an IPv6 socket *should* immediately give a "network unreachable" socket error. + // This will cause the code to immediately try the next address, + // so IPv6 just gets "skipped over" if you don't have it. + // I could find no other robust way to check "is there a chance in hell IPv6 works" other than "try it", + // so... try it we will. + var endPoint = context.DnsEndPoint; + var resolvedAddresses = await GetIpsForHost(endPoint, cancellationToken).ConfigureAwait(false); + if (resolvedAddresses.Length == 0) + throw new Exception($"Host {context.DnsEndPoint.Host} resolved to no IPs!"); - if (!_hasResolvedIPv6Availability) - { - // to make things move fast, use a very low timeout for the initial ipv6 attempt. - var quickFailCts = new CancellationTokenSource(FirstTryTimeout); - var linkedTokenSource = - CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, quickFailCts.Token); + // Sort as specified in the RFC, interleaving. + var ips = SortInterleaved(resolvedAddresses); - localToken = linkedTokenSource.Token; - } + Debug.Assert(ips.Length > 0); - return await AttemptConnection(AddressFamily.InterNetworkV6, context, localToken); - } - catch - { - // very naively fallback to ipv4 permanently for this execution based on the response of the first connection attempt. - // note that this may cause users to eventually get switched to ipv4 (on a random failure when they are switching networks, for instance) - // but in the interest of keeping this implementation simple, this is acceptable. - _useIPv6 = false; - } - finally - { - _hasResolvedIPv6Availability = true; - } - } + var (socket, index) = await ParallelTask( + ips.Length, + (i, cancel) => AttemptConnection(i, ips[i], endPoint.Port, cancel), + TimeSpan.FromMilliseconds(ConnectionAttemptDelay), + cancellationToken); - // fallback to IPv4. - return await AttemptConnection(AddressFamily.InterNetwork, context, cancellationToken); + // Log.Verbose("Successfully connected {EndPoint} to address: {Address}", endPoint, ips[index]); + + return new NetworkStream(socket, ownsSocket: true); } - private static async ValueTask AttemptConnection( - AddressFamily addressFamily, - SocketsHttpConnectionContext context, - CancellationToken cancellationToken) + private static async Task AttemptConnection( + int index, + IPAddress address, + int port, + CancellationToken cancel) { + // Log.Verbose("Trying IP {Address} for happy eyeballs [{Index}]", address, index); + // The following socket constructor will create a dual-mode socket on systems where IPV6 is available. - var socket = new Socket(addressFamily, SocketType.Stream, ProtocolType.Tcp) + var socket = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp) { // Turn off Nagle's algorithm since it degrades performance in most HttpClient scenarios. NoDelay = true @@ -89,15 +102,155 @@ private static async ValueTask AttemptConnection( try { - await socket.ConnectAsync(context.DnsEndPoint, cancellationToken).ConfigureAwait(false); - // The stream should take the ownership of the underlying socket, - // closing it when it's disposed. - return new NetworkStream(socket, ownsSocket: true); +#if DEBUG + if (address.AddressFamily == AddressFamily.InterNetworkV6) + { + await Task.Delay(SlowIpv6, cancel).ConfigureAwait(false); + + if (BrokenIpv6) + throw new Exception("Oh no I can't reach the network this is SO SAD."); + } +#endif + + await socket.ConnectAsync(new IPEndPoint(address, port), cancel).ConfigureAwait(false); + return socket; } - catch + catch (Exception e) { + // Log.Verbose(e, "Happy Eyeballs to {Address} [{Index}] failed", address, index); socket.Dispose(); throw; } } + + private static async Task GetIpsForHost(DnsEndPoint endPoint, CancellationToken cancel) + { + if (IPAddress.TryParse(endPoint.Host, out var ip)) + return [ip]; + + var entry = await Dns.GetHostEntryAsync(endPoint.Host, cancel).ConfigureAwait(false); + return entry.AddressList; + } + + private static IPAddress[] SortInterleaved(IPAddress[] addresses) + { + // Interleave returned addresses so that they are IPv6 -> IPv4 -> IPv6 -> IPv4. + // Assuming we have multiple addresses of the same type that is. + // As described in the RFC. + + var ipv6 = addresses.Where(x => x.AddressFamily == AddressFamily.InterNetworkV6).ToArray(); + var ipv4 = addresses.Where(x => x.AddressFamily == AddressFamily.InterNetwork).ToArray(); + + var commonLength = Math.Min(ipv6.Length, ipv4.Length); + + var result = new IPAddress[addresses.Length]; + for (var i = 0; i < commonLength; i++) + { + result[i * 2] = ipv6[i]; + result[1 + i * 2] = ipv4[i]; + } + + if (ipv4.Length > ipv6.Length) + { + ipv4.AsSpan(commonLength).CopyTo(result.AsSpan(commonLength * 2)); + } + else if (ipv6.Length > ipv4.Length) + { + ipv4.AsSpan(commonLength).CopyTo(result.AsSpan(commonLength * 2)); + } + + return result; + } + + [SuppressMessage("Usage", "RA0004:Risk of deadlock from accessing Task.Result")] + internal static async Task<(T, int)> ParallelTask( + int candidateCount, + Func> taskBuilder, + TimeSpan delay, + CancellationToken cancel) where T : IDisposable + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(candidateCount); + + using var successCts = CancellationTokenSource.CreateLinkedTokenSource(cancel); + + // All tasks we have ever tried. + var allTasks = new List>(); + // Tasks we are still waiting on. + var tasks = new List>(); + + // The general loop here is as follows: + // 1. Add a new task for the next IP to try. + // 2. Wait until any task completes OR the delay happens. + // If an error occurs, we stop checking that task and continue checking the next. + // Every iteration we add another task, until we're full on them. + // We keep looping until we have SUCCESS, or we run out of attempt tasks entirely. + + Task? successTask = null; + while (successTask == null && (allTasks.Count < candidateCount || tasks.Count > 0)) + { + if (allTasks.Count < candidateCount) + { + // We have to queue another task this iteration. + var newTask = taskBuilder(allTasks.Count, successCts.Token); + tasks.Add(newTask); + allTasks.Add(newTask); + } + + var whenAnyDone = Task.WhenAny(tasks); + Task completedTask; + + if (allTasks.Count < candidateCount) + { + // Log.Verbose("Waiting on ConnectionAttemptDelay"); + // If we have another one to queue, wait for a timeout instead of *just* waiting for a connection task. + var timeoutTask = Task.Delay(delay, successCts.Token); + var whenAnyOrTimeout = await Task.WhenAny(whenAnyDone, timeoutTask).ConfigureAwait(false); + if (whenAnyOrTimeout != whenAnyDone) + { + // Timeout finished. Go to next iteration so we queue another one. + continue; + } + + completedTask = whenAnyDone.Result; + } + else + { + completedTask = await whenAnyDone.ConfigureAwait(false); + } + + if (completedTask.IsCompletedSuccessfully) + { + // We did it. We have success. + successTask = completedTask; + break; + } + else + { + // Faulted. Remove it. + tasks.Remove(completedTask); + } + } + + Debug.Assert(allTasks.Count > 0); + + cancel.ThrowIfCancellationRequested(); + await successCts.CancelAsync().ConfigureAwait(false); + + if (successTask == null) + { + // We didn't get a single successful connection. Well heck. + throw new AggregateException( + allTasks.Where(x => x.IsFaulted).SelectMany(x => x.Exception!.InnerExceptions)); + } + + // I don't know if this is possible but MAKE SURE that we don't get two sockets completing at once. + // Just a safety measure. + foreach (var task in allTasks) + { + if (task.IsCompletedSuccessfully && task != successTask) + task.Result.Dispose(); + } + + return (successTask.Result, allTasks.IndexOf(successTask)); + } } diff --git a/Robust.Shared/Network/Messages/MsgPlayerList.cs b/Robust.Shared/Network/Messages/MsgPlayerList.cs index b77f44c027e..113769ed963 100644 --- a/Robust.Shared/Network/Messages/MsgPlayerList.cs +++ b/Robust.Shared/Network/Messages/MsgPlayerList.cs @@ -12,14 +12,13 @@ public sealed class MsgPlayerList : NetMessage { public override MsgGroups MsgGroup => MsgGroups.Core; - public byte PlyCount { get; set; } public List Plyrs { get; set; } public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer) { - Plyrs = new List(); - PlyCount = buffer.ReadByte(); - for (var i = 0; i < PlyCount; i++) + var playerCount = buffer.ReadInt32(); + Plyrs = new List(playerCount); + for (var i = 0; i < playerCount; i++) { var plyNfo = new SessionState { @@ -34,7 +33,7 @@ public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer) { - buffer.Write(PlyCount); + buffer.Write(Plyrs.Count); foreach (var ply in Plyrs) { diff --git a/Robust.Shared/Network/Messages/MsgState.cs b/Robust.Shared/Network/Messages/MsgState.cs index 41135b9c31d..95eed008e8d 100644 --- a/Robust.Shared/Network/Messages/MsgState.cs +++ b/Robust.Shared/Network/Messages/MsgState.cs @@ -17,15 +17,21 @@ public sealed class MsgState : NetMessage // Ideally we would peg this to the actual configured MTU instead of the default constant, but oh well... public const int ReliableThreshold = NetPeerConfiguration.kDefaultMTU - 20; - // If a state is larger than this, compress it with deflate. + // If a state is larger than this, we will compress it + // TODO PVS make this a cvar + // TODO PVS figure out optimal value public const int CompressionThreshold = 256; public override MsgGroups MsgGroup => MsgGroups.Entity; public GameState State; + public MemoryStream StateStream; + public ZStdCompressionContext CompressionContext; - internal bool _hasWritten; + internal bool HasWritten; + + internal bool ForceSendReliably; public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer) { @@ -53,33 +59,33 @@ public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer buffer.ReadAlignedMemory(finalStream, uncompressedLength); } - serializer.DeserializeDirect(finalStream, out State); + try + { + serializer.DeserializeDirect(finalStream, out State); + } + finally + { + finalStream.Dispose(); + } + State.PayloadSize = uncompressedLength; - finalStream.Dispose(); } public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer) { - using var stateStream = RobustMemoryManager.GetMemoryStream(); - serializer.SerializeDirect(stateStream, State); - buffer.WriteVariableInt32((int)stateStream.Length); + buffer.WriteVariableInt32((int)StateStream.Length); // We compress the state. - if (stateStream.Length > CompressionThreshold) + if (StateStream.Length > CompressionThreshold) { // var sw = Stopwatch.StartNew(); - stateStream.Position = 0; - var buf = ArrayPool.Shared.Rent(ZStd.CompressBound((int)stateStream.Length)); - var length = CompressionContext.Compress2(buf, stateStream.AsSpan()); + StateStream.Position = 0; + var buf = ArrayPool.Shared.Rent(ZStd.CompressBound((int)StateStream.Length)); + var length = CompressionContext.Compress2(buf, StateStream.AsSpan()); buffer.WriteVariableInt32(length); - buffer.Write(buf.AsSpan(0, length)); - // var elapsed = sw.Elapsed; - // System.Console.WriteLine( - // $"From: {State.FromSequence} To: {State.ToSequence} Size: {length} B Before: {stateStream.Length} B time: {elapsed}"); - ArrayPool.Shared.Return(buf); } // The state is sent as is. @@ -87,10 +93,10 @@ public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer { // 0 means that the state isn't compressed. buffer.WriteVariableInt32(0); - buffer.Write(stateStream.AsSpan()); + buffer.Write(StateStream.AsSpan()); } - _hasWritten = true; + HasWritten = true; MsgSize = buffer.LengthBytes; } @@ -101,21 +107,12 @@ public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer /// public bool ShouldSendReliably() { - DebugTools.Assert(_hasWritten, "Attempted to determine sending method before determining packet size."); - return State.ForceSendReliably || MsgSize > ReliableThreshold; + DebugTools.Assert(HasWritten, "Attempted to determine sending method before determining packet size."); + return ForceSendReliably || MsgSize > ReliableThreshold; } - public override NetDeliveryMethod DeliveryMethod - { - get - { - if (ShouldSendReliably()) - { - return NetDeliveryMethod.ReliableUnordered; - } - - return base.DeliveryMethod; - } - } + public override NetDeliveryMethod DeliveryMethod => ShouldSendReliably() + ? NetDeliveryMethod.ReliableUnordered + : base.DeliveryMethod; } } diff --git a/Robust.Shared/Physics/Systems/SharedBroadphaseSystem.cs b/Robust.Shared/Physics/Systems/SharedBroadphaseSystem.cs index ea4d54fca4a..46693f74afb 100644 --- a/Robust.Shared/Physics/Systems/SharedBroadphaseSystem.cs +++ b/Robust.Shared/Physics/Systems/SharedBroadphaseSystem.cs @@ -31,11 +31,14 @@ public abstract class SharedBroadphaseSystem : EntitySystem [Dependency] private readonly SharedTransformSystem _transform = default!; private EntityQuery _broadphaseQuery; + private EntityQuery _fixturesQuery; private EntityQuery _gridQuery; private EntityQuery _physicsQuery; private EntityQuery _xformQuery; private EntityQuery _mapQuery; + private float _broadphaseExpand; + /* * Okay so Box2D has its own "MoveProxy" stuff so you can easily find new contacts when required. * Our problem is that we have nested broadphases (rather than being on separate maps) which makes this @@ -43,23 +46,21 @@ public abstract class SharedBroadphaseSystem : EntitySystem * Hence we need to check which broadphases it does intersect and checkar for colliding bodies. */ - /// - /// How much to expand bounds by to check cross-broadphase collisions. - /// Ideally you want to set this to your largest body size. - /// This only has a noticeable performance impact where multiple broadphases are in close proximity. - /// - private float _broadphaseExpand; - - private const int PairBufferParallel = 8; - - private ObjectPool> _bufferPool = - new DefaultObjectPool>(new ListPolicy(), 2048); + private BroadphaseContactJob _contactJob; public override void Initialize() { base.Initialize(); + _contactJob = new() + { + _mapManager = _mapManager, + System = this, + BroadphaseExpand = _broadphaseExpand, + }; + _broadphaseQuery = GetEntityQuery(); + _fixturesQuery = GetEntityQuery(); _gridQuery = GetEntityQuery(); _physicsQuery = GetEntityQuery(); _xformQuery = GetEntityQuery(); @@ -71,7 +72,11 @@ public override void Initialize() Subs.CVar(_cfg, CVars.BroadphaseExpand, SetBroadphaseExpand, true); } - private void SetBroadphaseExpand(float value) => _broadphaseExpand = value; + private void SetBroadphaseExpand(float value) + { + _contactJob.BroadphaseExpand = value; + _broadphaseExpand = value; + } #region Find Contacts @@ -176,65 +181,34 @@ internal void FindNewContacts(PhysicsMapComponent component, MapId mapId) if (moveBuffer.Count == 0) return; - var count = moveBuffer.Count; - var contactBuffer = ArrayPool>.Shared.Rent(count); - var pMoveBuffer = ArrayPool<(FixtureProxy Proxy, Box2 AABB)>.Shared.Rent(count); - - var idx = 0; + _contactJob.MapUid = _mapManager.GetMapEntityIdOrThrow(mapId); + _contactJob.MoveBuffer.Clear(); foreach (var (proxy, aabb) in moveBuffer) { - contactBuffer[idx] = _bufferPool.Get(); - pMoveBuffer[idx++] = (proxy, aabb); + _contactJob.MoveBuffer.Add((proxy, aabb)); } - var options = new ParallelOptions - { - MaxDegreeOfParallelism = _parallel.ParallelProcessCount, - }; - - var batches = (int)MathF.Ceiling((float) count / PairBufferParallel); - - Parallel.For(0, batches, options, i => + for (var i = _contactJob.ContactBuffer.Count; i < _contactJob.MoveBuffer.Count; i++) { - var start = i * PairBufferParallel; - var end = Math.Min(start + PairBufferParallel, count); - - for (var j = start; j < end; j++) - { - var (proxy, worldAABB) = pMoveBuffer[j]; - var buffer = contactBuffer[j]; - - var proxyBody = proxy.Body; - DebugTools.Assert(!proxyBody.Deleted); - - var state = (this, proxy, worldAABB, buffer); + _contactJob.ContactBuffer.Add(new List()); + } - // Get every broadphase we may be intersecting. - _mapManager.FindGridsIntersecting(mapId, worldAABB.Enlarged(_broadphaseExpand), ref state, - static (EntityUid uid, MapGridComponent _, ref ( - SharedBroadphaseSystem system, - FixtureProxy proxy, - Box2 worldAABB, - List pairBuffer) tuple) => - { - ref var buffer = ref tuple.pairBuffer; - tuple.system.FindPairs(tuple.proxy, tuple.worldAABB, uid, buffer); - return true; - }, approx: true, includeMap: false); + var count = moveBuffer.Count; - // Struct ref moment, I have no idea what's fastest. - buffer = state.buffer; - FindPairs(proxy, worldAABB, _mapManager.GetMapEntityId(mapId), buffer); - } - }); + _parallel.ProcessNow(_contactJob, count); for (var i = 0; i < count; i++) { - var proxyA = pMoveBuffer[i].Proxy; - var proxies = contactBuffer[i]; + var proxies = _contactJob.ContactBuffer[i]; + + if (proxies.Count == 0) + continue; + + var proxyA = _contactJob.MoveBuffer[i].Proxy; var proxyABody = proxyA.Body; - FixturesComponent? manager = null; + + _fixturesQuery.TryGetComponent(proxyA.Entity, out var manager); foreach (var other in proxies) { @@ -253,13 +227,8 @@ internal void FindNewContacts(PhysicsMapComponent component, MapId mapId) _physicsSystem.AddPair(proxyA.FixtureId, other.FixtureId, proxyA, other); } - - _bufferPool.Return(contactBuffer[i]); - pMoveBuffer[i] = default; } - ArrayPool>.Shared.Return(contactBuffer); - ArrayPool<(FixtureProxy Proxy, Box2 AABB)>.Shared.Return(pMoveBuffer); moveBuffer.Clear(); movedGrids.Clear(); } @@ -516,5 +485,51 @@ public void Refilter(EntityUid uid, Fixture fixture, TransformComponent? xform = } } } + + private record struct BroadphaseContactJob() : IParallelRobustJob + { + public SharedBroadphaseSystem System = default!; + public IMapManager _mapManager = default!; + + public float BroadphaseExpand; + + public EntityUid MapUid; + + public List> ContactBuffer = new(); + public List<(FixtureProxy Proxy, Box2 WorldAABB)> MoveBuffer = new(); + + public int BatchSize => 8; + + public void Execute(int index) + { + var (proxy, worldAABB) = MoveBuffer[index]; + var buffer = ContactBuffer[index]; + buffer.Clear(); + + var proxyBody = proxy.Body; + DebugTools.Assert(!proxyBody.Deleted); + + var state = (System, proxy, worldAABB, buffer); + + // Get every broadphase we may be intersecting. + _mapManager.FindGridsIntersecting(MapUid, worldAABB.Enlarged(BroadphaseExpand), ref state, + static (EntityUid uid, MapGridComponent _, ref ( + SharedBroadphaseSystem system, + FixtureProxy proxy, + Box2 worldAABB, + List pairBuffer) tuple) => + { + ref var buffer = ref tuple.pairBuffer; + tuple.system.FindPairs(tuple.proxy, tuple.worldAABB, uid, buffer); + return true; + }, + approx: true, + includeMap: false); + + // Struct ref moment, I have no idea what's fastest. + buffer = state.buffer; + System.FindPairs(proxy, worldAABB, MapUid, buffer); + } + } } } diff --git a/Robust.Shared/Player/ActorSystem.cs b/Robust.Shared/Player/ActorSystem.cs index b725556e1ee..c34c5d8e83f 100644 --- a/Robust.Shared/Player/ActorSystem.cs +++ b/Robust.Shared/Player/ActorSystem.cs @@ -1,3 +1,4 @@ +using JetBrains.Annotations; using Robust.Shared.GameObjects; using Robust.Shared.IoC; @@ -20,4 +21,29 @@ private void OnActorShutdown(EntityUid entity, ActorComponent component, Compone { _playerManager.SetAttachedEntity(component.PlayerSession, null); } + + [PublicAPI] + public bool TryGetSession(EntityUid? uid, out ICommonSession? session) + { + if (TryComp(uid, out ActorComponent? actorComp)) + { + session = actorComp.PlayerSession; + return true; + } + + session = null; + return false; + } + + [PublicAPI] + [Pure] + public ICommonSession? GetSession(EntityUid? uid) + { + if (TryComp(uid, out ActorComponent? actorComp)) + { + return actorComp.PlayerSession; + } + + return null; + } } diff --git a/Robust.Shared/Prototypes/IPrototypeManager.cs b/Robust.Shared/Prototypes/IPrototypeManager.cs index 48b67cdce41..deea42796e7 100644 --- a/Robust.Shared/Prototypes/IPrototypeManager.cs +++ b/Robust.Shared/Prototypes/IPrototypeManager.cs @@ -143,17 +143,20 @@ bool TryGetInstances([NotNullWhen(true)] out FrozenDictionary? ins /// FrozenDictionary GetInstances() where T : IPrototype; - /// - bool TryIndex(EntProtoId id, [NotNullWhen(true)] out EntityPrototype? prototype); + /// + bool TryIndex(EntProtoId id, [NotNullWhen(true)] out EntityPrototype? prototype, bool logError = true); - /// - bool TryIndex(ProtoId id, [NotNullWhen(true)] out T? prototype) where T : class, IPrototype; + /// + /// Attempt to retrieve the prototype corresponding to the given prototype id. + /// Unless otherwise specified, this will log an error if the id does not match any known prototype. + /// + bool TryIndex(ProtoId id, [NotNullWhen(true)] out T? prototype, bool logError = true) where T : class, IPrototype; - /// - bool TryIndex(EntProtoId? id, [NotNullWhen(true)] out EntityPrototype? prototype); + /// + bool TryIndex(EntProtoId? id, [NotNullWhen(true)] out EntityPrototype? prototype, bool logError = true); - /// - bool TryIndex(ProtoId? id, [NotNullWhen(true)] out T? prototype) where T : class, IPrototype; + /// + bool TryIndex(ProtoId? id, [NotNullWhen(true)] out T? prototype, bool logError = true) where T : class, IPrototype; bool HasMapping(string id); bool TryGetMapping(Type kind, string id, [NotNullWhen(true)] out MappingDataNode? mappings); diff --git a/Robust.Shared/Prototypes/PrototypeManager.Categories.cs b/Robust.Shared/Prototypes/PrototypeManager.Categories.cs index 95e97768b50..33389f95721 100644 --- a/Robust.Shared/Prototypes/PrototypeManager.Categories.cs +++ b/Robust.Shared/Prototypes/PrototypeManager.Categories.cs @@ -108,7 +108,7 @@ private IReadOnlySet UpdateCategories(EntProtoId id, } } - DebugTools.Assert(!TryIndex(id, out var instance) + DebugTools.Assert(!TryIndex(id, out var instance, false) || instance.CategoriesInternal == null || instance.CategoriesInternal.All(x => set.Any(y => y.ID == x))); @@ -124,7 +124,7 @@ private IReadOnlySet UpdateCategories(EntProtoId id, } } - if (!TryIndex(id, out var protoInstance)) + if (!TryIndex(id, out var protoInstance, false)) { // Prototype is abstract cache.Add(id, set); diff --git a/Robust.Shared/Prototypes/PrototypeManager.cs b/Robust.Shared/Prototypes/PrototypeManager.cs index 2c99ca7376f..4116395d32a 100644 --- a/Robust.Shared/Prototypes/PrototypeManager.cs +++ b/Robust.Shared/Prototypes/PrototypeManager.cs @@ -744,19 +744,29 @@ public bool TryIndex(Type kind, string id, [NotNullWhen(true)] out IPrototype? p } /// - public bool TryIndex(EntProtoId id, [NotNullWhen(true)] out EntityPrototype? prototype) + public bool TryIndex(EntProtoId id, [NotNullWhen(true)] out EntityPrototype? prototype, bool logError = true) { - return TryIndex(id.Id, out prototype); + if (TryIndex(id.Id, out prototype)) + return true; + + if (logError) + Sawmill.Error($"Attempted to resolve invalid {nameof(EntProtoId)}: {id.Id}"); + return false; } /// - public bool TryIndex(ProtoId id, [NotNullWhen(true)] out T? prototype) where T : class, IPrototype + public bool TryIndex(ProtoId id, [NotNullWhen(true)] out T? prototype, bool logError = true) where T : class, IPrototype { - return TryIndex(id.Id, out prototype); + if (TryIndex(id.Id, out prototype)) + return true; + + if (logError) + Sawmill.Error($"Attempted to resolve invalid ProtoId<{typeof(T).Name}>: {id.Id}"); + return false; } /// - public bool TryIndex(EntProtoId? id, [NotNullWhen(true)] out EntityPrototype? prototype) + public bool TryIndex(EntProtoId? id, [NotNullWhen(true)] out EntityPrototype? prototype, bool logError = true) { if (id == null) { @@ -764,11 +774,11 @@ public bool TryIndex(EntProtoId? id, [NotNullWhen(true)] out EntityPrototype? pr return false; } - return TryIndex(id.Value, out prototype); + return TryIndex(id.Value, out prototype, logError); } /// - public bool TryIndex(ProtoId? id, [NotNullWhen(true)] out T? prototype) where T : class, IPrototype + public bool TryIndex(ProtoId? id, [NotNullWhen(true)] out T? prototype, bool logError = true) where T : class, IPrototype { if (id == null) { @@ -776,7 +786,7 @@ public bool TryIndex(ProtoId? id, [NotNullWhen(true)] out T? prototype) wh return false; } - return TryIndex(id.Value, out prototype); + return TryIndex(id.Value, out prototype, logError); } /// diff --git a/Robust.Shared/Random/RandomExtensions.cs b/Robust.Shared/Random/RandomExtensions.cs index 079e1981586..a499b4a7a6e 100644 --- a/Robust.Shared/Random/RandomExtensions.cs +++ b/Robust.Shared/Random/RandomExtensions.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Numerics; using Robust.Shared.Collections; using Robust.Shared.Maths; using Robust.Shared.Utility; @@ -128,6 +129,9 @@ public static Angle NextAngle(this System.Random random, Angle minAngle, Angle m return minAngle + (maxAngle - minAngle) * random.NextDouble(); } + public static Vector2 NextPolarVector2(this System.Random random, float minMagnitude, float maxMagnitude) + => random.NextAngle().RotateVec(new Vector2(random.NextFloat(minMagnitude, maxMagnitude), 0)); + public static float NextFloat(this IRobustRandom random) { // This is pretty much the CoreFX implementation. @@ -141,6 +145,9 @@ public static float NextFloat(this System.Random random) return random.Next() * 4.6566128752458E-10f; } + public static float NextFloat(this System.Random random, float minValue, float maxValue) + => random.NextFloat() * (maxValue - minValue) + minValue; + /// /// Have a certain chance to return a boolean. /// diff --git a/Robust.Shared/SharedIoC.cs b/Robust.Shared/SharedIoC.cs index 352a499da96..df176b1131a 100644 --- a/Robust.Shared/SharedIoC.cs +++ b/Robust.Shared/SharedIoC.cs @@ -1,5 +1,6 @@ using Robust.Shared.Asynchronous; using Robust.Shared.Configuration; +using Robust.Shared.Console; using Robust.Shared.ContentPack; using Robust.Shared.Exceptions; using Robust.Shared.GameObjects; @@ -50,6 +51,7 @@ public static void RegisterIoC(IDependencyCollection deps) deps.Register(); deps.Register(); deps.Register(); + deps.Register(); } } } diff --git a/Robust.Shared/Toolshed/Commands/Values/EntCommand.cs b/Robust.Shared/Toolshed/Commands/Values/EntCommand.cs index fa6c4f43460..7e96f2e27eb 100644 --- a/Robust.Shared/Toolshed/Commands/Values/EntCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Values/EntCommand.cs @@ -1,4 +1,5 @@ using Robust.Shared.GameObjects; +using Robust.Shared.Toolshed.Syntax; namespace Robust.Shared.Toolshed.Commands.Values; @@ -6,6 +7,6 @@ namespace Robust.Shared.Toolshed.Commands.Values; internal sealed class EntCommand : ToolshedCommand { [CommandImplementation] - public EntityUid Ent([CommandArgument] EntityUid ent) => ent; + public EntityUid Ent([CommandArgument] ValueRef ent, [CommandInvocationContext] IInvocationContext ctx) => ent.Evaluate(ctx); } diff --git a/Robust.Shared/Toolshed/TypeParsers/EntityTypeParser.cs b/Robust.Shared/Toolshed/TypeParsers/EntityTypeParser.cs index f9f7d38deef..d270e28ad00 100644 --- a/Robust.Shared/Toolshed/TypeParsers/EntityTypeParser.cs +++ b/Robust.Shared/Toolshed/TypeParsers/EntityTypeParser.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using Robust.Shared.Console; using Robust.Shared.GameObjects; +using Robust.Shared.IoC; using Robust.Shared.Maths; using Robust.Shared.Toolshed.Errors; using Robust.Shared.Toolshed.Syntax; @@ -12,13 +13,15 @@ namespace Robust.Shared.Toolshed.TypeParsers; internal sealed class EntityTypeParser : TypeParser { + [Dependency] private readonly IEntityManager _entityManager = default!; + public override bool TryParse(ParserContext parser, [NotNullWhen(true)] out object? result, out IConError? error) { var start = parser.Index; var word = parser.GetWord(ParserContext.IsToken); error = null; - if (!EntityUid.TryParse(word, out var ent)) + if (!NetEntity.TryParse(word, out var ent)) { result = null; @@ -31,7 +34,7 @@ public override bool TryParse(ParserContext parser, [NotNullWhen(true)] out obje return false; } - result = ent; + result = _entityManager.GetEntity(ent); return true; } diff --git a/Robust.Shared/Utility/DebugTools.cs b/Robust.Shared/Utility/DebugTools.cs index 2e63b6d968b..73ea1f3de23 100644 --- a/Robust.Shared/Utility/DebugTools.cs +++ b/Robust.Shared/Utility/DebugTools.cs @@ -220,14 +220,16 @@ public static void Assert( /// is . /// /// Condition that must be true. + /// Exception message. [Conditional("DEBUG")] [AssertionMethod] public static void AssertNotNull([AssertionCondition(AssertionConditionType.IS_NOT_NULL)] - object? arg) + object? arg, + string? message = null) { if (arg == null) { - throw new DebugAssertException(); + throw new DebugAssertException(message?? "value cannot be null"); } } @@ -236,14 +238,16 @@ public static void AssertNotNull([AssertionCondition(AssertionConditionType.IS_N /// is not . /// /// Condition that must be true. + /// Exception message. [Conditional("DEBUG")] [AssertionMethod] public static void AssertNull([AssertionCondition(AssertionConditionType.IS_NULL)] - object? arg) + object? arg, + string? message = null) { if (arg != null) { - throw new DebugAssertException(); + throw new DebugAssertException(message ?? "value should be null"); } } @@ -290,7 +294,7 @@ public DebugAssertException() { } - public DebugAssertException(string message) : base(message) + public DebugAssertException(string? message) : base(message) { } } diff --git a/Robust.Shared/Utility/FormattedMessage.cs b/Robust.Shared/Utility/FormattedMessage.cs index b6051eaf047..364e526d974 100644 --- a/Robust.Shared/Utility/FormattedMessage.cs +++ b/Robust.Shared/Utility/FormattedMessage.cs @@ -16,24 +16,30 @@ namespace Robust.Shared.Utility; /// [PublicAPI] [Serializable, NetSerializable] -public sealed partial class FormattedMessage +public sealed partial class FormattedMessage : IReadOnlyList { + public static FormattedMessage Empty => new(); + /// /// The list of nodes the formatted message is made out of /// - public IReadOnlyList Nodes => _nodes.AsReadOnly(); + public IReadOnlyList Nodes => _nodes; /// /// true if the formatted message doesn't contain any nodes /// public bool IsEmpty => _nodes.Count == 0; + public int Count => _nodes.Count; + + public MarkupNode this[int index] => _nodes[index]; + private readonly List _nodes; /// /// Used for inserting the correct closing node when calling /// - private readonly Stack _openNodeStack = new(); + private Stack? _openNodeStack; public FormattedMessage() { @@ -197,6 +203,7 @@ public void PushTag(MarkupNode markupNode, bool selfClosing = false) return; } + _openNodeStack ??= new Stack(); _openNodeStack.Push(markupNode); } @@ -205,7 +212,7 @@ public void PushTag(MarkupNode markupNode, bool selfClosing = false) /// public void Pop() { - if (!_openNodeStack.TryPop(out var node)) + if (_openNodeStack == null || !_openNodeStack.TryPop(out var node)) return; _nodes.Add(new MarkupNode(node.Name, null, null, true)); @@ -236,6 +243,16 @@ public FormattedMessageRuneEnumerator EnumerateRunes() return new FormattedMessageRuneEnumerator(this); } + public NodeEnumerator GetEnumerator() + { + return new NodeEnumerator(_nodes.GetEnumerator()); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + /// The string without markup tags. public override string ToString() { @@ -250,6 +267,11 @@ public override string ToString() return builder.ToString(); } + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + /// The string without filtering out markup tags. public string ToMarkup() { @@ -259,13 +281,13 @@ public string ToMarkup() public struct FormattedMessageRuneEnumerator : IEnumerable, IEnumerator { private readonly FormattedMessage _msg; - private IEnumerator _tagEnumerator; + private List.Enumerator _tagEnumerator; private StringRuneEnumerator _runeEnumerator; internal FormattedMessageRuneEnumerator(FormattedMessage msg) { _msg = msg; - _tagEnumerator = msg.Nodes.GetEnumerator(); + _tagEnumerator = msg._nodes.GetEnumerator(); // Rune enumerator will immediately give false on first iteration so I dont' need to special case anything. _runeEnumerator = "".EnumerateRunes(); } @@ -299,7 +321,7 @@ public bool MoveNext() public void Reset() { - _tagEnumerator = _msg.Nodes.GetEnumerator(); + _tagEnumerator = _msg._nodes.GetEnumerator(); _runeEnumerator = "".EnumerateRunes(); } @@ -311,4 +333,33 @@ void IDisposable.Dispose() { } } + + public struct NodeEnumerator : IEnumerator + { + private List.Enumerator _enumerator; + + internal NodeEnumerator(List.Enumerator enumerator) + { + _enumerator = enumerator; + } + + public bool MoveNext() + { + return _enumerator.MoveNext(); + } + + void IEnumerator.Reset() + { + ((IEnumerator) _enumerator).Reset(); + } + + public MarkupNode Current => _enumerator.Current; + + object IEnumerator.Current => Current; + + public void Dispose() + { + _enumerator.Dispose(); + } + } } diff --git a/Robust.Shared/Utility/MarkupNode.cs b/Robust.Shared/Utility/MarkupNode.cs index f6f79533aaa..fc6713bf486 100644 --- a/Robust.Shared/Utility/MarkupNode.cs +++ b/Robust.Shared/Utility/MarkupNode.cs @@ -11,7 +11,7 @@ public sealed class MarkupNode : IComparable { public readonly string? Name; public readonly MarkupParameter Value; - public readonly Dictionary Attributes = new(); + public readonly Dictionary Attributes; public readonly bool Closing; /// @@ -20,6 +20,7 @@ public sealed class MarkupNode : IComparable /// The plaintext the tag will consist of public MarkupNode(string text) { + Attributes = new Dictionary(); Value = new MarkupParameter(text); } diff --git a/Robust.UnitTesting/Server/GameObjects/Components/Container_Test.cs b/Robust.UnitTesting/Server/GameObjects/Components/Container_Test.cs index 434ec44fd52..80191f52cc3 100644 --- a/Robust.UnitTesting/Server/GameObjects/Components/Container_Test.cs +++ b/Robust.UnitTesting/Server/GameObjects/Components/Container_Test.cs @@ -267,7 +267,7 @@ public void Container_Serialize() var containerMan = entManager.GetComponent(entity); var getState = new ComponentGetState(); - entManager.EventBus.RaiseComponentEvent(containerMan, ref getState); + entManager.EventBus.RaiseComponentEvent(entity, containerMan, ref getState); var state = (ContainerManagerComponent.ContainerManagerComponentState)getState.State!; Assert.That(state.Containers, Has.Count.EqualTo(1)); diff --git a/Robust.UnitTesting/Server/RobustServerSimulation.cs b/Robust.UnitTesting/Server/RobustServerSimulation.cs index 2c1426feb88..d8c71cbff23 100644 --- a/Robust.UnitTesting/Server/RobustServerSimulation.cs +++ b/Robust.UnitTesting/Server/RobustServerSimulation.cs @@ -232,6 +232,7 @@ public ISimulation InitializeInstance() container.Register(); // Needed for grid fixture debugging. container.Register(); + container.Register(); // I just wanted to load pvs system container.Register(); diff --git a/Robust.UnitTesting/Shared/Collections/RingBufferListTest.cs b/Robust.UnitTesting/Shared/Collections/RingBufferListTest.cs new file mode 100644 index 00000000000..950bb3f2434 --- /dev/null +++ b/Robust.UnitTesting/Shared/Collections/RingBufferListTest.cs @@ -0,0 +1,95 @@ +using NUnit.Framework; +using Robust.Shared.Collections; + +namespace Robust.UnitTesting.Shared.Collections; + +[Parallelizable(ParallelScope.All | ParallelScope.Fixtures)] +[TestFixture, TestOf(typeof(RingBufferList<>))] +public sealed class RingBufferListTest +{ + [Test] + public void TestBasicAdd() + { + var list = new RingBufferList(); + list.Add(1); + list.Add(2); + list.Add(3); + + Assert.That(list, NUnit.Framework.Is.EquivalentTo(new[] {1, 2, 3})); + } + + [Test] + public void TestBasicAddAfterWrap() + { + var list = new RingBufferList(6); + list.Add(1); + list.Add(2); + list.Add(3); + list.RemoveAt(0); + list.Add(4); + list.Add(5); + list.Add(6); + + Assert.Multiple(() => + { + // Ensure wrapping properly happened and we didn't expand. + // (one slot is wasted by nature of implementation) + Assert.That(list.Capacity, NUnit.Framework.Is.EqualTo(6)); + Assert.That(list, NUnit.Framework.Is.EquivalentTo(new[] { 2, 3, 4, 5, 6 })); + }); + } + + [Test] + public void TestMiddleRemoveAtScenario1() + { + var list = new RingBufferList(6); + list.Add(-1); + list.Add(-1); + list.Add(-1); + list.Add(-1); + list.Add(1); + list.RemoveAt(0); + list.RemoveAt(0); + list.RemoveAt(0); + list.RemoveAt(0); + list.Add(2); + list.Add(3); + list.Add(4); + list.Add(5); + list.Remove(4); + + Assert.That(list, NUnit.Framework.Is.EquivalentTo(new[] {1, 2, 3, 5})); + } + + [Test] + public void TestMiddleRemoveAtScenario2() + { + var list = new RingBufferList(6); + list.Add(-1); + list.Add(-1); + list.Add(1); + list.RemoveAt(0); + list.RemoveAt(0); + list.Add(2); + list.Add(3); + list.Add(4); + list.Add(5); + list.Remove(3); + + Assert.That(list, NUnit.Framework.Is.EquivalentTo(new[] {1, 2, 4, 5})); + } + + [Test] + public void TestMiddleRemoveAtScenario3() + { + var list = new RingBufferList(6); + list.Add(1); + list.Add(2); + list.Add(3); + list.Add(4); + list.Add(5); + list.Remove(4); + + Assert.That(list, NUnit.Framework.Is.EquivalentTo(new[] {1, 2, 3, 5})); + } +} diff --git a/Robust.UnitTesting/Shared/GameObjects/EntityEventBusTests.ComponentEvent.cs b/Robust.UnitTesting/Shared/GameObjects/EntityEventBusTests.ComponentEvent.cs index fbe7a31204a..acd2f0a3d81 100644 --- a/Robust.UnitTesting/Shared/GameObjects/EntityEventBusTests.ComponentEvent.cs +++ b/Robust.UnitTesting/Shared/GameObjects/EntityEventBusTests.ComponentEvent.cs @@ -85,6 +85,7 @@ public void UnsubscribeCompEvent() compFacMock.Setup(m => m.GetRegistration(CompIdx.Index())).Returns(compRegistration); compFacMock.Setup(m => m.GetAllRegistrations()).Returns(new[] { compRegistration }); + compFacMock.Setup(m => m.GetIndex(typeof(MetaDataComponent))).Returns(CompIdx.Index()); entManMock.Setup(m => m.ComponentFactory).Returns(compFacMock.Object); IComponent? outIComponent = compInstance; @@ -143,6 +144,7 @@ public void SubscribeCompLifeEvent() compFacMock.Setup(m => m.GetRegistration(CompIdx.Index())).Returns(compRegistration); compFacMock.Setup(m => m.GetAllRegistrations()).Returns(new[] { compRegistration }); + compFacMock.Setup(m => m.GetIndex(typeof(MetaDataComponent))).Returns(CompIdx.Index()); entManMock.Setup(m => m.ComponentFactory).Returns(compFacMock.Object); IComponent? outIComponent = compInstance; @@ -167,7 +169,7 @@ public void SubscribeCompLifeEvent() entManMock.Raise(m => m.ComponentAdded += null, new AddedComponentEventArgs(new ComponentEventArgs(compInstance, entUid), reg)); // Raise - ((IEventBus)bus).RaiseComponentEvent(compInstance, new ComponentInit()); + ((IEventBus)bus).RaiseComponentEvent(entUid, compInstance, new ComponentInit()); // Assert Assert.That(calledCount, Is.EqualTo(1)); @@ -199,6 +201,7 @@ public void CompEventOrdered() CompIdx.Index()); compFacMock.Setup(m => m.GetRegistration(CompIdx.Index())).Returns(reg); + compFacMock.Setup(m => m.GetIndex(typeof(T))).Returns(CompIdx.Index()); entManMock.Setup(m => m.TryGetComponent(entUid, CompIdx.Index(), out inst)).Returns(true); entManMock.Setup(m => m.GetComponent(entUid, CompIdx.Index())).Returns(inst); entManMock.Setup(m => m.GetComponentInternal(entUid, CompIdx.Index())).Returns(inst); @@ -262,6 +265,95 @@ void HandlerB(EntityUid uid, Component comp, TestEvent ev) Assert.That(c, Is.True, "C did not fire"); } + [Test] + public void CompEventLoop() + { + var entUid = new EntityUid(7); + + var entManMock = new Mock(); + var compFacMock = new Mock(); + var reflectMock = new Mock(); + + List allRefTypes = new(); + void Setup(out T instance) where T : IComponent, new() + { + IComponent? inst = instance = new T(); + var reg = new ComponentRegistration( + typeof(T).Name, + typeof(T), + CompIdx.Index()); + + compFacMock.Setup(m => m.GetRegistration(CompIdx.Index())).Returns(reg); + compFacMock.Setup(m => m.GetIndex(typeof(T))).Returns(CompIdx.Index()); + entManMock.Setup(m => m.TryGetComponent(entUid, CompIdx.Index(), out inst)).Returns(true); + entManMock.Setup(m => m.GetComponent(entUid, CompIdx.Index())).Returns(inst); + entManMock.Setup(m => m.GetComponentInternal(entUid, CompIdx.Index())).Returns(inst); + allRefTypes.Add(reg); + } + + Setup(out var instA); + Setup(out var instB); + + compFacMock.Setup(m => m.GetAllRegistrations()).Returns(allRefTypes.ToArray()); + + entManMock.Setup(m => m.ComponentFactory).Returns(compFacMock.Object); + var bus = new EntityEventBus(entManMock.Object, reflectMock.Object); + bus.OnlyCallOnRobustUnitTestISwearToGodPleaseSomebodyKillThisNightmare(); + + var regA = compFacMock.Object.GetRegistration(CompIdx.Index()); + var regB = compFacMock.Object.GetRegistration(CompIdx.Index()); + + var handlerACount = 0; + void HandlerA(EntityUid uid, Component comp, TestEvent ev) + { + Assert.That(handlerACount, Is.EqualTo(0)); + handlerACount++; + + // add and then remove component B + bus.OnComponentRemoved(new RemovedComponentEventArgs(new ComponentEventArgs(instB, entUid), false, default!, CompIdx.Index())); + bus.OnComponentAdded(new AddedComponentEventArgs(new ComponentEventArgs(instB, entUid), regB)); + } + + var handlerBCount = 0; + void HandlerB(EntityUid uid, Component comp, TestEvent ev) + { + Assert.That(handlerBCount, Is.EqualTo(0)); + handlerBCount++; + + // add and then remove component A + bus.OnComponentRemoved(new RemovedComponentEventArgs(new ComponentEventArgs(instA, entUid), false, default!, CompIdx.Index())); + bus.OnComponentAdded(new AddedComponentEventArgs(new ComponentEventArgs(instA, entUid), regA)); + } + + bus.SubscribeLocalEvent(HandlerA, typeof(OrderAComponent)); + bus.SubscribeLocalEvent(HandlerB, typeof(OrderBComponent)); + bus.LockSubscriptions(); + + // add a component to the system + bus.OnEntityAdded(entUid); + + bus.OnComponentAdded(new AddedComponentEventArgs(new ComponentEventArgs(instA, entUid), regA)); + bus.OnComponentAdded(new AddedComponentEventArgs(new ComponentEventArgs(instB, entUid), regB)); + + // Event subscriptions currently use a linked list. + // Currently expect event subscriptions to be raised in order: handlerB -> handlerA + // If a component gets removed and added again, it gets moved back to the front of the linked list. + // I.e., adding and then removing compA changes the linked list order: handlerA -> handlerB + // + // This could in principle cause the event raising code to enter an infinite loop. + // Adding and removing a comp in an event handler may seem silly but: + // - it doesn't have to be the same component if you had a chain of three or more components + // - some event handlers raise other events and can lead to convoluted chains of interactions that might inadvertently trigger something like this. + + // Raise + bus.RaiseLocalEvent(entUid, new TestEvent(0)); + + // Assert + Assert.That(handlerACount, Is.LessThanOrEqualTo(1)); + Assert.That(handlerBCount, Is.LessThanOrEqualTo(1)); + Assert.That(handlerACount+handlerBCount, Is.GreaterThan(0)); + } + private sealed partial class DummyComponent : Component { } diff --git a/Robust.UnitTesting/Shared/GameState/VisibilityTest.cs b/Robust.UnitTesting/Shared/GameState/VisibilityTest.cs index ae67df6a855..2e65dad9339 100644 --- a/Robust.UnitTesting/Shared/GameState/VisibilityTest.cs +++ b/Robust.UnitTesting/Shared/GameState/VisibilityTest.cs @@ -44,7 +44,7 @@ await server.WaitPost(() => metaComp[i] = server.EntMan.GetComponent(ent); visComp[i] = server.EntMan.AddComponent(ent); - vis.AddLayer(ent, visComp[i], 1 << i); + vis.AddLayer((ent, visComp[i]), (ushort)(1 << i)); if (i > 0) xforms.SetParent(ent, ents[i - 1]); } @@ -62,7 +62,7 @@ await server.WaitPost(() => // Adding a layer to the root entity's mask will apply it to all children var extraMask = 1 << (N + 1); mask = RequiredMask | extraMask; - vis.AddLayer(ents[0], visComp[0], extraMask); + vis.AddLayer((ents[0], visComp[0]), (ushort)extraMask); for (int i = 0; i < N; i++) { mask |= 1 << i; @@ -71,7 +71,7 @@ await server.WaitPost(() => } // Removing the removes it from all children. - vis.RemoveLayer(ents[0], visComp[0], extraMask); + vis.RemoveLayer((ents[0], visComp[0]), (ushort)extraMask); mask = RequiredMask; for (int i = 0; i < N; i++) { @@ -101,7 +101,7 @@ await server.WaitPost(() => } // Re-attaching the entity also updates the masks. - await server.WaitPost(() => xforms.SetParent(ents[split], ents[split-1])); + await server.WaitPost(() => xforms.SetParent(ents[split], ents[split - 1])); mask = RequiredMask; for (int i = 0; i < N; i++) { @@ -111,7 +111,7 @@ await server.WaitPost(() => } // Setting a mask on a child does not propagate upwards, only downwards - vis.AddLayer(ents[split], visComp[split], extraMask); + vis.AddLayer((ents[split], visComp[split]), (ushort)extraMask); mask = RequiredMask; for (int i = 0; i < split; i++) {