diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 4f753f9e2c5..24428ef75d0 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -35,11 +35,31 @@ END TEMPLATE--> ### Breaking changes -*None yet* +- Engine has a new `ComponentFilter` type, which name conflicts with the one currently in content. + Content maintainers should apply [this patch](https://github.com/space-wizards/space-station-14/pull/43168). +- `ComponentRegistry` no longer inherits directly from Dictionary, and only provides a subset of the dictionary API + that was necessary to get the Space Station 14 Wizard's Den content package compiling. All Dictionary methods are + additionally obsolete, and you should consult `ComponentRegistry`'s documentation and method list for replacements. +- `IEntityLoadContext` has new required methods that take an `IComponentFactory`. ### New features -*None yet* +- Introduces `ComponentFilter`, a set of components that can be used with various filter methods on `IEntityManager`. + Please consult the documentation for the type, alongside `IEntityManager.Filters.cs`, for the full set of new features. +- `ComponentRegistry` now provides an improved API that does not require users manually insert components into the + dictionary. Consult the documentation for the type for the full feature list. +- `EntityQuery` now implements `IEnumerable>` and can be used with `foreach` performantly. +- `EntityQuery`, `EntityQuery`, and `EntityQuery` + have been added and implement `IEnumerable`. +

+ They're direct counterparts to `EntityQuery` with similar methods and constructors. + Additionally, like `EntityQuery`, all of these types can be resolved with a `[Dependency]` in systems. +- `DynamicEntityQuery` has been added. It implements component queries for an unbounded set of components, with the + ability to mark the presence of specific components as optional (may be null) or excluded (query doesn't match + entities with that component). +

+ It is currently primarily an implementation detail of `EntityQuery<>` but can be used directly and will lead to + expanded functionality in the future. ### Bugfixes @@ -47,11 +67,25 @@ END TEMPLATE--> ### Other -*None yet* +- `void AddComponents(EntityUid target, EntityPrototype prototype, bool removeExisting = true)` and + `void RemoveComponents(EntityUid target, EntityPrototype prototype)` were marked obsolete and will be removed without + replacement. Using EntityPrototype as a filter and registry is not supported. Additionally, + the former API has never functioned correctly with `removeExisting: true` and that functionality is not supported. +- `void RemoveComponents(EntityUid target, ComponentRegistry registry)` is now obsolete and a ComponentFilter solution + should be used instead. +- `CompRegistryEntityEnumerator` is now obsolete in favor of `ComponentFilterQuery`. +- `EntityQueryEnumerator`, `EntityQueryEnumerator`, `EntityQueryEnumerator`, + `EntityQueryEnumerator`, `AllEntityQueryEnumerator`, `AllEntityQueryEnumerator`, + `AllEntityQueryEnumerator`, `AllEntityQueryEnumerator`, + and `ComponentQueryEnumerator` are now obsolete. +- The EntityManager methods `EntityQuery`, `EntityQuery`, `EntityQuery`, + and `EntityQuery`, `EntityQueryEnumerator`, `AllEntityQueryEnumerator`, + `ComponentQueryEnumerator`, `AllComponentsList`, `AllEntityUids`, `AllEntityUids`, `AllEntities`, and `AllComponents` + are now obsolete. ### Internal -*None yet* +- `ComponentRegistry`-dependant surfaces in the engine have been rewritten to use the new methods wherever possible. ## 274.0.1 diff --git a/Robust.Client/ClientIoC.cs b/Robust.Client/ClientIoC.cs index 3df84845048..3c2732922f7 100644 --- a/Robust.Client/ClientIoC.cs +++ b/Robust.Client/ClientIoC.cs @@ -70,6 +70,7 @@ public static void RegisterIoC(GameController.DisplayMode mode, IDependencyColle deps.Register(); deps.Register(); deps.Register(); + deps.Register(); deps.Register(); deps.Register(); deps.Register(); diff --git a/Robust.Client/GameObjects/ClientEntityManager.Network.cs b/Robust.Client/GameObjects/ClientEntityManager.Network.cs index cf3ec76c813..b42d0e9b3e2 100644 --- a/Robust.Client/GameObjects/ClientEntityManager.Network.cs +++ b/Robust.Client/GameObjects/ClientEntityManager.Network.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading; using Robust.Shared.GameObjects; using Robust.Shared.Map; using Robust.Shared.Utility; @@ -8,7 +9,7 @@ namespace Robust.Client.GameObjects; public sealed partial class ClientEntityManager { - protected override NetEntity GenerateNetEntity() => new(NextNetworkId++ | NetEntity.ClientEntity); + protected override NetEntity GenerateNetEntity() => new(Interlocked.Increment(ref NextNetworkId) | NetEntity.ClientEntity); /// /// If the client fails to resolve a NetEntity then during component state handling or the likes we diff --git a/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs b/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs index d1dbd6587c6..a52d8d0e35d 100644 --- a/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs +++ b/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs @@ -243,12 +243,13 @@ public ShaderInstance? PostShader public ISpriteLayer this[object layerKey] => this[LayerMap[layerKey]]; public IEnumerable AllLayers => Layers; - void ISerializationHooks.AfterDeserialization() + void ISerializationHooks.AfterDeserialization(IDependencyCollection collection) { // Please somebody burn this to the ground. There is so much spaghetti. // Why has no one answered my prayers. + // Workin' on it --kaylie. - IoCManager.InjectDependencies(this); + collection.InjectDependencies(this); if (!string.IsNullOrWhiteSpace(rsi)) { var rsiPath = TextureRoot / rsi; diff --git a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Component.cs b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Component.cs index f96f1bb9ede..8cd1aa87766 100644 --- a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Component.cs +++ b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Component.cs @@ -1,13 +1,17 @@ using System; using System.Collections.Generic; using Robust.Shared.GameObjects; +using Robust.Shared.IoC; using Robust.Shared.Maths; +using Robust.Shared.Serialization.Manager; using Robust.Shared.Utility; namespace Robust.Client.GameObjects; public sealed partial class SpriteSystem { + [Dependency] private readonly ISerializationManager _serMan = default!; + /// /// Resets the sprite's animated layers to align with a given time (in seconds). /// @@ -54,6 +58,15 @@ public void CopySprite(Entity source, Entity if (!Resolve(target.Owner, ref target.Comp)) return; + // Some code in content decided it'd be cool to copy from uninitialized SpriteComponents. + // Because this no longer works, we copy some extra data to ensure it does anyway. + if (source.Comp.LifeStage == ComponentLifeStage.PreAdd) + { + Log.Info($"Hit a bodge case in CopySprite, please change your usage to use ISerializationManager.CopyTo instead: {Environment.StackTrace}"); + _serMan.CopyTo(source.Comp, ref target.Comp, notNullableOverride: true); + return; + } + target.Comp._baseRsi = source.Comp._baseRsi; target.Comp._bounds = source.Comp._bounds; target.Comp._visible = source.Comp._visible; diff --git a/Robust.Client/Graphics/Shaders/ShaderPrototype.cs b/Robust.Client/Graphics/Shaders/ShaderPrototype.cs index 3377bf519c7..04602844745 100644 --- a/Robust.Client/Graphics/Shaders/ShaderPrototype.cs +++ b/Robust.Client/Graphics/Shaders/ShaderPrototype.cs @@ -107,8 +107,9 @@ public ShaderInstance InstanceUnique() [DataField("blend_mode")] private string? _rawBlendMode; - void ISerializationHooks.AfterDeserialization() + void ISerializationHooks.AfterDeserialization(IDependencyCollection collection) { + // TODO: This isn't threadsafe and can't be! WGPU when. switch (_rawKind) { case "source": @@ -118,7 +119,7 @@ void ISerializationHooks.AfterDeserialization() if (_path == null) throw new InvalidOperationException("Source shaders must specify a source file."); - _source = IoCManager.Resolve().GetResource(_path.Value); + _source = collection.Resolve().GetResource(_path.Value); if (_paramMapping != null) { @@ -140,7 +141,7 @@ void ISerializationHooks.AfterDeserialization() case "canvas": Kind = ShaderKind.Canvas; - _source = IoCManager.Resolve().GetResource("/Shaders/Internal/default-sprite.swsl"); + _source = collection.Resolve().GetResource("/Shaders/Internal/default-sprite.swsl"); break; default: diff --git a/Robust.Server.IntegrationTests/GameObjects/ThrowingEntityDeletion_Test.cs b/Robust.Server.IntegrationTests/GameObjects/ThrowingEntityDeletion_Test.cs index 45e45130132..1470b505fca 100644 --- a/Robust.Server.IntegrationTests/GameObjects/ThrowingEntityDeletion_Test.cs +++ b/Robust.Server.IntegrationTests/GameObjects/ThrowingEntityDeletion_Test.cs @@ -48,7 +48,10 @@ public void Test(string prototypeName) _sim.Resolve().System().CreateMap(out var map); Assert.That(() => entMan.SpawnEntity(prototypeName, new MapCoordinates(0, 0, map)), - Throws.TypeOf()); + // Throws an aggregate containing only creation exceptions. + Throws.TypeOf() + .And.Property("InnerExceptions") + .All.TypeOf()); Assert.That(entMan.GetEntities().Where(p => entMan.GetComponent(p).EntityPrototype?.ID == prototypeName), Is.Empty); } diff --git a/Robust.Server.Testing/RobustServerSimulation.cs b/Robust.Server.Testing/RobustServerSimulation.cs index 66eb92d861d..6f793fcebd1 100644 --- a/Robust.Server.Testing/RobustServerSimulation.cs +++ b/Robust.Server.Testing/RobustServerSimulation.cs @@ -246,6 +246,7 @@ public ISimulation InitializeInstance() //Tier 2: Simulation container.RegisterInstance(new Mock().Object); //Console is technically a frontend, we want to run headless container.Register(); + container.Register(); container.Register(); container.Register(); container.Register(); diff --git a/Robust.Server/GameObjects/ServerEntityManager.cs b/Robust.Server/GameObjects/ServerEntityManager.cs index 1f47b2100b3..72983e8c83a 100644 --- a/Robust.Server/GameObjects/ServerEntityManager.cs +++ b/Robust.Server/GameObjects/ServerEntityManager.cs @@ -13,6 +13,8 @@ using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Log; +using Robust.Shared.Map; +using Robust.Shared.Maths; using Robust.Shared.Network; using Robust.Shared.Network.Messages; using Robust.Shared.Player; @@ -136,6 +138,58 @@ internal override void SetLifeStage(MetaDataComponent meta, EntityLifeStage stag public override IEntityNetworkManager EntityNetManager => this; + public override EntityUid Spawn( + string? protoName, + MapCoordinates coordinates, + ComponentRegistry? overrides = null, + Angle rotation = default) + { + // Start building the entity as described... + var builder = EntityBuilder(protoName) + .LocatedAt(coordinates, rotation); + + // If we got overrides, apply them here. + if (overrides is not null) + builder.ApplyRegistry(overrides); + + // Spawn it into the simulation and return the id. + return Spawn(builder); + } + + public override EntityUid SpawnAttachedTo( + string? protoName, + EntityCoordinates coordinates, + ComponentRegistry? overrides = null, + Angle rotation = default) + { + if (!coordinates.IsValid(this)) + throw new InvalidOperationException($"Tried to spawn entity {protoName} on invalid coordinates {coordinates}."); + + // Start building the entity as described... + var builder = EntityBuilder(protoName) + .ChildOf(coordinates, rotation); + + // If we got overrides, apply them here. + if (overrides is not null) + builder.ApplyRegistry(overrides); + + // Spawn it into the simulation and return the id. + return Spawn(builder); + } + + public override EntityUid Spawn(string? protoName = null, ComponentRegistry? overrides = null, bool doMapInit = true) + { + // Start building the entity as described... + var builder = EntityBuilder(protoName); + + // If we got overrides, apply them here. + if (overrides is not null) + builder.ApplyRegistry(overrides); + + // Spawn it into the simulation and return the id. + return Spawn(builder, doMapInit); + } + /// public event EventHandler? ReceivedSystemMessage; diff --git a/Robust.Server/ServerIoC.cs b/Robust.Server/ServerIoC.cs index 2c26ca5df06..2377cdbf116 100644 --- a/Robust.Server/ServerIoC.cs +++ b/Robust.Server/ServerIoC.cs @@ -60,6 +60,7 @@ internal static void RegisterIoC(IDependencyCollection deps) deps.Register(); deps.Register(); deps.Register(); + deps.Register(); deps.Register(); deps.Register(); deps.Register(); diff --git a/Robust.Shared.IntegrationTests/EntitySerialization/LifestageSerializationTest.cs b/Robust.Shared.IntegrationTests/EntitySerialization/LifestageSerializationTest.cs index ab5493f324f..b33c5dc5b5b 100644 --- a/Robust.Shared.IntegrationTests/EntitySerialization/LifestageSerializationTest.cs +++ b/Robust.Shared.IntegrationTests/EntitySerialization/LifestageSerializationTest.cs @@ -57,7 +57,7 @@ void AssertPaused(bool expected, params EntityUid[] uids) { foreach (var uid in uids) { - Assert.That(entMan.GetComponent(uid).EntityPaused, Is.EqualTo(expected)); + Assert.That(entMan.GetComponent(uid).EntityPaused, Is.EqualTo(expected), $"Entity {entMan.ToPrettyString(uid)} was not paused."); } } diff --git a/Robust.Shared.IntegrationTests/GameObjects/CommandBuffers/CommandConstructionTests.cs b/Robust.Shared.IntegrationTests/GameObjects/CommandBuffers/CommandConstructionTests.cs new file mode 100644 index 00000000000..72ea76982d0 --- /dev/null +++ b/Robust.Shared.IntegrationTests/GameObjects/CommandBuffers/CommandConstructionTests.cs @@ -0,0 +1,42 @@ +using NUnit.Framework; +using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.CommandBuffers; + +namespace Robust.UnitTesting.Shared.GameObjects.CommandBuffers; + +public sealed class CommandConstructionTests +{ + [Test] + public void ConstructQueuedActionT() + { + CommandBufferEntry.QueuedActionT(ctx => + { + Assert.That(ctx, NUnit.Framework.Is.Not.Null); + Assert.Pass(); + }, + this, + out var entry); + + entry.InvokeQueuedActionT(); + + Assert.Fail("Invocation did nothing?"); + } + + [Test] + public void ConstructQueuedActionTEnt() + { + CommandBufferEntry.QueuedActionTEnt(static (ctx, ent) => + { + Assert.That(ctx, NUnit.Framework.Is.Not.Null); + Assert.That(ent, NUnit.Framework.Is.EqualTo(EntityUid.FirstUid)); + Assert.Pass(); + }, + this, + EntityUid.FirstUid, + out var entry); + + entry.InvokeQueuedActionTEnt(); + + Assert.Fail("Invocation did nothing?"); + } +} diff --git a/Robust.Shared.IntegrationTests/GameObjects/EntityBuilder/ClientEntityBuilderTests.cs b/Robust.Shared.IntegrationTests/GameObjects/EntityBuilder/ClientEntityBuilderTests.cs new file mode 100644 index 00000000000..e99645a4c77 --- /dev/null +++ b/Robust.Shared.IntegrationTests/GameObjects/EntityBuilder/ClientEntityBuilderTests.cs @@ -0,0 +1,41 @@ +using NUnit.Framework; +using Robust.Client.GameObjects; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Serialization.Manager; + +namespace Robust.UnitTesting.Shared.GameObjects.EntityBuilder; + +internal sealed class ClientEntityBuilderTests : OurRobustUnitTest +{ + private IEntityManager _entMan = default!; + + public override UnitTestProject Project => UnitTestProject.Client; + + [OneTimeSetUp] + public void Setup() + { + IoCManager.Resolve().Initialize(); + + _entMan = IoCManager.Resolve(); + } + + [Test] + [Description("Ensure that adding SpriteComponent to a builder doesn't immediately explode.")] + public void CreateWithSprite() + { + Robust.Shared.GameObjects.EntityBuilders.EntityBuilder b = null!; + // NUnit [RequiresThread] is old and crusty and doesn't work for us. + // So we need to isolate IoC this way instead. + var t = new Thread(() => + { + b = _entMan.EntityBuilder() + .AddComp(); + }); + + t.Start(); + t.Join(); + + _entMan.Spawn(b); + } +} diff --git a/Robust.Shared.IntegrationTests/GameObjects/EntityBuilder/EntityBuilderTests.cs b/Robust.Shared.IntegrationTests/GameObjects/EntityBuilder/EntityBuilderTests.cs new file mode 100644 index 00000000000..c4bb1fefa36 --- /dev/null +++ b/Robust.Shared.IntegrationTests/GameObjects/EntityBuilder/EntityBuilderTests.cs @@ -0,0 +1,158 @@ +using System.Numerics; +using NUnit.Framework; +using NUnit.Framework.Internal.Execution; +using Robust.Client.GameObjects; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Map; +using Robust.Shared.Map.Components; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.Manager; + +namespace Robust.UnitTesting.Shared.GameObjects.EntityBuilder; + +internal sealed class EntityBuilderTests : OurRobustUnitTest +{ + private const string TestEnt1 = "T_TestEnt1"; + + private PrototypeManager _protoMan = default!; + private IEntityManager _entMan = default!; + + private const string Prototypes = $""" + - type: entity + id: {TestEnt1} + components: + - type: MetaData # Test of new spawn logic.. + - type: Transform + noRot: true + - type: Marker1 + - type: Marker2 + - type: Marker3 + - type: Marker4 + """; + + protected override Type[] ExtraComponents => + [ + typeof(Marker1Component), typeof(Marker2Component), typeof(Marker3Component), typeof(Marker4Component) + ]; + + [OneTimeSetUp] + public void Setup() + { + IoCManager.Resolve().Initialize(); + + _protoMan = (PrototypeManager) IoCManager.Resolve(); + _protoMan.RegisterKind(typeof(EntityPrototype), typeof(EntityCategoryPrototype)); + _protoMan.LoadString(Prototypes); + _protoMan.ResolveResults(); + + _entMan = IoCManager.Resolve(); + } + + [Test] + [Description("Creates a few entities with blank EntityBuilders, ensuring component adds work.")] + public void CreateEntity() + { + var builder = _entMan.EntityBuilder() + .Named("Test entity") + .AddComp() + .AddComp(); + + var ent = _entMan.Spawn(builder); + + Assert.That(_entMan.HasComponent(ent), "Expected builder added components on the new entity"); + Assert.That(_entMan.HasComponent(ent), "Expected builder added components on the new entity"); + } + + [Test] + [Description("Creates a hierarchy of entities, a map and some grids, using blank entity builders.")] + public void CreateHierarchy() + { + var root = _entMan.EntityBuilder() + .Named("Test entity") + .AddComp() + .AddComp(); + + var child1 = _entMan.EntityBuilder() + .Named("Test child 1") + .ChildOf(root.ReservedEntity, new Vector2(-4, -4)) + .AddComp() + .AddComp(); + + var child2 = _entMan.EntityBuilder() + .Named("Test child 2") + .ChildOf(root.ReservedEntity, new Vector2(4, 4)) + .AddComp(); + + // Spawn the map and its children. + // Note: Currently order does matter. I'd like to lift that requirement sometime, but it does. + _entMan.SpawnBulk([root, child1, child2]); + + Assert.That(_entMan.GetComponent(root.ReservedEntity).MapId, + NUnit.Framework.Is.Not.EqualTo(MapId.Nullspace)); + + Assert.That(_entMan.GetComponent(root.ReservedEntity)._children, + NUnit.Framework.Is.EquivalentTo([child1.ReservedEntity, child2.ReservedEntity]), + "Expected the hierarchy we set up to be respected."); + } + + [Test] + [Description("Creates an unordered hierarchy of entities, a map and some grids, using blank entity builders. Ensures SpawnBulkUnordered works as expected.")] + public void CreateUnorderedHierarchy() + { + var root = _entMan.EntityBuilder() + .Named("Test entity") + .AddComp() + .AddComp(); + + var child1 = _entMan.EntityBuilder() + .Named("Test child 1") + .ChildOf(root.ReservedEntity, new Vector2(-4, -4)) + .AddComp(); + + var child2 = _entMan.EntityBuilder() + .Named("Test child 2") + .ChildOf(child1.ReservedEntity, new Vector2(4, 4)) + .AddComp(); + + // Spawn the map and its children. + // They're in the wrong order, so they do need fixed up. + _entMan.SpawnBulkUnordered([child2, root, child1]); + + using (Assert.EnterMultipleScope()) + { + Assert.That(_entMan.GetComponent(root.ReservedEntity).MapId, + NUnit.Framework.Is.Not.EqualTo(MapId.Nullspace)); + + Assert.That(_entMan.GetComponent(root.ReservedEntity)._children, + NUnit.Framework.Is.EquivalentTo([child1.ReservedEntity]), + "Expected the hierarchy we set up to be respected."); + + Assert.That(_entMan.GetComponent(child1.ReservedEntity)._children, + NUnit.Framework.Is.EquivalentTo([child2.ReservedEntity]), + "Expected the hierarchy we set up to be respected."); + } + } + + [Test] + [Description("Ensure EntityBuilder successfully creates entities from prototypes.")] + public void CreateFromPrototype() + { + var builder = _entMan.EntityBuilder(TestEnt1); + + _entMan.Spawn(builder); + + using (Assert.EnterMultipleScope()) + { + // Assert we have the expected components. + Assert.That(_entMan.HasComponent(builder.ReservedEntity)); + Assert.That(_entMan.HasComponent(builder.ReservedEntity)); + Assert.That(_entMan.HasComponent(builder.ReservedEntity)); + Assert.That(_entMan.HasComponent(builder.ReservedEntity)); + Assert.That(_entMan.HasComponent(builder.ReservedEntity)); + Assert.That(_entMan.HasComponent(builder.ReservedEntity)); + Assert.That(_entMan.GetComponent(builder.ReservedEntity)._noLocalRotation, + "Expected fields from the prototype to be reflected."); + } + } +} diff --git a/Robust.Shared.IntegrationTests/GameObjects/EntityManagerFilterTests.cs b/Robust.Shared.IntegrationTests/GameObjects/EntityManagerFilterTests.cs new file mode 100644 index 00000000000..8faffcfe39a --- /dev/null +++ b/Robust.Shared.IntegrationTests/GameObjects/EntityManagerFilterTests.cs @@ -0,0 +1,186 @@ +using NUnit.Framework; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.Manager; + +namespace Robust.UnitTesting.Shared.GameObjects; + +[TestOf(typeof(EntityManager))] +[TestOf(typeof(ComponentFilter))] +internal sealed class EntityManagerFilterTests : OurRobustUnitTest +{ + private const string TestEnt1 = "T_TestEnt1"; + + private const string Prototypes = $""" + - type: entity + id: {TestEnt1} + components: + - type: Marker1 + - type: Marker2 + - type: Marker3 + - type: Marker4 + """; + + protected override Type[]? ExtraComponents => + [ + typeof(Marker1Component), typeof(Marker2Component), typeof(Marker3Component), typeof(Marker4Component) + ]; + + private PrototypeManager _protoMan = default!; + private IEntityManager _entMan = default!; + + [OneTimeSetUp] + public void Setup() + { + IoCManager.Resolve().Initialize(); + + _protoMan = (PrototypeManager) IoCManager.Resolve(); + _protoMan.RegisterKind(typeof(EntityPrototype), typeof(EntityCategoryPrototype)); + _protoMan.LoadString(Prototypes); + _protoMan.ResolveResults(); + + _entMan = IoCManager.Resolve(); + } + + [Test] + [TestOf(typeof(ComponentFilterQuery))] + [Description(""" + Tests that filters work against some test targets with various permutations of components. + Covers MatchesFilter, ExactlyMatchesFilter, and ComponentFilterQuery. + """)] + public void FilterEntities( + [Values(null, typeof(Marker1Component))] + Type? m1, + [Values(null, typeof(Marker2Component))] + Type? m2, + [Values(null, typeof(Marker3Component))] + Type? m3, + [Values(null, typeof(Marker4Component))] + Type? m4 + ) + { + EntityUid? target = null; + EntityUid? target2 = null; + try + { + var filter = new ComponentFilter(new [] {m1, m2, m3, m4}.Where(x => x is not null).Cast()); + + target = _entMan.Spawn(TestEnt1); + + Assert.That(_entMan.MatchesFilter(target.Value, filter)); + + // If there's four entries, then it should be an exact match. + if (filter.Count == 4) + Assert.That(_entMan.ExactlyMatchesFilter(target.Value, filter)); + else + Assert.That(_entMan.ExactlyMatchesFilter(target.Value, filter), NUnit.Framework.Is.False); + + var query = _entMan.ComponentFilterQuery(filter); + Assert.That(query.Count(), NUnit.Framework.Is.EqualTo(1)); + + target2 = _entMan.Spawn(TestEnt1); + + Assert.That(query.Count(), NUnit.Framework.Is.EqualTo(2)); + } + finally + { + _entMan.DeleteEntity(target); + _entMan.DeleteEntity(target2); + } + } + + [Test] + [Description("Asserts that using FillMissesWithNewComponents should make the entity match the filter afterward.")] + public void FillMisses( + [Values(null, typeof(Marker1Component))] + Type? m1, + [Values(null, typeof(Marker2Component))] + Type? m2, + [Values(null, typeof(Marker3Component))] + Type? m3, + [Values(null, typeof(Marker4Component))] + Type? m4 + ) + { + var target = _entMan.Spawn(); + try + { + var filter = new ComponentFilter(new [] {m1, m2, m3, m4}.Where(x => x is not null).Cast()); + + _entMan.FillMissesWithNewComponents(target, filter); + + Assert.That(_entMan.MatchesFilter(target, filter)); + } + finally + { + _entMan.DeleteEntity(target); + } + } + + [Test] + [Description(""" + Tests the various component-set operations, on a full (all markers) and empty (no markers) entity. + Ensures an edge case in EnumerateEntityMisses (xform and metadata existing) is present. + """)] + public void EnumerateFilterHits( + [Values(null, typeof(Marker1Component))] + Type? m1, + [Values(null, typeof(Marker2Component))] + Type? m2, + [Values(null, typeof(Marker3Component))] + Type? m3, + [Values(null, typeof(Marker4Component))] + Type? m4 + ) + { + var target = _entMan.Spawn(TestEnt1); + var target2 = _entMan.Spawn(); + try + { + var filter = new ComponentFilter(new [] {m1, m2, m3, m4}.Where(x => x is not null).Cast()); + + using (Assert.EnterMultipleScope()) + { + Assert.That(_entMan.EnumerateFilterHits(target, filter), + NUnit.Framework.Is.EquivalentTo(filter), + "Expected the test entity with all markers to match the filter and the resulting set intersection to match as well."); + Assert.That(_entMan.EnumerateFilterMisses(target, filter), + NUnit.Framework.Is.Empty, + "Expected there to be no misses on an entity with every marker that can be in the filter."); + Assert.That(_entMan.EnumerateFilterMisses(target2, filter), + NUnit.Framework.Is.EquivalentTo(filter), + "Expected every item in the filter to miss on an entity with no markers."); + Assert.That(_entMan.EnumerateEntityMisses(target2, filter), + NUnit.Framework.Is.EquivalentTo([typeof(MetaDataComponent), typeof(TransformComponent)]), + "Expected the entity to have two components the filter does not, transform and metadata."); + } + } + finally + { + _entMan.DeleteEntity(target); + _entMan.DeleteEntity(target2); + } + } + +} + +internal sealed partial class Marker1Component : Component +{ + +} + +internal sealed partial class Marker2Component : Component +{ + +} + +internal sealed partial class Marker3Component : Component +{ + +} + +internal sealed partial class Marker4Component : Component +{ + +} diff --git a/Robust.Shared.IntegrationTests/GameObjects/IEntityManagerTests.cs b/Robust.Shared.IntegrationTests/GameObjects/IEntityManagerTests.cs index d168c7fe1a2..b24f3595cd4 100644 --- a/Robust.Shared.IntegrationTests/GameObjects/IEntityManagerTests.cs +++ b/Robust.Shared.IntegrationTests/GameObjects/IEntityManagerTests.cs @@ -1,8 +1,7 @@ -using System.Numerics; using NUnit.Framework; using Robust.Shared.GameObjects; using Robust.Shared.Map; -using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Map.Components; using Robust.UnitTesting.Server; namespace Robust.UnitTesting.Shared.GameObjects @@ -33,6 +32,141 @@ public void SpawnEntity_PrototypeTransform_Works() Assert.That(newEnt, Is.Not.EqualTo(EntityUid.Invalid)); } + [Test] + [Description(""" + Tests that a three-component EntityQuery behaves as expected. + This covers the backend for EntityQuery`2 and EntityQuery`4 as well due to shared code. + """)] + public void EntityQuery3_Works() + { + var sim = SimulationFactory(); + var map = sim.CreateMap(); + var map2 = sim.CreateMap(); + + var entMan = sim.Resolve(); + // Query all maps. + var query = entMan.GetEntityQuery(); + + Assert.That(query.Count(), NUnit.Framework.Is.EqualTo(2)); + + Assert.That(query.Matches(map.Uid)); + + foreach (var entity in query) + { + Assert.That(entity.Owner, NUnit.Framework.Is.EqualTo(map.Uid).Or.EqualTo(map2.Uid)); + Assert.That(entity.Comp1, NUnit.Framework.Is.TypeOf()); + Assert.That(entity.Comp2, NUnit.Framework.Is.TypeOf()); + Assert.That(entity.Comp3, NUnit.Framework.Is.TypeOf()); +#pragma warning disable CS0618 // Type or member is obsolete + Assert.That(entity.Comp1.Owner, NUnit.Framework.Is.EqualTo(entity.Owner)); + Assert.That(entity.Comp2.Owner, NUnit.Framework.Is.EqualTo(entity.Owner)); + Assert.That(entity.Comp3.Owner, NUnit.Framework.Is.EqualTo(entity.Owner)); +#pragma warning restore CS0618 // Type or member is obsolete + } + } + + [Test] + [Description(""" + Tests DynamicEntityQuery behavior with Without and Optional, ensuring they behave as expected. + Uses a few maps and blank entities as test subjects. + """)] + public void DynamicQueryTest_OptionalWithout() + { + var sim = SimulationFactory(); + _ = sim.CreateMap(); + _ = sim.CreateMap(); + _ = sim.SpawnEntity(null, MapCoordinates.Nullspace); + _ = sim.SpawnEntity(null, MapCoordinates.Nullspace); + + var entMan = sim.Resolve(); + + // Should contain all spawned entities. + var queryAll = entMan.GetDynamicQuery( + (typeof(TransformComponent), DynamicEntityQuery.QueryFlags.None), + (typeof(MetaDataComponent), DynamicEntityQuery.QueryFlags.None) + ); + + // Should contain all spawned entities. + var queryAllAndMaps = entMan.GetDynamicQuery( + (typeof(TransformComponent), DynamicEntityQuery.QueryFlags.None), + (typeof(MetaDataComponent), DynamicEntityQuery.QueryFlags.None), + (typeof(MapComponent), DynamicEntityQuery.QueryFlags.Optional) + ); + + // Should only contain the non-map entities. + var queryNotMaps = entMan.GetDynamicQuery( + (typeof(TransformComponent), DynamicEntityQuery.QueryFlags.None), + (typeof(MetaDataComponent), DynamicEntityQuery.QueryFlags.None), + (typeof(MapComponent), DynamicEntityQuery.QueryFlags.Without) + ); + + var buffer = new IComponent?[4].AsSpan(); + + var queryAllEnum = queryAll.GetEnumerator(false); + var queryAllCount = 0; + + while (queryAllEnum.MoveNext(out _, buffer[0..2])) + { + queryAllCount += 1; + // Ensure components get filled out as we expect, only meta and transform. + using (Assert.EnterMultipleScope()) + { + Assert.That(buffer[0], NUnit.Framework.Is.TypeOf()); + Assert.That(buffer[1], NUnit.Framework.Is.TypeOf()); + } + } + + Assert.That(queryAllCount, NUnit.Framework.Is.EqualTo(4), "Expected to iterate all entities"); + + var queryAllAndMapsEnum = queryAllAndMaps.GetEnumerator(false); + var queryAllAndMapsCount = 0; + var mapCount = 0; + + while (queryAllAndMapsEnum.MoveNext(out _, buffer[0..3])) + { + queryAllAndMapsCount += 1; + using (Assert.EnterMultipleScope()) + { + using (Assert.EnterMultipleScope()) + { + Assert.That(buffer[0], NUnit.Framework.Is.TypeOf()); + Assert.That(buffer[1], NUnit.Framework.Is.TypeOf()); + Assert.That(buffer[2], NUnit.Framework.Is.TypeOf().Or.Null); + } + + if (buffer[2] is not null) + mapCount += 1; + } + } + + using (Assert.EnterMultipleScope()) + { + Assert.That(queryAllAndMapsCount, NUnit.Framework.Is.EqualTo(4), "Expected to iterate all entities."); + Assert.That(mapCount, NUnit.Framework.Is.EqualTo(2), "Expected to pick up both maps in the query."); + } + + var queryNotMapsEnum = queryNotMaps.GetEnumerator(false); + var queryNotMapsCount = 0; + + while (queryNotMapsEnum.MoveNext(out var ent, buffer[0..3])) + { + queryNotMapsCount += 1; + using (Assert.EnterMultipleScope()) + { + using (Assert.EnterMultipleScope()) + { + Assert.That(buffer[0], NUnit.Framework.Is.TypeOf()); + Assert.That(buffer[1], NUnit.Framework.Is.TypeOf()); + Assert.That(buffer[2], NUnit.Framework.Is.Null, "Without constraints should never fill their slot."); + Assert.That(entMan.HasComponent(ent), NUnit.Framework.Is.False, "Without constraints shouldn't return entities that match it."); + } + } + } + + + Assert.That(queryNotMapsCount, NUnit.Framework.Is.EqualTo(2), "Expected to only iterate non-maps."); + } + [Test] public void ComponentCount_Works() { diff --git a/Robust.Shared.IntegrationTests/Serialization/TypeSerializers/ComponentRegistrySerializerTest.cs b/Robust.Shared.IntegrationTests/Serialization/TypeSerializers/ComponentRegistrySerializerTest.cs index 8c3eed5acaa..0cab86db6e9 100644 --- a/Robust.Shared.IntegrationTests/Serialization/TypeSerializers/ComponentRegistrySerializerTest.cs +++ b/Robust.Shared.IntegrationTests/Serialization/TypeSerializers/ComponentRegistrySerializerTest.cs @@ -25,7 +25,7 @@ internal sealed class ComponentRegistrySerializerTest : OurSerializationTest public void SerializationTest() { var component = new TestComponent(); - var registry = new ComponentRegistry {{"Test", new ComponentRegistryEntry(component, new MappingDataNode())}}; + var registry = new ComponentRegistry(new Dictionary() {{"Test", new ComponentRegistryEntry(component, new MappingDataNode())}}); var node = Serialization.WriteValueAs(registry); Assert.That(node.Sequence.Count, Is.EqualTo(1)); diff --git a/Robust.Shared/Containers/ContainerManagerComponent.cs b/Robust.Shared/Containers/ContainerManagerComponent.cs index f72fd57eb48..c296b5e5693 100644 --- a/Robust.Shared/Containers/ContainerManagerComponent.cs +++ b/Robust.Shared/Containers/ContainerManagerComponent.cs @@ -23,12 +23,19 @@ public sealed partial class ContainerManagerComponent : Component, ISerializatio [DataField] public Dictionary Containers = new(); - // Requires a custom serializer + copier to get rid of. Good luck + // Waiting on everything to use EntityBuilder, then it's gone. void ISerializationHooks.AfterDeserialization() { +#pragma warning disable CS0618 // Type or member is obsolete + if (Owner == EntityUid.Invalid) + return; +#pragma warning restore CS0618 // Type or member is obsolete + foreach (var (id, container) in Containers) { - container.Init(default!, id, (Owner, this)); +#pragma warning disable CS0618 // Type or member is obsolete + container.Init(null!, id, (Owner, this)); +#pragma warning restore CS0618 // Type or member is obsolete } } diff --git a/Robust.Shared/Containers/SharedContainerSystem.Remove.cs b/Robust.Shared/Containers/SharedContainerSystem.Remove.cs index 2e311553d37..59031b2a819 100644 --- a/Robust.Shared/Containers/SharedContainerSystem.Remove.cs +++ b/Robust.Shared/Containers/SharedContainerSystem.Remove.cs @@ -101,8 +101,6 @@ public bool Remove( RaiseLocalEvent(container.Owner, new EntRemovedFromContainerMessage(toRemove, container), true); RaiseLocalEvent(toRemove, new EntGotRemovedFromContainerMessage(toRemove, container), false); - DebugTools.Assert(destination == null || xform.Coordinates.Equals(destination.Value), $"Failed to set coordinates of {ToPrettyString(toRemove, meta)} to be inside {ToPrettyString(container.Owner)} container '{container.ID}'"); - Dirty(container.Owner, container.Manager); return true; } diff --git a/Robust.Shared/Containers/SharedContainerSystem.cs b/Robust.Shared/Containers/SharedContainerSystem.cs index b86a0a999d9..b76fbd83075 100644 --- a/Robust.Shared/Containers/SharedContainerSystem.cs +++ b/Robust.Shared/Containers/SharedContainerSystem.cs @@ -42,7 +42,7 @@ public override void Initialize() base.Initialize(); SubscribeLocalEvent(OnParentChanged); - SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnAdd); SubscribeLocalEvent(OnStartupValidation); SubscribeLocalEvent(OnContainerGetState); SubscribeLocalEvent(OnContainerManagerRemove); @@ -56,11 +56,11 @@ public override void Initialize() TransformQuery = GetEntityQuery(); } - private void OnInit(Entity ent, ref ComponentInit args) + private void OnAdd(EntityUid ent, ContainerManagerComponent component, ref ComponentAdd args) { - foreach (var (id, container) in ent.Comp.Containers) + foreach (var (id, container) in component.Containers) { - container.Init(this, id, ent); + container.Init(this, id, (ent, component)); } } diff --git a/Robust.Shared/EntitySerialization/EntityDeserializer.cs b/Robust.Shared/EntitySerialization/EntityDeserializer.cs index cd4bae33d45..34457fbba3b 100644 --- a/Robust.Shared/EntitySerialization/EntityDeserializer.cs +++ b/Robust.Shared/EntitySerialization/EntityDeserializer.cs @@ -617,8 +617,8 @@ private void LoadEntity( } var datanode = compData; - if (proto != null && proto.Components.TryGetValue(name, out var protoData)) - datanode = _seriMan.CombineMappings(compData, protoData.Mapping); + if (proto != null && proto.Components.TryGetEntry(_factory, name, out var protoData)) + datanode = protoData?.Mapping is not null ? _seriMan.CombineMappings(compData, protoData.Mapping!) : compData; _components.Add(name, datanode); } @@ -628,7 +628,7 @@ private void LoadEntity( // that component from the map file if (proto != null) { - foreach (var (name, entry) in proto.Components) + foreach (var (name, toClone) in proto.Components.ComponentsAndNames(_factory)) { if (missingComps != null && missingComps.Contains(name)) continue; @@ -646,9 +646,9 @@ private void LoadEntity( component = newComponent; } - _seriMan.CopyTo(entry.Component, ref component, this, notNullableOverride: true); + _seriMan.CopyTo(toClone, ref component, this, notNullableOverride: true); - if (!entry.Component.NetSyncEnabled && compReg.NetID is { } netId) + if (!toClone.NetSyncEnabled && compReg.NetID is { } netId) meta.NetComponents.Remove(netId); } } @@ -1003,7 +1003,7 @@ private void ResetNetTicks(EntityUid uid, MetaDataComponent metadata) continue; } - if (prototype.Components.ContainsKey(compName)) + if (prototype.Components.ContainsComponentByName(_factory, compName)) { // This component is modified by the map so we have to send state. // Though it's still in the prototype itself so creation doesn't need to be sent. diff --git a/Robust.Shared/GameObjects/CommandBuffers/CommandBuffer.InvokeAction.cs b/Robust.Shared/GameObjects/CommandBuffers/CommandBuffer.InvokeAction.cs new file mode 100644 index 00000000000..62b63bd3ee7 --- /dev/null +++ b/Robust.Shared/GameObjects/CommandBuffers/CommandBuffer.InvokeAction.cs @@ -0,0 +1,40 @@ +using System; + +namespace Robust.Shared.GameObjects.CommandBuffers; + +public sealed partial class CommandBuffer +{ + /// + /// Adds an Action<T> invocation to the buffer. + /// + /// + /// It is recommended to use static lambdas and static methods with this. + /// + /// The action to invoke. + /// The context object to invoke it with. + /// The type of the context object. + /// The command buffer, for chaining. + public CommandBuffer InvokeAction(Action action, T context) + where T : class + { + CommandBufferEntry.QueuedActionT(action, context, out NextEntry()); + return this; + } + + /// + /// Adds an Action<T, EntityUid> invocation to the buffer. + /// + /// + /// It is recommended to use static lambdas and static methods with this. + /// + /// The action to invoke. + /// The context object to invoke it with. + /// The entity to use as a target. + /// The type of the context object. + public CommandBuffer InvokeAction(Action action, T context, EntityUid target) + where T : class + { + CommandBufferEntry.QueuedActionTEnt(action, context, target, out NextEntry()); + return this; + } +} diff --git a/Robust.Shared/GameObjects/CommandBuffers/CommandBuffer.cs b/Robust.Shared/GameObjects/CommandBuffers/CommandBuffer.cs new file mode 100644 index 00000000000..5ee4efd09ed --- /dev/null +++ b/Robust.Shared/GameObjects/CommandBuffers/CommandBuffer.cs @@ -0,0 +1,92 @@ +using System; +using System.Diagnostics; +using Robust.Shared.Collections; +using Robust.Shared.IoC; +using Robust.Shared.Prototypes; + +namespace Robust.Shared.GameObjects.CommandBuffers; + +/// +/// +/// A command buffer, which is a queue of operations to apply to the ECS. They allow you to queue entity +/// mutating operations like spawning, deletion, and component add & remove. +/// +/// +/// Actions added to the command buffer are executed in order of addition when +/// is called, which then also clears and returns the CommandBuffer. +/// +/// +/// +public sealed partial class CommandBuffer : IDisposable +{ + private readonly IEntityManager _entMan; + private readonly IPrototypeManager _protoMan; + + /// + /// The underlying list of entries in the buffer. + /// + internal ValueList Entries = []; + + /// + /// The capacity of the underlying entry collection, for object pooling usage. + /// + internal int Capacity => Entries.Capacity; + + /// + /// Construct a new command buffer. As these are meant to be pooled by entity manager, the constructor is + /// internal. + /// + internal CommandBuffer(IDependencyCollection collection) + { + _entMan = collection.Resolve(); + _protoMan = collection.Resolve(); + } + + private ref CommandBufferEntry NextEntry() + { + Entries.Add(default); + + return ref Entries[^1]; + } + + /// + /// Creates a new CommandBuffer that will execute at precisely this point. + /// This buffer is independent of its parent and can be safely given to another thread. + /// + /// The command buffer, for chaining. + public CommandBuffer CreateSubBuffer(out CommandBuffer subBuffer) + { + subBuffer = _entMan.GetCommandBuffer(); + + CommandBufferEntry.SubBuffer(subBuffer, out NextEntry()); + + return this; + } + + /// + /// Adds an entity deletion to the buffer. + /// + /// The entity to delete immediately. + /// The command buffer, for chaining. + public CommandBuffer DeleteEntity(EntityUid target) + { + CommandBufferEntry.DeleteEntity(target, out NextEntry()); + return this; + } + + /// + /// Returns the CommandBuffer without applying it. + /// + public void Dispose() + { + _entMan.CommandBufferPool.Return(this); + } + + /// + /// Internal method used for cleaning up a CommandBuffer on return. + /// + internal void Clear() + { + Entries.Clear(); + } +} diff --git a/Robust.Shared/GameObjects/CommandBuffers/CommandBufferEntry.QueuedAction.cs b/Robust.Shared/GameObjects/CommandBuffers/CommandBufferEntry.QueuedAction.cs new file mode 100644 index 00000000000..147130bade3 --- /dev/null +++ b/Robust.Shared/GameObjects/CommandBuffers/CommandBufferEntry.QueuedAction.cs @@ -0,0 +1,85 @@ +using System; +using System.Reflection; +using Robust.Shared.Utility; + +namespace Robust.Shared.GameObjects.CommandBuffers; + +internal partial struct CommandBufferEntry +{ + /// + /// Creates a new queued invocation to add to a command buffer. + /// + /// The action to invoke. + /// Context to provide to the action. + /// The location to place the new entry within. + /// The type of the context object to pass to the action. + public static void QueuedActionT(Action action, T context, out CommandBufferEntry entry) + where T : class + { + entry.Command = (long)CmdKind.QueuedActionT; + + entry.Field1 = 0; + entry.Field2 = action; + entry.Field3 = context; + } + + /// + /// Creates a new queued invocation to add to a command buffer. + /// + /// The action to invoke. + /// Context to provide to the action. + /// The entity to pass along. + /// The location to place the new entry within. + /// + public static void QueuedActionTEnt( + Action action, + T context, + EntityUid ent, + out CommandBufferEntry entry) + where T : class + { + entry.Command = (long)CmdKind.QueuedActionTEnt; + + entry.Field1 = (int)ent; + entry.Field2 = action; + entry.Field3 = context; + } + + /// + /// Handles invoking a queued action with context argument.. + /// + public void InvokeQueuedActionT() + { + DebugTools.AssertEqual(Kind, CmdKind.QueuedActionT); + + // Yes, we potentially allocate at invocation time. + // A shame, but we can't invoke Action otherwise. + // The actual allocation is a single entry object array, which should never leave Gen0 and thus should be quite + // cheap. + // + // So it's still better we allocate here at the call site rather than in a potentially long-lived command buffer. + var d = (Delegate)Field2!; + // We don't check if the Delegate has anything like multi-invocation because the only constructor we have + // makes sure it's an Action. + d.Method.Invoke(d.Target, BindingFlags.DoNotWrapExceptions, null, [Field3!], null); + } + + /// + /// Handles invoking a queued action with context argument and entity. + /// + public void InvokeQueuedActionTEnt() + { + DebugTools.AssertEqual(Kind, CmdKind.QueuedActionTEnt); + + // Yes, we potentially allocate at invocation time. + // A shame, but we can't invoke Action otherwise. + // The actual allocation is a single entry object array, which should never leave Gen0 and thus should be quite + // cheap. + // + // So it's still better we allocate here at the call site rather than in a potentially long-lived command buffer. + var d = (Delegate)Field2!; + // We don't check if the Delegate has anything like multi-invocation because the only constructor we have + // makes sure it's an Action. + d.Method.Invoke(d.Target, BindingFlags.DoNotWrapExceptions, null, [Field3!, new EntityUid((int)Field1)], null); + } +} diff --git a/Robust.Shared/GameObjects/CommandBuffers/CommandBufferEntry.cs b/Robust.Shared/GameObjects/CommandBuffers/CommandBufferEntry.cs new file mode 100644 index 00000000000..3cf9fe6bcf0 --- /dev/null +++ b/Robust.Shared/GameObjects/CommandBuffers/CommandBufferEntry.cs @@ -0,0 +1,209 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using Robust.Shared.GameObjects.EntityBuilders; + +namespace Robust.Shared.GameObjects.CommandBuffers; + +/// +/// +/// A 32-byte structure used to contain an untyped command buffer command, +/// to help reduce the number of allocations necessary to buffer commands. +/// +/// +/// The meanings of , , and are dependent on the +/// contents of . Static constructors are provided for commands themselves. +/// +/// +/// This is, essentially, a union of all the possible command buffer commands, to the best of C#'s ability. +/// The majority of command buffer commands do not need any more storage than the entry itself, reducing +/// heap fragmentation and long-lived micro-allocations significantly. +/// +/// +[StructLayout(LayoutKind.Sequential)] +internal partial struct CommandBufferEntry +{ + /// + /// The command data. This is bitpacked and contains some extra information. + /// + public long Command; + public long Field1; + public object? Field2; + public object? Field3; + + /// + /// The kind of the command. + /// + /// This is always valid. + public CmdKind Kind => (CmdKind)(Command & 0xFF); + /// + /// The target EntityUid. + /// + /// + /// This is only valid if the contained command uses the field for an EntityUid. + /// In anticipation of future refactors, we assume EntityUid is long sized and takes up the entirety + /// of Field1. + /// + public EntityUid TargetEnt => new (unchecked((int)Field1)); + + + static unsafe CommandBufferEntry() + { +#pragma warning disable CS8500 // This takes the address of, gets the size of, or declares a pointer to a managed type + // Ensure we're the expected size, because this is performance sensitive and we make assumptions about this. + // Yet another reason this engine is 32-bit incompatible right here. + if (sizeof(CommandBufferEntry) != 32) + { + throw new Exception( + $"{nameof(CommandBufferEntry)} was modified to no longer be half-cacheline sized. You're going to need to go adjust things!"); + } +#pragma warning restore CS8500 // This takes the address of, gets the size of, or declares a pointer to a managed type + } + + /// + /// Creates a new command buffer entry to delete a given entity. + /// + /// Target entity. + /// The location to place the new entry within. + public static void DeleteEntity(EntityUid target, out CommandBufferEntry entry) + { + entry.Command = (long)CmdKind.DeleteEntity; + entry.Field1 = (long)target; + entry.Field2 = null; + entry.Field3 = null; + } + + /// + /// Creates a new command buffer entry to apply another command buffer. + /// + /// The buffer to apply. + /// The location to place the new entry within. + public static void SubBuffer(CommandBuffer subBuffer, out CommandBufferEntry entry) + { + entry.Command = (long)CmdKind.SubBuffer; + entry.Field1 = 0; + entry.Field2 = subBuffer; + entry.Field3 = null; + } + + /// + /// Creates a new command buffer entry to spawn an entity with a builder. + /// + /// The builder to apply. + /// The location to place the new entry within. + public static void SpawnEntity(EntityBuilder builder, out CommandBufferEntry entry) + { + entry.Command = (long)CmdKind.SpawnEntity; + entry.Field1 = 0; + entry.Field2 = builder; + entry.Field3 = null; + } + + /// + /// Creates a new command buffer entry to spawn entities with a builder. + /// + /// The builders to apply. + /// The location to place the new entry within. + public static void SpawnEntities(List builder, out CommandBufferEntry entry) + { + entry.Command = (long)CmdKind.SpawnEntity; + entry.Field1 = 0; + entry.Field2 = builder; + entry.Field3 = null; + } + + /// + /// The kind of command in an entry. + /// + public enum CmdKind : byte + { + /// + /// An invalid command. Causes to throw. + /// + Invalid = 0, + + /// + /// A command for running an Action<T>. + /// + /// unused Field1; + /// Action<T> Action; + /// T Context; + /// + /// + QueuedActionT, + + /// + /// A command for running an Action<T, EntityUid>. + /// + /// EntityUid Target; + /// Action<T, EntityUid> Action; + /// T Context; + /// + /// + QueuedActionTEnt, + + /// + /// Handles executing another command buffer that was branched from this one. + /// + /// unused Field1; + /// CommandBuffer SubBuffer; + /// unused Field3; + /// + /// + SubBuffer, + + /// + /// Handles deleting an entity. + /// + /// EntityUid Target; + /// unused Field2; + /// unused Field3; + /// + /// + DeleteEntity, + + /// + /// Handles spawning an entity with a builder. + /// Either an entity builder, or a list of entity builders. + /// + /// unused Field1; + /// EntityBuilder | List<EntityBuilder> EntityBuilder; + /// unused Field3; + /// + /// + SpawnEntity, + + /// + /// Handles adding components to an entity. + /// Either a component, or a list of components. + /// + /// EntityUid Target; + /// IComponent | IComponent[] Components; + /// unused Field3; + /// + /// + AddComponents, + + /// + /// Handles ensuring components on an entity. + /// Similar to AddComponents, this is either a Type, or a list of Types. + /// + /// EntityUid Target; + /// Type | Type[] Components; + /// unused Field3; + /// + /// + EnsureComponents, + + /// + /// Handles removing components from an entity. + /// Similar to AddComponents, this is either a Type, or a list of Types. + /// + /// EntityUid Target; + /// Type | Type[] Components; + /// unused Field3; + /// + /// + RemoveComponents, + } +} diff --git a/Robust.Shared/GameObjects/ComponentExtensions.cs b/Robust.Shared/GameObjects/ComponentExtensions.cs new file mode 100644 index 00000000000..9e78c42d8cd --- /dev/null +++ b/Robust.Shared/GameObjects/ComponentExtensions.cs @@ -0,0 +1,17 @@ +namespace Robust.Shared.GameObjects; + +public static class ComponentExtensions +{ + extension(T comp) + where T : IComponent + { + /// + /// Checks if a component is "attached" and in use by the game simulation. + /// Unattached components are data (i.e. in prototypes). + /// + public bool IsUnattached() + { + return comp.LifeStage == ComponentLifeStage.PreAdd; + } + } +} diff --git a/Robust.Shared/GameObjects/ComponentFilterQuery.cs b/Robust.Shared/GameObjects/ComponentFilterQuery.cs new file mode 100644 index 00000000000..fb340bf184d --- /dev/null +++ b/Robust.Shared/GameObjects/ComponentFilterQuery.cs @@ -0,0 +1,126 @@ +using System.Collections; +using System.Collections.Generic; + +namespace Robust.Shared.GameObjects; + +/// +/// A cacheable query for every entity that fulfills . +/// This provides both iteration through , and also direct queries through . +/// +/// +/// Unlike you can in fact just use foreach with this. It's fine! +/// A concrete implementation of is provided, so the compiler knows how to optimize it. +/// +public readonly struct ComponentFilterQuery : IEnumerable +{ + private readonly Dictionary _metaData; + private readonly Dictionary _lead; + private readonly Dictionary[] _tails; + private readonly bool _matchPaused; + + internal ComponentFilterQuery(Dictionary metaData, Dictionary lead, Dictionary[] tails, bool matchPaused) + { + _metaData = metaData; + _lead = lead; + _tails = tails; + _matchPaused = matchPaused; + } + + /// + /// Tests if an entity matches this query. + /// + /// The entity to test against. + /// True on match, false otherwise. + public bool Matches(EntityUid ent) + { + if (!_lead.TryGetValue(ent, out var c1) || c1.Deleted) + return false; + + if (!_matchPaused && ((MetaDataComponent)_metaData[ent]).EntityPaused) + return false; + + foreach (var tail in _tails) + { + if (!tail.TryGetValue(ent, out c1) || c1.Deleted) + return false; + } + + return true; + } + + /// + /// Returns an enumerator for this query. + /// + /// + public Enumerator GetEnumerator() + { + return new Enumerator(this); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public struct Enumerator : IEnumerator + { + private readonly ComponentFilterQuery _query; + private Dictionary.Enumerator _leadEnumerator; + private EntityUid _current; + + internal Enumerator(ComponentFilterQuery query) : this() + { + _query = query; + Reset(); + } + + public bool MoveNext() + { + // Loop until we find something that matches every tail. + while (true) + { + if (!_leadEnumerator.MoveNext()) + return false; + + var (workingEnt, c1) = _leadEnumerator.Current; + if (c1.Deleted) + continue; + + if (!_query._matchPaused && ((MetaDataComponent)_query._metaData[workingEnt]).EntityPaused) + continue; + + foreach (var tail in _query._tails) + { + if ((!tail.TryGetValue(workingEnt, out var c2)) || c2.Deleted) + goto end; // Retry. We can't continue the outer loop from here easily so goto it is. + } + + _current = workingEnt; + break; + + end: ; + } + + return true; + } + + public void Reset() + { + _leadEnumerator = _query._lead.GetEnumerator(); + } + + EntityUid IEnumerator.Current => _current; + + object IEnumerator.Current => _current; + + public void Dispose() + { + _leadEnumerator.Dispose(); + } + } +} diff --git a/Robust.Shared/GameObjects/Docs.xml b/Robust.Shared/GameObjects/Docs.xml index 6eaf60eb8b2..ec08f371290 100644 --- a/Robust.Shared/GameObjects/Docs.xml +++ b/Robust.Shared/GameObjects/Docs.xml @@ -11,4 +11,40 @@ This is also preferable if you may have already looked up the component, saving on lookup time. + + + An index of all entities with a given component, avoiding looking up the component's storage every time. + Using these saves on dictionary lookups, making your code slightly more efficient, and ties in nicely with + . + + + + public sealed class MySystem : EntitySystem + { + [Dependency] private EntityQuery<TransformComponent> _transforms = default!; +
+ public void Update(float ft) + { + foreach (var ent in _transforms) + { + // iterate matching entities, excluding paused ones. + } + } +
+ public void DoThings(EntityUid myEnt) + { + var ent = _transforms.Get(myEnt); + // ... + } + } +
+
+ + Queries hold references to internals, and are always up to date with the world. + They can not however perform mutation, if you need to add or remove components you must use + or methods. + + EntitySystem.GetEntityQuery() + EntityManager.GetEntityQuery() +
diff --git a/Robust.Shared/GameObjects/DynamicEntityQuery.cs b/Robust.Shared/GameObjects/DynamicEntityQuery.cs new file mode 100644 index 00000000000..d42b8b09281 --- /dev/null +++ b/Robust.Shared/GameObjects/DynamicEntityQuery.cs @@ -0,0 +1,427 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Robust.Shared.GameObjects; + +/// +/// +/// A typeless version of entity queries optimized for more complex query behaviors. +/// This isn't enumerable for allocation reasons, but works for an arbitrary set of components. +/// +/// +/// DynamicEntityQuery supports query constraints, as described by , +/// that control how the query should treat the presence of a given component (is it optional, is it required). +/// +/// +/// +/// The component-returning methods on this type all take in spans to output into, it is recommended to +/// allocate an array once (within a method) and reuse it regularly. +/// +/// +/// +/// var query = GetDynamicQuery( +/// // Query every map, +/// (typeof(MapComponent), DynamicEntityQuery.QueryFlags.None), +/// // that may also be a grid, +/// (typeof(MapGridComponent), DynamicEntityQuery.QueryFlags.Optional), +/// // that is absolutely not funny. +/// (typeof(FunnyComponent), DynamicEntityQuery.QueryFlags.Without) +/// ); +///
+/// var components = new IComponent?[3]; // Must match the number of queried components. +/// var enumerator = query.GetEnumerator(); +///
+/// while (enumerator.MoveNext(out var ent, components)) +/// { +/// // all the components we wanted from this ent are in components. +/// var mapComp = (MapComponent)components[0]!; +/// var mapGridComp = (MapGridComponent?)components[1]; +/// // components[2] is where FunnyComponent would be, but these entities are never funny so it's always null. +/// Obliterate((ent, mapComp, mapGridComp)); +/// } +///
+///
+/// +public readonly struct DynamicEntityQuery +{ + /// + /// Information on a query item, describing how to handle it. + /// + internal readonly struct QueryEntry(Dictionary dict, QueryFlags flags) + { + public readonly Dictionary Dict = dict; + + public readonly QueryFlags Flags = flags; + } + + [Flags] + public enum QueryFlags + { + /// + /// Indicates no special behavior, the component is required. + /// + None = 0, + /// + /// Indicates this entry is optional. + /// + Optional = 1, + /// + /// Indicates this entry is excluded, and the query fails if it's present. + /// + Without = 2, + } + + private readonly QueryEntry[] _entries; + private readonly Dictionary _metaData; + + /// + /// The number of components this query is set up to emit. + /// + /// + /// Components marked Without always get a slot in the list regardless of being used. + /// + public int OutputCount => _entries.Length; + + internal DynamicEntityQuery(QueryEntry[] entries, Dictionary metaData) + { + _entries = entries; + _metaData = metaData; + } + + /// + /// Tries to query an entity for this query's components. + /// + /// The entity to look up components for. + /// The span to output components into. + /// True when all components were found, false otherwise. + public bool TryGet(EntityUid ent, in Span output) + { + // SAFETY: This ensures that the span is exactly as long as we need. + // Any less and we'd write out of bounds, which is Very Bad. + if (output.Length != OutputCount) + ThrowBadLength(OutputCount, output.Length); + + ref var spanEntry = ref MemoryMarshal.GetReference(output); + + var entriesLength = _entries.Length; + + if (entriesLength == 0) + return true; // Okay we got everything. And by everything, I mean nothing whatsoever. + + ref var entryRef = ref MemoryMarshal.GetReference(_entries); + + // REMARK: All this work in here is to avoid a handful of bounds checks. + // Frankly, this isn't that critical given we're also.. indexing a dict. + // and as such bounds checks probably disappear into overhead. + // but I figure every little bit helps in a critical path like this. + for (var i = 0; i < entriesLength; i++) + { + var exists = !entryRef.Dict.TryGetValue(ent, out spanEntry) || spanEntry.Deleted; + // If it exists (or doesn't exist when without is set) and optional is not set, bail. + if ((exists ^ ((entryRef.Flags & QueryFlags.Without) != 0)) + && (entryRef.Flags & QueryFlags.Optional) == 0) + return false; + + if (i >= entriesLength - 1) + break; // Don't create out of bounds refs, I like having a face. + + // Increment our index.. + // ReSharper disable once RedundantTypeArgumentsOfMethod + spanEntry = ref Unsafe.Add(ref spanEntry, 1); + + // and increment our index into the tails array, too. + entryRef = ref Unsafe.Add(ref entryRef, 1); + } + + return true; // We iterated all our tails + } + + /// + /// Tries to query an entity for this query's components, skipping already-filled entries. + /// + /// The entity to look up components for. + /// The span to output components into. + /// True when all components were found, false otherwise. + public bool TryResolve(EntityUid ent, in Span output) + { + // SAFETY: This ensures that the span is exactly as long as we need. + // Any less and we'd write out of bounds, which is Very Bad. + if (output.Length != OutputCount) + ThrowBadLength(OutputCount, output.Length); + + ref var spanEntry = ref MemoryMarshal.GetReference(output); + + var entriesLength = _entries.Length; + + if (entriesLength == 0) + return true; // Okay we got everything. And by everything, I mean nothing whatsoever. + + ref var entryRef = ref MemoryMarshal.GetReference(_entries); + + // REMARK: All this work in here is to avoid a handful of bounds checks. + // Frankly, this isn't that critical given we're also.. indexing a dict. + // and as such bounds checks probably disappear into overhead. + // but I figure every little bit helps in a critical path like this. + for (var i = 0; i < entriesLength; i++) + { + // If the entry is null and we're marked Without, continue. + // and vice versa, if it's not null and we're marked Without, don't continue. + // ..and if it's not null and we're not marked without, continue. + // Resolve behavior here is a little silly, I think. Oh well. + if (spanEntry is not null ^ ((entryRef.Flags & QueryFlags.Without) != 0)) + continue; + + var exists = !entryRef.Dict.TryGetValue(ent, out spanEntry) || spanEntry.Deleted; + // If it exists (or doesn't exist when without is set) and optional is not set, bail. + if ((exists ^ ((entryRef.Flags & QueryFlags.Without) != 0)) + && (entryRef.Flags & QueryFlags.Optional) == 0) + return false; + + if (i >= entriesLength - 1) + break; // Don't create out of bounds refs, I like having a face. + + // Increment our index.. + // ReSharper disable once RedundantTypeArgumentsOfMethod + spanEntry = ref Unsafe.Add(ref spanEntry, 1); + + // and increment our index into the tails array, too. + entryRef = ref Unsafe.Add(ref entryRef, 1); + } + + return true; // We iterated all our tails + } + + /// + /// Tests if a given entity matches this query (ala ) + /// + /// The entity to try matching against. + /// True if the entity matches this query. + public bool Matches(EntityUid ent) + { + var entriesLength = _entries.Length; + + if (entriesLength == 0) + return true; // Okay we got everything, as in literally nothing, but it DOES match. + + ref var entryRef = ref MemoryMarshal.GetReference(_entries); + + // REMARK: All this work in here is to avoid a handful of bounds checks. + // Frankly, this isn't that critical given we're also.. indexing a dict. + // and as such bounds checks probably disappear into overhead. + // but I figure every little bit helps in a critical path like this. + for (var i = 0; i < entriesLength; i++) + { + var exists = !entryRef.Dict.TryGetValue(ent, out var entry) || entry.Deleted; + + // If it exists (or doesn't exist when without is set) and optional is not set, bail. + if ((exists ^ ((entryRef.Flags & QueryFlags.Without) != 0)) + && (entryRef.Flags & QueryFlags.Optional) == 0) + return false; + + if (i >= entriesLength - 1) + break; // Don't create out of bounds refs, I like having a face. + + // and increment our index into the tails array, too. + entryRef = ref Unsafe.Add(ref entryRef, 1); + } + + return true; // We iterated all our component dicts, we win. + } + + /// + /// Tries to query an entity for this query's components. Implementation detail of the enumerator, this skips + /// the first entry. + /// + /// The entity to look up components for. + /// The span to output components into, in ref form. + /// True when all components were found, false otherwise. + private bool TryGetAfterFirst(EntityUid ent, ref IComponent? spanEntry) + { + // SAFETY: We already checked the bounds if this is called. + + var entriesLength = _entries.Length; + + if (entriesLength - 1 == 0) + return true; // Okay we got everything. And by everything, I mean nothing whatsoever. + + ref var entryRef = ref MemoryMarshal.GetReference(_entries); + + // + 1, we already got the first. + entryRef = ref Unsafe.Add(ref entryRef, 1); + + // REMARK: All this work in here is to avoid a handful of bounds checks. + // Frankly, this isn't that critical given we're also.. indexing a dict. + // and as such bounds checks probably disappear into overhead. + // but I figure every little bit helps in a critical path like this. + for (var i = 1; i < entriesLength; i++) + { + var exists = !entryRef.Dict.TryGetValue(ent, out spanEntry) || spanEntry.Deleted; + // If it exists (or doesn't exist when without is set) and optional is not set, bail. + if ((exists ^ ((entryRef.Flags & QueryFlags.Without) != 0)) + && (entryRef.Flags & QueryFlags.Optional) == 0) + return false; + + if (i >= entriesLength - 1) + break; // Don't create out of bounds refs, I like having a face. + + // Increment our index.. + // ReSharper disable once RedundantTypeArgumentsOfMethod + spanEntry = ref Unsafe.Add(ref spanEntry, 1); + + // and increment our index into the tails array, too. + entryRef = ref Unsafe.Add(ref entryRef, 1); + } + + return true; // We iterated all our tails + } + + public Enumerator GetEnumerator(bool checkPaused) + { + return new Enumerator(this, checkPaused); + } + + /// + /// The custom enumerator for dynamic queries. + /// This is intended to be an implementation detail for other queries. + /// + public struct Enumerator + { + private readonly DynamicEntityQuery _owner; + private readonly bool _checkPaused; + private Dictionary.Enumerator _lead; +#if DEBUG + // Anti-misuse assertions, store enumerators for every entry and Reset() them constantly so that + // if you update the ECS while we're enumerating it blows up. + // This does mean DynamicEntityQuery allocates in debug, but it also means I won't have to sort through + // all the ways content code breaks basic assumptions down the line because it'll explode in tests. + private readonly Dictionary.Enumerator[] _mines; +#endif + + internal Enumerator(DynamicEntityQuery owner, bool checkPaused) + { + QueryFlags flags; + _owner = owner; + _checkPaused = checkPaused; + + if (_owner._entries.Length == 0) + { + flags = QueryFlags.None; + } + else + { + flags = _owner._entries[0].Flags; + } + + if (flags != QueryFlags.None) + { + throw new NotSupportedException( + "Query enumerators do not support optional or excluded first components."); + } + +#if DEBUG + if (_owner._entries.Length > 0) + _mines = _owner._entries.Select(x => x.Dict.GetEnumerator()).ToArray(); + else + _mines = [_owner._metaData.GetEnumerator()]; +#endif + + Reset(); + } + +#if DEBUG + private void StepOnMines() + { + try + { + foreach (var mine in _mines) + { + ((IEnumerator)mine).Reset(); + } + } + catch (InvalidOperationException versionError) + { + throw new InvalidOperationException( + "Tried to use an Enumerator that was invalidated by changes to the ECS. You cannot add or remove the components you're querying for, nor add or remove entities with them.", + versionError); + } + } +#endif + + /// + /// Attempts to find the next entity in the query iterator. + /// + /// The discovered entity, if any. + /// The storage for components queried for this entity. + /// True if ent and components are valid, false if there's no items left. + public bool MoveNext(out EntityUid ent, in Span output) + { + if (output.Length != _owner.OutputCount) + ThrowBadLength( _owner.OutputCount, output.Length); + +#if DEBUG + StepOnMines(); +#endif + + // We grab this here to pin it all function instead of constantly pinning in the loop. + ref var span = ref MemoryMarshal.GetReference(output); + var meta = _owner._metaData; + + ent = EntityUid.Invalid; + while (true) + { + if (!_lead.MoveNext()) + return false; + + ref var spanEntry = ref span; + + ent = _lead.Current.Key; + spanEntry = _lead.Current.Value; + + if (_checkPaused && ((MetaDataComponent)meta[ent]).EntityPaused) + continue; // Oops, paused. + + if (spanEntry.Deleted) + continue; // Nevermind, move along. + + if (output.Length == 1) + return true; // Already done. Do NOT create out of bounds refs! + + // Increment our index. + // ReSharper disable once RedundantTypeArgumentsOfMethod + spanEntry = ref Unsafe.Add(ref spanEntry, 1); + + if (_owner.TryGetAfterFirst(ent, ref spanEntry)) + return true; + + // Oops, we failed, try again. + } + } + + public void Reset() + { + if (_owner._entries.Length == 0) + { + _lead = _owner._metaData.GetEnumerator(); + } + else + { + _lead = _owner._entries[0].Dict.GetEnumerator(); + } + +#if DEBUG + StepOnMines(); +#endif + } + } + + [DoesNotReturn] + private static void ThrowBadLength(int expected, int length) + { + throw new IndexOutOfRangeException($"The given span is not large enough to fit all of the query's outputs. Expected {expected}, got {length}"); + } +} diff --git a/Robust.Shared/GameObjects/EntityBuilders/Docs.xml b/Robust.Shared/GameObjects/EntityBuilders/Docs.xml new file mode 100644 index 00000000000..1817738c9e7 --- /dev/null +++ b/Robust.Shared/GameObjects/EntityBuilders/Docs.xml @@ -0,0 +1,9 @@ + + + + If the provided parent of the entity to be built is deleted in the meantime, entity construction will likely + fail with exceptions. If you need to check later for the parent existing, you should consider spawning the entity + in nullspace or checking right before applying the builders. + + + diff --git a/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.AddComp.cs b/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.AddComp.cs new file mode 100644 index 00000000000..943458c51f1 --- /dev/null +++ b/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.AddComp.cs @@ -0,0 +1,259 @@ +using System; +using System.Collections.Generic; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Utility; + +namespace Robust.Shared.GameObjects.EntityBuilders; + +public sealed partial class EntityBuilder +{ + /// + /// Adds a component to the entity being built. + /// + /// The type of component to add. + /// Thrown if the component already exists on the in progress entity. + /// The builder, for chaining. + public EntityBuilder AddComp() + where T : IComponent, new() + { + var component = _factory.GetComponent(CompIdx.Index()); + + if (!EntityComponents.TryAdd(typeof(T), component)) + { + throw new ArgumentException( + $"The component {_factory.GetComponentName()} already existed in the builder."); + } + +#pragma warning disable CS0618 // Type or member is obsolete + component.Owner = ReservedEntity; +#pragma warning restore CS0618 // Type or member is obsolete + + return this; + } + + /// + /// Adds a component to the entity being built. + /// + /// The type of component to add. + /// Thrown if the component already exists on the in progress entity. + /// The builder, for chaining. + public EntityBuilder AddComp(Type t) + { + var component = _factory.GetComponent(t); + + if (!EntityComponents.TryAdd(t, component)) + { + throw new ArgumentException( + $"The component {_factory.GetComponentName(t)} already existed in the builder."); + } + +#pragma warning disable CS0618 // Type or member is obsolete + component.Owner = ReservedEntity; +#pragma warning restore CS0618 // Type or member is obsolete + + return this; + } + + /// + /// Adds a component to the entity being built. + /// + /// + /// + /// This directly adds the provided component, without making a copy. + /// Do not use this with components you got from a registry without copying them! + /// + /// + /// Works with IComponent, but the concrete type of must be a constructable, + /// registered component. + /// + /// + /// The component to add. + /// The type of component to add. + /// Thrown if the component already exists on the in progress entity. + /// The builder, for chaining. + public EntityBuilder AddComp(T component) + where T : IComponent + { + DebugTools.AssertEqual(component.LifeStage, ComponentLifeStage.PreAdd); + DebugTools.Assert(_factory.TryGetRegistration(component.GetType(), out _)); + +#pragma warning disable CS0618 // Type or member is obsolete + component.Owner = ReservedEntity; +#pragma warning restore CS0618 // Type or member is obsolete + + if (!EntityComponents.TryAdd(component.GetType(), component)) + { + throw new ArgumentException( + $"The component {_factory.GetComponentName(component.GetType())} already existed in the builder."); + } + + return this; + } + + /// + /// Copies a component to the entity being built. + /// + /// + /// Works with IComponent, but the concrete type of must be a constructable, + /// registered component. + /// + /// The component to copy. + /// The optional serialization context to use when copying. + /// The type of component to add. + /// Thrown if the component already exists on the in progress entity. + /// The builder, for chaining. + public EntityBuilder CopyComp(T component, ISerializationContext? context = null) + where T : IComponent + { + var newComp = _factory.GetComponent(component.GetType()); + + _serMan.CopyTo(component, ref newComp, context, notNullableOverride: true); + + if (!EntityComponents.TryAdd(component.GetType(), newComp)) + { + throw new ArgumentException( + $"The component {_factory.GetComponentName(component.GetType())} already existed in the builder."); + } + +#pragma warning disable CS0618 // Type or member is obsolete + newComp.Owner = ReservedEntity; +#pragma warning restore CS0618 // Type or member is obsolete + + return this; + } + + /// + /// Copies a component to the entity being built, or writes over an existing one. + /// + /// + /// Works with IComponent, but the concrete type of must be a constructable, + /// registered component. + /// + /// The component to copy. + /// The optional serialization context to use when copying. + /// The type of component to add. + /// Thrown if the component already exists on the in progress entity. + /// The builder, for chaining. + public EntityBuilder EnsureCopyComp(T component, ISerializationContext? context = null) + where T : IComponent + { + if (!EntityComponents.TryGetValue(component.GetType(), out var comp)) + comp = _factory.GetComponent(component.GetType()); + +#pragma warning disable CS0618 // Type or member is obsolete + comp.Owner = ReservedEntity; +#pragma warning restore CS0618 // Type or member is obsolete + + _serMan.CopyTo(component, ref comp, context, notNullableOverride: true); + + EntityComponents.TryAdd(component.GetType(), comp); + + return this; + } + + + /// + /// Directly adds the given set of components to the entity being built. + /// + /// + /// + /// This directly adds the provided components, without making a copy. + /// Do not use this with components you got from a registry without copying them! + /// + /// + /// Works with IComponent, but the concrete types of must be constructable, + /// registered components. + /// + /// + /// The set of components to add. + /// Thrown if any component already exists on the in progress entity. + /// The builder, for chaining. + public EntityBuilder AddComps(IEnumerable components) + { + foreach (var component in components) + { + AddComp(component); + } + + return this; + } + + /// + public EntityBuilder AddComps(Span components) + { + foreach (var component in components) + { + AddComp(component); + } + + return this; + } + + /// + /// Directly adds the given set of components to the entity being built. + /// + /// + /// + /// Works with IComponent, but the concrete types of must be constructable, + /// registered components. + /// + /// + /// The set of components to add. + /// Thrown if any component already exists on the in progress entity. + /// The builder, for chaining. + public EntityBuilder AddComps(T components) + where T : IEnumerable + { + foreach (var component in components) + { + AddComp(component); + } + + return this; + } + + /// + public EntityBuilder AddComps(Span components) + { + foreach (var component in components) + { + AddComp(component); + } + + return this; + } + + /// + /// Copies a component to the entity being built. + /// + /// + /// Works with IComponent, but the concrete types of must be constructable, + /// registered components. + /// + /// The components to copy. + /// The optional serialization context to use when copying. + /// Thrown if any component already exists on the in progress entity. + /// The builder, for chaining. + public EntityBuilder CopyComps(T components, ISerializationContext? context = null) + where T : IEnumerable + { + foreach (var component in components) + { + CopyComp(component, context); + } + + return this; + } + + /// + /// The builder, for chaining. + public EntityBuilder CopyComps(Span components, ISerializationContext? context = null) + { + foreach (var component in components) + { + CopyComp(component, context); + } + + return this; + } +} diff --git a/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.Initialize.cs b/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.Initialize.cs new file mode 100644 index 00000000000..24fc415c6e4 --- /dev/null +++ b/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.Initialize.cs @@ -0,0 +1,58 @@ +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.Manager; + +namespace Robust.Shared.GameObjects.EntityBuilders; + +public sealed partial class EntityBuilder +{ + /// + /// Creates the bare minimal spawnable entity with metadata and a transform. + /// + /// The serialization context to use, if any. + private void InitializeMinimalEntity(ISerializationContext? context = null) + { +#pragma warning disable CS0618 // Type or member is obsolete + MetaData = new MetaDataComponent() { Owner = ReservedEntity }; +#pragma warning restore CS0618 // Type or member is obsolete + AddComp(MetaData); + + // This thing is so legacy it has a [Dependency] in it. + Transform = _factory.GetComponent(); + AddComp(Transform); + } + + /// + /// Initializes a command buffer from a prototype, cloning all the components onto the new entity. + /// + /// The prototype to construct from. + /// The serialization context to use, if any. + private void InitializeFromPrototype(EntProtoId entityProtoId, ISerializationContext? context = null) + { + var entityProto = _protoMan.Index(entityProtoId); + + InitializeMinimalEntity(context); + + MetaData._entityPrototype = entityProto; + + foreach (var component in entityProto.Components.Components()) + { + EnsureCopyComp(component, context); + } + } + + /// + /// Applies the given component registry to the builder, copying over all components it contains. + /// + /// The registry to apply. + /// The serialization context to use, if any. + /// + public EntityBuilder ApplyRegistry(ComponentRegistry registry, ISerializationContext? context = null) + { + foreach (var component in registry.Components()) + { + EnsureCopyComp(component, context); + } + + return this; + } +} diff --git a/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.Meta.cs b/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.Meta.cs new file mode 100644 index 00000000000..75ad252cee8 --- /dev/null +++ b/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.Meta.cs @@ -0,0 +1,42 @@ +using Robust.Shared.Localization; + +namespace Robust.Shared.GameObjects.EntityBuilders; + +public sealed partial class EntityBuilder +{ + /// + /// Sets the name of the entity. + /// + public EntityBuilder Named(string newName) + { + MetaData._entityName = newName; + return this; + } + + /// + /// Sets the name of the entity to the given localization string. + /// + public EntityBuilder NamedLoc(LocId newName) + { + MetaData._entityName = _locMan.GetString(newName); + return this; + } + + /// + /// Sets the description of the entity. + /// + public EntityBuilder Described(string newDesc) + { + MetaData._entityDescription = newDesc; + return this; + } + + /// + /// Sets the description of the entity to the given localization string. + /// + public EntityBuilder DescribedLoc(LocId newDesc) + { + MetaData._entityDescription = _locMan.GetString(newDesc); + return this; + } +} diff --git a/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.cs b/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.cs new file mode 100644 index 00000000000..3a979c89321 --- /dev/null +++ b/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.cs @@ -0,0 +1,369 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using JetBrains.Annotations; +using Robust.Shared.GameObjects.CommandBuffers; +using Robust.Shared.IoC; +using Robust.Shared.Localization; +using Robust.Shared.Map; +using Robust.Shared.Maths; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager; + +namespace Robust.Shared.GameObjects.EntityBuilders; + +/// +/// +/// A builder for an entity, allowing you to construct and mutate an incomplete entity before spawning it. +/// Supports construction from a prototype, and from a fresh list of components. +/// +/// +/// +/// +/// EntityBuilder is complete, but differs in behavior from the old systems. Some engine refactors need done before it is fully functional. +/// The following are behaviors that differ from old-style entity spawning: +/// +/// +/// - EntityBuilder performs all deserialization before ComponentAdd, not after.
+/// - EntityBuilder applies additional s before ComponentAdd, not after.
+/// - EntityBuilder does not inject dependencies into components. +///
+/// +/// With those limitations in mind, EntityBuilder currently doesn't work for spawning entities with +/// on the client. This does not impact server-side spawning, +/// and EntityBuilder is now the internal implementation of most spawning operations on the server for a performance +/// uplift. +/// +/// +/// As long as your content pack does not contain legacy code relying on main-thread-only +/// or within components, EntityBuilder works for you immediately on client as well. +/// +///
+[PublicAPI] +public sealed partial class EntityBuilder +{ + private readonly IComponentFactory _factory; + private readonly IPrototypeManager _protoMan; + private readonly ISerializationManager _serMan; + private readonly ILocalizationManager _locMan; + private readonly IRemoteEntityManager _remoteEntMan; + + /// + /// The EntityUid we have reserved. No entity exists here yet unless the builder has been + /// applied and consumed. + /// + public readonly EntityUid ReservedEntity; + + /// + /// The components we'll be adding to the entity we construct. + /// + internal readonly Dictionary EntityComponents; + + /// + /// The coordinates to spawn the entity at, if any. + /// Due to how map coordinates work, these have to be resolved at spawn time just before initializing + /// the entities. + /// + internal MapCoordinates? MapCoordinates; + + /// + /// Any exceptions thrown trying to create the entity, ensuring it doesn't continue. + /// + public EntityCreationException2? CreationFailure { get; internal set; } + + public MetaDataComponent MetaData { get; private set; } = default!; + public TransformComponent Transform { get; private set; } = default!; + + internal CommandBuffer? PostInitCommands { get; private set; } + + internal EntityBuilder(IDependencyCollection collection, EntityUid reservedEntity) + { + _factory = collection.Resolve(); + _protoMan = collection.Resolve(); + _serMan = collection.Resolve(); + _locMan = collection.Resolve(); + _remoteEntMan = collection.Resolve(); + ReservedEntity = reservedEntity; + EntityComponents = new(); + } + + /// + /// Constructs a blank (MetaData & Transform only) entity builder for the given reserved ID. + /// + internal static EntityBuilder BlankEntity( + IDependencyCollection collection, + EntityUid reservedId, + ISerializationContext? context) + { + var self = new EntityBuilder(collection, reservedId); + + self.InitializeMinimalEntity(context); + + return self; + } + + /// + /// Constructs an entity builder for the given prototype and reserved entity ID. + /// + internal static EntityBuilder PrototypedEntity( + IDependencyCollection collection, + EntityUid reservedId, + EntProtoId proto, + ISerializationContext? context) + { + var self = new EntityBuilder(collection, reservedId); + + self.InitializeFromPrototype(proto, context); + + return self; + } + + // wishing we had rust alloc-free lambdas rn. + /// + /// Mutates a given component in place using the provided, ideally static, action. + /// + /// The mutator to run + /// The context object for the action. + /// + /// The concrete type of the component. + /// The builder, for chaining. + /// + /// If you need this often you may be better off making an extension method. + /// + /// + /// + /// // Allocation-free usage of MutateComp. + /// public sealed class MySystem : EntitySystem + /// { + /// public EntityUid TestMethod() + /// { + /// var builder = EntityManager.BlankEntityBuilder() + /// .AddComp<MapGridComponent>() + /// .MutateComp<MySystem, MetaDataComponent>( + /// static (ctx, meta) => ctx.DoThing(meta), + /// this); + /// + /// } + /// + /// public void DoThing(MetaDataComponent meta) + /// { + /// // ... + /// } + /// } + /// + /// + public EntityBuilder MutateComp(Action action, TContext context) + where TComp: IComponent, new() + { + var comp = EntityComponents[typeof(TComp)]; + + action(context, (TComp)comp); + + return this; + } + + /// + /// Attempts to mutate a given component in place using the provided, ideally static, action. + /// If the component doesn't exist, nothing happens. + /// + /// The mutator to run + /// The context object for the action. + /// + /// The concrete type of the component. + /// The builder, for chaining. + /// + /// If you need this often you may be better off making an extension method. + /// + /// + /// + /// // Allocation-free usage of MutateComp. + /// public sealed class MySystem : EntitySystem + /// { + /// public EntityUid TestMethod() + /// { + /// var builder = EntityManager.BlankEntityBuilder() + /// .AddComp<MapGridComponent>() + /// .MutateComp<MySystem, MetaDataComponent>( + /// static (ctx, meta) => ctx.DoThing(meta), + /// this); + /// + /// } + /// + /// public void DoThing(MetaDataComponent meta) + /// { + /// // ... + /// } + /// } + /// + /// + public EntityBuilder TryMutateComp(Action action, TContext context) + where TComp: IComponent, new() + { + if (EntityComponents.TryGetValue(typeof(TComp), out var comp)) + { + action(context, (TComp)comp); + } + + return this; + } + + /// + /// Retrieves the given concretely typed component from the builder. + /// + /// The retrieved component. + /// The type of component to retrieve. + /// The builder, for chaining. + [PreferNonGenericVariantFor(typeof(MetaDataComponent), typeof(TransformComponent))] + public EntityBuilder GetComp(out TComp comp) + where TComp : IComponent, new() + { + comp = (TComp) EntityComponents[typeof(TComp)]; + return this; + } + + /// + /// Retrieves the given component from the builder. + /// + /// The type of component to retrieve. + /// The retrieved component. + /// The builder, for chaining. + [PreferGenericVariant] + public EntityBuilder GetComp(Type t, out IComponent comp) + { + comp = EntityComponents[t]; + return this; + } + + /// + /// Attempts to retrieve the given component from the builder. + /// + /// The component, if any. + /// The type of component to retrieve. + /// Not chainable. + /// Whether the operation succeeded. + [PreferNonGenericVariantFor(typeof(MetaDataComponent), typeof(TransformComponent))] + public bool TryComp([NotNullWhen(true)] out TComp? comp) + where TComp : IComponent, new() + { + if (EntityComponents.TryGetValue(typeof(TComp), out var c)) + { + comp = (TComp)c; + return true; + } + + comp = default; + return false; + } + + /// + /// Attempts to retrieve the given component from the builder. + /// + /// The type of component to retrieve. + /// The component, if any. + /// Not chainable. + /// Whether the operation succeeded. + [PreferGenericVariant] + public bool TryComp(Type t, [NotNullWhen(true)] out IComponent? comp) + { + return EntityComponents.TryGetValue(t, out comp); + } + + /// + /// Ensures the entity is spawned as a transform child of the given parent. + /// + /// The parent to attach to. + /// The parent-relative position to use for coordinates. + /// The parent-relative angle to use. + /// + /// The builder, for chaining. + public EntityBuilder ChildOf(EntityUid parent, Vector2 relativePos = default, Angle? rotation = null) + { + if (MapCoordinates is not null) + MapCoordinates = null; // One or the other. + + Transform._parent = parent; + Transform._localPosition = relativePos; + + if (rotation is not null) + Transform._localRotation = rotation.Value; + + return this; + } + + /// + /// Ensures the entity is spawned as a transform child of the given parent. + /// + /// The relative coordinates to give this entity. + /// The parent-relative angle to use. + /// + /// The builder, for chaining. + public EntityBuilder ChildOf(EntityCoordinates coordinates, Angle? rotation = null) + { + if (MapCoordinates is not null) + MapCoordinates = null; // One or the other. + + Transform._parent = coordinates.EntityId; + Transform._localPosition = coordinates.Position; + + if (rotation is not null) + Transform._localRotation = rotation.Value; + + return this; + } + + /// + /// Ensures the entity is spawned at the given map coordinate, automatically finding a parent. + /// + /// The coordinates to spawn at + /// The map-relative angle to spawn the entity at, if any. + /// + /// The builder, for chaining. + public EntityBuilder LocatedAt(MapCoordinates mapCoordinates, Angle? angle = null) + { + MapCoordinates = mapCoordinates; + + if (Transform._parent != EntityUid.Invalid) + { + Transform._parent = EntityUid.Invalid; + Transform._localPosition = default; + } + + if (angle is not null) + Transform._localRotation = angle.Value; + + return this; + } + + /// + /// Returns a buffer that will be run when the entity has been fully constructed. + /// + /// The command buffer that post init commands should be added to. + /// The builder, for chaining. + /// + /// + /// It is strongly discouraged to use the post init command buffer to construct additional entities, or + /// add additional components. + /// Content systems should support working with entity builders directly when possible as they perform better than + /// adding additional entities and components after-the-fact. + /// + /// + /// The buffer is guaranteed to run only after all entities in a invocation + /// have been started up (and optionally map initialized), and no sooner. + /// The order in which post init buffers run is undefined. + /// + /// + /// This only ever creates one buffer, and upon being called again, will return the same buffer. As adding to + /// command buffers is not threadsafe and should only be done from one thread at a time, if you need multiple + /// please use . + /// + /// + public EntityBuilder WithPostInitCommands(out CommandBuffer buffer) + { + PostInitCommands ??= _remoteEntMan.GetCommandBuffer(); + + buffer = PostInitCommands; + return this; + } +} diff --git a/Robust.Shared/GameObjects/EntityCreationException2.cs b/Robust.Shared/GameObjects/EntityCreationException2.cs new file mode 100644 index 00000000000..3e8aebced20 --- /dev/null +++ b/Robust.Shared/GameObjects/EntityCreationException2.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using Robust.Shared.GameObjects.EntityBuilders; +using Robust.Shared.Prototypes; + +namespace Robust.Shared.GameObjects; + +/// +/// A revised version of with additional metadata. +/// This exception is thrown as part of a by +/// . +/// +public sealed class EntityCreationException2 : Exception +{ + public CreationStep Step { get; } + public EntityBuilder FailureSource { get; } + public EntProtoId? FailurePrototype => FailureSource.MetaData.EntityPrototype?.ID; + + public override IDictionary Data => new Dictionary() + { + { nameof(Step), Step.ToString() }, + { nameof(FailureSource), FailureSource }, + { nameof(FailurePrototype), FailurePrototype }, + }; + + /// + /// A non-exhaustive set of steps for entity creation. + /// You must handle new cases sanely, it is not a breaking change to add new steps. + /// + public enum CreationStep + { + /// + /// Entity allocation and addition. + /// + AllocAdd, + /// + /// Entity ComponentInit. + /// + Initialize, + /// + /// Entity ComponentStartup. + /// + Startup, + /// + /// Entity MapInit. + /// + MapInit, + /// + /// Post-creation command buffer application. + /// + PostInitCommandBuffer, + /// + /// Cleanup after a failure elsewhere in spawning. + /// + FailureCleanup, + } + + public override string Message => + $"The entity {FailureSource} failed to be created during step {Step} due to an inner exception."; + + public EntityCreationException2(Exception? innerException, CreationStep step, EntityBuilder failureSource) : base(null, innerException) + { + Step = step; + FailureSource = failureSource; + } +} diff --git a/Robust.Shared/GameObjects/EntityManager.ApplyCommandBuffer.cs b/Robust.Shared/GameObjects/EntityManager.ApplyCommandBuffer.cs new file mode 100644 index 00000000000..7040c47329a --- /dev/null +++ b/Robust.Shared/GameObjects/EntityManager.ApplyCommandBuffer.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using Robust.Shared.GameObjects.CommandBuffers; +using Robust.Shared.GameObjects.EntityBuilders; + +namespace Robust.Shared.GameObjects; + +public abstract partial class EntityManager +{ + public void ApplyCommandBuffer(CommandBuffer buffer) + { + // Time to do the thing. + var len = buffer.Entries.Count; + var entries = buffer.Entries.Span; + + for (var i = 0; i < len; i++) + { + switch ((CommandBufferEntry.CmdKind)(entries[i].Command & 0xFF)) + { + case CommandBufferEntry.CmdKind.QueuedActionT: + { + entries[i].InvokeQueuedActionT(); + break; + } + case CommandBufferEntry.CmdKind.QueuedActionTEnt: + { + entries[i].InvokeQueuedActionTEnt(); + break; + } + case CommandBufferEntry.CmdKind.SubBuffer: + { + ApplyCommandBuffer((CommandBuffer)entries[i].Field2!); + break; + } + case CommandBufferEntry.CmdKind.DeleteEntity: + { + DeleteEntity(entries[i].TargetEnt); + break; + } + case CommandBufferEntry.CmdKind.SpawnEntity: + { + switch (entries[i].Field2) + { + case EntityBuilder b: + { + Spawn(b); + break; + } + case EntityBuilder[] b: + { + SpawnBulk(b); + break; + } + } + break; + } + case CommandBufferEntry.CmdKind.AddComponents: + { + switch (entries[i].Field2) + { + case IComponent b: + { + AddComponent(entries[i].TargetEnt, b); + break; + } + case IComponent[] b: + { + foreach (var comp in b) + { + AddComponent(entries[i].TargetEnt, comp); + } + break; + } + } + break; + } + case CommandBufferEntry.CmdKind.EnsureComponents: + { + break; + } + case CommandBufferEntry.CmdKind.RemoveComponents: + { + break; + } + case CommandBufferEntry.CmdKind.Invalid: + default: + { + throw new InvalidOperationException("Command buffer reached invalid command during execution."); + } + } + } + } +} diff --git a/Robust.Shared/GameObjects/EntityManager.CommandBuffers.cs b/Robust.Shared/GameObjects/EntityManager.CommandBuffers.cs new file mode 100644 index 00000000000..3fc04cccde4 --- /dev/null +++ b/Robust.Shared/GameObjects/EntityManager.CommandBuffers.cs @@ -0,0 +1,51 @@ +using System; +using Microsoft.Extensions.ObjectPool; +using Robust.Shared.GameObjects.CommandBuffers; +using Robust.Shared.GameObjects.EntityBuilders; +using Robust.Shared.IoC; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.Manager; + +namespace Robust.Shared.GameObjects; + +public abstract partial class EntityManager +{ + private DefaultObjectPool CommandBufferPool { get; set; } = default!; + + DefaultObjectPool IEntityManager.CommandBufferPool => CommandBufferPool; + + public CommandBuffer GetCommandBuffer() + { + return CommandBufferPool.Get(); + } + + public EntityBuilder EntityBuilder(EntProtoId? protoId = null, ISerializationContext? context = null) + { + if (protoId is null) + return EntityBuilders.EntityBuilder.BlankEntity(_dependencyCollection, GenerateEntityUid(), context); + + return EntityBuilders.EntityBuilder.PrototypedEntity(_dependencyCollection, GenerateEntityUid(), protoId.Value, context); + } + + EntityUid IRemoteEntityManager.GetUnusedEntityUid() + { + return GenerateEntityUid(); + } + + private sealed class CommandBufferPolicy(IDependencyCollection collection) : IPooledObjectPolicy + { + public CommandBuffer Create() + { + return new CommandBuffer(collection); + } + + public bool Return(CommandBuffer obj) + { + if (obj.Capacity > 1024) + return false; // Get rid of it, too big (32KiB or larger) + + obj.Clear(); + return true; + } + } +} diff --git a/Robust.Shared/GameObjects/EntityManager.Components.cs b/Robust.Shared/GameObjects/EntityManager.Components.cs index d7308446005..10ae9fbe1f8 100644 --- a/Robust.Shared/GameObjects/EntityManager.Components.cs +++ b/Robust.Shared/GameObjects/EntityManager.Components.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Frozen; using System.Collections.Generic; -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.CompilerServices; @@ -35,6 +34,7 @@ public partial class EntityManager private const int ComponentCollectionCapacity = 1024; private const int EntityCapacity = 1024; private const int NetComponentCapacity = 8; + private static readonly Dictionary EmptyTraitDict = new(); private FrozenDictionary> _entTraitDict = FrozenDictionary>.Empty; @@ -192,14 +192,14 @@ public void AddComponents(EntityUid target, ComponentRegistry registry, bool rem var metadata = MetaQuery.GetComponent(target); - foreach (var (name, entry) in registry) + foreach (var srcComp in registry.Components()) { - var reg = _componentFactory.GetRegistration(name); + var reg = _componentFactory.GetRegistration(srcComp); if (removeExisting) { var comp = _componentFactory.GetComponent(reg); - _serManager.CopyTo(entry.Component, ref comp, notNullableOverride: true); + _serManager.CopyTo(srcComp, ref comp, notNullableOverride: true); AddComponentInternal(target, comp, reg, overwrite: true, metadata: metadata); } else @@ -210,7 +210,7 @@ public void AddComponents(EntityUid target, ComponentRegistry registry, bool rem } var comp = _componentFactory.GetComponent(reg); - _serManager.CopyTo(entry.Component, ref comp, notNullableOverride: true); + _serManager.CopyTo(srcComp, ref comp, notNullableOverride: true); AddComponentInternal(target, comp, reg, overwrite: false, metadata: metadata); } } @@ -231,9 +231,9 @@ public void RemoveComponents(EntityUid target, ComponentRegistry registry) var metadata = MetaQuery.GetComponent(target); - foreach (var entry in registry.Values) + foreach (var entry in registry.Components()) { - RemoveComponent(target, entry.Component.GetType(), metadata); + RemoveComponent(target, entry.GetType(), metadata); } } @@ -1186,20 +1186,6 @@ private T CopyComponentInternal(EntityUid source, EntityUid target, T sourceC return component; } - public EntityQuery GetEntityQuery() where TComp1 : IComponent - { - var comps = _entTraitArray[CompIdx.ArrayIndex()]; - DebugTools.Assert(comps != null, $"Unknown component: {typeof(TComp1).Name}"); - return new EntityQuery(this, comps); - } - - public EntityQuery GetEntityQuery(Type type) - { - var comps = _entTraitDict[type]; - DebugTools.Assert(comps != null, $"Unknown component: {type.Name}"); - return new EntityQuery(this, comps); - } - /// public IEnumerable GetComponents(EntityUid uid) { @@ -1387,6 +1373,7 @@ public ComponentQueryEnumerator ComponentQueryEnumerator(ComponentRegistry regis } /// + [Obsolete($"Use {nameof(ComponentFilterQuery)} instead.")] public CompRegistryEntityEnumerator CompRegistryQueryEnumerator(ComponentRegistry registry) { if (registry.Count == 0) @@ -1765,342 +1752,12 @@ public NetComponentEnumerator(Dictionary dictionary) => } } - /// - /// An index of all entities with a given component, avoiding looking up the component's storage every time. - /// Using these saves on dictionary lookups, making your code slightly more efficient, and ties in nicely with - /// . - /// - /// Any component type. - /// - /// - /// public sealed class MySystem : EntitySystem - /// { - /// private EntityQuery<TransformComponent> _transforms = default!; - ///
- /// public override void Initialize() - /// { - /// _transforms = GetEntityQuery<TransformComponent>(); - /// } - ///
- /// public void DoThings(EntityUid myEnt) - /// { - /// var ent = _transforms.Get(myEnt); - /// // ... - /// } - /// } - ///
- ///
- /// - /// Queries hold references to internals, and are always up to date with the world. - /// They can not however perform mutation, if you need to add or remove components you must use - /// or methods. - /// - /// EntitySystem.GetEntityQuery() - /// EntityManager.GetEntityQuery() - public readonly struct EntityQuery where TComp1 : IComponent - { - private readonly EntityManager _entMan; - private readonly Dictionary _traitDict; - - internal EntityQuery(EntityManager entMan, Dictionary traitDict) - { - _entMan = entMan; - _traitDict = traitDict; - } - - /// - /// Gets for an entity, throwing if it can't find it. - /// - /// The entity to do a lookup for. - /// The located component. - /// Thrown if the entity does not have a component of type . - /// - /// IEntityManager.GetComponent<T>(EntityUid) - /// - /// - /// EntitySystem.Comp<T>(EntityUid) - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - [Pure] - public TComp1 GetComponent(EntityUid uid) - { - if (_traitDict.TryGetValue(uid, out var comp) && !comp.Deleted) - return (TComp1) comp; - - throw new KeyNotFoundException($"Entity {uid} does not have a component of type {typeof(TComp1)}"); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining), Pure] - public Entity Get(EntityUid uid) - { - if (_traitDict.TryGetValue(uid, out var comp) && !comp.Deleted) - return new Entity(uid, (TComp1) comp); - - throw new KeyNotFoundException($"Entity {uid} does not have a component of type {typeof(TComp1)}"); - } - - /// - /// Gets for an entity, if it's present. - /// - /// - /// If it is strictly errorenous for a component to not be present, you may want to use - /// instead. - /// - /// The entity to do a lookup for. - /// The located component, if any. - /// Whether the component was found. - /// - /// IEntityManager.TryGetComponent<T>(EntityUid, out T?) - /// - /// - /// EntitySystem.TryComp<T>(EntityUid, out T?) - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - [Pure] - public bool TryGetComponent([NotNullWhen(true)] EntityUid? uid, [NotNullWhen(true)] out TComp1? component) - { - if (uid == null) - { - component = default; - return false; - } - - return TryGetComponent(uid.Value, out component); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - [Pure] - public bool TryGetComponent(EntityUid uid, [NotNullWhen(true)] out TComp1? component) - { - if (_traitDict.TryGetValue(uid, out var comp) && !comp.Deleted) - { - component = (TComp1) comp; - return true; - } - - component = default; - return false; - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - [Pure] - public bool TryComp(EntityUid uid, [NotNullWhen(true)] out TComp1? component) - => TryGetComponent(uid, out component); - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - [Pure] - public bool TryComp([NotNullWhen(true)] EntityUid? uid, [NotNullWhen(true)] out TComp1? component) - => TryGetComponent(uid, out component); - - /// - /// Tests if the given entity has . - /// - /// The entity to do a lookup for. - /// Whether the component exists for that entity. - /// If you immediately need to then look up that component, it's more efficient to use . - /// - /// IEntityManager.HasComponent<T>(EntityUid) - /// - /// - /// EntitySystem.HasComp<T>(EntityUid) - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - [Pure] - public bool HasComp(EntityUid uid) => HasComponent(uid); - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - [Pure] - public bool HasComp([NotNullWhen(true)] EntityUid? uid) => HasComponent(uid); - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - [Pure] - public bool HasComponent(EntityUid uid) - { - return _traitDict.TryGetValue(uid, out var comp) && !comp.Deleted; - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - [Pure] - public bool HasComponent([NotNullWhen(true)] EntityUid? uid) - { - return uid != null && HasComponent(uid.Value); - } - - /// - /// The entity to do a lookup for. - /// The space to write the component into if found. - /// Whether to log if the component is missing, for diagnostics. - /// Whether the component was found. - /// - /// EntitySystem.Resolve<T>(EntityUid, out T?) - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool Resolve(EntityUid uid, [NotNullWhen(true)] ref TComp1? component, bool logMissing = true) - { - if (component != null) - { - DebugTools.AssertOwner(uid, component); - return true; - } - - if (_traitDict.TryGetValue(uid, out var comp) && !comp.Deleted) - { - component = (TComp1)comp; - return true; - } - - if (logMissing) - _entMan.ResolveSawmill.Error($"Can't resolve \"{typeof(TComp1)}\" on entity {_entMan.ToPrettyString(uid)}!\n{Environment.StackTrace}"); - - return false; - } - - /// - /// The space to write the component into if found. - /// Whether to log if the component is missing, for diagnostics. - /// Whether the component was found. - /// - /// EntitySystem.Resolve<T>(EntityUid, out T?) - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool Resolve(ref Entity entity, bool logMissing = true) - { - return Resolve(entity.Owner, ref entity.Comp, logMissing); - } - - /// - /// Gets for an entity if it's present, or null if it's not. - /// - /// The entity to do the lookup on. - /// The component, if it exists. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - [Pure] - public TComp1? CompOrNull(EntityUid uid) - { - if (TryGetComponent(uid, out var comp)) - return comp; - - return default; - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - [Pure] - public TComp1 Comp(EntityUid uid) - { - return GetComponent(uid); - } - - #region Internal - - /// - /// Elides the component.Deleted check of - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - [Pure] - internal TComp1 GetComponentInternal(EntityUid uid) - { - if (_traitDict.TryGetValue(uid, out var comp)) - return (TComp1) comp; - - throw new KeyNotFoundException($"Entity {uid} does not have a component of type {typeof(TComp1)}"); - } - - /// - /// Elides the component.Deleted check of - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - [Pure] - internal bool TryGetComponentInternal([NotNullWhen(true)] EntityUid? uid, [NotNullWhen(true)] out TComp1? component) - { - if (uid == null) - { - component = default; - return false; - } - - return TryGetComponentInternal(uid.Value, out component); - } - - /// - /// Elides the component.Deleted check of - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - [Pure] - internal bool TryGetComponentInternal(EntityUid uid, [NotNullWhen(true)] out TComp1? component) - { - if (_traitDict.TryGetValue(uid, out var comp)) - { - component = (TComp1) comp; - return true; - } - - component = default; - return false; - } - - /// - /// Elides the component.Deleted check of - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - [Pure] - internal bool HasComponentInternal(EntityUid uid) - { - return _traitDict.TryGetValue(uid, out var comp) && !comp.Deleted; - } - - /// - /// Elides the component.Deleted check of - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - [Pure] - internal bool ResolveInternal(EntityUid uid, [NotNullWhen(true)] ref TComp1? component, bool logMissing = true) - { - if (component != null) - { - DebugTools.AssertOwner(uid, component); - return true; - } - - if (_traitDict.TryGetValue(uid, out var comp)) - { - component = (TComp1)comp; - return true; - } - - if (logMissing) - _entMan.ResolveSawmill.Error($"Can't resolve \"{typeof(TComp1)}\" on entity {_entMan.ToPrettyString(uid)}!\n{new StackTrace(1, true)}"); - - return false; - } - /// - /// Elides the component.Deleted check of - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - [Pure] - internal TComp1? CompOrNullInternal(EntityUid uid) - { - if (TryGetComponent(uid, out var comp)) - return comp; - - return default; - } - - #endregion - } - #region ComponentRegistry Query /// /// Returns entities that match the ComponentRegistry. /// + [Obsolete($"Use {nameof(ComponentFilterQuery)} instead.")] public struct CompRegistryEntityEnumerator : IDisposable { private IEntityManager _entManager; @@ -2170,6 +1827,7 @@ public void Dispose() /// /// Non-generic version of /// + [Obsolete($"Use {nameof(EntityQuery<>)} and non-generic {nameof(IEntityManager.GetEntityQuery)}")] public struct ComponentQueryEnumerator : IDisposable { private Dictionary.Enumerator _traitDict; @@ -2249,6 +1907,7 @@ public void Dispose() /// /// IEntityManager.EntityQueryEnumerator<TComp1, ...>() /// + [Obsolete($"Use {nameof(EntityQuery<>)} and generic {nameof(IEntityManager.GetEntityQuery)}")] public struct EntityQueryEnumerator : IDisposable where TComp1 : IComponent { @@ -2594,6 +2253,7 @@ public void Dispose() /// /// IEntityManager.AllEntityQueryEnumerator<TComp1, ...>() /// + [Obsolete($"Prefer {nameof(IEntityManager.GetEntityQuery)} and dependencies on {nameof(GameObjects.EntityQuery<>)}, using the All property.")] public struct AllEntityQueryEnumerator : IDisposable where TComp1 : IComponent { @@ -2643,6 +2303,7 @@ public void Dispose() } /// + [Obsolete($"Prefer {nameof(IEntityManager.GetEntityQuery)} and dependencies on {nameof(GameObjects.EntityQuery<>)}, using the All property.")] public struct AllEntityQueryEnumerator : IDisposable where TComp1 : IComponent where TComp2 : IComponent @@ -2703,6 +2364,7 @@ public void Dispose() } /// + [Obsolete($"Prefer {nameof(IEntityManager.GetEntityQuery)} and dependencies on {nameof(GameObjects.EntityQuery<>)}, using the All property.")] public struct AllEntityQueryEnumerator : IDisposable where TComp1 : IComponent where TComp2 : IComponent @@ -2777,6 +2439,7 @@ public void Dispose() } /// + [Obsolete($"Prefer {nameof(IEntityManager.GetEntityQuery)} and dependencies on {nameof(GameObjects.EntityQuery<>)}, using the All property.")] public struct AllEntityQueryEnumerator : IDisposable where TComp1 : IComponent where TComp2 : IComponent diff --git a/Robust.Shared/GameObjects/EntityManager.EntityBuilder.cs b/Robust.Shared/GameObjects/EntityManager.EntityBuilder.cs new file mode 100644 index 00000000000..e262dc9ca92 --- /dev/null +++ b/Robust.Shared/GameObjects/EntityManager.EntityBuilder.cs @@ -0,0 +1,241 @@ +using System; +using System.Collections.Generic; +using Robust.Shared.GameObjects.EntityBuilders; + +namespace Robust.Shared.GameObjects; + +public abstract partial class EntityManager +{ + public EntityUid Spawn(EntityBuilder builder, bool? mapInit = null) + { + var ent = builder.ReservedEntity; + // Doesn't allocate. Not that it matters, we're about to allocate a lot. + // Also, the remaining arguments to SpawnBulk don't matter here. + // Code comprehension exercise for the reader to determine why. + SpawnBulk([builder], mapInit); + return ent; + } + + public void SpawnBulk(ReadOnlySpan builders, bool? mapInit = null, bool abortOnAnyFailure = true, bool deleteNonFailingEntities = true) + { + var anyFails = false; + + // Create entities + ComponentAdd + foreach (var builder in builders) + { + try + { + AllocBuilderEntity(builder); + + // Pause inheritance REALLY should not be here, but the old system handled it explicitly too to my understanding. + // Transform recursively inherits paused status... + // TODO: This should really be handled by TransformSystem itself! + if (builder.Transform.ParentUid.IsValid() && MetaQuery.Comp(builder.Transform.ParentUid).EntityPaused) + { + EntitySysManager.GetEntitySystem() + .SetEntityPaused(builder.ReservedEntity, true, builder.MetaData); + } + } + catch (Exception e) + { + DeleteEntity(builder.ReservedEntity, builder.MetaData, builder.Transform); + builder.CreationFailure = new(e, EntityCreationException2.CreationStep.AllocAdd, builder); + anyFails = true; + if (abortOnAnyFailure) + goto handleFailure; + } + } + + // ComponentInit + foreach (var builder in builders) + { + if (builder.CreationFailure is not null) + continue; // Don't go any further. + + // Stuff happens, don't keep going if we don't exist. + if (Deleted(builder.ReservedEntity)) + continue; + + try + { + InitializeEntity(builder.ReservedEntity, builder.MetaData); + } + catch (Exception e) + { + DeleteEntity(builder.ReservedEntity, builder.MetaData, builder.Transform); + builder.CreationFailure = new(e, EntityCreationException2.CreationStep.Initialize, builder);; + anyFails = true; + if (abortOnAnyFailure) + goto handleFailure; + } + } + + // ComponentStartup + foreach (var builder in builders) + { + if (builder.CreationFailure is not null) + continue; // Don't go any further. + + // Stuff happens, don't keep going if we don't exist. + if (Deleted(builder.ReservedEntity)) + continue; + + try + { + StartEntity(builder.ReservedEntity); + } + catch (Exception e) + { + DeleteEntity(builder.ReservedEntity, builder.MetaData, builder.Transform); + builder.CreationFailure = new(e, EntityCreationException2.CreationStep.Startup, builder);; + anyFails = true; + if (abortOnAnyFailure) + goto handleFailure; + } + } + + foreach (var builder in builders) + { + if (builder.CreationFailure is not null) + continue; // Don't go any further. + + // Stuff happens, don't keep going if we don't exist. + if (Deleted(builder.ReservedEntity)) + continue; + + try + { + // MapInit is inherited, if we're on an initialized map we should also map init unless otherwise told. + var doMapInit = mapInit; + // Replicate whatever map we're on if mapInit is null. + doMapInit ??= _mapSystem.IsInitialized(builder.Transform.MapID); + + if (doMapInit.Value) + RunMapInit(builder.ReservedEntity, builder.MetaData); + } + catch (Exception e) + { + DeleteEntity(builder.ReservedEntity, builder.MetaData, builder.Transform); + builder.CreationFailure = new(e, EntityCreationException2.CreationStep.MapInit, builder);; + anyFails = true; + if (abortOnAnyFailure) + goto handleFailure; + } + } + +#if DEBUG + // Prevent people from relying on entity builder command buffer order. + // If you need this reliably, make a single CommandBuffer to run after calling SpawnBulk, or even call SpawnBulk + // with that CommandBuffer to begin with. + var buildersShuffled = builders.ToArray(); + _random.Shuffle(buildersShuffled); +#else + var buildersShuffled = builders; +#endif + + foreach (var builder in buildersShuffled) + { + if (builder.CreationFailure is not null) + continue; // Don't go any further. + + // Stuff happens, don't keep going if we don't exist. + if (Deleted(builder.ReservedEntity)) + continue; + + try + { + if (builder.PostInitCommands is { } commands) + ApplyCommandBuffer(commands); + } + catch (Exception e) + { + DeleteEntity(builder.ReservedEntity, builder.MetaData, builder.Transform); + builder.CreationFailure = new(e, EntityCreationException2.CreationStep.PostInitCommandBuffer, builder);; + anyFails = true; + if (abortOnAnyFailure) + goto handleFailure; + } + } + + if (!anyFails) + return; // We done! + + handleFailure: + + var fails = new List(); + + // We're not done. Gotta aggregate all our failures. + foreach (var builder in builders) + { + if (builder.CreationFailure is not { } exception) + continue; + + fails.Add(exception); + } + + if (deleteNonFailingEntities) + { + foreach (var builder in builders) + { + try + { + if (builder.CreationFailure is not null) + continue; // already dead. + + if (Deleted(builder.ReservedEntity)) + continue; // We killed it already. + + DeleteEntity(builder.ReservedEntity, builder.MetaData, builder.Transform); + } + catch (Exception e) + { + // More?? Oh no. + fails.Add(new (e, EntityCreationException2.CreationStep.FailureCleanup, builder)); + } + } + } + + throw new AggregateException("One or more entities failed to spawn.", fails); + } + + public void SpawnBulkUnordered(Span builders, bool? mapInit = null) + { + // Create an index of entity uids to builders, + var index = new Dictionary(); + + foreach (var builder in builders) + { + index.Add(builder.ReservedEntity, builder); + } + + var keys = new int[builders.Length]; + + // Go through and figure out how "deep" any given entity is in the hierarchy, + // i.e. how many parents it has. + // + // We can then order the builders by depth ascending to get creation order. + for (var i = 0; i < builders.Length; i++) + { + var builder = builders[i]; + var depth = 0; + var curr = builder; + + while (curr.Transform._parent != EntityUid.Invalid) + { + depth += 1; + // If we're also spawning their parent, keep going. + if (index.TryGetValue(curr.Transform._parent, out var parent)) + curr = parent; + else // Otherwise, the entity already exists or is otherwise outside our purview, so move on. + break; + } + + keys[i] = depth; + } + + // Sort ascending. + keys.Sort(builders); + + SpawnBulk(builders); + } +} diff --git a/Robust.Shared/GameObjects/EntityManager.Filters.cs b/Robust.Shared/GameObjects/EntityManager.Filters.cs new file mode 100644 index 00000000000..431a48cfb62 --- /dev/null +++ b/Robust.Shared/GameObjects/EntityManager.Filters.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Generic; +using Robust.Shared.Prototypes; + +namespace Robust.Shared.GameObjects; + +public abstract partial class EntityManager +{ + public bool MatchesFilter(EntityUid ent, ComponentFilter filter) + { + foreach (var comp in filter) + { + if (!HasComponent(ent, comp)) + return false; + } + + return true; + } + + public bool AnyMatchingComponent(EntityUid ent, ComponentFilter filter) + { + foreach (var comp in filter) + { + if (HasComponent(ent, comp)) + return true; + } + + return false; + } + + public bool ExactlyMatchesFilter(EntityUid ent, ComponentFilter filter) + { + foreach (var comp in _entCompIndex[ent]) + { + if (comp.Deleted) + continue; + + if (comp is MetaDataComponent or TransformComponent) + continue; + + if (!filter.Contains(comp.GetType())) + return false; + } + + return true; + } + + public IEnumerable EnumerateFilterMisses(EntityUid ent, ComponentFilter filter) + { + foreach (var comp in filter) + { + if (!HasComponent(ent, comp)) + yield return comp; + } + } + + public IEnumerable EnumerateEntityMisses(EntityUid ent, ComponentFilter filter) + { + foreach (var comp in _entCompIndex[ent]) + { + if (comp.Deleted) + continue; + + var ty = comp.GetType(); + + if (!filter.Contains(ty)) + yield return ty; + } + } + + public IEnumerable EnumerateFilterHits(EntityUid ent, ComponentFilter filter) + { + foreach (var comp in filter) + { + if (HasComponent(ent, comp)) + yield return comp; + } + } + + public void FillMissesFromRegistry(EntityUid ent, ComponentFilter filter, ComponentRegistry registry) + { + var meta = MetaQuery.GetComponent(ent); + + foreach (var comp in filter) + { + if (!HasComponent(ent, comp)) + { + if (!registry.TryGetComponent(_componentFactory, comp, out var toClone)) + { + throw new InvalidOperationException( + $"Tried to fill in a missing component, {comp}, from a registry, but couldn't find it."); + } + + AddComponent(ent, new EntityPrototype.ComponentRegistryEntry(toClone), false, meta); + } + } + } + + public void FillMissesWithNewComponents(EntityUid ent, ComponentFilter filter) + { + var meta = MetaQuery.GetComponent(ent); + + foreach (var comp in filter) + { + if (!HasComponent(ent, comp)) + { + AddComponent(ent, _componentFactory.GetComponent(comp), false, meta); + } + } + } + + public ComponentFilterQuery ComponentFilterQuery(ComponentFilter filter, bool matchPaused = false) + { + var meta = _entTraitDict[typeof(MetaDataComponent)]; + if (filter.Count == 0) + { + // Iterate everything, all entities match. + return new ComponentFilterQuery(meta, meta, [], matchPaused); + } + + var tailCount = filter.Count - 1; + var tails = new Dictionary[tailCount]; + + Dictionary? lead = null; + var tailIdx = 0; + + foreach (var entry in filter) + { + if (lead is null) + lead = _entTraitDict[entry]; + else + { + tails[tailIdx++] = _entTraitDict[entry]; + } + } + + return new ComponentFilterQuery(meta, lead!, tails, matchPaused); + } + + public bool RemoveComponents(EntityUid target, ComponentFilter filter) + { + var didWork = false; + + foreach (var c in filter) + { + didWork |= RemoveComponent(target, c); + } + + return didWork; + } + + public bool RemoveComponentsExact(EntityUid target, ComponentFilter filter) + { + if (!MatchesFilter(target, filter)) + return false; // Do nothing + + return RemoveComponents(target, filter); + } +} diff --git a/Robust.Shared/GameObjects/EntityManager.Queries.cs b/Robust.Shared/GameObjects/EntityManager.Queries.cs new file mode 100644 index 00000000000..64ce17f5972 --- /dev/null +++ b/Robust.Shared/GameObjects/EntityManager.Queries.cs @@ -0,0 +1,77 @@ +using System; +using Robust.Shared.Utility; + +namespace Robust.Shared.GameObjects; + +public partial class EntityManager +{ + public EntityQuery GetEntityQuery() + where TComp1 : IComponent + { + DebugTools.Assert(_entTraitArray.Length > CompIdx.ArrayIndex(), + $"Unknown component: {typeof(TComp1).Name}"); + var comps = _entTraitArray[CompIdx.ArrayIndex()]; + var meta = _entTraitArray[CompIdx.ArrayIndex()]; + + return new EntityQuery(this, comps, meta); + } + + public EntityQuery GetEntityQuery(Type type) + { + DebugTools.Assert(_entTraitDict.ContainsKey(type), $"Unknown component: {type.Name}"); + var comps = _entTraitDict[type]; + var meta = _entTraitArray[CompIdx.ArrayIndex()]; + + return new EntityQuery(this, comps, meta); + } + + public DynamicEntityQuery GetDynamicQuery(params (Type, DynamicEntityQuery.QueryFlags)[] userEntries) + { + var entries = new DynamicEntityQuery.QueryEntry[userEntries.Length]; + + for (var i = 0; i < userEntries.Length; i++) + { + var entry = userEntries[i]; + DebugTools.Assert(_entTraitDict.ContainsKey(entry.Item1), $"Unknown component: {entry.Item1.Name}"); + entries[i] = new(_entTraitDict[entry.Item1], entry.Item2); + } + + return new DynamicEntityQuery(entries, _entTraitArray[CompIdx.ArrayIndex()]); + } + + public EntityQuery GetEntityQuery() + where TComp1 : IComponent + where TComp2 : IComponent + { + var dyQuery = GetDynamicQuery((typeof(TComp1), DynamicEntityQuery.QueryFlags.None), + (typeof(TComp2), DynamicEntityQuery.QueryFlags.None)); + + return new(dyQuery, this); + } + + public EntityQuery GetEntityQuery() + where TComp1 : IComponent + where TComp2 : IComponent + where TComp3 : IComponent + { + var dyQuery = GetDynamicQuery((typeof(TComp1), DynamicEntityQuery.QueryFlags.None), + (typeof(TComp2), DynamicEntityQuery.QueryFlags.None), + (typeof(TComp3), DynamicEntityQuery.QueryFlags.None)); + + return new(dyQuery, this); + } + + public EntityQuery GetEntityQuery() + where TComp1 : IComponent + where TComp2 : IComponent + where TComp3 : IComponent + where TComp4 : IComponent + { + var dyQuery = GetDynamicQuery((typeof(TComp1), DynamicEntityQuery.QueryFlags.None), + (typeof(TComp2), DynamicEntityQuery.QueryFlags.None), + (typeof(TComp3), DynamicEntityQuery.QueryFlags.None), + (typeof(TComp4), DynamicEntityQuery.QueryFlags.None)); + + return new(dyQuery, this); + } +} diff --git a/Robust.Shared/GameObjects/EntityManager.Spawn.cs b/Robust.Shared/GameObjects/EntityManager.Spawn.cs index 94a86489b37..87d0133c5ad 100644 --- a/Robust.Shared/GameObjects/EntityManager.Spawn.cs +++ b/Robust.Shared/GameObjects/EntityManager.Spawn.cs @@ -13,15 +13,14 @@ namespace Robust.Shared.GameObjects; public partial class EntityManager { - // This method will soon(TM) be marked as obsolete. + [Obsolete("Use Spawn() instead, ideally EntityBuilder-based.")] public EntityUid SpawnEntity(string? protoName, EntityCoordinates coordinates, ComponentRegistry? overrides = null) => SpawnAttachedTo(protoName, coordinates, overrides); - // This method will soon(TM) be marked as obsolete. + [Obsolete("Use Spawn() instead, ideally EntityBuilder-based.")] public EntityUid SpawnEntity(string? protoName, MapCoordinates coordinates, ComponentRegistry? overrides = null) => Spawn(protoName, coordinates, overrides); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public EntityUid[] SpawnEntitiesAttachedTo(EntityCoordinates coordinates, params string?[] protoNames) { var ents = new EntityUid[protoNames.Length]; @@ -32,7 +31,6 @@ public EntityUid[] SpawnEntitiesAttachedTo(EntityCoordinates coordinates, params return ents; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public EntityUid[] SpawnEntities(MapCoordinates coordinates, params string?[] protoNames) { var ents = new EntityUid[protoNames.Length]; @@ -63,7 +61,6 @@ public EntityUid[] SpawnEntitiesAttachedTo(EntityCoordinates coordinates, params return ents; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public EntityUid[] SpawnEntitiesAttachedTo(EntityCoordinates coordinates, List protoNames) { var ents = new EntityUid[protoNames.Count]; @@ -74,7 +71,6 @@ public EntityUid[] SpawnEntitiesAttachedTo(EntityCoordinates coordinates, List protoNames) { var ents = new EntityUid[protoNames.Count]; @@ -108,8 +104,7 @@ public virtual EntityUid SpawnAttachedTo(string? protoName, EntityCoordinates co return entity; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public EntityUid Spawn(string? protoName = null, ComponentRegistry? overrides = null, bool doMapInit = true) + public virtual EntityUid Spawn(string? protoName = null, ComponentRegistry? overrides = null, bool doMapInit = true) { var entity = CreateEntityUninitialized(protoName, MapCoordinates.Nullspace, overrides); InitializeAndStartEntity(entity, doMapInit); @@ -123,7 +118,6 @@ public virtual EntityUid Spawn(string? protoName, MapCoordinates coordinates, Co return entity; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public EntityUid SpawnAtPosition(string? protoName, EntityCoordinates coordinates, ComponentRegistry? overrides = null) => Spawn(protoName, _xforms.ToMapCoordinates(coordinates), overrides); diff --git a/Robust.Shared/GameObjects/EntityManager.cs b/Robust.Shared/GameObjects/EntityManager.cs index 7cc13328e05..feb6e4ac0be 100644 --- a/Robust.Shared/GameObjects/EntityManager.cs +++ b/Robust.Shared/GameObjects/EntityManager.cs @@ -4,10 +4,13 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.CompilerServices; +using System.Threading; using Prometheus; using Robust.Shared.Console; using Robust.Shared.Containers; +using Robust.Shared.GameObjects.EntityBuilders; using Robust.Shared.GameStates; +using Robust.Shared.IoC; using Robust.Shared.Log; using Robust.Shared.Map; using Robust.Shared.Map.Components; @@ -17,6 +20,7 @@ using Robust.Shared.Player; using Robust.Shared.Profiling; using Robust.Shared.Prototypes; +using Robust.Shared.Random; using Robust.Shared.Reflection; using Robust.Shared.Serialization.Manager; using Robust.Shared.Serialization.Markdown.Mapping; @@ -34,6 +38,8 @@ public abstract partial class EntityManager : IEntityManager { #region Dependencies + [IoC.Dependency] private readonly IDependencyCollection _dependencyCollection = default!; + [IoC.Dependency] private readonly IRobustRandom _random = default!; [IoC.Dependency] protected readonly IPrototypeManager PrototypeManager = default!; [IoC.Dependency] protected readonly ILogManager LogManager = default!; [IoC.Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; @@ -82,9 +88,9 @@ public abstract partial class EntityManager : IEntityManager internal EntityEventBus EventBusInternal = null!; - protected int NextEntityUid = (int) EntityUid.FirstUid; + protected int NextEntityUid = (int) EntityUid.Invalid; - protected int NextNetworkId = (int) NetEntity.First; + protected int NextNetworkId = (int) NetEntity.Invalid; /// public IEventBus EventBus => EventBusInternal; @@ -141,6 +147,9 @@ public virtual void Initialize() if (Initialized) throw new InvalidOperationException("Initialize() called multiple times"); + // This can store at most 8MiB worth of command buffers. + CommandBufferPool = new(new CommandBufferPolicy(_dependencyCollection), 256); + EventBusInternal = new EntityEventBus(this, _reflection); InitializeComponents(); @@ -348,11 +357,22 @@ public virtual EntityUid CreateEntityUninitialized(string? prototypeName, MapCoo var newEntity = CreateEntity(prototypeName, out _, overrides); var transform = TransformQuery.GetComponent(newEntity); + SetMapCoordinates(coordinates, rotation, newEntity, transform); + + return newEntity; + } + + protected internal void SetMapCoordinates( + MapCoordinates coordinates, + Angle rotation, + EntityUid newEntity, + TransformComponent transform) + { if (coordinates.MapId == MapId.Nullspace) { transform._parent = EntityUid.Invalid; transform.Anchored = false; - return newEntity; + return; } var mapEnt = _mapSystem.GetMap(coordinates.MapId); @@ -373,8 +393,6 @@ public virtual EntityUid CreateEntityUninitialized(string? prototypeName, MapCoo coords = new EntityCoordinates(mapEnt, coordinates.Position); _xforms.SetCoordinates(newEntity, transform, coords, rotation, newParent: mapXform); } - - return newEntity; } /// @@ -949,6 +967,50 @@ private EntityUid AllocEntity(out MetaDataComponent metadata) return uid; } + private void AllocBuilderEntity(EntityBuilder builder) + { + ThreadCheck(); + + var uid = builder.ReservedEntity; + +#if DEBUG + if (EntityExists(uid)) + { + throw new InvalidOperationException($"Entity builder entity already taken: {uid}, did something unusual happen?"); + } +#endif + + // Mark ourselves as having been modified for the first time Right Now. + builder.MetaData.EntityLastModifiedTick = _gameTiming.CurTick; + + var netEntity = GenerateNetEntity(); + SetNetEntity(uid, netEntity, builder.MetaData); + + + EntityAdded?.Invoke((uid, builder.MetaData)); + EventBusInternal.OnEntityAdded(uid); + + Entities.Add(uid); + + // TODO: Clean up xform and metadata logic enough we don't need to special case their addition anymore. + // as builders always have both anyway. + // Add our initial components, starting with the most important two. + AddComponentInternal(uid, builder.MetaData, _metaReg, false, true, builder.MetaData); + AddComponentInternal(uid, builder.Transform, false, true, builder.MetaData); + + // Add the rest of our components. + foreach (var (_, component) in builder.EntityComponents) + { + if (component is MetaDataComponent or TransformComponent) + continue; // Don't add them twice. + + AddComponentInternal(uid, component, false, true, builder.MetaData); + } + + if (builder.MapCoordinates is {} coords) + SetMapCoordinates(coords, builder.Transform._localRotation, uid, builder.Transform); + } + /// /// Allocates an entity and loads components but does not do initialization. /// @@ -1113,13 +1175,13 @@ public virtual void RaisePredictiveEvent(T msg) where T : EntityEventArgs /// internal EntityUid GenerateEntityUid() { - return new EntityUid(NextEntityUid++); + return new EntityUid(Interlocked.Increment(ref NextEntityUid)); } /// /// Generates a unique network id and increments /// - protected virtual NetEntity GenerateNetEntity() => new(NextNetworkId++); + protected virtual NetEntity GenerateNetEntity() => new(Interlocked.Increment(ref NextNetworkId)); [Conditional("DEBUG")] protected void ThreadCheck() diff --git a/Robust.Shared/GameObjects/EntityQuery.cs b/Robust.Shared/GameObjects/EntityQuery.cs new file mode 100644 index 00000000000..9f56124f1d7 --- /dev/null +++ b/Robust.Shared/GameObjects/EntityQuery.cs @@ -0,0 +1,459 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using JetBrains.Annotations; +using Robust.Shared.Utility; + +namespace Robust.Shared.GameObjects; + +/// +/// An index of all entities with a given component, avoiding looking up the component's storage every time. +/// Using these saves on dictionary lookups, making your code slightly more efficient, and ties in nicely with +/// . +/// +/// Any component type. +/// +/// +/// public sealed class MySystem : EntitySystem +/// { +/// [Dependency] private EntityQuery<TransformComponent> _transforms = default!; +///
+/// public void Update(float ft) +/// { +/// foreach (var ent in _transforms) +/// { +/// // iterate matching entities, excluding paused ones. +/// } +/// } +///
+/// public void DoThings(EntityUid myEnt) +/// { +/// var ent = _transforms.Get(myEnt); +/// // ... +/// } +/// } +///
+///
+/// +/// Queries hold references to internals, and are always up to date with the world. +/// They can not however perform mutation, if you need to add or remove components you must use +/// or methods. +/// +/// EntitySystem.GetEntityQuery() +/// EntityManager.GetEntityQuery() +[PublicAPI] +public readonly struct EntityQuery : IEnumerable> + where TComp1 : IComponent +{ + private readonly EntityManager _entMan; + private readonly Dictionary _traitDict; + private readonly Dictionary _metaData; + + private readonly bool _enumeratePaused; + + /// + /// Returns an entity query that will include paused entities when enumerated. + /// + /// + /// You shouldn't cache this, please, there is no way to turn it back into a normal query and it's a shorthand + /// only meant for foreach. + /// + /// + /// + /// public sealed class MySystem : EntitySystem + /// { + /// [Dependency] private EntityQuery<TransformComponent> _transforms = default!; + ///
+ /// public void Update(float ft) + /// { + /// foreach (var ent in _transforms.All) + /// { + /// // iterate matching entities, including paused ones. + /// } + /// } + /// } + ///
+ ///
+ public EntityQuery All => new(this, true); + + internal EntityQuery(EntityManager entMan, Dictionary traitDict, Dictionary metaData) + { + _entMan = entMan; + _traitDict = traitDict; + _metaData = metaData; + _enumeratePaused = false; + } + + /// + /// Internal constructor used for . + /// + private EntityQuery(EntityQuery derived, bool enumeratePaused) + { + _entMan = derived._entMan; + _traitDict = derived._traitDict; + _metaData = derived._metaData; + _enumeratePaused = enumeratePaused; + } + + /// + /// Gets for an entity, throwing if it can't find it. + /// + /// The entity to do a lookup for. + /// The located component. + /// Thrown if the entity does not have a component of type . + /// + /// IEntityManager.GetComponent<T>(EntityUid) + /// + /// + /// EntitySystem.Comp<T>(EntityUid) + /// + [Pure] + public TComp1 GetComponent(EntityUid uid) + { + if (_traitDict.TryGetValue(uid, out var comp) && !comp.Deleted) + return (TComp1) comp; + + throw new KeyNotFoundException($"Entity {uid} does not have a component of type {typeof(TComp1)}"); + } + + /// + [Pure] + public Entity Get(EntityUid uid) + { + if (_traitDict.TryGetValue(uid, out var comp) && !comp.Deleted) + return new Entity(uid, (TComp1) comp); + + throw new KeyNotFoundException($"Entity {uid} does not have a component of type {typeof(TComp1)}"); + } + + /// + /// Gets for an entity, if it's present. + /// + /// + /// If it is strictly errorenous for a component to not be present, you may want to use + /// instead. + /// + /// The entity to do a lookup for. + /// The located component, if any. + /// Whether the component was found. + /// + /// IEntityManager.TryGetComponent<T>(EntityUid, out T?) + /// + /// + /// EntitySystem.TryComp<T>(EntityUid, out T?) + /// + [Pure] + public bool TryGetComponent([NotNullWhen(true)] EntityUid? uid, [NotNullWhen(true)] out TComp1? component) + { + if (uid == null) + { + component = default; + return false; + } + + return TryGetComponent(uid.Value, out component); + } + + /// + [Pure] + public bool TryGetComponent(EntityUid uid, [NotNullWhen(true)] out TComp1? component) + { + if (_traitDict.TryGetValue(uid, out var comp) && !comp.Deleted) + { + component = (TComp1) comp; + return true; + } + + component = default; + return false; + } + + /// + [Pure] + public bool TryComp(EntityUid uid, [NotNullWhen(true)] out TComp1? component) + => TryGetComponent(uid, out component); + + /// + [Pure] + public bool TryComp([NotNullWhen(true)] EntityUid? uid, [NotNullWhen(true)] out TComp1? component) + => TryGetComponent(uid, out component); + + /// + /// Tests if the given entity has . + /// + /// The entity to do a lookup for. + /// Whether the component exists for that entity. + /// If you immediately need to then look up that component, it's more efficient to use . + /// + /// IEntityManager.HasComponent<T>(EntityUid) + /// + /// + /// EntitySystem.HasComp<T>(EntityUid) + /// + [Pure] + public bool HasComp(EntityUid uid) => HasComponent(uid); + + /// + [Pure] + public bool HasComp([NotNullWhen(true)] EntityUid? uid) => HasComponent(uid); + + /// + [Pure] + public bool HasComponent(EntityUid uid) + { + return _traitDict.TryGetValue(uid, out var comp) && !comp.Deleted; + } + + /// + [Pure] + public bool HasComponent([NotNullWhen(true)] EntityUid? uid) + { + return uid != null && HasComponent(uid.Value); + } + + /// + /// The entity to do a lookup for. + /// The space to write the component into if found. + /// Whether to log if the component is missing, for diagnostics. + /// Whether the component was found. + /// + /// EntitySystem.Resolve<T>(EntityUid, out T?) + /// + public bool Resolve(EntityUid uid, [NotNullWhen(true)] ref TComp1? component, bool logMissing = true) + { + if (component != null) + { + DebugTools.AssertOwner(uid, component); + return true; + } + + if (_traitDict.TryGetValue(uid, out var comp) && !comp.Deleted) + { + component = (TComp1)comp; + return true; + } + + if (logMissing) + _entMan.ResolveSawmill.Error($"Can't resolve \"{typeof(TComp1)}\" on entity {_entMan.ToPrettyString(uid)}!\n{Environment.StackTrace}"); + + return false; + } + + /// + /// The space to write the component into if found. + /// Whether to log if the component is missing, for diagnostics. + /// Whether the component was found. + /// + /// EntitySystem.Resolve<T>(EntityUid, out T?) + /// + public bool Resolve(ref Entity entity, bool logMissing = true) + { + return Resolve(entity.Owner, ref entity.Comp, logMissing); + } + + /// + /// Gets for an entity if it's present, or null if it's not. + /// + /// The entity to do the lookup on. + /// The component, if it exists. + [Pure] + public TComp1? CompOrNull(EntityUid uid) + { + if (TryGetComponent(uid, out var comp)) + return comp; + + return default; + } + + /// + [Pure] + public TComp1 Comp(EntityUid uid) + { + return GetComponent(uid); + } + + #region Internal + + /// + /// Elides the component.Deleted check of + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [Pure] + internal TComp1 GetComponentInternal(EntityUid uid) + { + if (_traitDict.TryGetValue(uid, out var comp)) + return (TComp1) comp; + + throw new KeyNotFoundException($"Entity {uid} does not have a component of type {typeof(TComp1)}"); + } + + /// + /// Elides the component.Deleted check of + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [Pure] + internal bool TryGetComponentInternal([NotNullWhen(true)] EntityUid? uid, [NotNullWhen(true)] out TComp1? component) + { + if (uid == null) + { + component = default; + return false; + } + + return TryGetComponentInternal(uid.Value, out component); + } + + /// + /// Elides the component.Deleted check of + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [Pure] + internal bool TryGetComponentInternal(EntityUid uid, [NotNullWhen(true)] out TComp1? component) + { + if (_traitDict.TryGetValue(uid, out var comp)) + { + component = (TComp1) comp; + return true; + } + + component = default; + return false; + } + + /// + /// Elides the component.Deleted check of + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [Pure] + internal bool HasComponentInternal(EntityUid uid) + { + return _traitDict.TryGetValue(uid, out var comp) && !comp.Deleted; + } + + /// + /// Elides the component.Deleted check of + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [Pure] + internal bool ResolveInternal(EntityUid uid, [NotNullWhen(true)] ref TComp1? component, bool logMissing = true) + { + if (component != null) + { + DebugTools.AssertOwner(uid, component); + return true; + } + + if (_traitDict.TryGetValue(uid, out var comp)) + { + component = (TComp1)comp; + return true; + } + + if (logMissing) + _entMan.ResolveSawmill.Error($"Can't resolve \"{typeof(TComp1)}\" on entity {_entMan.ToPrettyString(uid)}!\n{new StackTrace(1, true)}"); + + return false; + } + /// + /// Elides the component.Deleted check of + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [Pure] + internal TComp1? CompOrNullInternal(EntityUid uid) + { + if (TryGetComponent(uid, out var comp)) + return comp; + + return default; + } + + #endregion + + public Enumerator GetEnumerator() + { + return new Enumerator(this); + } + + IEnumerator> IEnumerable>.GetEnumerator() + { + return GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + /// The concrete enumerator for an EntityQuery, to assist the C# compiler in optimization. + /// + public struct Enumerator : IEnumerator> + { + private readonly EntityQuery _query; + private Dictionary.Enumerator _traitDictEnumerator; + + /// + public Entity Current { get; private set; } + + internal Enumerator(EntityQuery query) + { + _query = query; + Reset(); + } + + public bool MoveNext() + { + // Loop until we find something that matches, or run out of entities. + while (true) + { + if (!_traitDictEnumerator.MoveNext()) + return false; + + var (workingEnt, c) = _traitDictEnumerator.Current; + + if (c.Deleted) + continue; + + // REMARK: You might think this would be better as two separate Enumerator implementations, + // but i'm not actually convinced. The memory overhead of one extra ref is small, + // and the branch is guaranteed to be consistent so the CPU will just skip over + // this check every time in the ignore-paused case. + if (!_query._enumeratePaused && ((MetaDataComponent)_query._metaData[workingEnt]).EntityPaused) + continue; + + Current = new(workingEnt, (TComp1)c); + break; + } + + return true; + } + + public void Reset() + { + _traitDictEnumerator = _query._traitDict.GetEnumerator(); + } + + Entity IEnumerator>.Current => Current; + + object IEnumerator.Current => Current; + + public void Dispose() + { + _traitDictEnumerator.Dispose(); + } + } + + // I expect this one in particular to get used a bit more than most so.. optimize it :) + /// + public List> ToList() + { + // Estimate the number of entries first. + var list = new List>(_traitDict.Count); + // Then add to it. Saving some allocs. + list.AddRange(this); + return list; + } +} diff --git a/Robust.Shared/GameObjects/EntityQuery2.cs b/Robust.Shared/GameObjects/EntityQuery2.cs new file mode 100644 index 00000000000..6bae70b0dad --- /dev/null +++ b/Robust.Shared/GameObjects/EntityQuery2.cs @@ -0,0 +1,223 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using JetBrains.Annotations; +using Robust.Shared.Log; +using Robust.Shared.Utility; + +namespace Robust.Shared.GameObjects; + +/// Any component type. +/// Any component type. +/// +[PublicAPI] +public readonly struct EntityQuery : IEnumerable> + where TComp1 : IComponent + where TComp2 : IComponent +{ + /// + /// Our dynamic query. Will always be of form [TComp1, ...] + /// + private readonly DynamicEntityQuery _query; + private readonly EntityManager _entMan; + + private readonly bool _enumeratePaused; + + /// + /// Returns an entity query that will include paused entities when enumerated. + /// + /// + /// You shouldn't cache this, please, there is no way to turn it back into a normal query and it's a shorthand + /// only meant for foreach. + /// + /// + /// + /// public sealed class MySystem : EntitySystem + /// { + /// [Dependency] private EntityQuery<TransformComponent> _transforms = default!; + ///
+ /// public void Update(float ft) + /// { + /// foreach (var ent in _transforms.All) + /// { + /// // iterate matching entities, including paused ones. + /// } + /// } + /// } + ///
+ ///
+ public EntityQuery All => new(this, true); + + internal EntityQuery(DynamicEntityQuery query, EntityManager entMan) + { + DebugTools.AssertEqual(query.OutputCount, 2); + _query = query; + _entMan = entMan; + _enumeratePaused = false; + } + + /// + /// Internal constructor used for . + /// + private EntityQuery(EntityQuery derived, bool enumeratePaused) + { + _query = derived._query; + _entMan = derived._entMan; + _enumeratePaused = enumeratePaused; + } + + /// + /// Tries to query an entity for this query's components. + /// + /// The entity to look up components for. + /// The first component + /// The second component + /// True when all components were found, false otherwise. + public bool TryGet(EntityUid ent, [NotNullWhen(true)] out TComp1? comp1, [NotNullWhen(true)] out TComp2? comp2) + { + var buffer = new ComponentArray(); + + if (_query.TryGet(ent, buffer)) + { + comp1 = (TComp1)buffer[0]!; + comp2 = (TComp2)buffer[1]!; + return true; + } + + comp1 = default; + comp2 = default; + return false; + } + + /// + /// Tries to query an entity for this query's components. + /// + /// The entity to look up components for. + /// The resolved entity. + /// True when all components were found, false otherwise. + public bool TryGet(EntityUid ent, out Entity resolved) + { + if (TryGet(ent, out var c1, out var c2)) + { + resolved = new(ent, c1, c2); + return true; + } + + resolved = default; + return false; + } + + /// + /// Tries to query an entity for this query's components, skipping already-filled entries. + /// + /// The entity to look up components for. + /// The first component + /// The second component + /// Whether to log if the resolve fails. + /// True when all components were found, false otherwise. + public bool Resolve(EntityUid ent, [NotNullWhen(true)] ref TComp1? comp1, [NotNullWhen(true)] ref TComp2? comp2, bool logMissing = true) + { + var buffer = new ComponentArray(); + buffer[0] = comp1; + buffer[1] = comp2; + + if (_query.TryResolve(ent, buffer)) + { + comp1 = (TComp1)buffer[0]!; + comp2 = (TComp2)buffer[1]!; + return true; + } + + if (logMissing) + _entMan.ResolveSawmill.Error($"Can't resolve \"{typeof(TComp1)}\" and \"{typeof(TComp2)}\" on entity {_entMan.ToPrettyString(ent)}!\n{Environment.StackTrace}"); + + return false; + } + + /// + /// Tries to query an entity for this query's components, skipping already-filled entries. + /// + /// The entity to look up components for. + /// Whether to log if the resolve fails. + /// True when all components were found, false otherwise. + public bool Resolve(ref Entity entity, bool logMissing = true) + { + return Resolve(entity.Owner, ref entity.Comp1, ref entity.Comp2, logMissing); + } + + /// + /// Tests if a given entity matches this query (ala ) + /// + /// The entity to try matching against. + /// True if the entity matches this query. + public bool Matches(EntityUid ent) + { + return _query.Matches(ent); + } + + public Enumerator GetEnumerator() + { + return new Enumerator(this); + } + + IEnumerator> IEnumerable>.GetEnumerator() + { + return GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + /// Inline storage for the components we're enumerating. + /// Basically just working around the fact you can't stackalloc the span, error CS0208. + /// + [InlineArray(2)] + private struct ComponentArray + { + public IComponent? Entry; + } + + public struct Enumerator : IEnumerator> + { + private DynamicEntityQuery.Enumerator _enumerator; + public Entity Current { get; private set; } + + internal Enumerator(EntityQuery owner) + { + _enumerator = owner._query.GetEnumerator(!owner._enumeratePaused); + } + + public bool MoveNext() + { + var buffer = new ComponentArray(); + + if (_enumerator.MoveNext(out var ent, buffer!)) + { + Current = new(ent, (TComp1)buffer[0]!, (TComp2)buffer[1]!); + return true; + } + + return false; + } + + public void Reset() + { + _enumerator.Reset(); + } + + Entity IEnumerator>.Current => Current; + + object IEnumerator.Current => Current; + + public void Dispose() + { + // Nothin' + } + } +} diff --git a/Robust.Shared/GameObjects/EntityQuery3.cs b/Robust.Shared/GameObjects/EntityQuery3.cs new file mode 100644 index 00000000000..3e464e375fe --- /dev/null +++ b/Robust.Shared/GameObjects/EntityQuery3.cs @@ -0,0 +1,231 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using JetBrains.Annotations; +using Robust.Shared.Log; +using Robust.Shared.Utility; + +namespace Robust.Shared.GameObjects; + +/// Any component type. +/// Any component type. +/// Any component type. +/// +[PublicAPI] +public readonly struct EntityQuery : IEnumerable> + where TComp1 : IComponent + where TComp2 : IComponent + where TComp3 : IComponent +{ + /// + /// Our dynamic query. Will always be of form [TComp1, ...] + /// + private readonly DynamicEntityQuery _query; + private readonly EntityManager _entMan; + + private readonly bool _enumeratePaused; + + /// + /// Returns an entity query that will include paused entities when enumerated. + /// + /// + /// You shouldn't cache this, please, there is no way to turn it back into a normal query and it's a shorthand + /// only meant for foreach. + /// + /// + /// + /// public sealed class MySystem : EntitySystem + /// { + /// [Dependency] private EntityQuery<TransformComponent> _transforms = default!; + ///
+ /// public void Update(float ft) + /// { + /// foreach (var ent in _transforms.All) + /// { + /// // iterate matching entities, including paused ones. + /// } + /// } + /// } + ///
+ ///
+ public EntityQuery All => new(this, true); + + internal EntityQuery(DynamicEntityQuery query, EntityManager entMan) + { + DebugTools.AssertEqual(query.OutputCount, 3); + _query = query; + _entMan = entMan; + _enumeratePaused = false; + } + + /// + /// Internal constructor used for . + /// + private EntityQuery(EntityQuery derived, bool enumeratePaused) + { + _query = derived._query; + _entMan = derived._entMan; + _enumeratePaused = enumeratePaused; + } + + /// + /// Tries to query an entity for this query's components. + /// + /// The entity to look up components for. + /// The first component + /// The second component + /// The third component + /// True when all components were found, false otherwise. + public bool TryGet(EntityUid ent, [NotNullWhen(true)] out TComp1? comp1, [NotNullWhen(true)] out TComp2? comp2, [NotNullWhen(true)] out TComp3? comp3) + { + var buffer = new ComponentArray(); + + if (_query.TryGet(ent, buffer)) + { + comp1 = (TComp1)buffer[0]!; + comp2 = (TComp2)buffer[1]!; + comp3 = (TComp3)buffer[2]!; + return true; + } + + comp1 = default; + comp2 = default; + comp3 = default; + return false; + } + + /// + /// Tries to query an entity for this query's components. + /// + /// The entity to look up components for. + /// The resolved entity. + /// True when all components were found, false otherwise. + public bool TryGet(EntityUid ent, out Entity resolved) + { + if (TryGet(ent, out var c1, out var c2, out var c3)) + { + resolved = new(ent, c1, c2, c3); + return true; + } + + resolved = default; + return false; + } + + /// + /// Tries to query an entity for this query's components, skipping already-filled entries. + /// + /// The entity to look up components for. + /// The first component. + /// The second component. + /// The third component. + /// Whether to log if the resolve fails. + /// True when all components were found, false otherwise. + public bool Resolve(EntityUid ent, [NotNullWhen(true)] ref TComp1? comp1, [NotNullWhen(true)] ref TComp2? comp2, [NotNullWhen(true)] ref TComp3? comp3, bool logMissing = true) + { + var buffer = new ComponentArray(); + buffer[0] = comp1; + buffer[1] = comp2; + buffer[2] = comp3; + + if (_query.TryResolve(ent, buffer)) + { + comp1 = (TComp1)buffer[0]!; + comp2 = (TComp2)buffer[1]!; + comp3 = (TComp3)buffer[2]!; + return true; + } + + if (logMissing) + _entMan.ResolveSawmill.Error($"Can't resolve \"{typeof(TComp1)}\", \"{typeof(TComp2)}\", and \"{typeof(TComp3)}\" on entity {_entMan.ToPrettyString(ent)}!\n{Environment.StackTrace}"); + + return false; + } + + /// + /// Tries to query an entity for this query's components, skipping already-filled entries. + /// + /// The entity to look up components for. + /// Whether to log if the resolve fails. + /// True when all components were found, false otherwise. + public bool Resolve(ref Entity entity, bool logMissing = true) + { + return Resolve(entity.Owner, ref entity.Comp1, ref entity.Comp2, ref entity.Comp3, logMissing); + } + + /// + /// Tests if a given entity matches this query (ala ) + /// + /// The entity to try matching against. + /// True if the entity matches this query. + public bool Matches(EntityUid ent) + { + return _query.Matches(ent); + } + + public Enumerator GetEnumerator() + { + return new Enumerator(this); + } + + IEnumerator> IEnumerable>.GetEnumerator() + { + return GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + /// Inline storage for the components we're enumerating. + /// Basically just working around the fact you can't stackalloc the span, error CS0208. + /// + [InlineArray(3)] + private struct ComponentArray + { + public IComponent? Entry; + } + + public struct Enumerator : IEnumerator> + { + private DynamicEntityQuery.Enumerator _enumerator; + public Entity Current { get; private set; } + + internal Enumerator(EntityQuery owner) + { + _enumerator = owner._query.GetEnumerator(!owner._enumeratePaused); + } + + public bool MoveNext() + { + var buffer = new ComponentArray(); + + if (_enumerator.MoveNext(out var ent, buffer!)) + { + Current = new(ent, (TComp1)buffer[0]!, (TComp2)buffer[1]!, (TComp3)buffer[2]!); + return true; + } + + return false; + } + + public void Reset() + { + _enumerator.Reset(); + } + + Entity IEnumerator>.Current => Current; + + object IEnumerator.Current => Current; + + public void Dispose() + { + // Nothin' + } + } +} diff --git a/Robust.Shared/GameObjects/EntityQuery4.cs b/Robust.Shared/GameObjects/EntityQuery4.cs new file mode 100644 index 00000000000..1834c276ab7 --- /dev/null +++ b/Robust.Shared/GameObjects/EntityQuery4.cs @@ -0,0 +1,237 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using JetBrains.Annotations; +using Robust.Shared.Log; +using Robust.Shared.Utility; + +namespace Robust.Shared.GameObjects; + +/// Any component type. +/// Any component type. +/// Any component type. +/// Any component type. +/// +[PublicAPI] +public readonly struct EntityQuery : IEnumerable> + where TComp1 : IComponent + where TComp2 : IComponent + where TComp3 : IComponent + where TComp4 : IComponent +{ + /// + /// Our dynamic query. Will always be of form [TComp1, ...] + /// + private readonly DynamicEntityQuery _query; + private readonly EntityManager _entMan; + + private readonly bool _enumeratePaused; + + /// + /// Returns an entity query that will include paused entities when enumerated. + /// + /// + /// You shouldn't cache this, please, there is no way to turn it back into a normal query and it's a shorthand + /// only meant for foreach. + /// + /// + /// + /// public sealed class MySystem : EntitySystem + /// { + /// [Dependency] private EntityQuery<TransformComponent> _transforms = default!; + ///
+ /// public void Update(float ft) + /// { + /// foreach (var ent in _transforms.All) + /// { + /// // iterate matching entities, including paused ones. + /// } + /// } + /// } + ///
+ ///
+ public EntityQuery All => new(this, true); + + internal EntityQuery(DynamicEntityQuery query, EntityManager entMan) + { + DebugTools.AssertEqual(query.OutputCount, 4); + _query = query; + _entMan = entMan; + _enumeratePaused = false; + } + + /// + /// Internal constructor used for . + /// + private EntityQuery(EntityQuery derived, bool enumeratePaused) + { + _query = derived._query; + _entMan = derived._entMan; + _enumeratePaused = enumeratePaused; + } + + /// + /// Tries to query an entity for this query's components. + /// + /// The entity to look up components for. + /// The first component + /// The second component + /// The third component + /// True when all components were found, false otherwise. + public bool TryGet(EntityUid ent, [NotNullWhen(true)] out TComp1? comp1, [NotNullWhen(true)] out TComp2? comp2, [NotNullWhen(true)] out TComp3? comp3, [NotNullWhen(true)] out TComp4? comp4) + { + var buffer = new ComponentArray(); + + if (_query.TryGet(ent, buffer)) + { + comp1 = (TComp1)buffer[0]!; + comp2 = (TComp2)buffer[1]!; + comp3 = (TComp3)buffer[2]!; + comp4 = (TComp4)buffer[3]!; + return true; + } + + comp1 = default; + comp2 = default; + comp3 = default; + comp4 = default; + return false; + } + + /// + /// Tries to query an entity for this query's components. + /// + /// The entity to look up components for. + /// The resolved entity. + /// True when all components were found, false otherwise. + public bool TryGet(EntityUid ent, out Entity resolved) + { + if (TryGet(ent, out var c1, out var c2, out var c3, out var c4)) + { + resolved = new(ent, c1, c2, c3, c4); + return true; + } + + resolved = default; + return false; + } + + /// + /// Tries to query an entity for this query's components, skipping already-filled entries. + /// + /// The entity to look up components for. + /// The first component. + /// The second component. + /// The third component. + /// Whether to log if the resolve fails. + /// True when all components were found, false otherwise. + public bool Resolve(EntityUid ent, [NotNullWhen(true)] ref TComp1? comp1, [NotNullWhen(true)] ref TComp2? comp2, [NotNullWhen(true)] ref TComp3? comp3, [NotNullWhen(true)] ref TComp4? comp4, bool logMissing = true) + { + var buffer = new ComponentArray(); + buffer[0] = comp1; + buffer[1] = comp2; + buffer[2] = comp3; + buffer[3] = comp4; + + if (_query.TryResolve(ent, buffer)) + { + comp1 = (TComp1)buffer[0]!; + comp2 = (TComp2)buffer[1]!; + comp3 = (TComp3)buffer[2]!; + comp4 = (TComp4)buffer[3]!; + return true; + } + + if (logMissing) + _entMan.ResolveSawmill.Error($"Can't resolve \"{typeof(TComp1)}\", \"{typeof(TComp2)}\", \"{typeof(TComp3)}\" and \"{typeof(TComp4)}\" on entity {_entMan.ToPrettyString(ent)}!\n{Environment.StackTrace}"); + + return false; + } + + /// + /// Tries to query an entity for this query's components, skipping already-filled entries. + /// + /// The entity to look up components for. + /// Whether to log if the resolve fails. + /// True when all components were found, false otherwise. + public bool Resolve(ref Entity entity, bool logMissing = true) + { + return Resolve(entity.Owner, ref entity.Comp1, ref entity.Comp2, ref entity.Comp3, ref entity.Comp4, logMissing); + } + + /// + /// Tests if a given entity matches this query (ala ) + /// + /// The entity to try matching against. + /// True if the entity matches this query. + public bool Matches(EntityUid ent) + { + return _query.Matches(ent); + } + + public Enumerator GetEnumerator() + { + return new Enumerator(this); + } + + IEnumerator> IEnumerable>.GetEnumerator() + { + return GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + /// Inline storage for the components we're enumerating. + /// Basically just working around the fact you can't stackalloc the span, error CS0208. + /// + [InlineArray(4)] + private struct ComponentArray + { + public IComponent? Entry; + } + + public struct Enumerator : IEnumerator> + { + private DynamicEntityQuery.Enumerator _enumerator; + public Entity Current { get; private set; } + + internal Enumerator(EntityQuery owner) + { + _enumerator = owner._query.GetEnumerator(!owner._enumeratePaused); + } + + public bool MoveNext() + { + var buffer = new ComponentArray(); + + if (_enumerator.MoveNext(out var ent, buffer!)) + { + Current = new(ent, (TComp1)buffer[0]!, (TComp2)buffer[1]!, (TComp3)buffer[2]!, (TComp4)buffer[3]!); + return true; + } + + return false; + } + + public void Reset() + { + _enumerator.Reset(); + } + + Entity IEnumerator>.Current => Current; + + object IEnumerator.Current => Current; + + public void Dispose() + { + // Nothin' + } + } +} diff --git a/Robust.Shared/GameObjects/EntitySystem.Proxy.Queries.cs b/Robust.Shared/GameObjects/EntitySystem.Proxy.Queries.cs new file mode 100644 index 00000000000..2886b43226d --- /dev/null +++ b/Robust.Shared/GameObjects/EntitySystem.Proxy.Queries.cs @@ -0,0 +1,57 @@ +using System; +using JetBrains.Annotations; + +namespace Robust.Shared.GameObjects; + +public partial class EntitySystem +{ + /// + [ProxyFor(typeof(EntityManager))] + [Pure] + protected DynamicEntityQuery GetDynamicQuery(params (Type, DynamicEntityQuery.QueryFlags)[] userEntries) + { + return EntityManager.GetDynamicQuery(userEntries); + } + + /// + [ProxyFor(typeof(EntityManager))] + [Pure] + protected EntityQuery GetEntityQuery() + where TComp1 : IComponent + { + return EntityManager.GetEntityQuery(); + } + + /// + [ProxyFor(typeof(EntityManager))] + [Pure] + protected EntityQuery GetEntityQuery() + where TComp1 : IComponent + where TComp2 : IComponent + { + return EntityManager.GetEntityQuery(); + } + + /// + [ProxyFor(typeof(EntityManager))] + [Pure] + protected EntityQuery GetEntityQuery() + where TComp1 : IComponent + where TComp2 : IComponent + where TComp3 : IComponent + { + return EntityManager.GetEntityQuery(); + } + + /// + [ProxyFor(typeof(EntityManager))] + [Pure] + protected EntityQuery GetEntityQuery() + where TComp1 : IComponent + where TComp2 : IComponent + where TComp3 : IComponent + where TComp4 : IComponent + { + return EntityManager.GetEntityQuery(); + } +} diff --git a/Robust.Shared/GameObjects/EntitySystem.Proxy.cs b/Robust.Shared/GameObjects/EntitySystem.Proxy.cs index 06b852ceaf5..1c06c1729c3 100644 --- a/Robust.Shared/GameObjects/EntitySystem.Proxy.cs +++ b/Robust.Shared/GameObjects/EntitySystem.Proxy.cs @@ -1080,15 +1080,15 @@ private static KeyNotFoundException CompNotFound(EntityUid uid) #region All Entity Query - [MethodImpl(MethodImplOptions.AggressiveInlining)] [ProxyFor(typeof(EntityManager), nameof(EntityManager.AllEntityQueryEnumerator))] + [Obsolete($"Prefer {nameof(IEntityManager.GetEntityQuery)} and dependencies on {nameof(GameObjects.EntityQuery<>)}, using the All property.")] protected AllEntityQueryEnumerator AllEntityQuery() where TComp1 : IComponent { return EntityManager.AllEntityQueryEnumerator(); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] [ProxyFor(typeof(EntityManager), nameof(EntityManager.AllEntityQueryEnumerator))] + [Obsolete($"Prefer {nameof(IEntityManager.GetEntityQuery)} and dependencies on {nameof(GameObjects.EntityQuery<>)}, using the All property.")] protected AllEntityQueryEnumerator AllEntityQuery() where TComp1 : IComponent where TComp2 : IComponent @@ -1096,8 +1096,8 @@ protected AllEntityQueryEnumerator AllEntityQuery(); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] [ProxyFor(typeof(EntityManager), nameof(EntityManager.AllEntityQueryEnumerator))] + [Obsolete($"Prefer {nameof(IEntityManager.GetEntityQuery)} and dependencies on {nameof(GameObjects.EntityQuery<>)}, using the All property.")] protected AllEntityQueryEnumerator AllEntityQuery() where TComp1 : IComponent where TComp2 : IComponent @@ -1106,8 +1106,8 @@ protected AllEntityQueryEnumerator AllEntityQuery(); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] [ProxyFor(typeof(EntityManager), nameof(EntityManager.AllEntityQueryEnumerator))] + [Obsolete($"Prefer {nameof(IEntityManager.GetEntityQuery)} and dependencies on {nameof(GameObjects.EntityQuery<>)}, using the All property.")] protected AllEntityQueryEnumerator AllEntityQuery() where TComp1 : IComponent where TComp2 : IComponent @@ -1121,15 +1121,15 @@ protected AllEntityQueryEnumerator AllEntityQuer #region Get Entity Query - [MethodImpl(MethodImplOptions.AggressiveInlining)] [ProxyFor(typeof(EntityManager))] + [Obsolete($"Prefer {nameof(GetEntityQuery)} and dependencies on {nameof(GameObjects.EntityQuery<>)}")] protected EntityQueryEnumerator EntityQueryEnumerator() where TComp1 : IComponent { return EntityManager.EntityQueryEnumerator(); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] [ProxyFor(typeof(EntityManager))] + [Obsolete($"Prefer {nameof(GetEntityQuery)} and dependencies on {nameof(GameObjects.EntityQuery<>)}")] protected EntityQueryEnumerator EntityQueryEnumerator() where TComp1 : IComponent where TComp2 : IComponent @@ -1137,8 +1137,8 @@ protected EntityQueryEnumerator EntityQueryEnumerator(); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] [ProxyFor(typeof(EntityManager))] + [Obsolete($"Prefer {nameof(GetEntityQuery)} and dependencies on {nameof(GameObjects.EntityQuery<>)}")] protected EntityQueryEnumerator EntityQueryEnumerator() where TComp1 : IComponent where TComp2 : IComponent @@ -1147,8 +1147,8 @@ protected EntityQueryEnumerator EntityQueryEnumerator(); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] [ProxyFor(typeof(EntityManager))] + [Obsolete($"Prefer {nameof(GetEntityQuery)} and dependencies on {nameof(GameObjects.EntityQuery<>)}")] protected EntityQueryEnumerator EntityQueryEnumerator() where TComp1 : IComponent where TComp2 : IComponent @@ -1161,23 +1161,11 @@ protected EntityQueryEnumerator EntityQueryEnume #endregion #region Entity Query - - /// - /// If you need the EntityUid, use - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - [ProxyFor(typeof(EntityManager))] - [Pure] - protected EntityQuery GetEntityQuery() where T : IComponent - { - return EntityManager.GetEntityQuery(); - } - /// /// If you need the EntityUid, use /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] [ProxyFor(typeof(EntityManager))] + [Obsolete($"Prefer {nameof(GetEntityQuery)} and dependencies on {nameof(GameObjects.EntityQuery<>)}")] protected IEnumerable EntityQuery(bool includePaused = false) where TComp1 : IComponent { return EntityManager.EntityQuery(includePaused); @@ -1186,8 +1174,8 @@ protected IEnumerable EntityQuery(bool includePaused = false) wh /// /// If you need the EntityUid, use /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] [ProxyFor(typeof(EntityManager))] + [Obsolete($"Prefer {nameof(GetEntityQuery)} and dependencies on {nameof(GameObjects.EntityQuery<>)}")] protected IEnumerable<(TComp1, TComp2)> EntityQuery(bool includePaused = false) where TComp1 : IComponent where TComp2 : IComponent @@ -1198,8 +1186,8 @@ protected IEnumerable EntityQuery(bool includePaused = false) wh /// /// If you need the EntityUid, use /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] [ProxyFor(typeof(EntityManager))] + [Obsolete($"Prefer {nameof(GetEntityQuery)} and dependencies on {nameof(GameObjects.EntityQuery<>)}")] protected IEnumerable<(TComp1, TComp2, TComp3)> EntityQuery(bool includePaused = false) where TComp1 : IComponent where TComp2 : IComponent @@ -1211,8 +1199,8 @@ protected IEnumerable EntityQuery(bool includePaused = false) wh /// /// If you need the EntityUid, use /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] [ProxyFor(typeof(EntityManager))] + [Obsolete($"Prefer {nameof(GetEntityQuery)} and dependencies on {nameof(GameObjects.EntityQuery<>)}")] protected IEnumerable<(TComp1, TComp2, TComp3, TComp4)> EntityQuery(bool includePaused = false) where TComp1 : IComponent where TComp2 : IComponent @@ -1783,4 +1771,71 @@ protected NetCoordinates[] GetNetCoordinatesArray(EntityCoordinates[] entities) } #endregion + + #region Filters + /// + [ProxyFor(typeof(EntityManager))] + public bool MatchesFilter(EntityUid ent, ComponentFilter filter) + { + return EntityManager.MatchesFilter(ent, filter); + } + + /// + [ProxyFor(typeof(EntityManager))] + public bool AnyMatchingComponent(EntityUid ent, ComponentFilter filter) + { + return EntityManager.AnyMatchingComponent(ent, filter); + } + + + /// + [ProxyFor(typeof(EntityManager))] + public bool ExactlyMatchesFilter(EntityUid ent, ComponentFilter filter) + { + return EntityManager.ExactlyMatchesFilter(ent, filter); + } + + /// + [ProxyFor(typeof(EntityManager))] + public IEnumerable EnumerateFilterMisses(EntityUid ent, ComponentFilter filter) + { + return EntityManager.EnumerateFilterMisses(ent, filter); + } + + /// + [ProxyFor(typeof(EntityManager))] + public IEnumerable EnumerateEntityMisses(EntityUid ent, ComponentFilter filter) + { + return EntityManager.EnumerateEntityMisses(ent, filter); + } + + /// + [ProxyFor(typeof(EntityManager))] + public IEnumerable EnumerateFilterHits(EntityUid ent, ComponentFilter filter) + { + return EntityManager.EnumerateFilterHits(ent, filter); + } + + /// + [ProxyFor(typeof(EntityManager))] + public void FillMissesFromRegistry(EntityUid ent, ComponentFilter filter, ComponentRegistry registry) + { + EntityManager.FillMissesFromRegistry(ent, filter, registry); + } + + /// + [ProxyFor(typeof(EntityManager))] + public void FillMissesWithNewComponents(EntityUid ent, ComponentFilter filter) + { + EntityManager.FillMissesWithNewComponents(ent, filter); + } + + /// + [ProxyFor(typeof(EntityManager))] + public ComponentFilterQuery ComponentFilterQuery(ComponentFilter filter, bool matchPaused = false) + { + return EntityManager.ComponentFilterQuery(filter, matchPaused); + } + + #endregion } diff --git a/Robust.Shared/GameObjects/EntitySystem.Resolve.cs b/Robust.Shared/GameObjects/EntitySystem.Resolve.cs index 589ae44037a..843bdc89ea8 100644 --- a/Robust.Shared/GameObjects/EntitySystem.Resolve.cs +++ b/Robust.Shared/GameObjects/EntitySystem.Resolve.cs @@ -1,5 +1,4 @@ using System; -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using Robust.Shared.Utility; @@ -22,6 +21,9 @@ protected bool Resolve(EntityUid uid, [NotNullWhen(true)] ref TComp? comp { DebugTools.AssertOwner(uid, component); + if (component?.LifeStage == ComponentLifeStage.PreAdd) + Log.Info($"Tried to resolve for {uid} using a component of type {typeof(TComp)}, which was not initialized to begin with. Do not pass uninitialized components to resolve, and do not bundle uninitialized components with entities. This will be a warning in the future."); + if (component != null && !component.Deleted) return true; diff --git a/Robust.Shared/GameObjects/EntitySystemManager.cs b/Robust.Shared/GameObjects/EntitySystemManager.cs index 3a4d3eddb0b..ee42375e3c6 100644 --- a/Robust.Shared/GameObjects/EntitySystemManager.cs +++ b/Robust.Shared/GameObjects/EntitySystemManager.cs @@ -201,6 +201,39 @@ public void Initialize(bool discover = true) .Invoke(dep.Resolve(), null)! ); + var queryMethod2 = typeof(EntityManager).GetMethod(nameof(EntityManager.GetEntityQuery), 2, [])!; + SystemDependencyCollection.RegisterBaseGenericLazy( + typeof(EntityQuery<,>), + (queryType, dep) => + { + var args = queryType.GetGenericArguments(); + return queryMethod2 + .MakeGenericMethod(args[0], args[1]) + .Invoke(dep.Resolve(), null)!; + }); + + var queryMethod3 = typeof(EntityManager).GetMethod(nameof(EntityManager.GetEntityQuery), 3, [])!; + SystemDependencyCollection.RegisterBaseGenericLazy( + typeof(EntityQuery<,,>), + (queryType, dep) => + { + var args = queryType.GetGenericArguments(); + return queryMethod3 + .MakeGenericMethod(args[0], args[1], args[2]) + .Invoke(dep.Resolve(), null)!; + }); + + var queryMethod4 = typeof(EntityManager).GetMethod(nameof(EntityManager.GetEntityQuery), 4, [])!; + SystemDependencyCollection.RegisterBaseGenericLazy( + typeof(EntityQuery<,,,>), + (queryType, dep) => + { + var args = queryType.GetGenericArguments(); + return queryMethod4 + .MakeGenericMethod(args[0], args[1], args[2], args[3]) + .Invoke(dep.Resolve(), null)!; + }); + SystemDependencyCollection.BuildGraph(); foreach (var systemType in _systemTypes) diff --git a/Robust.Shared/GameObjects/IEntityLoadContext.cs b/Robust.Shared/GameObjects/IEntityLoadContext.cs index 6b174d98381..33d7948b6dc 100644 --- a/Robust.Shared/GameObjects/IEntityLoadContext.cs +++ b/Robust.Shared/GameObjects/IEntityLoadContext.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; @@ -13,16 +14,34 @@ internal interface IEntityLoadContext /// Found component or null. /// True if the component was found, false otherwise. /// + [Obsolete("The IComponentFactory receiving method must be used.")] bool TryGetComponent(string componentName, [NotNullWhen(true)] out IComponent? component); + /// Tries getting the data of the given component. + /// The global component factory. + /// Name of component to find. + /// Found component or null. + /// True if the component was found, false otherwise. + /// + bool TryGetComponent(IComponentFactory factory, string componentName, [NotNullWhen(true)] out IComponent? component); + + /// Tries getting the data of the given component. + /// The global component factory. + /// Type of component to find. + /// Found component or null. + /// True if the component was found, false otherwise. + /// + bool TryGetComponent(IComponentFactory factory, Type componentType, [NotNullWhen(true)] out IComponent? component); + + /// Tries getting the data of the given component. /// Type of component to be found. - /// Component factory required for the lookup. + /// Component factory required for the lookup. /// Found component or null. /// True if the component was found, false otherwise. /// bool TryGetComponent( - IComponentFactory componentFactory, + IComponentFactory factory, [NotNullWhen(true)] out TComponent? component ) where TComponent : class, IComponent, new(); @@ -35,6 +54,7 @@ bool TryGetComponent( /// Checks whether a given component should be added to an entity. /// Used to prevent certain prototype components from being added while spawning an entity. /// + [Obsolete("Not properly implemented, functional API for at request.")] bool ShouldSkipComponent(string compName); } } diff --git a/Robust.Shared/GameObjects/IEntityManager.CommandBuffers.cs b/Robust.Shared/GameObjects/IEntityManager.CommandBuffers.cs new file mode 100644 index 00000000000..5dc691599cd --- /dev/null +++ b/Robust.Shared/GameObjects/IEntityManager.CommandBuffers.cs @@ -0,0 +1,70 @@ +using System; +using Microsoft.Extensions.ObjectPool; +using Robust.Shared.GameObjects.CommandBuffers; +using Robust.Shared.GameObjects.EntityBuilders; + +namespace Robust.Shared.GameObjects; + +public partial interface IEntityManager +{ + /// + /// The object pool containing unused command buffers. + /// + internal DefaultObjectPool CommandBufferPool { get; } + + /// + /// Applies a command buffer to the simulation, running all queued commands. + /// + /// The command buffer to apply to sim. + public void ApplyCommandBuffer(CommandBuffer buffer); + + /// + /// Applies an entity builder to the simulation, spawning the entity it describes. + /// + /// The entity builder to spawn into the world. + /// Whether map init should be run for the built entities, or automatically inferred if null. + /// The constructed entity. + public EntityUid Spawn(EntityBuilder builder, bool? mapInit = null); + + /// + /// Spawns the provided set of entity builders, in a manner much like loading a map does (with initialization + /// occuring in stages.) + /// + /// + /// If you want to create your own map loading logic, this is pretty much how. + /// Entities given to this should be ordered such that transform parents initialize before their children do, + /// this limitation may be lifted in the future. + /// + /// The entity builders to spawn into the world. + /// Whether map init should be run for the built entities, or automatically inferred if null. + /// Whether + /// Whether to delete all spawned entities if any fail to spawn. + /// + public void SpawnBulk(ReadOnlySpan builders, bool? mapInit = null, bool abortOnAnyFailure = true, bool deleteNonFailingEntities = true); + + /// + /// Spawns the provided set of entity builders, in a manner much like loading a map does (with initialization + /// occuring in stages.) + /// + /// + /// + /// This accepts an unordered set of entities, and will rewrite the given span in place to be ordered by depth + /// in the hierarchy. + /// + /// + /// This only accounts for engine-imposed entity order constraints, if you need more complex behavior then + /// using and your own sorting is better. + /// + /// + /// This has to sort the entire input list for you, if possible (i.e. your input is already hierarchically ordered) + /// use instead. + /// + /// + /// This will hang if you give it a looping hierarchy! Do not create looping entity hierarchies. + /// + /// + /// The entity builders to spawn into the world. + /// Whether map init should be run for the built entities. + /// + public void SpawnBulkUnordered(Span builders, bool? mapInit = null); +} diff --git a/Robust.Shared/GameObjects/IEntityManager.Components.cs b/Robust.Shared/GameObjects/IEntityManager.Components.cs index 3b71a269df8..e00fd491e49 100644 --- a/Robust.Shared/GameObjects/IEntityManager.Components.cs +++ b/Robust.Shared/GameObjects/IEntityManager.Components.cs @@ -45,23 +45,52 @@ public partial interface IEntityManager /// /// Adds the specified components from the /// + [Obsolete("Use the ComponentRegistry variant instead. Merging prototypes at runtime like this is unsupported.")] void AddComponents(EntityUid target, EntityPrototype prototype, bool removeExisting = true); /// - /// Adds the specified registry components to the target entity. + /// Adds the specified registry components to the target entity. /// + /// The target entity to add to. + /// The registry to copy components from. + /// Whether to remove and replace any components the registry contains. + /// + /// The provided registry must not contain MetaData or Transform components. + /// + /// + /// void AddComponents(EntityUid target, ComponentRegistry registry, bool removeExisting = true); /// /// Removes the specified entity prototype components from the target entity. /// + [Obsolete("Use the ComponentFilter variant instead. Using EntityPrototypes as a mask is wholly unsupported.")] void RemoveComponents(EntityUid target, EntityPrototype prototype); /// /// Removes the specified registry components from the target entity. /// + [Obsolete("Use the ComponentFilter variant instead, registries are not masks.")] void RemoveComponents(EntityUid target, ComponentRegistry registry); + /// + /// Removes all components matching a given filter from the target entity. + /// This will ignore components the entity doesn't have. + /// + /// The target entity to remove from. + /// A filter to use as a mask. + /// True if any components are removed, false if nothing happened. + bool RemoveComponents(EntityUid target, ComponentFilter filter); + + /// + /// Removes all components matching a given filter from the target entity, if and only if + /// the entity actually has every component. + /// + /// The target entity to remove from. + /// A filter to use as a mask. + /// True if all components are removed, false if nothing happened. + bool RemoveComponentsExact(EntityUid target, ComponentFilter filter); + /// /// Adds a Component type to an entity. If the entity is already Initialized, the component will /// automatically be Initialized and Started. @@ -404,13 +433,6 @@ bool TryCopyComponent( /// Array of component instances to copy. void CopyComponents(EntityUid source, EntityUid target, MetaDataComponent? meta = null, params IComponent[] sourceComponents); - /// - /// Returns a cached struct enumerator with the specified component. - /// - EntityQuery GetEntityQuery() where TComp1 : IComponent; - - EntityQuery GetEntityQuery(Type type); - /// /// Returns ALL component type instances on an entity. A single component instance /// can have multiple component types. @@ -471,80 +493,94 @@ bool TryCopyComponent( /// Returns all instances of a component in an array. /// Use sparingly. /// + [Obsolete($"Prefer generic {nameof(GetEntityQuery)} and dependencies on {nameof(GameObjects.EntityQuery<>)}, using the All property")] (EntityUid Uid, T Component)[] AllComponents() where T : IComponent; /// /// Returns an array of all entities that have the given component. /// Use sparingly. /// + [Obsolete($"Prefer generic {nameof(GetEntityQuery)} and dependencies on {nameof(GameObjects.EntityQuery<>)}, using the All property")] Entity[] AllEntities() where T : IComponent; /// /// Returns an array of all entities that have the given component. /// Use sparingly. /// + [Obsolete($"Prefer non-generic {nameof(GetEntityQuery)} and dependencies on {nameof(GameObjects.EntityQuery<>)}, using the All property")] Entity[] AllEntities(Type tComp); /// /// Returns an array uids of all entities that have the given component. /// Use sparingly. /// + [Obsolete($"Prefer generic {nameof(GetEntityQuery)} and dependencies on {nameof(GameObjects.EntityQuery<>)}, using the All property")] EntityUid[] AllEntityUids() where T : IComponent; /// /// Returns an array uids of all entities that have the given component. /// Use sparingly. /// + [Obsolete($"Prefer generic {nameof(GetEntityQuery)} and dependencies on {nameof(GameObjects.EntityQuery<>)}, using the All property")] EntityUid[] AllEntityUids(Type tComp); /// /// Returns all instances of a component in a List. /// Use sparingly. /// + [Obsolete($"Prefer generic {nameof(GetEntityQuery)} and dependencies on {nameof(GameObjects.EntityQuery<>)}, using the All property and .ToList()")] List<(EntityUid Uid, T Component)> AllComponentsList() where T : IComponent; - /// - /// - /// + [Obsolete($"Use {nameof(GameObjects.EntityQuery<>)}.All and non-generic {nameof(IEntityManager.GetEntityQuery)}. This API is questionable.")] public ComponentQueryEnumerator ComponentQueryEnumerator(ComponentRegistry registry); /// - /// + /// /// + [Obsolete($"Use {nameof(ComponentFilterQuery)} instead.")] public CompRegistryEntityEnumerator CompRegistryQueryEnumerator(ComponentRegistry registry); + [Obsolete($"Prefer generic {nameof(GetEntityQuery)} and dependencies on {nameof(GameObjects.EntityQuery<>)}, using the All property")] AllEntityQueryEnumerator AllEntityQueryEnumerator(Type comp); + [Obsolete($"Prefer generic {nameof(GetEntityQuery)} and dependencies on {nameof(GameObjects.EntityQuery<>)}, using the All property")] AllEntityQueryEnumerator AllEntityQueryEnumerator() where TComp1 : IComponent; + [Obsolete($"Prefer generic {nameof(GetEntityQuery)} and dependencies on {nameof(GameObjects.EntityQuery<>)}, using the All property")] AllEntityQueryEnumerator AllEntityQueryEnumerator() where TComp1 : IComponent where TComp2 : IComponent; + [Obsolete($"Prefer generic {nameof(GetEntityQuery)} and dependencies on {nameof(GameObjects.EntityQuery<>)}, using the All property")] AllEntityQueryEnumerator AllEntityQueryEnumerator() where TComp1 : IComponent where TComp2 : IComponent where TComp3 : IComponent; + [Obsolete($"Prefer generic {nameof(GetEntityQuery)} and dependencies on {nameof(GameObjects.EntityQuery<>)}, using the All property")] AllEntityQueryEnumerator AllEntityQueryEnumerator() where TComp1 : IComponent where TComp2 : IComponent where TComp3 : IComponent where TComp4 : IComponent; + [Obsolete($"Prefer generic {nameof(GetEntityQuery)} and dependencies on {nameof(GameObjects.EntityQuery<>)}")] EntityQueryEnumerator EntityQueryEnumerator() where TComp1 : IComponent; + [Obsolete($"Prefer generic {nameof(GetEntityQuery)} and dependencies on {nameof(GameObjects.EntityQuery<>)}")] EntityQueryEnumerator EntityQueryEnumerator() where TComp1 : IComponent where TComp2 : IComponent; + [Obsolete($"Prefer generic {nameof(GetEntityQuery)} and dependencies on {nameof(GameObjects.EntityQuery<>)}")] EntityQueryEnumerator EntityQueryEnumerator() where TComp1 : IComponent where TComp2 : IComponent where TComp3 : IComponent; + [Obsolete($"Prefer generic {nameof(GetEntityQuery)} and dependencies on {nameof(GameObjects.EntityQuery<>)}")] EntityQueryEnumerator EntityQueryEnumerator() where TComp1 : IComponent where TComp2 : IComponent @@ -556,6 +592,7 @@ EntityQueryEnumerator EntityQueryEnumerator /// A trait or type of a component to retrieve. /// All components that have the specified type. + [Obsolete($"Prefer {nameof(GetEntityQuery)} and dependencies on {nameof(GameObjects.EntityQuery<>)}")] IEnumerable EntityQuery(bool includePaused = false) where T: IComponent; /// @@ -564,6 +601,7 @@ EntityQueryEnumerator EntityQueryEnumeratorFirst required component. /// Second required component. /// The pairs of components from each entity that has the two required components. + [Obsolete($"Prefer {nameof(GetEntityQuery)} and dependencies on {nameof(GameObjects.EntityQuery<>)}")] IEnumerable<(TComp1, TComp2)> EntityQuery(bool includePaused = false) where TComp1 : IComponent where TComp2 : IComponent; @@ -575,6 +613,7 @@ EntityQueryEnumerator EntityQueryEnumeratorSecond required component. /// Third required component. /// The pairs of components from each entity that has the three required components. + [Obsolete($"Prefer {nameof(GetEntityQuery)} and dependencies on {nameof(GameObjects.EntityQuery<>)}")] IEnumerable<(TComp1, TComp2, TComp3)> EntityQuery(bool includePaused = false) where TComp1 : IComponent where TComp2 : IComponent @@ -588,6 +627,7 @@ EntityQueryEnumerator EntityQueryEnumeratorThird required component. /// Fourth required component. /// The pairs of components from each entity that has the four required components. + [Obsolete($"Prefer {nameof(GetEntityQuery)} and dependencies on {nameof(GameObjects.EntityQuery<>)}")] IEnumerable<(TComp1, TComp2, TComp3, TComp4)> EntityQuery(bool includePaused = false) where TComp1 : IComponent where TComp2 : IComponent @@ -600,6 +640,7 @@ EntityQueryEnumerator EntityQueryEnumeratorA trait or component type to check for. /// /// All components that are the specified type. + [Obsolete($"Prefer {nameof(GetEntityQuery)} and dependencies on {nameof(GameObjects.EntityQuery<>)}")] IEnumerable<(EntityUid Uid, IComponent Component)> GetAllComponents(Type type, bool includePaused = false); /// diff --git a/Robust.Shared/GameObjects/IEntityManager.Filters.cs b/Robust.Shared/GameObjects/IEntityManager.Filters.cs new file mode 100644 index 00000000000..7b1cbeeabc2 --- /dev/null +++ b/Robust.Shared/GameObjects/IEntityManager.Filters.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using Robust.Shared.Prototypes; + +namespace Robust.Shared.GameObjects; + +public partial interface IEntityManager +{ + /// + /// Tests whether an entity matches a filter, i.e. its components are a superset of the filter's. + /// + /// The entity to test against. + /// The filter to use. + /// True if match, false if not. + public bool MatchesFilter(EntityUid ent, ComponentFilter filter); + + /// + /// Tests whether any component on an entity is within the filter. + /// + /// The entity to test against. + /// The filter to use. + /// True if match, false if not. + public bool AnyMatchingComponent(EntityUid ent, ComponentFilter filter); + + /// + /// Tests whether an entity matches a filter exactly, i.e. its components are identical to the filter's. + /// + /// The entity to test against. + /// The filter to use. + /// True if match, false if not. + /// + /// + /// For performance and technical reasons, this matches the exact type, not any shared component. + /// This also does NOT match for Transform or Metadata, as both are obligatory. + /// + /// + public bool ExactlyMatchesFilter(EntityUid ent, ComponentFilter filter); + + /// + /// Enumerates all the components the filter has, but the entity does not. + /// + /// The entity to test against. + /// The filter to use. + /// A list of component types. + /// This does not obey the paused status of an entity. If this matters for you, you should use yourself. + public IEnumerable EnumerateFilterMisses(EntityUid ent, ComponentFilter filter); + + /// + /// Enumerates all the components the entity has, but the filter does not. + /// + /// The entity to test against. + /// The filter to use. + /// A list of component types. + /// This does not obey the paused status of an entity. If this matters for you, you should use yourself. + public IEnumerable EnumerateEntityMisses(EntityUid ent, ComponentFilter filter); + + /// + /// Enumerates all components the filter and the entity have in common. + /// + /// The entity to test against. + /// The filter to use. + /// A list of component types. + /// This does not obey the paused status of an entity. If this matters for you, you should use yourself. + public IEnumerable EnumerateFilterHits(EntityUid ent, ComponentFilter filter); + + /// + /// Computes the filter misses for the given entity, and then looks up those components from a registry to add them. + /// + /// The entity to test against. + /// The filter to use. + /// The registry to introduce components from. + /// + /// Like all filter methods, this is dynamic and is slower than using EnsureComp if you know the types in advance. + /// + /// This does not obey the paused status of an entity. If this matters for you, you should use yourself. + public void FillMissesFromRegistry(EntityUid ent, ComponentFilter filter, ComponentRegistry registry); + + /// + /// Computes the filter misses for the given entity, and then adds the missing components. + /// + /// The entity to test against. + /// The filter to use. + /// + /// Like all filter methods, this is dynamic and is slower than using EnsureComp if you know the types in advance. + /// + /// This does not obey the paused status of an entity. If this matters for you, you should use yourself. + public void FillMissesWithNewComponents(EntityUid ent, ComponentFilter filter); + + /// + /// Constructs a cacheable query for a given filter. + /// This optimizes the performance of slightly and allows you to iterate matches. + /// + /// The filter to construct a query from. + /// Whether this query should match paused entities. + /// A ComponentFilterQuery. + public ComponentFilterQuery ComponentFilterQuery(ComponentFilter filter, bool matchPaused = false); +} diff --git a/Robust.Shared/GameObjects/IEntityManager.Queries.cs b/Robust.Shared/GameObjects/IEntityManager.Queries.cs new file mode 100644 index 00000000000..16c51abf61f --- /dev/null +++ b/Robust.Shared/GameObjects/IEntityManager.Queries.cs @@ -0,0 +1,62 @@ +using System; + +namespace Robust.Shared.GameObjects; + +public partial interface IEntityManager +{ + /// + /// Returns a dynamic query over the given component list and properties. + /// + /// The query entries to build into a cached query. + /// A constructed, optimized query for the given entries. + /// + /// + /// Currently, this is the only API that exposes the Optional and Without component query configurations. + /// A typed API to expose them is unfortunately TBD. + /// + /// + /// Having an Optional or Without entry as the first entry prevents you from enumerating the query. + /// + /// + DynamicEntityQuery GetDynamicQuery(params (Type, DynamicEntityQuery.QueryFlags)[] entries); + + /// + /// Returns a query for entities with the given component. + /// + /// + EntityQuery GetEntityQuery() where TComp1 : IComponent; + + /// + /// Returns a generic query for entities with the given component. + /// + /// + /// + EntityQuery GetEntityQuery(Type type); + + /// + /// Returns a query for entities with the given components. + /// + /// + EntityQuery GetEntityQuery() + where TComp1 : IComponent + where TComp2 : IComponent; + + /// + /// Returns a query for entities with the given components. + /// + /// + EntityQuery GetEntityQuery() + where TComp1 : IComponent + where TComp2 : IComponent + where TComp3 : IComponent; + + /// + /// Returns a query for entities with the given components. + /// + /// + EntityQuery GetEntityQuery() + where TComp1 : IComponent + where TComp2 : IComponent + where TComp3 : IComponent + where TComp4 : IComponent; +} diff --git a/Robust.Shared/GameObjects/IEntityManager.cs b/Robust.Shared/GameObjects/IEntityManager.cs index d0763cf1bea..95325340166 100644 --- a/Robust.Shared/GameObjects/IEntityManager.cs +++ b/Robust.Shared/GameObjects/IEntityManager.cs @@ -14,7 +14,7 @@ namespace Robust.Shared.GameObjects /// Holds a collection of entities and the components attached to them. /// [PublicAPI] - public partial interface IEntityManager + public partial interface IEntityManager : IRemoteEntityManager { /// /// The current simulation tick being processed. diff --git a/Robust.Shared/GameObjects/IRemoteEntityManager.cs b/Robust.Shared/GameObjects/IRemoteEntityManager.cs new file mode 100644 index 00000000000..8ad54e5ff98 --- /dev/null +++ b/Robust.Shared/GameObjects/IRemoteEntityManager.cs @@ -0,0 +1,34 @@ +using Robust.Shared.GameObjects.CommandBuffers; +using Robust.Shared.GameObjects.EntityBuilders; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.Manager; + +namespace Robust.Shared.GameObjects; + +/// +/// A thread-safe subset of 's interface. +/// These can be used in parallel with a running game thread mutating the ECS. +/// +public interface IRemoteEntityManager +{ + /// + /// Creates a new, empty command buffer. + /// + public CommandBuffer GetCommandBuffer(); + + /// + /// Creates a new entity builder, optionally for an entity with the given prototype. + /// + /// The entity prototype to use, if any. + /// A serialization context to use if constructing an entity from a prototype. + /// An entity builder with the expected set of components (MetaData, Transform, and any prototype-provided components.) + public EntityBuilder EntityBuilder(EntProtoId? protoId = null, ISerializationContext? context = null); + + /// + /// Retrieves an unused entity slot, which command buffer application can fill in when spawning entities. + /// + /// A completely unallocated, now reserved entity id. + internal EntityUid GetUnusedEntityUid(); + + +} diff --git a/Robust.Shared/GameObjects/Systems/PrototypeReloadSystem.cs b/Robust.Shared/GameObjects/Systems/PrototypeReloadSystem.cs index 074183ba1c5..352c06e9263 100644 --- a/Robust.Shared/GameObjects/Systems/PrototypeReloadSystem.cs +++ b/Robust.Shared/GameObjects/Systems/PrototypeReloadSystem.cs @@ -40,12 +40,12 @@ private void UpdateEntity(EntityUid entity, MetaDataComponent metaData, EntityPr { var oldPrototype = metaData.EntityPrototype; - var oldPrototypeComponents = oldPrototype?.Components.Keys + var oldPrototypeComponents = oldPrototype?.Components.Names(_componentFactory) .Where(n => n != "Transform" && n != "MetaData") .Select(name => (name, _componentFactory.GetRegistration(name).Type)) .ToList() ?? new List<(string name, Type Type)>(); - var newPrototypeComponents = newPrototype.Components.Keys + var newPrototypeComponents = newPrototype.Components.Names(_componentFactory) .Where(n => n != "Transform" && n != "MetaData") .Select(name => (name, _componentFactory.GetRegistration(name).Type)) .ToList(); @@ -55,7 +55,7 @@ private void UpdateEntity(EntityUid entity, MetaDataComponent metaData, EntityPr // Find components to be removed, and remove them foreach (var (name, type) in oldPrototypeComponents.Except(newPrototypeComponents)) { - if (newPrototype.Components.ContainsKey(name)) + if (newPrototype.Components.ContainsComponentByName(_componentFactory, name)) { ignoredComponents.Add(name); continue; @@ -70,7 +70,7 @@ private void UpdateEntity(EntityUid entity, MetaDataComponent metaData, EntityPr foreach (var (name, _) in newPrototypeComponents.Where(t => !ignoredComponents.Contains(t.name)) .Except(oldPrototypeComponents)) { - var data = newPrototype.Components[name]; + var data = newPrototype.Components.GetComponentByName(_componentFactory, name); var component = _componentFactory.GetComponent(name); if (!HasComp(entity, component.GetType())) diff --git a/Robust.Shared/GameObjects/Systems/SharedTransformSystem.Component.cs b/Robust.Shared/GameObjects/Systems/SharedTransformSystem.Component.cs index 91336d37b6e..190f653b69a 100644 --- a/Robust.Shared/GameObjects/Systems/SharedTransformSystem.Component.cs +++ b/Robust.Shared/GameObjects/Systems/SharedTransformSystem.Component.cs @@ -218,12 +218,9 @@ public bool IsParentOf(TransformComponent parent, EntityUid child) { if (mapComp.MapId == MapId.Nullspace) { -#if !EXCEPTION_TOLERANCE - throw new Exception("Transform is initialising before map ids have been assigned?"); -#else - Log.Error($"Transform is initialising before map ids have been assigned?"); + // We need to initialize the map now, then. Because ordering of init is not something + // we support. _map.AssignMapId((uid, mapComp)); -#endif } xform.MapUid = uid; diff --git a/Robust.Shared/Prototypes/ComponentFilter.cs b/Robust.Shared/Prototypes/ComponentFilter.cs new file mode 100644 index 00000000000..1cacbf6b568 --- /dev/null +++ b/Robust.Shared/Prototypes/ComponentFilter.cs @@ -0,0 +1,228 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using JetBrains.Annotations; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Utility; + +namespace Robust.Shared.Prototypes; + +/// +/// A "filter" for entities, allowing you to describe a set of components they match and test for matches. +/// This is intended for cases where you want to be able to see if an entity matches a given dynamically defined set +/// of components, for example a YAML defined set. +/// +/// +/// This is adjacent to, but disjoint from . Registries contain a fully instantiated +/// set of components, including their fields, and are intended for additive operations only. +/// +/// +/// +[PublicAPI] +public sealed class ComponentFilter : ISet +{ + private HashSet _components; + + /// + /// Constructs a new, blank component filter. + /// + public ComponentFilter() + { + _components = new(); + } + + /// + /// Constructs a new component filter from the given set of component types. + /// + /// The set of components to turn into a filter. + /// + /// Duplicates will be removed. + /// + public ComponentFilter(params Type[] components) + { + _components = components.ToHashSet(); + ValidateContents(); + } + + /// + /// Constructs a new component filter from the given set of component types. + /// + /// The set of components to turn into a filter. + /// + /// Duplicates will be removed. + /// + public ComponentFilter(IEnumerable components) + { + _components = components.ToHashSet(); + ValidateContents(); + } + + /// + /// Constructs a new component filter from the given set of component names. + /// + /// The global component factory. + /// The set of components to turn into a filter. + /// + /// Duplicates will be removed. + /// + public ComponentFilter(IComponentFactory factory, params string[] components) + { + _components = components.Select(x => factory.GetRegistration(x).Type).ToHashSet(); + // No need to validate. + } + + /// + /// Constructs a filter out of the contents of a registry. + /// + /// The global component factory. + /// The registry to obtain component types from. + /// + /// + /// Filters do not retain any of the component's data, they only contain component types. + /// There is no engine API to filter for component data. + /// + /// + /// This is a new allocation, so if you need to do this regularly it's preferable to cache the filter. + /// + /// + public ComponentFilter(IComponentFactory factory, ComponentRegistry registry) + { + _components = registry.Components().Select(x => x.GetType()).ToHashSet(); + ValidateContents(); + } + + public bool Add(Type component) + { +#if DEBUG + var factory = IoCManager.Resolve(); + DebugTools.Assert(factory.TryGetRegistration(component, out _), + "Cannot add non-components to a filter."); +#endif + + return _components.Add(component); + } + + /// + /// Adds a given component to the filter. + /// + /// The global component factory. + /// The component to add by name. + /// True if the component was added, false if it was already present. + public bool Add(IComponentFactory factory, string componentName) + { + var component = factory.GetRegistration(componentName).Type; + return _components.Add(component); + } + + public void ExceptWith(IEnumerable other) + { + _components.ExceptWith(other); + ValidateContents(); + } + + public void IntersectWith(IEnumerable other) + { + _components.IntersectWith(other); + ValidateContents(); + } + + public bool IsProperSubsetOf(IEnumerable other) + { + return _components.IsProperSubsetOf(other); + } + + public bool IsProperSupersetOf(IEnumerable other) + { + return _components.IsProperSupersetOf(other); + } + + public bool IsSubsetOf(IEnumerable other) + { + return _components.IsSubsetOf(other); + } + + public bool IsSupersetOf(IEnumerable other) + { + return _components.IsSupersetOf(other); + } + + public bool Overlaps(IEnumerable other) + { + return _components.Overlaps(other); + } + + public bool SetEquals(IEnumerable other) + { + return _components.SetEquals(other); + } + + public void SymmetricExceptWith(IEnumerable other) + { + _components.SymmetricExceptWith(other); + ValidateContents(); + } + + public void UnionWith(IEnumerable other) + { + _components.UnionWith(other); + ValidateContents(); + } + + public void Clear() + { + _components.Clear(); + } + + public bool Contains(Type item) + { + return _components.Contains(item); + } + + public void CopyTo(Type[] array, int arrayIndex) + { + _components.CopyTo(array, arrayIndex); + } + + void ICollection.Add(Type item) + { + Add(item); + } + + public bool Remove(Type component) + { +#if DEBUG + var factory = IoCManager.Resolve(); + DebugTools.Assert(factory.TryGetRegistration(component, out _), + "Cannot remove non-components from a filter."); +#endif + + return _components.Remove(component); + } + + public int Count => _components.Count; + public bool IsReadOnly => false; + + public IEnumerator GetEnumerator() + { + return _components.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + /// Validates the component filter only contains components. + /// + [Conditional("DEBUG")] + private void ValidateContents() + { + var factory = IoCManager.Resolve(); + DebugTools.Assert(_components.All(x => factory.TryGetRegistration(x, out _)), + "All types in a filter list must be valid, registered components."); + } +} diff --git a/Robust.Shared/Prototypes/ComponentRegistry.cs b/Robust.Shared/Prototypes/ComponentRegistry.cs new file mode 100644 index 00000000000..41c2b451638 --- /dev/null +++ b/Robust.Shared/Prototypes/ComponentRegistry.cs @@ -0,0 +1,381 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Robust.Shared.GameObjects; +using Robust.Shared.Serialization.TypeSerializers.Implementations; +using Robust.Shared.Utility; + +namespace Robust.Shared.Prototypes; + +/// +/// A registry of instantiated, deserialized components. +/// +/// +/// This is distinctly not a filter list, and contains fully constructed components. +/// To filter for components, use a ComponentFilterList instead. +/// +/// Currently, working with names in a registry is most efficient. However when all obsolete API usage has been +/// cleaned up, registry internals will be changed and functionality used in prototypes/yaml will be split off. +/// +public sealed class ComponentRegistry : IEntityLoadContext, IEnumerable> +{ + /// + /// The underlying dictionary of this registry. This stores component names mapped to entries. + /// + private readonly Dictionary _inner; + + /// + /// The number of components contained within the registry. + /// + public int Count => _inner.Count; + + [Obsolete("Use Components, ComponentsAndNames, or ComponentTypes instead.")] + public IReadOnlyCollection Values => _inner.Values; + + [Obsolete("Use Components, ComponentsAndNames, or ComponentTypes instead.")] + public IReadOnlyCollection Keys => _inner.Keys; + + + public ComponentRegistry() + { + _inner = new(); + } + + internal ComponentRegistry(Dictionary components) + { + _inner = components; + } + + /// + /// Constructs a new component registry from a list of components and the global component factory. + /// + /// The global component factory. + /// The components to build from. + public ComponentRegistry(IComponentFactory factory, params IEnumerable component) + { + _inner = component.ToDictionary( + x => factory.GetRegistration(x.GetType()).Name, + x => new EntityPrototype.ComponentRegistryEntry(x) + ); + + ValidateContents(); + } + + /// + /// Adds a component to the registry, constructing it. + /// + /// The global component factory. + /// The type of component to construct. + public IComponent AddComponent(IComponentFactory factory, Type component) + { + var name = factory.GetComponentName(component); + var c = factory.GetComponent(component); + + AddComponentManual(name, c); + + return c; + } + + /// + /// Adds a component to the registry, constructing it. + /// + /// The global component factory. + /// The type of component to construct. + public T AddComponent(IComponentFactory factory) + where T: IComponent + { + return (T)AddComponent(factory, typeof(T)); + } + + /// + /// Adds a pre-constructed component to this registry. + /// + public void AddComponent(IComponentFactory factory, IComponent component) + { + var name = factory.GetComponentName(component.GetType()); + + AddComponentManual(name, component); + } + + /// + /// Manually inserts a pre-constructed component into the registry by name. + /// + public void AddComponentManual(IComponentFactory factory, string componentName, IComponent component) + { + AddComponentManual(componentName, component); + } + + /// + /// Manually inserts a pre-constructed component into the registry by name. + /// + /// + /// You almost never want this, this is for situations where you have no choice (i.e. cannot use IoC). + /// + internal void AddComponentManual(string componentName, IComponent component) + { + _inner[componentName] = new EntityPrototype.ComponentRegistryEntry(component); + ValidateContents(); + } + + /// + /// Gets a component out of the registry by type. + /// + /// The global component factory. + /// The component type to retrieve. + /// The component. + /// Thrown when the given component is not in the registry. + public T GetComponent(IComponentFactory factory) + where T: class, IComponent, new() + { + if (TryGetComponent(factory, out var c)) + return c; + + throw new KeyNotFoundException($"Couldn't find {typeof(T)} in the registry."); + } + + /// + /// Gets a component out of the registry by type. + /// + /// The global component factory. + /// The component type to retrieve. + /// The component. + /// Thrown when the given component is not in the registry. + public IComponent GetComponent(IComponentFactory factory, Type component) + { + if (TryGetComponent(factory, component, out var c)) + return c; + + throw new KeyNotFoundException($"Couldn't find {component} in the registry."); + } + + /// + /// Gets a component out of the registry by name. + /// + /// The global component factory + /// The component to retrieve. + /// The component. + /// Thrown when the given component is not in the registry. + public IComponent GetComponentByName(IComponentFactory factory, string name) + { + return _inner[name].Component; + } + + /// + [Obsolete("The IComponentFactory receiving method must be used.")] + public bool TryGetComponent(string componentName, [NotNullWhen(true)] out IComponent? component) + { + var success = _inner.TryGetValue(componentName, out var comp); + component = comp?.Component; + + return success; + } + + /// + public bool TryGetComponent(IComponentFactory factory, string componentName, [NotNullWhen(true)] out IComponent? component) + { + var success = _inner.TryGetValue(componentName, out var comp); + component = comp?.Component; + + return success; + } + + /// + public bool TryGetComponent( + IComponentFactory factory, + [NotNullWhen(true)] out TComponent? component + ) where TComponent : class, IComponent, new() + { + component = null; + var componentName = factory.GetComponentName(); + if (TryGetComponent(factory, componentName, out var foundComponent)) + { + component = (TComponent)foundComponent; + return true; + } + + return false; + } + + /// + public bool TryGetComponent(IComponentFactory factory, Type componentType, [NotNullWhen(true)] out IComponent? component) + { + component = null; + var componentName = factory.GetComponentName(componentType); + if (TryGetComponent(factory, componentName, out component)) + { + return true; + } + + return false; + } + + /// + public IEnumerable GetExtraComponentTypes() + { + return _inner.Keys; + } + + /// + public bool ShouldSkipComponent(string compName) + { + return false; //Registries cannot represent the "remove this component" state. + } + + /// + /// Enumerate components in the registry by type. + /// + public IEnumerable ComponentTypes() + { + return _inner.Values.Select(x => x.Component.GetType()); + } + + /// + /// Enumerate components in the registry by value. + /// + public IEnumerable Components() + { + return _inner.Values.Select(x => x.Component); + } + + /// + /// Enumerate components in the registry by value, with their names. + /// + public IEnumerable<(string, IComponent)> ComponentsAndNames(IComponentFactory factory) + { + return _inner.Select(x => (x.Key, x.Value.Component)); + } + + /// + /// Enumerates the names of all components in the registry. + /// + public IEnumerable Names(IComponentFactory factory) + { + return _inner.Keys; + } + + /// + /// Enumerate components in the registry by value that are assignable to T. + /// + /// The type to enumerate for. + /// All components assignable to T. + /// + /// + /// // Get all components that implement the IFunny interface. + /// var funnyComponents = registry.ComponentsAssignableTo<IFunny>(); + /// + /// + public IEnumerable ComponentsAssignableTo() + { + return _inner.Values + .Where(x => x.Component is T) + .Select(x => (T)x.Component); + } + + /// + /// Tests if the registry contains the given component. + /// + /// The global component factory. + /// The type of component to check for. + /// Whether the registry contains the given component. + public bool ContainsComponent(IComponentFactory factory, Type component) + { + var name = factory.GetComponentName(component); + + return _inner.ContainsKey(name); + } + + /// + /// Tests if the registry contains the given component by name. You should prefer to use types! + /// + /// The global component factory. + /// The name of the component to check for. + /// Whether the registry contains the given component. + public bool ContainsComponentByName(IComponentFactory factory, string name) + { + return _inner.ContainsKey(name); + } + + [Obsolete("ComponentRegistry is no longer an exposed dictionary, use the new methods like Components.")] + public IEnumerator> GetEnumerator() + { + return _inner.GetEnumerator(); + } + + [Obsolete("ComponentRegistry is no longer an exposed dictionary, use the new methods like Components.")] + IEnumerator IEnumerable.GetEnumerator() + { + return _inner.GetEnumerator(); + } + + [Obsolete("Legacy dictionary API compatibility. Use ContainsComponent.")] + public bool ContainsKey(string name) + { + return _inner.ContainsKey(name); + } + + [Obsolete("Legacy dictionary API compatibility. Use TryGetComponent.")] + public bool TryGetValue(string name, [NotNullWhen(true)] out EntityPrototype.ComponentRegistryEntry? o) + { + return _inner.TryGetValue(name, out o); + } + + /// + /// Retrieves an underlying component entry, if possible. + /// + /// The global component factory. + /// The component name to look up. + /// The component entry, if any. + /// True if entry was found, false otherwise. + public bool TryGetEntry(IComponentFactory factory, string name, [NotNullWhen(true)] out EntityPrototype.ComponentRegistryEntry? entry) + { + return _inner.TryGetValue(name, out entry); + } + + [Obsolete("Legacy dictionary API compatibility. Use AddComponent and TryGetComponent.")] + public EntityPrototype.ComponentRegistryEntry this[string compType] + { + get => _inner[compType]; + set => _inner[compType] = value; + } + + /// + /// Empties the component registry entirely. + /// + public void Clear() + { + _inner.Clear(); + } + + /// + /// Ensures the component registry has capacity for N components. + /// + /// + /// + public void EnsureCapacity(int count) + { + _inner.EnsureCapacity(count); + } + + /// + /// Method for use by when deserializing. + /// + /// + /// + internal void AddEntry(string name, EntityPrototype.ComponentRegistryEntry entry) + { + _inner[name] = entry; + ValidateContents(); + } + + [Conditional("DEBUG")] + private void ValidateContents() + { + foreach (var (key, comp) in _inner) + { + DebugTools.Assert(comp.Component.IsUnattached(), $"Components that have been part of sim should never be in a registry. Culprit was {key}."); + } + } +} diff --git a/Robust.Shared/Prototypes/EntityPrototype.cs b/Robust.Shared/Prototypes/EntityPrototype.cs index b5be7c3702b..686ff79e978 100644 --- a/Robust.Shared/Prototypes/EntityPrototype.cs +++ b/Robust.Shared/Prototypes/EntityPrototype.cs @@ -158,14 +158,14 @@ public sealed partial class EntityPrototype : IPrototype, IInheritingPrototype, public EntityPrototype() { // Everybody gets a transform component! - Components.Add("Transform", new ComponentRegistryEntry(new TransformComponent(), new MappingDataNode())); + Components.AddComponentManual("Transform", new TransformComponent()); // And a metadata component too! - Components.Add("MetaData", new ComponentRegistryEntry(new MetaDataComponent(), new MappingDataNode())); + Components.AddComponentManual("MetaData", new MetaDataComponent()); } - void ISerializationHooks.AfterDeserialization() + void ISerializationHooks.AfterDeserialization(IDependencyCollection collection) { - _loc = IoCManager.Resolve(); + _loc = collection.Resolve(); } [Obsolete("Pass in IComponentFactory")] @@ -215,16 +215,18 @@ internal static void LoadEntity( if (prototype != null) { - foreach (var (name, entry) in prototype.Components) + foreach (var comp in prototype.Components.Components()) { - if (context != null && context.ShouldSkipComponent(name)) + var compReg = factory.GetRegistration(comp); + + if (context != null && context.ShouldSkipComponent(compReg.Name)) continue; - var fullData = context != null && context.TryGetComponent(name, out var data) ? data : entry.Component; - var compReg = factory.GetRegistration(name); - EnsureCompExistsAndDeserialize(entity, compReg, factory, entityManager, serManager, name, fullData, ctx); + var fullData = context != null && context.TryGetComponent(factory, comp.GetType(), out var data) ? data : comp; - if (!entry.Component.NetSyncEnabled && compReg.NetID is {} netId) + EnsureCompExistsAndDeserialize(entity, compReg, factory, entityManager, serManager, fullData, ctx); + + if (!fullData.NetSyncEnabled && compReg.NetID is {} netId) meta.NetComponents.Remove(netId); } } @@ -233,7 +235,7 @@ internal static void LoadEntity( { foreach (var name in context.GetExtraComponentTypes()) { - if (prototype != null && prototype.Components.ContainsKey(name)) + if (prototype != null && prototype.Components.ContainsComponentByName(factory, name)) { // This component also exists in the prototype. // This means that the previous step already caught both the prototype data AND map data. @@ -241,14 +243,14 @@ internal static void LoadEntity( continue; } - if (!context.TryGetComponent(name, out var data)) + if (!context.TryGetComponent(factory, name, out var data)) { throw new InvalidOperationException( $"{nameof(IEntityLoadContext)} provided component name {name} but refused to provide data"); } var compReg = factory.GetRegistration(name); - EnsureCompExistsAndDeserialize(entity, compReg, factory, entityManager, serManager, name, data, ctx); + EnsureCompExistsAndDeserialize(entity, compReg, factory, entityManager, serManager, data, ctx); } } } @@ -258,13 +260,12 @@ public static void EnsureCompExistsAndDeserialize(EntityUid entity, IComponentFactory factory, IEntityManager entityManager, ISerializationManager serManager, - string compName, IComponent data, ISerializationContext? context) { if (!entityManager.TryGetComponent(entity, compReg.Idx, out var component)) { - var newComponent = factory.GetComponent(compName); + var newComponent = factory.GetComponent(data.GetType()); entityManager.AddComponent(entity, newComponent); component = newComponent; } @@ -275,7 +276,7 @@ public static void EnsureCompExistsAndDeserialize(EntityUid entity, return; } - map.CurrentComponent = compName; + map.CurrentComponent = factory.GetComponentName(data.GetType()); serManager.CopyTo(data, ref component, context, notNullableOverride: true); map.CurrentComponent = null; } @@ -286,7 +287,14 @@ public override string ToString() } [DataRecord] - public partial record ComponentRegistryEntry(IComponent Component, MappingDataNode Mapping); + public partial record ComponentRegistryEntry( + IComponent Component, + MappingDataNode? Mapping) + { + public ComponentRegistryEntry(IComponent component) : this(component, null) + { + } + } [DataDefinition] public sealed partial class EntityPlacementProperties @@ -402,53 +410,4 @@ public override bool TryGetDataCache(string field, out object? value) } }*/ } - - public sealed class ComponentRegistry : Dictionary, IEntityLoadContext - { - public ComponentRegistry() - { - } - - public ComponentRegistry(Dictionary components) : base(components) - { - } - - /// - public bool TryGetComponent(string componentName, [NotNullWhen(true)] out IComponent? component) - { - var success = TryGetValue(componentName, out var comp); - component = comp?.Component; - - return success; - } - - /// - public bool TryGetComponent( - IComponentFactory componentFactory, - [NotNullWhen(true)] out TComponent? component - ) where TComponent : class, IComponent, new() - { - component = null; - var componentName = componentFactory.GetComponentName(); - if (TryGetComponent(componentName, out var foundComponent)) - { - component = (TComponent)foundComponent; - return true; - } - - return false; - } - - /// - public IEnumerable GetExtraComponentTypes() - { - return Keys; - } - - /// - public bool ShouldSkipComponent(string compName) - { - return false; //Registries cannot represent the "remove this component" state. - } - } } diff --git a/Robust.Shared/Prototypes/PrototypeManager.Categories.cs b/Robust.Shared/Prototypes/PrototypeManager.Categories.cs index 739a8eee3ba..8978359c78a 100644 --- a/Robust.Shared/Prototypes/PrototypeManager.Categories.cs +++ b/Robust.Shared/Prototypes/PrototypeManager.Categories.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; using Robust.Shared.Serialization.Markdown.Mapping; using Robust.Shared.Serialization.Markdown.Sequence; using Robust.Shared.Serialization.Markdown.Value; @@ -13,6 +15,8 @@ namespace Robust.Shared.Prototypes; // This partial class handles entity prototype categories public abstract partial class PrototypeManager : IPrototypeManagerInternal { + [Dependency] private readonly IComponentFactory _componentFactory = default!; + /// /// Cached array of components with the /// @@ -132,7 +136,7 @@ private IReadOnlySet UpdateCategories(EntProtoId id, } // Get automated categories inferred from components - foreach (var comp in protoInstance.Components.Keys) + foreach (var comp in protoInstance.Components.Names(_componentFactory)) { if (autoCategories.TryGetValue(comp, out var autoCats)) set.UnionWith(autoCats); diff --git a/Robust.Shared/Prototypes/PrototypeManager.cs b/Robust.Shared/Prototypes/PrototypeManager.cs index c727ff6cee9..aca1edae4f2 100644 --- a/Robust.Shared/Prototypes/PrototypeManager.cs +++ b/Robust.Shared/Prototypes/PrototypeManager.cs @@ -29,6 +29,7 @@ namespace Robust.Shared.Prototypes public abstract partial class PrototypeManager : IPrototypeManagerInternal { [Dependency] private readonly IReflectionManager _reflectionManager = default!; + [Dependency] private readonly IDependencyCollection _dependencyCollection = default!; [Dependency] protected readonly IResourceManager Resources = default!; [Dependency] protected readonly ITaskManager TaskManager = default!; [Dependency] private readonly ISerializationManager _serializationManager = default!; @@ -554,6 +555,8 @@ private void InstantiateKinds(KindData[] kinds, Dictionary inheritan // On the game thread: process AfterDeserialization hooks from the channel. var channelReader = hooksChannel.Reader; + // TODO: Currently we always run these hooks on the main thread, and the only prototype left requiring this + // is ShaderPrototype. ShaderPrototype needs updated such that we don't have to rely on that behavior! #pragma warning disable RA0004 while (channelReader.WaitToReadAsync().AsTask().Result) #pragma warning restore RA0004 @@ -561,6 +564,7 @@ private void InstantiateKinds(KindData[] kinds, Dictionary inheritan while (channelReader.TryRead(out var hooks)) { hooks.AfterDeserialization(); + hooks.AfterDeserialization(_dependencyCollection); } } diff --git a/Robust.Shared/Serialization/ISerializationHooks.cs b/Robust.Shared/Serialization/ISerializationHooks.cs index 88f0223a38f..8d7b3487ee0 100644 --- a/Robust.Shared/Serialization/ISerializationHooks.cs +++ b/Robust.Shared/Serialization/ISerializationHooks.cs @@ -1,3 +1,5 @@ +using Robust.Shared.IoC; + namespace Robust.Shared.Serialization; /// @@ -7,7 +9,25 @@ namespace Robust.Shared.Serialization; public interface ISerializationHooks { /// - /// Gets executed after deserialization is complete + /// Gets executed after deserialization is complete. + /// + /// + /// This may run on any thread at any time unless otherwise specified. Invoking IoCManager within this method is not supported, use the + /// other method. + /// + void AfterDeserialization() + { + } + + /// + /// Gets executed after deserialization is complete. /// - void AfterDeserialization() {} + /// The main thread dependency collection. + /// + /// This may run on any thread at any time unless otherwise specified. While the main thread dependency collection + /// is provided, care must be taken to not induce race conditions. + /// + void AfterDeserialization(IDependencyCollection collection) + { + } } diff --git a/Robust.Shared/Serialization/Manager/SerializationManager.cs b/Robust.Shared/Serialization/Manager/SerializationManager.cs index 60895b6220f..24b3eaf4d25 100644 --- a/Robust.Shared/Serialization/Manager/SerializationManager.cs +++ b/Robust.Shared/Serialization/Manager/SerializationManager.cs @@ -20,6 +20,7 @@ namespace Robust.Shared.Serialization.Manager public sealed partial class SerializationManager : ISerializationManager { [Dependency] private readonly IReflectionManager _reflectionManager = default!; + [Dependency] private readonly IDependencyCollection _dependencyCollection = default!; public IReflectionManager ReflectionManager => _reflectionManager; @@ -355,13 +356,13 @@ private Type ResolveConcreteType(Type baseType, string typeName) } #pragma warning disable CS0618 - private static void RunAfterHook(TValue instance, SerializationHookContext ctx) + private void RunAfterHook(TValue instance, SerializationHookContext ctx) { if (instance is ISerializationHooks hooks) RunAfterHookGenerated(hooks, ctx); } - private static void RunAfterHookGenerated(TValue instance, SerializationHookContext ctx) where TValue : ISerializationHooks + private void RunAfterHookGenerated(TValue instance, SerializationHookContext ctx) where TValue : ISerializationHooks { if (ctx.SkipHooks) return; @@ -371,7 +372,10 @@ private static void RunAfterHookGenerated(TValue instance, Serialization if (ctx.DeferQueue != null) ctx.DeferQueue.TryWrite(instance); else + { instance.AfterDeserialization(); + instance.AfterDeserialization(_dependencyCollection); + } } #pragma warning restore CS0618 } diff --git a/Robust.Shared/Serialization/TypeSerializers/Implementations/ComponentFilterSerializer.cs b/Robust.Shared/Serialization/TypeSerializers/Implementations/ComponentFilterSerializer.cs new file mode 100644 index 00000000000..c313c3918f9 --- /dev/null +++ b/Robust.Shared/Serialization/TypeSerializers/Implementations/ComponentFilterSerializer.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.Markdown.Sequence; +using Robust.Shared.Serialization.Markdown.Validation; +using Robust.Shared.Serialization.Markdown.Value; +using Robust.Shared.Serialization.TypeSerializers.Interfaces; + +namespace Robust.Shared.Serialization.TypeSerializers.Implementations; + +[TypeSerializer] +public sealed class ComponentFilterSerializer : ITypeSerializer, ITypeInheritanceHandler, ITypeCopier +{ + public ValidationNode Validate( + ISerializationManager serializationManager, + SequenceDataNode node, + IDependencyCollection dependencies, + ISerializationContext? context = null) + { + var factory = dependencies.Resolve(); + var filter = new ComponentFilter(); + var list = new List(); + + foreach (var seqEntry in node) + { + if (seqEntry is not ValueDataNode value) + { + list.Add(new ErrorNode(seqEntry, "Expected a single, scalar value.")); + continue; + } + + var compType = value.Value; + + switch (factory.GetComponentAvailability(compType)) + { + case ComponentAvailability.Available: + break; + + case ComponentAvailability.Ignore: + list.Add(new ValidatedValueNode(seqEntry)); + continue; + + case ComponentAvailability.Unknown: + list.Add(new ErrorNode(seqEntry, $"Unknown component type {compType}.")); + continue; + } + + if (!filter.Add(factory, compType)) + { + list.Add(new ErrorNode(seqEntry, "Duplicate Component.")); + continue; + } + + list.Add(new ValidatedValueNode(seqEntry)); + } + + var referenceTypes = new List(); + + // Assert that there are no conflicting component references. + foreach (var component in filter) + { + var registration = factory.GetRegistration(component); + var compType = registration.Idx; + + if (referenceTypes.Contains(compType)) + { + return new ErrorNode(node, "Contains a duplicate component reference."); + } + + referenceTypes.Add(compType); + } + + return new ValidatedSequenceNode(list); + } + + public ComponentFilter Read( + ISerializationManager serializationManager, + SequenceDataNode node, + IDependencyCollection dependencies, + SerializationHookContext hookCtx, + ISerializationContext? context = null, + ISerializationManager.InstantiationDelegate? instanceProvider = null) + { + var factory = dependencies.Resolve(); + var filter = new ComponentFilter(); + + foreach (var seqEntry in node) + { + if (seqEntry is not ValueDataNode value) + throw new InvalidOperationException("ComponentFilter contained a non-value entry."); + + filter.Add(factory, value.Value); + } + + return filter; + } + + public DataNode Write( + ISerializationManager serializationManager, + ComponentFilter value, + IDependencyCollection dependencies, + bool alwaysWrite = false, + ISerializationContext? context = null) + { + var factory = dependencies.Resolve(); + var seq = new SequenceDataNode(); + + foreach (var componentType in value) + { + seq.Add(new ValueDataNode(factory.GetComponentName(componentType))); + } + + return seq; + } + + public SequenceDataNode PushInheritance( + ISerializationManager serializationManager, + SequenceDataNode child, + SequenceDataNode parent, + IDependencyCollection dependencies, + ISerializationContext? context) + { + var setLeft = child.Select(x => (x as ValueDataNode)?.Value); + var setRight = child.Select(x => (x as ValueDataNode)?.Value); + + var newSet = setLeft.Concat(setRight).ToHashSet(); + + if (newSet.Contains(null)) + throw new InvalidOperationException("Pushing inheritance for filter failed due to non-value entries"); + + return new SequenceDataNode(newSet.Select(x => new ValueDataNode(x!)).ToArray()); + } + + public void CopyTo( + ISerializationManager serializationManager, + ComponentFilter source, + ref ComponentFilter target, + IDependencyCollection dependencies, + SerializationHookContext hookCtx, + ISerializationContext? context = null) + { + target.Clear(); + target.UnionWith(source); + } +} diff --git a/Robust.Shared/Serialization/TypeSerializers/Implementations/ComponentRegistrySerializer.cs b/Robust.Shared/Serialization/TypeSerializers/Implementations/ComponentRegistrySerializer.cs index 8101c34a8fe..21a041c6103 100644 --- a/Robust.Shared/Serialization/TypeSerializers/Implementations/ComponentRegistrySerializer.cs +++ b/Robust.Shared/Serialization/TypeSerializers/Implementations/ComponentRegistrySerializer.cs @@ -12,7 +12,6 @@ using Robust.Shared.Serialization.Markdown.Validation; using Robust.Shared.Serialization.Markdown.Value; using Robust.Shared.Serialization.TypeSerializers.Interfaces; -using Robust.Shared.Utility; using static Robust.Shared.Prototypes.EntityPrototype; namespace Robust.Shared.Serialization.TypeSerializers.Implementations @@ -20,7 +19,8 @@ namespace Robust.Shared.Serialization.TypeSerializers.Implementations [TypeSerializer] public sealed class ComponentRegistrySerializer : ITypeSerializer, ITypeInheritanceHandler, ITypeCopier { - public ComponentRegistry Read(ISerializationManager serializationManager, + public ComponentRegistry Read( + ISerializationManager serializationManager, SequenceDataNode node, IDependencyCollection dependencies, SerializationHookContext hookCtx, @@ -52,7 +52,7 @@ public ComponentRegistry Read(ISerializationManager serializationManager, } // Has this type already been added? - if (components.ContainsKey(compType)) + if (components.ContainsComponentByName(factory, compType)) { dependencies .Resolve() @@ -61,20 +61,20 @@ public ComponentRegistry Read(ISerializationManager serializationManager, continue; } - var copy = componentMapping.Copy()!; + var copy = componentMapping.Copy(); copy.Remove("type"); var type = factory.GetRegistration(compType).Type; var read = (IComponent)serializationManager.Read(type, copy, hookCtx, context)!; - components[compType] = new ComponentRegistryEntry(read, copy); + components.AddEntry(compType, new ComponentRegistryEntry(read, copy)); } var referenceTypes = new List(); // Assert that there are no conflicting component references. - foreach (var componentName in components.Keys) + foreach (var component in components.Components()) { - var registration = factory.GetRegistration(componentName); + var registration = factory.GetRegistration(component); var compType = registration.Idx; if (referenceTypes.Contains(compType)) @@ -89,7 +89,8 @@ public ComponentRegistry Read(ISerializationManager serializationManager, return components; } - public ValidationNode Validate(ISerializationManager serializationManager, + public ValidationNode Validate( + ISerializationManager serializationManager, SequenceDataNode node, IDependencyCollection dependencies, ISerializationContext? context = null) @@ -122,13 +123,13 @@ public ValidationNode Validate(ISerializationManager serializationManager, } // Has this type already been added? - if (components.ContainsKey(compType)) + if (components.ContainsComponentByName(factory, compType)) { list.Add(new ErrorNode(componentMapping, "Duplicate Component.")); continue; } - var copy = componentMapping.Copy()!; + var copy = componentMapping.Copy(); copy.Remove("type"); var type = factory.GetRegistration(compType).Type; @@ -139,9 +140,9 @@ public ValidationNode Validate(ISerializationManager serializationManager, var referenceTypes = new List(); // Assert that there are no conflicting component references. - foreach (var componentName in components.Keys) + foreach (var component in components.Components()) { - var registration = factory.GetRegistration(componentName); + var registration = factory.GetRegistration(component); var compType = registration.Idx; if (referenceTypes.Contains(compType)) @@ -155,17 +156,21 @@ public ValidationNode Validate(ISerializationManager serializationManager, return new ValidatedSequenceNode(list); } - public DataNode Write(ISerializationManager serializationManager, ComponentRegistry value, + public DataNode Write( + ISerializationManager serializationManager, + ComponentRegistry value, IDependencyCollection dependencies, bool alwaysWrite = false, ISerializationContext? context = null) { var compSequence = new SequenceDataNode(); - foreach (var (type, component) in value) + var factory = dependencies.Resolve(); + + foreach (var (type, component) in value.ComponentsAndNames(factory)) { var node = serializationManager.WriteValue( - component.Component.GetType(), - component.Component, + component.GetType(), + component, alwaysWrite, context); @@ -178,21 +183,31 @@ public DataNode Write(ISerializationManager serializationManager, ComponentRegis return compSequence; } - public void CopyTo(ISerializationManager serializationManager, ComponentRegistry source, ref ComponentRegistry target, - IDependencyCollection dependencies, SerializationHookContext hookCtx, ISerializationContext? context = null) + public void CopyTo( + ISerializationManager serializationManager, + ComponentRegistry source, + ref ComponentRegistry target, + IDependencyCollection dependencies, + SerializationHookContext hookCtx, + ISerializationContext? context = null) { target.Clear(); target.EnsureCapacity(source.Count); + var factory = dependencies.Resolve(); - foreach (var (id, component) in source) + // We have legitimate use here for the obsolete member. + foreach (var (id, component) in source.ComponentsAndNames(factory)) { - target.Add(id, serializationManager.CreateCopy(component, context, notNullableOverride: true)); + target.AddComponentManual(id, serializationManager.CreateCopy(component, context, notNullableOverride: true)); } } - public SequenceDataNode PushInheritance(ISerializationManager serializationManager, SequenceDataNode child, + public SequenceDataNode PushInheritance( + ISerializationManager serializationManager, + SequenceDataNode child, SequenceDataNode parent, - IDependencyCollection dependencies, ISerializationContext? context) + IDependencyCollection dependencies, + ISerializationContext? context) { var componentFactory = dependencies.Resolve(); var newCompReg = child.Copy(); diff --git a/Robust.Shared/Toolshed/Commands/Entities/WithCommand.cs b/Robust.Shared/Toolshed/Commands/Entities/WithCommand.cs index 17f84745cfd..e9a93699cc0 100644 --- a/Robust.Shared/Toolshed/Commands/Entities/WithCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Entities/WithCommand.cs @@ -24,7 +24,7 @@ [CommandInverted] bool inverted return input.Where(x => !EntityManager.HasComponent(x, component)); if (input is EntitiesCommand.AllEntityEnumerator) - return EntityManager.AllEntityUids(component); + return EntityManager.GetEntityQuery(component).Select(x => x.Owner); return input.Where(x => EntityManager.HasComponent(x, component)); } diff --git a/Robust.UnitTesting/Pool/TestPair.Helpers.cs b/Robust.UnitTesting/Pool/TestPair.Helpers.cs index 624b7241e35..db274d3c4e5 100644 --- a/Robust.UnitTesting/Pool/TestPair.Helpers.cs +++ b/Robust.UnitTesting/Pool/TestPair.Helpers.cs @@ -71,8 +71,17 @@ public async Task WaitClientCommand(string cmd, int numTicks = 10) bool ignoreTestPrototypes = true) where T : IComponent, new() { - if (!Server.Resolve().TryGetRegistration(out var reg) - && !Client.Resolve().TryGetRegistration(out reg)) + IComponentFactory? factory = null; + if (Server.Resolve().TryGetRegistration(out var reg)) + { + factory = Server.Resolve(); + } + else if (Client.Resolve().TryGetRegistration(out reg)) + { + factory = Client.Resolve(); + } + + if (factory is null || reg is null) { Assert.Fail($"Unknown component: {typeof(T).Name}"); return new(); @@ -91,7 +100,7 @@ public async Task WaitClientCommand(string cmd, int numTicks = 10) if (ignoreTestPrototypes && IsTestPrototype(proto)) continue; - if (proto.Components.TryGetComponent(id, out var cmp)) + if (proto.Components.TryGetComponent(factory, id, out var cmp)) list.Add((proto, (T)cmp)); }