From 69b67ba621710adce8dcebe55f89589be9cb87fc Mon Sep 17 00:00:00 2001 From: kaylie Date: Mon, 9 Mar 2026 15:48:15 +0100 Subject: [PATCH 01/77] Pt. 1 --- .../ComponentRegistrySerializerTest.cs | 2 +- .../EntitySerialization/EntityDeserializer.cs | 4 +- .../GameObjects/EntityManager.Components.cs | 4 +- .../Systems/PrototypeReloadSystem.cs | 8 +- Robust.Shared/Prototypes/ComponentRegistry.cs | 344 ++++++++++++++++++ Robust.Shared/Prototypes/EntityPrototype.cs | 62 +--- .../Prototypes/PrototypeManager.Categories.cs | 2 +- .../ComponentRegistrySerializer.cs | 57 +-- 8 files changed, 399 insertions(+), 84 deletions(-) create mode 100644 Robust.Shared/Prototypes/ComponentRegistry.cs 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/EntitySerialization/EntityDeserializer.cs b/Robust.Shared/EntitySerialization/EntityDeserializer.cs index cd4bae33d45..41ea1b25cc5 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(name, out var protoData)) + datanode = protoData?.Mapping is not null ? _seriMan.CombineMappings(compData, protoData.Mapping!) : compData; _components.Add(name, datanode); } diff --git a/Robust.Shared/GameObjects/EntityManager.Components.cs b/Robust.Shared/GameObjects/EntityManager.Components.cs index d7308446005..3c3dffc7fd0 100644 --- a/Robust.Shared/GameObjects/EntityManager.Components.cs +++ b/Robust.Shared/GameObjects/EntityManager.Components.cs @@ -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); } } diff --git a/Robust.Shared/GameObjects/Systems/PrototypeReloadSystem.cs b/Robust.Shared/GameObjects/Systems/PrototypeReloadSystem.cs index 074183ba1c5..92d5d6d3e30 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() .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() .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(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(name); var component = _componentFactory.GetComponent(name); if (!HasComp(entity, component.GetType())) diff --git a/Robust.Shared/Prototypes/ComponentRegistry.cs b/Robust.Shared/Prototypes/ComponentRegistry.cs new file mode 100644 index 00000000000..05a540267b2 --- /dev/null +++ b/Robust.Shared/Prototypes/ComponentRegistry.cs @@ -0,0 +1,344 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Robust.Shared.GameObjects; +using Robust.Shared.Serialization.TypeSerializers.Implementations; + +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. +/// +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; + + public ComponentRegistry() + { + _inner = new(); + } + + public 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((Type)x.GetType()).Name, + x => new EntityPrototype.ComponentRegistryEntry(x) + ); + } + + /// + /// 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. + /// + /// + /// You almost never want this, this is for situations where you have no choice (i.e. cannot use IoC). + /// + public void AddComponentManual(string componentName, IComponent component) + { + _inner[componentName] = new EntityPrototype.ComponentRegistryEntry(component); + } + + /// + /// 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 component to retrieve. + /// The component. + /// Thrown when the given component is not in the registry. + public IComponent GetComponentByName(string name) + { + return _inner[name].Component; + } + + /// + 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 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; + } + + /// 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. + /// + public bool TryGetComponent(IComponentFactory componentFactory, Type componentType, [NotNullWhen(true)] out IComponent? component) + { + component = null; + var componentName = componentFactory.GetComponentName(componentType); + if (TryGetComponent(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() + { + return _inner.Select(x => (x.Key, x.Value.Component)); + } + + /// + /// Enumerates the names of all components in the registry. + /// + /// + public IEnumerable Names() + { + 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 name of the component to check for. + /// Whether the registry contains the given component. + public bool ContainsComponentByName(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. + /// + /// + /// + /// + public bool TryGetEntry(string name, [NotNullWhen(true)] out EntityPrototype.ComponentRegistryEntry? o) + { + return _inner.TryGetValue(name, out o); + } + + [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; + } +} diff --git a/Robust.Shared/Prototypes/EntityPrototype.cs b/Robust.Shared/Prototypes/EntityPrototype.cs index b5be7c3702b..65f8b254400 100644 --- a/Robust.Shared/Prototypes/EntityPrototype.cs +++ b/Robust.Shared/Prototypes/EntityPrototype.cs @@ -158,9 +158,9 @@ 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() @@ -286,7 +286,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 +409,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..e5d1b1eed58 100644 --- a/Robust.Shared/Prototypes/PrototypeManager.Categories.cs +++ b/Robust.Shared/Prototypes/PrototypeManager.Categories.cs @@ -132,7 +132,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()) { if (autoCategories.TryGetValue(comp, out var autoCats)) set.UnionWith(autoCats); diff --git a/Robust.Shared/Serialization/TypeSerializers/Implementations/ComponentRegistrySerializer.cs b/Robust.Shared/Serialization/TypeSerializers/Implementations/ComponentRegistrySerializer.cs index 8101c34a8fe..7d6c209a0e0 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(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(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,20 @@ 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) + + foreach (var (type, component) in value.ComponentsAndNames()) { var node = serializationManager.WriteValue( - component.Component.GetType(), - component.Component, + component.GetType(), + component, alwaysWrite, context); @@ -178,21 +182,30 @@ 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); - foreach (var (id, component) in source) + // We have legitimate use here for the obsolete member. + foreach (var (id, component) in source.ComponentsAndNames()) { - 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(); From 98dbe6f9a900d3f71b60a72627b29cfac0a85ad6 Mon Sep 17 00:00:00 2001 From: kaylie Date: Tue, 10 Mar 2026 10:54:04 +0100 Subject: [PATCH 02/77] Fixups and mandatory IComponentFactory. --- .../GameObjects/IEntityLoadContext.cs | 23 +++++++- .../Systems/PrototypeReloadSystem.cs | 6 +- Robust.Shared/Prototypes/ComponentRegistry.cs | 58 +++++++++++++------ Robust.Shared/Prototypes/EntityPrototype.cs | 18 +++--- .../Prototypes/PrototypeManager.Categories.cs | 6 +- .../ComponentRegistrySerializer.cs | 6 +- Robust.UnitTesting/Pool/TestPair.Helpers.cs | 15 ++++- 7 files changed, 94 insertions(+), 38 deletions(-) diff --git a/Robust.Shared/GameObjects/IEntityLoadContext.cs b/Robust.Shared/GameObjects/IEntityLoadContext.cs index 6b174d98381..54c3cf40e6d 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(); diff --git a/Robust.Shared/GameObjects/Systems/PrototypeReloadSystem.cs b/Robust.Shared/GameObjects/Systems/PrototypeReloadSystem.cs index 92d5d6d3e30..21abf9d3a8e 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.Names() + 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.Names() + var newPrototypeComponents = newPrototype.Components.Names(_componentFactory) .Where(n => n != "Transform" && n != "MetaData") .Select(name => (name, _componentFactory.GetRegistration(name).Type)) .ToList(); @@ -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.GetComponentByName(name); + var data = newPrototype.Components.GetComponentByName(_componentFactory, name); var component = _componentFactory.GetComponent(name); if (!HasComp(entity, component.GetType())) diff --git a/Robust.Shared/Prototypes/ComponentRegistry.cs b/Robust.Shared/Prototypes/ComponentRegistry.cs index 05a540267b2..5817df2ed94 100644 --- a/Robust.Shared/Prototypes/ComponentRegistry.cs +++ b/Robust.Shared/Prototypes/ComponentRegistry.cs @@ -14,6 +14,9 @@ namespace Robust.Shared.Prototypes; /// /// 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> { @@ -30,12 +33,16 @@ public sealed class ComponentRegistry : IEntityLoadContext, IEnumerable Values => _inner.Values; + [Obsolete("Use Components, ComponentsAndNames, or ComponentTypes instead.")] + public IReadOnlyCollection Keys => _inner.Keys; + + public ComponentRegistry() { _inner = new(); } - public ComponentRegistry(Dictionary components) + internal ComponentRegistry(Dictionary components) { _inner = components; } @@ -48,7 +55,7 @@ public ComponentRegistry(Dictionary component) { _inner = component.ToDictionary( - x => factory.GetRegistration((Type)x.GetType()).Name, + x => factory.GetRegistration(x.GetType()).Name, x => new EntityPrototype.ComponentRegistryEntry(x) ); } @@ -89,13 +96,21 @@ public void AddComponent(IComponentFactory factory, IComponent component) 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). /// - public void AddComponentManual(string componentName, IComponent component) + internal void AddComponentManual(string componentName, IComponent component) { _inner[componentName] = new EntityPrototype.ComponentRegistryEntry(component); } @@ -134,15 +149,17 @@ public IComponent GetComponent(IComponentFactory factory, Type component) /// /// 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(string name) + 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); @@ -151,15 +168,24 @@ public bool TryGetComponent(string componentName, [NotNullWhen(true)] out ICompo 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 componentFactory, + IComponentFactory factory, [NotNullWhen(true)] out TComponent? component ) where TComponent : class, IComponent, new() { component = null; - var componentName = componentFactory.GetComponentName(); - if (TryGetComponent(componentName, out var foundComponent)) + var componentName = factory.GetComponentName(); + if (TryGetComponent(factory, componentName, out var foundComponent)) { component = (TComponent)foundComponent; return true; @@ -168,17 +194,12 @@ public bool TryGetComponent( return false; } - /// 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. - /// - public bool TryGetComponent(IComponentFactory componentFactory, Type componentType, [NotNullWhen(true)] out IComponent? component) + /// + public bool TryGetComponent(IComponentFactory factory, Type componentType, [NotNullWhen(true)] out IComponent? component) { component = null; - var componentName = componentFactory.GetComponentName(componentType); - if (TryGetComponent(componentName, out component)) + var componentName = factory.GetComponentName(componentType); + if (TryGetComponent(factory, componentName, out component)) { return true; } @@ -217,7 +238,7 @@ public IEnumerable Components() /// /// Enumerate components in the registry by value, with their names. /// - public IEnumerable<(string, IComponent)> ComponentsAndNames() + public IEnumerable<(string, IComponent)> ComponentsAndNames(IComponentFactory factory) { return _inner.Select(x => (x.Key, x.Value.Component)); } @@ -225,8 +246,7 @@ public IEnumerable Components() /// /// Enumerates the names of all components in the registry. /// - /// - public IEnumerable Names() + public IEnumerable Names(IComponentFactory factory) { return _inner.Keys; } diff --git a/Robust.Shared/Prototypes/EntityPrototype.cs b/Robust.Shared/Prototypes/EntityPrototype.cs index 65f8b254400..3b988a5384e 100644 --- a/Robust.Shared/Prototypes/EntityPrototype.cs +++ b/Robust.Shared/Prototypes/EntityPrototype.cs @@ -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; + + EnsureCompExistsAndDeserialize(entity, compReg, factory, entityManager, serManager, compReg.Name, fullData, ctx); - if (!entry.Component.NetSyncEnabled && compReg.NetID is {} netId) + 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(name)) { // This component also exists in the prototype. // This means that the previous step already caught both the prototype data AND map data. @@ -241,7 +243,7 @@ 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"); diff --git a/Robust.Shared/Prototypes/PrototypeManager.Categories.cs b/Robust.Shared/Prototypes/PrototypeManager.Categories.cs index e5d1b1eed58..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.Names()) + foreach (var comp in protoInstance.Components.Names(_componentFactory)) { if (autoCategories.TryGetValue(comp, out var autoCats)) set.UnionWith(autoCats); diff --git a/Robust.Shared/Serialization/TypeSerializers/Implementations/ComponentRegistrySerializer.cs b/Robust.Shared/Serialization/TypeSerializers/Implementations/ComponentRegistrySerializer.cs index 7d6c209a0e0..180f692fdaf 100644 --- a/Robust.Shared/Serialization/TypeSerializers/Implementations/ComponentRegistrySerializer.cs +++ b/Robust.Shared/Serialization/TypeSerializers/Implementations/ComponentRegistrySerializer.cs @@ -164,8 +164,9 @@ public DataNode Write( ISerializationContext? context = null) { var compSequence = new SequenceDataNode(); + var factory = dependencies.Resolve(); - foreach (var (type, component) in value.ComponentsAndNames()) + foreach (var (type, component) in value.ComponentsAndNames(factory)) { var node = serializationManager.WriteValue( component.GetType(), @@ -192,9 +193,10 @@ public void CopyTo( { target.Clear(); target.EnsureCapacity(source.Count); + var factory = dependencies.Resolve(); // We have legitimate use here for the obsolete member. - foreach (var (id, component) in source.ComponentsAndNames()) + foreach (var (id, component) in source.ComponentsAndNames(factory)) { target.AddComponentManual(id, serializationManager.CreateCopy(component, context, notNullableOverride: true)); } 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)); } From eed0eca41f491407472564bfde634ed92093e31a Mon Sep 17 00:00:00 2001 From: kaylie Date: Tue, 10 Mar 2026 11:53:52 +0100 Subject: [PATCH 03/77] Filters. --- .../GameObjects/EntityManager.Filters.cs | 95 ++++++++ .../GameObjects/IEntityManager.Filters.cs | 72 ++++++ Robust.Shared/Prototypes/ComponentFilter.cs | 205 ++++++++++++++++++ .../ComponentFilterSerializer.cs | 151 +++++++++++++ 4 files changed, 523 insertions(+) create mode 100644 Robust.Shared/GameObjects/EntityManager.Filters.cs create mode 100644 Robust.Shared/GameObjects/IEntityManager.Filters.cs create mode 100644 Robust.Shared/Prototypes/ComponentFilter.cs create mode 100644 Robust.Shared/Serialization/TypeSerializers/Implementations/ComponentFilterSerializer.cs diff --git a/Robust.Shared/GameObjects/EntityManager.Filters.cs b/Robust.Shared/GameObjects/EntityManager.Filters.cs new file mode 100644 index 00000000000..48d4024f024 --- /dev/null +++ b/Robust.Shared/GameObjects/EntityManager.Filters.cs @@ -0,0 +1,95 @@ +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 ExactlyMatchesFilter(EntityUid ent, ComponentFilter filter) + { + foreach (var comp in _entCompIndex[ent]) + { + if (comp.Deleted) + 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); + } + } + } +} diff --git a/Robust.Shared/GameObjects/IEntityManager.Filters.cs b/Robust.Shared/GameObjects/IEntityManager.Filters.cs new file mode 100644 index 00000000000..9fd604e9f6f --- /dev/null +++ b/Robust.Shared/GameObjects/IEntityManager.Filters.cs @@ -0,0 +1,72 @@ +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 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. + /// + 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. + 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. + 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. + 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. + /// + 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. + /// + public void FillMissesWithNewComponents(EntityUid ent, ComponentFilter filter); +} diff --git a/Robust.Shared/Prototypes/ComponentFilter.cs b/Robust.Shared/Prototypes/ComponentFilter.cs new file mode 100644 index 00000000000..13359eb3c3c --- /dev/null +++ b/Robust.Shared/Prototypes/ComponentFilter.cs @@ -0,0 +1,205 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +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. +/// +/// +/// Cache your filters! Filter baking TBD. +/// +public sealed class ComponentFilter : ISet +{ + /// + /// Internals of a filter. + /// Do not use outside of serialization, i beg. + /// + internal 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(IReadOnlyCollection 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. + } + + 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/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); + } +} From d17290d6f2b827cb8685ad81c27e01b239c7beb4 Mon Sep 17 00:00:00 2001 From: kaylie Date: Tue, 10 Mar 2026 12:28:01 +0100 Subject: [PATCH 04/77] ComponentFilterQuery. --- .../GameObjects/ComponentFilterQuery.cs | 116 ++++++++++++++++++ .../GameObjects/EntityManager.Components.cs | 1 + .../GameObjects/EntityManager.Filters.cs | 23 ++++ .../GameObjects/IEntityManager.Filters.cs | 8 ++ Robust.Shared/Prototypes/ComponentFilter.cs | 4 +- 5 files changed, 149 insertions(+), 3 deletions(-) create mode 100644 Robust.Shared/GameObjects/ComponentFilterQuery.cs diff --git a/Robust.Shared/GameObjects/ComponentFilterQuery.cs b/Robust.Shared/GameObjects/ComponentFilterQuery.cs new file mode 100644 index 00000000000..31895d4a334 --- /dev/null +++ b/Robust.Shared/GameObjects/ComponentFilterQuery.cs @@ -0,0 +1,116 @@ +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 _lead; + private readonly Dictionary[] _tails; + + internal ComponentFilterQuery(Dictionary lead, Dictionary[] tails) + { + _lead = lead; + _tails = tails; + } + + /// + /// 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; + + 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; + + 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/EntityManager.Components.cs b/Robust.Shared/GameObjects/EntityManager.Components.cs index 3c3dffc7fd0..863c780ce3a 100644 --- a/Robust.Shared/GameObjects/EntityManager.Components.cs +++ b/Robust.Shared/GameObjects/EntityManager.Components.cs @@ -2101,6 +2101,7 @@ internal bool ResolveInternal(EntityUid uid, [NotNullWhen(true)] ref TComp1? com /// /// Returns entities that match the ComponentRegistry. /// + [Obsolete($"Use {nameof(ComponentFilterQuery)} instead.")] public struct CompRegistryEntityEnumerator : IDisposable { private IEntityManager _entManager; diff --git a/Robust.Shared/GameObjects/EntityManager.Filters.cs b/Robust.Shared/GameObjects/EntityManager.Filters.cs index 48d4024f024..766caa9342c 100644 --- a/Robust.Shared/GameObjects/EntityManager.Filters.cs +++ b/Robust.Shared/GameObjects/EntityManager.Filters.cs @@ -72,8 +72,10 @@ public void FillMissesFromRegistry(EntityUid ent, ComponentFilter filter, Compon 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); } @@ -92,4 +94,25 @@ public void FillMissesWithNewComponents(EntityUid ent, ComponentFilter filter) } } } + + public ComponentFilterQuery ConstructFilterQuery(ComponentFilter filter) + { + 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(lead!, tails); + } } diff --git a/Robust.Shared/GameObjects/IEntityManager.Filters.cs b/Robust.Shared/GameObjects/IEntityManager.Filters.cs index 9fd604e9f6f..c11f6e69aea 100644 --- a/Robust.Shared/GameObjects/IEntityManager.Filters.cs +++ b/Robust.Shared/GameObjects/IEntityManager.Filters.cs @@ -69,4 +69,12 @@ public partial interface IEntityManager /// Like all filter methods, this is dynamic and is slower than using EnsureComp if you know the types in advance. /// 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. + /// A ComponentFilterQuery + public ComponentFilterQuery ConstructFilterQuery(ComponentFilter filter); } diff --git a/Robust.Shared/Prototypes/ComponentFilter.cs b/Robust.Shared/Prototypes/ComponentFilter.cs index 13359eb3c3c..3dd63976994 100644 --- a/Robust.Shared/Prototypes/ComponentFilter.cs +++ b/Robust.Shared/Prototypes/ComponentFilter.cs @@ -12,9 +12,7 @@ namespace Robust.Shared.Prototypes; /// /// A "filter" for entities, allowing you to describe a set of components they match and test for matches. /// -/// -/// Cache your filters! Filter baking TBD. -/// +/// public sealed class ComponentFilter : ISet { /// From bb3b58a879df67d3e2eed5ca081290cd92d96c1e Mon Sep 17 00:00:00 2001 From: kaylie Date: Tue, 10 Mar 2026 12:41:26 +0100 Subject: [PATCH 05/77] fixes --- Robust.Shared/Prototypes/ComponentFilter.cs | 74 +++++++++++++-------- 1 file changed, 46 insertions(+), 28 deletions(-) diff --git a/Robust.Shared/Prototypes/ComponentFilter.cs b/Robust.Shared/Prototypes/ComponentFilter.cs index 3dd63976994..d7727584f0e 100644 --- a/Robust.Shared/Prototypes/ComponentFilter.cs +++ b/Robust.Shared/Prototypes/ComponentFilter.cs @@ -3,6 +3,7 @@ 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; @@ -13,20 +14,17 @@ namespace Robust.Shared.Prototypes; /// A "filter" for entities, allowing you to describe a set of components they match and test for matches. /// /// +[PublicAPI] public sealed class ComponentFilter : ISet { - /// - /// Internals of a filter. - /// Do not use outside of serialization, i beg. - /// - internal HashSet Components; + private HashSet _components; /// /// Constructs a new, blank component filter. /// public ComponentFilter() { - Components = new(); + _components = new(); } /// @@ -38,7 +36,7 @@ public ComponentFilter() /// public ComponentFilter(params Type[] components) { - Components = components.ToHashSet(); + _components = components.ToHashSet(); ValidateContents(); } @@ -51,7 +49,7 @@ public ComponentFilter(params Type[] components) /// public ComponentFilter(IReadOnlyCollection components) { - Components = components.ToHashSet(); + _components = components.ToHashSet(); ValidateContents(); } @@ -65,10 +63,30 @@ public ComponentFilter(IReadOnlyCollection components) /// public ComponentFilter(IComponentFactory factory, params string[] components) { - Components = components.Select(x => factory.GetRegistration(x).Type).ToHashSet(); + _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 @@ -77,7 +95,7 @@ public bool Add(Type component) "Cannot add non-components to a filter."); #endif - return Components.Add(component); + return _components.Add(component); } /// @@ -89,76 +107,76 @@ public bool Add(Type component) public bool Add(IComponentFactory factory, string componentName) { var component = factory.GetRegistration(componentName).Type; - return Components.Add(component); + return _components.Add(component); } public void ExceptWith(IEnumerable other) { - Components.ExceptWith(other); + _components.ExceptWith(other); ValidateContents(); } public void IntersectWith(IEnumerable other) { - Components.IntersectWith(other); + _components.IntersectWith(other); ValidateContents(); } public bool IsProperSubsetOf(IEnumerable other) { - return Components.IsProperSubsetOf(other); + return _components.IsProperSubsetOf(other); } public bool IsProperSupersetOf(IEnumerable other) { - return Components.IsProperSupersetOf(other); + return _components.IsProperSupersetOf(other); } public bool IsSubsetOf(IEnumerable other) { - return Components.IsSubsetOf(other); + return _components.IsSubsetOf(other); } public bool IsSupersetOf(IEnumerable other) { - return Components.IsSupersetOf(other); + return _components.IsSupersetOf(other); } public bool Overlaps(IEnumerable other) { - return Components.Overlaps(other); + return _components.Overlaps(other); } public bool SetEquals(IEnumerable other) { - return Components.SetEquals(other); + return _components.SetEquals(other); } public void SymmetricExceptWith(IEnumerable other) { - Components.SymmetricExceptWith(other); + _components.SymmetricExceptWith(other); ValidateContents(); } public void UnionWith(IEnumerable other) { - Components.UnionWith(other); + _components.UnionWith(other); ValidateContents(); } public void Clear() { - Components.Clear(); + _components.Clear(); } public bool Contains(Type item) { - return Components.Contains(item); + return _components.Contains(item); } public void CopyTo(Type[] array, int arrayIndex) { - Components.CopyTo(array, arrayIndex); + _components.CopyTo(array, arrayIndex); } void ICollection.Add(Type item) @@ -174,15 +192,15 @@ public bool Remove(Type component) "Cannot remove non-components from a filter."); #endif - return Components.Remove(component); + return _components.Remove(component); } - public int Count => Components.Count; + public int Count => _components.Count; public bool IsReadOnly => false; public IEnumerator GetEnumerator() { - return Components.GetEnumerator(); + return _components.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() @@ -197,7 +215,7 @@ IEnumerator IEnumerable.GetEnumerator() private void ValidateContents() { var factory = IoCManager.Resolve(); - DebugTools.Assert(Components.All(x => factory.TryGetRegistration(x, out _)), + DebugTools.Assert(_components.All(x => factory.TryGetRegistration(x, out _)), "All types in a filter list must be valid, registered components."); } } From 2b0b778d5667f28f859c65f9fad0d68f650c69e8 Mon Sep 17 00:00:00 2001 From: kaylie Date: Tue, 10 Mar 2026 13:15:58 +0100 Subject: [PATCH 06/77] Tests pt 1. --- .../GameObjects/EntityManagerFilterTests.cs | 132 ++++++++++++++++++ .../GameObjects/EntityManager.Components.cs | 1 + .../GameObjects/EntityManager.Filters.cs | 11 +- .../GameObjects/IEntityManager.Filters.cs | 5 +- Robust.Shared/Prototypes/ComponentFilter.cs | 2 +- 5 files changed, 147 insertions(+), 4 deletions(-) create mode 100644 Robust.Shared.IntegrationTests/GameObjects/EntityManagerFilterTests.cs diff --git a/Robust.Shared.IntegrationTests/GameObjects/EntityManagerFilterTests.cs b/Robust.Shared.IntegrationTests/GameObjects/EntityManagerFilterTests.cs new file mode 100644 index 00000000000..bdbb8f850b7 --- /dev/null +++ b/Robust.Shared.IntegrationTests/GameObjects/EntityManagerFilterTests.cs @@ -0,0 +1,132 @@ +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; + +[TestFixture, TestOf(typeof(EntityManager))] +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] + public void FilterEnt( + [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 (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] + 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); + } + } +} + +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/GameObjects/EntityManager.Components.cs b/Robust.Shared/GameObjects/EntityManager.Components.cs index 863c780ce3a..1cf806eff7f 100644 --- a/Robust.Shared/GameObjects/EntityManager.Components.cs +++ b/Robust.Shared/GameObjects/EntityManager.Components.cs @@ -35,6 +35,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; diff --git a/Robust.Shared/GameObjects/EntityManager.Filters.cs b/Robust.Shared/GameObjects/EntityManager.Filters.cs index 766caa9342c..f9de8ec41a0 100644 --- a/Robust.Shared/GameObjects/EntityManager.Filters.cs +++ b/Robust.Shared/GameObjects/EntityManager.Filters.cs @@ -24,6 +24,9 @@ public bool ExactlyMatchesFilter(EntityUid ent, ComponentFilter filter) if (comp.Deleted) continue; + if (comp is MetaDataComponent or TransformComponent) + continue; + if (!filter.Contains(comp.GetType())) return false; } @@ -95,8 +98,14 @@ public void FillMissesWithNewComponents(EntityUid ent, ComponentFilter filter) } } - public ComponentFilterQuery ConstructFilterQuery(ComponentFilter filter) + public ComponentFilterQuery ComponentFilterQuery(ComponentFilter filter) { + if (filter.Count == 0) + { + // Iterate everything, all entities match. + return new ComponentFilterQuery(_entTraitDict[typeof(MetaDataComponent)], []); + } + var tailCount = filter.Count - 1; var tails = new Dictionary[tailCount]; diff --git a/Robust.Shared/GameObjects/IEntityManager.Filters.cs b/Robust.Shared/GameObjects/IEntityManager.Filters.cs index c11f6e69aea..8587ac8efa3 100644 --- a/Robust.Shared/GameObjects/IEntityManager.Filters.cs +++ b/Robust.Shared/GameObjects/IEntityManager.Filters.cs @@ -22,6 +22,7 @@ public partial interface IEntityManager /// 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. /// public bool ExactlyMatchesFilter(EntityUid ent, ComponentFilter filter); @@ -75,6 +76,6 @@ public partial interface IEntityManager /// This optimizes the performance of slightly and allows you to iterate matches. /// /// The filter to construct a query from. - /// A ComponentFilterQuery - public ComponentFilterQuery ConstructFilterQuery(ComponentFilter filter); + /// A ComponentFilterQuery. + public ComponentFilterQuery ComponentFilterQuery(ComponentFilter filter); } diff --git a/Robust.Shared/Prototypes/ComponentFilter.cs b/Robust.Shared/Prototypes/ComponentFilter.cs index d7727584f0e..bbc5281149a 100644 --- a/Robust.Shared/Prototypes/ComponentFilter.cs +++ b/Robust.Shared/Prototypes/ComponentFilter.cs @@ -47,7 +47,7 @@ public ComponentFilter(params Type[] components) /// /// Duplicates will be removed. /// - public ComponentFilter(IReadOnlyCollection components) + public ComponentFilter(IEnumerable components) { _components = components.ToHashSet(); ValidateContents(); From 27f03b1512fca49997da16bdef4869564f8f737c Mon Sep 17 00:00:00 2001 From: kaylie Date: Tue, 10 Mar 2026 13:18:31 +0100 Subject: [PATCH 07/77] test fixes --- .../GameObjects/EntityManagerFilterTests.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Robust.Shared.IntegrationTests/GameObjects/EntityManagerFilterTests.cs b/Robust.Shared.IntegrationTests/GameObjects/EntityManagerFilterTests.cs index bdbb8f850b7..a260d8c8048 100644 --- a/Robust.Shared.IntegrationTests/GameObjects/EntityManagerFilterTests.cs +++ b/Robust.Shared.IntegrationTests/GameObjects/EntityManagerFilterTests.cs @@ -6,7 +6,9 @@ namespace Robust.UnitTesting.Shared.GameObjects; -[TestFixture, TestOf(typeof(EntityManager))] +[TestFixture] +[TestOf(typeof(EntityManager))] +[TestOf(typeof(ComponentFilter))] internal sealed class EntityManagerFilterTests : OurRobustUnitTest { private const string TestEnt1 = "T_TestEnt1"; @@ -43,7 +45,8 @@ public void Setup() } [Test] - public void FilterEnt( + [TestOf(typeof(ComponentFilterQuery))] + public void FilterEntities( [Values(null, typeof(Marker1Component))] Type? m1, [Values(null, typeof(Marker2Component))] From 2cd3ccc31ab36f1f62e459b04f8e8acd55949c6f Mon Sep 17 00:00:00 2001 From: kaylie Date: Tue, 10 Mar 2026 13:22:47 +0100 Subject: [PATCH 08/77] Final tests. --- .../GameObjects/EntityManagerFilterTests.cs | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/Robust.Shared.IntegrationTests/GameObjects/EntityManagerFilterTests.cs b/Robust.Shared.IntegrationTests/GameObjects/EntityManagerFilterTests.cs index a260d8c8048..f62c1884e73 100644 --- a/Robust.Shared.IntegrationTests/GameObjects/EntityManagerFilterTests.cs +++ b/Robust.Shared.IntegrationTests/GameObjects/EntityManagerFilterTests.cs @@ -112,6 +112,41 @@ public void FillMisses( _entMan.DeleteEntity(target); } } + + [Test] + 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)); + Assert.That(_entMan.EnumerateFilterMisses(target, filter), NUnit.Framework.Is.Empty); + Assert.That(_entMan.EnumerateFilterMisses(target2, filter), NUnit.Framework.Is.EquivalentTo(filter)); + Assert.That(_entMan.EnumerateEntityMisses(target2, filter), + NUnit.Framework.Is.EquivalentTo([typeof(MetaDataComponent), typeof(TransformComponent)])); + } + } + finally + { + _entMan.DeleteEntity(target); + _entMan.DeleteEntity(target2); + } + } + } internal sealed partial class Marker1Component : Component From 67b079ab6c070048892fe7e66cd85ce9de132109 Mon Sep 17 00:00:00 2001 From: kaylie Date: Tue, 10 Mar 2026 13:27:05 +0100 Subject: [PATCH 09/77] obsolete --- Robust.Shared/GameObjects/IEntityManager.Components.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Robust.Shared/GameObjects/IEntityManager.Components.cs b/Robust.Shared/GameObjects/IEntityManager.Components.cs index 3b71a269df8..cdc370201d9 100644 --- a/Robust.Shared/GameObjects/IEntityManager.Components.cs +++ b/Robust.Shared/GameObjects/IEntityManager.Components.cs @@ -511,6 +511,7 @@ bool TryCopyComponent( /// /// /// + [Obsolete($"Use {nameof(ComponentFilterQuery)} instead.")] public CompRegistryEntityEnumerator CompRegistryQueryEnumerator(ComponentRegistry registry); AllEntityQueryEnumerator AllEntityQueryEnumerator(Type comp); From 05b8689993d1181949ed5128901ee47459298049 Mon Sep 17 00:00:00 2001 From: kaylie Date: Tue, 10 Mar 2026 13:28:24 +0100 Subject: [PATCH 10/77] fix warning --- Robust.Shared/EntitySerialization/EntityDeserializer.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Robust.Shared/EntitySerialization/EntityDeserializer.cs b/Robust.Shared/EntitySerialization/EntityDeserializer.cs index 41ea1b25cc5..26098bd4326 100644 --- a/Robust.Shared/EntitySerialization/EntityDeserializer.cs +++ b/Robust.Shared/EntitySerialization/EntityDeserializer.cs @@ -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); } } From 167d096345fd372e5fd7ef3be9a07767eb3e5188 Mon Sep 17 00:00:00 2001 From: kaylie Date: Tue, 10 Mar 2026 13:56:12 +0100 Subject: [PATCH 11/77] Pause handling. --- .../GameObjects/ComponentFilterQuery.cs | 12 +++++++++++- .../GameObjects/EntityManager.Filters.cs | 17 ++++++++++++----- .../GameObjects/IEntityManager.Filters.cs | 18 ++++++++++++++---- 3 files changed, 37 insertions(+), 10 deletions(-) diff --git a/Robust.Shared/GameObjects/ComponentFilterQuery.cs b/Robust.Shared/GameObjects/ComponentFilterQuery.cs index 31895d4a334..fb340bf184d 100644 --- a/Robust.Shared/GameObjects/ComponentFilterQuery.cs +++ b/Robust.Shared/GameObjects/ComponentFilterQuery.cs @@ -13,13 +13,17 @@ namespace Robust.Shared.GameObjects; /// public readonly struct ComponentFilterQuery : IEnumerable { + private readonly Dictionary _metaData; private readonly Dictionary _lead; private readonly Dictionary[] _tails; + private readonly bool _matchPaused; - internal ComponentFilterQuery(Dictionary lead, Dictionary[] tails) + internal ComponentFilterQuery(Dictionary metaData, Dictionary lead, Dictionary[] tails, bool matchPaused) { + _metaData = metaData; _lead = lead; _tails = tails; + _matchPaused = matchPaused; } /// @@ -32,6 +36,9 @@ 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) @@ -84,6 +91,9 @@ public bool MoveNext() 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) diff --git a/Robust.Shared/GameObjects/EntityManager.Filters.cs b/Robust.Shared/GameObjects/EntityManager.Filters.cs index f9de8ec41a0..a8663dafeb2 100644 --- a/Robust.Shared/GameObjects/EntityManager.Filters.cs +++ b/Robust.Shared/GameObjects/EntityManager.Filters.cs @@ -6,8 +6,11 @@ namespace Robust.Shared.GameObjects; public abstract partial class EntityManager { - public bool MatchesFilter(EntityUid ent, ComponentFilter filter) + public bool MatchesFilter(EntityUid ent, ComponentFilter filter, bool matchPaused = false) { + if (!matchPaused && IsPaused(ent)) + return false; + foreach (var comp in filter) { if (!HasComponent(ent, comp)) @@ -17,8 +20,11 @@ public bool MatchesFilter(EntityUid ent, ComponentFilter filter) return true; } - public bool ExactlyMatchesFilter(EntityUid ent, ComponentFilter filter) + public bool ExactlyMatchesFilter(EntityUid ent, ComponentFilter filter, bool matchPaused = false) { + if (!matchPaused && IsPaused(ent)) + return false; + foreach (var comp in _entCompIndex[ent]) { if (comp.Deleted) @@ -98,12 +104,13 @@ public void FillMissesWithNewComponents(EntityUid ent, ComponentFilter filter) } } - public ComponentFilterQuery ComponentFilterQuery(ComponentFilter filter) + 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(_entTraitDict[typeof(MetaDataComponent)], []); + return new ComponentFilterQuery(meta, meta, [], matchPaused); } var tailCount = filter.Count - 1; @@ -122,6 +129,6 @@ public ComponentFilterQuery ComponentFilterQuery(ComponentFilter filter) } } - return new ComponentFilterQuery(lead!, tails); + return new ComponentFilterQuery(meta, lead!, tails, matchPaused); } } diff --git a/Robust.Shared/GameObjects/IEntityManager.Filters.cs b/Robust.Shared/GameObjects/IEntityManager.Filters.cs index 8587ac8efa3..c3b4e71a964 100644 --- a/Robust.Shared/GameObjects/IEntityManager.Filters.cs +++ b/Robust.Shared/GameObjects/IEntityManager.Filters.cs @@ -11,20 +11,24 @@ public partial interface IEntityManager /// /// The entity to test against. /// The filter to use. + /// Whether this query should match paused entities. /// True if match, false if not. - public bool MatchesFilter(EntityUid ent, ComponentFilter filter); + public bool MatchesFilter(EntityUid ent, ComponentFilter filter, bool matchPaused = false); /// /// 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. + /// Whether this query should match paused entities. /// 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. + /// This also does NOT match for Transform or Metadata, as both are obligatory. + /// /// - public bool ExactlyMatchesFilter(EntityUid ent, ComponentFilter filter); + public bool ExactlyMatchesFilter(EntityUid ent, ComponentFilter filter, bool matchPaused = false); /// /// Enumerates all the components the filter has, but the entity does not. @@ -32,6 +36,7 @@ public partial interface IEntityManager /// 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); /// @@ -40,6 +45,7 @@ public partial interface IEntityManager /// 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); /// @@ -48,6 +54,7 @@ public partial interface IEntityManager /// 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); /// @@ -59,6 +66,7 @@ public partial interface IEntityManager /// /// 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); /// @@ -69,6 +77,7 @@ public partial interface IEntityManager /// /// 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); /// @@ -76,6 +85,7 @@ public partial interface IEntityManager /// 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); + public ComponentFilterQuery ComponentFilterQuery(ComponentFilter filter, bool matchPaused = false); } From 614cb0ec2818e1f2271358b9e68e7aeb1cec24c1 Mon Sep 17 00:00:00 2001 From: kaylie Date: Tue, 10 Mar 2026 14:17:14 +0100 Subject: [PATCH 12/77] Proxy methods --- .../GameObjects/EntitySystem.Proxy.cs | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/Robust.Shared/GameObjects/EntitySystem.Proxy.cs b/Robust.Shared/GameObjects/EntitySystem.Proxy.cs index 06b852ceaf5..f400cbd0cd3 100644 --- a/Robust.Shared/GameObjects/EntitySystem.Proxy.cs +++ b/Robust.Shared/GameObjects/EntitySystem.Proxy.cs @@ -1783,4 +1783,47 @@ protected NetCoordinates[] GetNetCoordinatesArray(EntityCoordinates[] entities) } #endregion + + #region Filters + public bool MatchesFilter(EntityUid ent, ComponentFilter filter, bool matchPaused = false) + { + return EntityManager.MatchesFilter(ent, filter, matchPaused); + } + + public bool ExactlyMatchesFilter(EntityUid ent, ComponentFilter filter, bool matchPaused = false) + { + return EntityManager.ExactlyMatchesFilter(ent, filter, matchPaused); + } + + public IEnumerable EnumerateFilterMisses(EntityUid ent, ComponentFilter filter) + { + return EntityManager.EnumerateFilterMisses(ent, filter); + } + + public IEnumerable EnumerateEntityMisses(EntityUid ent, ComponentFilter filter) + { + return EntityManager.EnumerateEntityMisses(ent, filter); + } + + public IEnumerable EnumerateFilterHits(EntityUid ent, ComponentFilter filter) + { + return EntityManager.EnumerateFilterHits(ent, filter); + } + + public void FillMissesFromRegistry(EntityUid ent, ComponentFilter filter, ComponentRegistry registry) + { + EntityManager.FillMissesFromRegistry(ent, filter, registry); + } + + public void FillMissesWithNewComponents(EntityUid ent, ComponentFilter filter) + { + EntityManager.FillMissesWithNewComponents(ent, filter); + } + + public ComponentFilterQuery ComponentFilterQuery(ComponentFilter filter, bool matchPaused = false) + { + return EntityManager.ComponentFilterQuery(filter, matchPaused); + } + + #endregion } From abddc2d84d27c6d091b7126ba61e162ba705c5f9 Mon Sep 17 00:00:00 2001 From: kaylie Date: Tue, 10 Mar 2026 14:19:38 +0100 Subject: [PATCH 13/77] Proxy marks + xmldoc. --- Robust.Shared/GameObjects/EntitySystem.Proxy.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Robust.Shared/GameObjects/EntitySystem.Proxy.cs b/Robust.Shared/GameObjects/EntitySystem.Proxy.cs index f400cbd0cd3..bf3ec88cfb8 100644 --- a/Robust.Shared/GameObjects/EntitySystem.Proxy.cs +++ b/Robust.Shared/GameObjects/EntitySystem.Proxy.cs @@ -1785,41 +1785,57 @@ protected NetCoordinates[] GetNetCoordinatesArray(EntityCoordinates[] entities) #endregion #region Filters + /// + [ProxyFor(typeof(EntityManager))] public bool MatchesFilter(EntityUid ent, ComponentFilter filter, bool matchPaused = false) { return EntityManager.MatchesFilter(ent, filter, matchPaused); } + /// + [ProxyFor(typeof(EntityManager))] public bool ExactlyMatchesFilter(EntityUid ent, ComponentFilter filter, bool matchPaused = false) { return EntityManager.ExactlyMatchesFilter(ent, filter, matchPaused); } + /// + [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); From d530e17790474c0d48ae58be6463443e279b8b9c Mon Sep 17 00:00:00 2001 From: kaylie Date: Tue, 10 Mar 2026 14:32:48 +0100 Subject: [PATCH 14/77] Fix more obsoletes in engine. --- .../EntitySerialization/EntityDeserializer.cs | 2 +- .../GameObjects/Systems/PrototypeReloadSystem.cs | 2 +- Robust.Shared/Prototypes/ComponentRegistry.cs | 14 ++++++++------ Robust.Shared/Prototypes/EntityPrototype.cs | 2 +- .../Implementations/ComponentRegistrySerializer.cs | 4 ++-- 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/Robust.Shared/EntitySerialization/EntityDeserializer.cs b/Robust.Shared/EntitySerialization/EntityDeserializer.cs index 26098bd4326..abb89e5d435 100644 --- a/Robust.Shared/EntitySerialization/EntityDeserializer.cs +++ b/Robust.Shared/EntitySerialization/EntityDeserializer.cs @@ -617,7 +617,7 @@ private void LoadEntity( } var datanode = compData; - if (proto != null && proto.Components.TryGetEntry(name, out var protoData)) + 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); diff --git a/Robust.Shared/GameObjects/Systems/PrototypeReloadSystem.cs b/Robust.Shared/GameObjects/Systems/PrototypeReloadSystem.cs index 21abf9d3a8e..352c06e9263 100644 --- a/Robust.Shared/GameObjects/Systems/PrototypeReloadSystem.cs +++ b/Robust.Shared/GameObjects/Systems/PrototypeReloadSystem.cs @@ -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.ContainsComponentByName(name)) + if (newPrototype.Components.ContainsComponentByName(_componentFactory, name)) { ignoredComponents.Add(name); continue; diff --git a/Robust.Shared/Prototypes/ComponentRegistry.cs b/Robust.Shared/Prototypes/ComponentRegistry.cs index 5817df2ed94..7fa6d1c4599 100644 --- a/Robust.Shared/Prototypes/ComponentRegistry.cs +++ b/Robust.Shared/Prototypes/ComponentRegistry.cs @@ -285,9 +285,10 @@ public bool ContainsComponent(IComponentFactory factory, Type component) /// /// 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(string name) + public bool ContainsComponentByName(IComponentFactory factory, string name) { return _inner.ContainsKey(name); } @@ -319,12 +320,13 @@ public bool TryGetValue(string name, [NotNullWhen(true)] out EntityPrototype.Com /// /// Retrieves an underlying component entry, if possible. /// - /// - /// - /// - public bool TryGetEntry(string name, [NotNullWhen(true)] out EntityPrototype.ComponentRegistryEntry? o) + /// 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 o); + return _inner.TryGetValue(name, out entry); } [Obsolete("Legacy dictionary API compatibility. Use AddComponent and TryGetComponent.")] diff --git a/Robust.Shared/Prototypes/EntityPrototype.cs b/Robust.Shared/Prototypes/EntityPrototype.cs index 3b988a5384e..dec9da87776 100644 --- a/Robust.Shared/Prototypes/EntityPrototype.cs +++ b/Robust.Shared/Prototypes/EntityPrototype.cs @@ -235,7 +235,7 @@ internal static void LoadEntity( { foreach (var name in context.GetExtraComponentTypes()) { - if (prototype != null && prototype.Components.ContainsComponentByName(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. diff --git a/Robust.Shared/Serialization/TypeSerializers/Implementations/ComponentRegistrySerializer.cs b/Robust.Shared/Serialization/TypeSerializers/Implementations/ComponentRegistrySerializer.cs index 180f692fdaf..21a041c6103 100644 --- a/Robust.Shared/Serialization/TypeSerializers/Implementations/ComponentRegistrySerializer.cs +++ b/Robust.Shared/Serialization/TypeSerializers/Implementations/ComponentRegistrySerializer.cs @@ -52,7 +52,7 @@ public ComponentRegistry Read( } // Has this type already been added? - if (components.ContainsComponentByName(compType)) + if (components.ContainsComponentByName(factory, compType)) { dependencies .Resolve() @@ -123,7 +123,7 @@ public ValidationNode Validate( } // Has this type already been added? - if (components.ContainsComponentByName(compType)) + if (components.ContainsComponentByName(factory, compType)) { list.Add(new ErrorNode(componentMapping, "Duplicate Component.")); continue; From 5d5d82c6d6221f7a4c15a5664502b55c4832a8dd Mon Sep 17 00:00:00 2001 From: kaylie Date: Tue, 10 Mar 2026 15:33:55 +0100 Subject: [PATCH 15/77] more warning fixes. --- Robust.Shared/EntitySerialization/EntityDeserializer.cs | 2 +- Robust.Shared/GameObjects/EntityManager.Components.cs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Robust.Shared/EntitySerialization/EntityDeserializer.cs b/Robust.Shared/EntitySerialization/EntityDeserializer.cs index abb89e5d435..34457fbba3b 100644 --- a/Robust.Shared/EntitySerialization/EntityDeserializer.cs +++ b/Robust.Shared/EntitySerialization/EntityDeserializer.cs @@ -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/EntityManager.Components.cs b/Robust.Shared/GameObjects/EntityManager.Components.cs index 1cf806eff7f..958853a0c8c 100644 --- a/Robust.Shared/GameObjects/EntityManager.Components.cs +++ b/Robust.Shared/GameObjects/EntityManager.Components.cs @@ -1388,6 +1388,7 @@ public ComponentQueryEnumerator ComponentQueryEnumerator(ComponentRegistry regis } /// + [Obsolete($"Use {nameof(ComponentFilterQuery)} instead.")] public CompRegistryEntityEnumerator CompRegistryQueryEnumerator(ComponentRegistry registry) { if (registry.Count == 0) From 2a34a7cbeab4e01428746dfbb95162b8caf2335a Mon Sep 17 00:00:00 2001 From: kaylie Date: Tue, 10 Mar 2026 16:16:52 +0100 Subject: [PATCH 16/77] Assertions in ComponentRegistry. --- .../GameObjects/ComponentExtensions.cs | 17 +++++++++++++++++ Robust.Shared/Prototypes/ComponentRegistry.cs | 15 +++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 Robust.Shared/GameObjects/ComponentExtensions.cs 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/Prototypes/ComponentRegistry.cs b/Robust.Shared/Prototypes/ComponentRegistry.cs index 7fa6d1c4599..41c2b451638 100644 --- a/Robust.Shared/Prototypes/ComponentRegistry.cs +++ b/Robust.Shared/Prototypes/ComponentRegistry.cs @@ -1,10 +1,12 @@ 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; @@ -58,6 +60,8 @@ public ComponentRegistry(IComponentFactory factory, params IEnumerable factory.GetRegistration(x.GetType()).Name, x => new EntityPrototype.ComponentRegistryEntry(x) ); + + ValidateContents(); } /// @@ -113,6 +117,7 @@ public void AddComponentManual(IComponentFactory factory, string componentName, internal void AddComponentManual(string componentName, IComponent component) { _inner[componentName] = new EntityPrototype.ComponentRegistryEntry(component); + ValidateContents(); } /// @@ -362,5 +367,15 @@ public void EnsureCapacity(int count) 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}."); + } } } From f53f7ae16ee5a592b1e2ea240ba127511a7340f0 Mon Sep 17 00:00:00 2001 From: kaylie Date: Tue, 10 Mar 2026 21:54:59 +0100 Subject: [PATCH 17/77] Better RemoveComponents API. AddComponents is covered by FillMissesWithNewComponents and FillMissesFromRegistry. --- .../GameObjects/EntityManager.Components.cs | 8 ++--- .../GameObjects/EntityManager.Filters.cs | 20 ++++++++++++ .../GameObjects/IEntityManager.Components.cs | 31 ++++++++++++++++++- 3 files changed, 54 insertions(+), 5 deletions(-) diff --git a/Robust.Shared/GameObjects/EntityManager.Components.cs b/Robust.Shared/GameObjects/EntityManager.Components.cs index 958853a0c8c..9ce68cee829 100644 --- a/Robust.Shared/GameObjects/EntityManager.Components.cs +++ b/Robust.Shared/GameObjects/EntityManager.Components.cs @@ -193,14 +193,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 @@ -211,7 +211,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); } } diff --git a/Robust.Shared/GameObjects/EntityManager.Filters.cs b/Robust.Shared/GameObjects/EntityManager.Filters.cs index a8663dafeb2..b39892617d0 100644 --- a/Robust.Shared/GameObjects/EntityManager.Filters.cs +++ b/Robust.Shared/GameObjects/EntityManager.Filters.cs @@ -131,4 +131,24 @@ public ComponentFilterQuery ComponentFilterQuery(ComponentFilter filter, bool ma 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/IEntityManager.Components.cs b/Robust.Shared/GameObjects/IEntityManager.Components.cs index cdc370201d9..ccb8c991a87 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. From ff0e49782bfb1eecd496a621ecfa47d2d19aca35 Mon Sep 17 00:00:00 2001 From: kaylie Date: Wed, 11 Mar 2026 00:08:35 +0100 Subject: [PATCH 18/77] test docs. --- .../GameObjects/EntityManagerFilterTests.cs | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/Robust.Shared.IntegrationTests/GameObjects/EntityManagerFilterTests.cs b/Robust.Shared.IntegrationTests/GameObjects/EntityManagerFilterTests.cs index f62c1884e73..8faffcfe39a 100644 --- a/Robust.Shared.IntegrationTests/GameObjects/EntityManagerFilterTests.cs +++ b/Robust.Shared.IntegrationTests/GameObjects/EntityManagerFilterTests.cs @@ -6,7 +6,6 @@ namespace Robust.UnitTesting.Shared.GameObjects; -[TestFixture] [TestOf(typeof(EntityManager))] [TestOf(typeof(ComponentFilter))] internal sealed class EntityManagerFilterTests : OurRobustUnitTest @@ -46,6 +45,10 @@ public void Setup() [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, @@ -67,6 +70,7 @@ public void FilterEntities( 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 @@ -87,6 +91,7 @@ public void FilterEntities( } [Test] + [Description("Asserts that using FillMissesWithNewComponents should make the entity match the filter afterward.")] public void FillMisses( [Values(null, typeof(Marker1Component))] Type? m1, @@ -114,6 +119,10 @@ public void FillMisses( } [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, @@ -133,11 +142,18 @@ public void EnumerateFilterHits( using (Assert.EnterMultipleScope()) { - Assert.That(_entMan.EnumerateFilterHits(target, filter), NUnit.Framework.Is.EquivalentTo(filter)); - Assert.That(_entMan.EnumerateFilterMisses(target, filter), NUnit.Framework.Is.Empty); - Assert.That(_entMan.EnumerateFilterMisses(target2, filter), NUnit.Framework.Is.EquivalentTo(filter)); + 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)])); + NUnit.Framework.Is.EquivalentTo([typeof(MetaDataComponent), typeof(TransformComponent)]), + "Expected the entity to have two components the filter does not, transform and metadata."); } } finally From 7eaff617495f3561217128d2f8bf198248a3475c Mon Sep 17 00:00:00 2001 From: kaylie Date: Wed, 11 Mar 2026 00:11:19 +0100 Subject: [PATCH 19/77] docs --- Robust.Shared/Prototypes/ComponentFilter.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Robust.Shared/Prototypes/ComponentFilter.cs b/Robust.Shared/Prototypes/ComponentFilter.cs index bbc5281149a..1cacbf6b568 100644 --- a/Robust.Shared/Prototypes/ComponentFilter.cs +++ b/Robust.Shared/Prototypes/ComponentFilter.cs @@ -12,8 +12,15 @@ 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 { From 99155102c2daac0edf35424a2ed0235ea79c4359 Mon Sep 17 00:00:00 2001 From: kaylie Date: Wed, 11 Mar 2026 00:29:54 +0100 Subject: [PATCH 20/77] AnyMatchingComponent --- .../GameObjects/EntityManager.Filters.cs | 18 ++++++++++++++++-- .../GameObjects/EntitySystem.Proxy.cs | 12 ++++++++++-- .../GameObjects/IEntityManager.Filters.cs | 11 ++++++++++- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/Robust.Shared/GameObjects/EntityManager.Filters.cs b/Robust.Shared/GameObjects/EntityManager.Filters.cs index b39892617d0..aab3adabb07 100644 --- a/Robust.Shared/GameObjects/EntityManager.Filters.cs +++ b/Robust.Shared/GameObjects/EntityManager.Filters.cs @@ -6,7 +6,7 @@ namespace Robust.Shared.GameObjects; public abstract partial class EntityManager { - public bool MatchesFilter(EntityUid ent, ComponentFilter filter, bool matchPaused = false) + public bool MatchesFilter(EntityUid ent, ComponentFilter filter, bool matchPaused = true) { if (!matchPaused && IsPaused(ent)) return false; @@ -20,7 +20,21 @@ public bool MatchesFilter(EntityUid ent, ComponentFilter filter, bool matchPause return true; } - public bool ExactlyMatchesFilter(EntityUid ent, ComponentFilter filter, bool matchPaused = false) + public bool AnyMatchingComponent(EntityUid ent, ComponentFilter filter, bool matchPaused = true) + { + if (!matchPaused && IsPaused(ent)) + return false; + + foreach (var comp in filter) + { + if (HasComponent(ent, comp)) + return true; + } + + return false; + } + + public bool ExactlyMatchesFilter(EntityUid ent, ComponentFilter filter, bool matchPaused = true) { if (!matchPaused && IsPaused(ent)) return false; diff --git a/Robust.Shared/GameObjects/EntitySystem.Proxy.cs b/Robust.Shared/GameObjects/EntitySystem.Proxy.cs index bf3ec88cfb8..2c20cc8fe5c 100644 --- a/Robust.Shared/GameObjects/EntitySystem.Proxy.cs +++ b/Robust.Shared/GameObjects/EntitySystem.Proxy.cs @@ -1787,14 +1787,22 @@ protected NetCoordinates[] GetNetCoordinatesArray(EntityCoordinates[] entities) #region Filters /// [ProxyFor(typeof(EntityManager))] - public bool MatchesFilter(EntityUid ent, ComponentFilter filter, bool matchPaused = false) + public bool MatchesFilter(EntityUid ent, ComponentFilter filter, bool matchPaused = true) { return EntityManager.MatchesFilter(ent, filter, matchPaused); } + /// + [ProxyFor(typeof(EntityManager))] + public bool AnyMatchingComponent(EntityUid ent, ComponentFilter filter, bool matchPaused = true) + { + return EntityManager.AnyMatchingComponent(ent, filter, matchPaused); + } + + /// [ProxyFor(typeof(EntityManager))] - public bool ExactlyMatchesFilter(EntityUid ent, ComponentFilter filter, bool matchPaused = false) + public bool ExactlyMatchesFilter(EntityUid ent, ComponentFilter filter, bool matchPaused = true) { return EntityManager.ExactlyMatchesFilter(ent, filter, matchPaused); } diff --git a/Robust.Shared/GameObjects/IEntityManager.Filters.cs b/Robust.Shared/GameObjects/IEntityManager.Filters.cs index c3b4e71a964..a4ced82ff80 100644 --- a/Robust.Shared/GameObjects/IEntityManager.Filters.cs +++ b/Robust.Shared/GameObjects/IEntityManager.Filters.cs @@ -13,7 +13,16 @@ public partial interface IEntityManager /// The filter to use. /// Whether this query should match paused entities. /// True if match, false if not. - public bool MatchesFilter(EntityUid ent, ComponentFilter filter, bool matchPaused = false); + public bool MatchesFilter(EntityUid ent, ComponentFilter filter, bool matchPaused = true); + + /// + /// Tests whether any component on an entity is within the filter. + /// + /// The entity to test against. + /// The filter to use. + /// Whether this query should match paused entities. + /// True if match, false if not. + public bool AnyMatchingComponent(EntityUid ent, ComponentFilter filter, bool matchPaused = true); /// /// Tests whether an entity matches a filter exactly, i.e. its components are identical to the filter's. From 33ac93f28ef5a4ef983fdde3b68ca64cfa4478a9 Mon Sep 17 00:00:00 2001 From: kaylie Date: Wed, 11 Mar 2026 00:33:13 +0100 Subject: [PATCH 21/77] Snip some bad API design. --- .../GameObjects/EntityManager.Filters.cs | 15 +++------------ .../GameObjects/EntitySystem.Proxy.cs | 18 +++++++++--------- .../GameObjects/IEntityManager.Filters.cs | 9 +++------ 3 files changed, 15 insertions(+), 27 deletions(-) diff --git a/Robust.Shared/GameObjects/EntityManager.Filters.cs b/Robust.Shared/GameObjects/EntityManager.Filters.cs index aab3adabb07..431a48cfb62 100644 --- a/Robust.Shared/GameObjects/EntityManager.Filters.cs +++ b/Robust.Shared/GameObjects/EntityManager.Filters.cs @@ -6,11 +6,8 @@ namespace Robust.Shared.GameObjects; public abstract partial class EntityManager { - public bool MatchesFilter(EntityUid ent, ComponentFilter filter, bool matchPaused = true) + public bool MatchesFilter(EntityUid ent, ComponentFilter filter) { - if (!matchPaused && IsPaused(ent)) - return false; - foreach (var comp in filter) { if (!HasComponent(ent, comp)) @@ -20,11 +17,8 @@ public bool MatchesFilter(EntityUid ent, ComponentFilter filter, bool matchPause return true; } - public bool AnyMatchingComponent(EntityUid ent, ComponentFilter filter, bool matchPaused = true) + public bool AnyMatchingComponent(EntityUid ent, ComponentFilter filter) { - if (!matchPaused && IsPaused(ent)) - return false; - foreach (var comp in filter) { if (HasComponent(ent, comp)) @@ -34,11 +28,8 @@ public bool AnyMatchingComponent(EntityUid ent, ComponentFilter filter, bool mat return false; } - public bool ExactlyMatchesFilter(EntityUid ent, ComponentFilter filter, bool matchPaused = true) + public bool ExactlyMatchesFilter(EntityUid ent, ComponentFilter filter) { - if (!matchPaused && IsPaused(ent)) - return false; - foreach (var comp in _entCompIndex[ent]) { if (comp.Deleted) diff --git a/Robust.Shared/GameObjects/EntitySystem.Proxy.cs b/Robust.Shared/GameObjects/EntitySystem.Proxy.cs index 2c20cc8fe5c..12304c18a2e 100644 --- a/Robust.Shared/GameObjects/EntitySystem.Proxy.cs +++ b/Robust.Shared/GameObjects/EntitySystem.Proxy.cs @@ -1785,26 +1785,26 @@ protected NetCoordinates[] GetNetCoordinatesArray(EntityCoordinates[] entities) #endregion #region Filters - /// + /// [ProxyFor(typeof(EntityManager))] - public bool MatchesFilter(EntityUid ent, ComponentFilter filter, bool matchPaused = true) + public bool MatchesFilter(EntityUid ent, ComponentFilter filter) { - return EntityManager.MatchesFilter(ent, filter, matchPaused); + return EntityManager.MatchesFilter(ent, filter); } - /// + /// [ProxyFor(typeof(EntityManager))] - public bool AnyMatchingComponent(EntityUid ent, ComponentFilter filter, bool matchPaused = true) + public bool AnyMatchingComponent(EntityUid ent, ComponentFilter filter) { - return EntityManager.AnyMatchingComponent(ent, filter, matchPaused); + return EntityManager.AnyMatchingComponent(ent, filter); } - /// + /// [ProxyFor(typeof(EntityManager))] - public bool ExactlyMatchesFilter(EntityUid ent, ComponentFilter filter, bool matchPaused = true) + public bool ExactlyMatchesFilter(EntityUid ent, ComponentFilter filter) { - return EntityManager.ExactlyMatchesFilter(ent, filter, matchPaused); + return EntityManager.ExactlyMatchesFilter(ent, filter); } /// diff --git a/Robust.Shared/GameObjects/IEntityManager.Filters.cs b/Robust.Shared/GameObjects/IEntityManager.Filters.cs index a4ced82ff80..7b1cbeeabc2 100644 --- a/Robust.Shared/GameObjects/IEntityManager.Filters.cs +++ b/Robust.Shared/GameObjects/IEntityManager.Filters.cs @@ -11,25 +11,22 @@ public partial interface IEntityManager /// /// The entity to test against. /// The filter to use. - /// Whether this query should match paused entities. /// True if match, false if not. - public bool MatchesFilter(EntityUid ent, ComponentFilter filter, bool matchPaused = true); + 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. - /// Whether this query should match paused entities. /// True if match, false if not. - public bool AnyMatchingComponent(EntityUid ent, ComponentFilter filter, bool matchPaused = true); + 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. - /// Whether this query should match paused entities. /// True if match, false if not. /// /// @@ -37,7 +34,7 @@ public partial interface IEntityManager /// This also does NOT match for Transform or Metadata, as both are obligatory. /// /// - public bool ExactlyMatchesFilter(EntityUid ent, ComponentFilter filter, bool matchPaused = false); + public bool ExactlyMatchesFilter(EntityUid ent, ComponentFilter filter); /// /// Enumerates all the components the filter has, but the entity does not. From 0747af21f526bbd6309342ddda516fb90f4a7698 Mon Sep 17 00:00:00 2001 From: kaylie Date: Wed, 11 Mar 2026 21:21:24 +0100 Subject: [PATCH 22/77] Release notes. --- RELEASE-NOTES.md | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 4c55457d3e2..c66d4802195 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -39,7 +39,18 @@ END TEMPLATE--> ### New features -*None yet* +- `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,7 +58,12 @@ END TEMPLATE--> ### Other -*None yet* +- `EntityQueryEnumerator`, `EntityQueryEnumerator`, `EntityQueryEnumerator`, + `EntityQueryEnumerator`, `AllEntityQueryEnumerator`, `AllEntityQueryEnumerator`, + `AllEntityQueryEnumerator`, `AllEntityQueryEnumerator`, + and `ComponentQueryEnumerator` are now obsolete. +- The EntityManager methods `EntityQuery`, `EntityQuery`, `EntityQuery`, + and `EntityQuery` are now obsolete. ### Internal From 86267c0ad9513718d27053028127a513bb6f17ce Mon Sep 17 00:00:00 2001 From: kaylie Date: Wed, 11 Mar 2026 21:22:12 +0100 Subject: [PATCH 23/77] Revert "Release notes.", the notes are for another PR. This reverts commit 8e6a885057a0f944c1c4adaf77fa71d7fcd35f94. --- RELEASE-NOTES.md | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index c66d4802195..4c55457d3e2 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -39,18 +39,7 @@ END TEMPLATE--> ### New features -- `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. +*None yet* ### Bugfixes @@ -58,12 +47,7 @@ END TEMPLATE--> ### Other -- `EntityQueryEnumerator`, `EntityQueryEnumerator`, `EntityQueryEnumerator`, - `EntityQueryEnumerator`, `AllEntityQueryEnumerator`, `AllEntityQueryEnumerator`, - `AllEntityQueryEnumerator`, `AllEntityQueryEnumerator`, - and `ComponentQueryEnumerator` are now obsolete. -- The EntityManager methods `EntityQuery`, `EntityQuery`, `EntityQuery`, - and `EntityQuery` are now obsolete. +*None yet* ### Internal From 3fa678609e829c8fabd2b0f42dca161babba07ea Mon Sep 17 00:00:00 2001 From: kaylie Date: Wed, 11 Mar 2026 21:38:03 +0100 Subject: [PATCH 24/77] Release notes. --- RELEASE-NOTES.md | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 4c55457d3e2..e9bc5b5f172 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -35,11 +35,19 @@ 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. ### Bugfixes @@ -47,11 +55,17 @@ 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`. ### Internal -*None yet* +- `ComponentRegistry`-dependant surfaces in the engine have been rewritten to use the new methods wherever possible. ## 274.0.0 From 82aa5f3323b2c0bac67e07ee83326a8e28010af9 Mon Sep 17 00:00:00 2001 From: kaylie Date: Tue, 10 Mar 2026 16:42:24 +0100 Subject: [PATCH 25/77] Add an efficient enumerator impl to EntityQuery. --- .../GameObjects/EntityManager.Components.cs | 345 +------------- Robust.Shared/GameObjects/EntityQuery.cs | 449 ++++++++++++++++++ 2 files changed, 458 insertions(+), 336 deletions(-) create mode 100644 Robust.Shared/GameObjects/EntityQuery.cs diff --git a/Robust.Shared/GameObjects/EntityManager.Components.cs b/Robust.Shared/GameObjects/EntityManager.Components.cs index d7308446005..3d6a7b4f14c 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; @@ -1188,16 +1187,21 @@ private T CopyComponentInternal(EntityUid source, EntityUid target, T sourceC public EntityQuery GetEntityQuery() where TComp1 : IComponent { + DebugTools.Assert(_entTraitArray.Length > CompIdx.ArrayIndex(), + $"Unknown component: {typeof(TComp1).Name}"); var comps = _entTraitArray[CompIdx.ArrayIndex()]; - DebugTools.Assert(comps != null, $"Unknown component: {typeof(TComp1).Name}"); - return new EntityQuery(this, comps); + 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]; - DebugTools.Assert(comps != null, $"Unknown component: {type.Name}"); - return new EntityQuery(this, comps); + var meta = _entTraitArray[CompIdx.ArrayIndex()]; + + return new EntityQuery(this, comps, meta); } /// @@ -1765,337 +1769,6 @@ 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 /// diff --git a/Robust.Shared/GameObjects/EntityQuery.cs b/Robust.Shared/GameObjects/EntityQuery.cs new file mode 100644 index 00000000000..bf4565e4bcd --- /dev/null +++ b/Robust.Shared/GameObjects/EntityQuery.cs @@ -0,0 +1,449 @@ +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() +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. + /// + /// + /// + /// 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 . + /// + internal 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) + /// + [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 + + public Enumerator GetEnumerator() + { + return new Enumerator(this); + } + + IEnumerator> IEnumerable>.GetEnumerator() + { + return GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + 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; + + 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(); + } + } +} From 1a775febfe501509d667824ded83575db3b8f365 Mon Sep 17 00:00:00 2001 From: kaylie Date: Tue, 10 Mar 2026 16:48:07 +0100 Subject: [PATCH 26/77] Mark the legacy entity queries as obsolete. --- Robust.Shared/GameObjects/IEntityManager.Components.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Robust.Shared/GameObjects/IEntityManager.Components.cs b/Robust.Shared/GameObjects/IEntityManager.Components.cs index 3b71a269df8..9525c53ca31 100644 --- a/Robust.Shared/GameObjects/IEntityManager.Components.cs +++ b/Robust.Shared/GameObjects/IEntityManager.Components.cs @@ -556,6 +556,7 @@ EntityQueryEnumerator EntityQueryEnumerator /// A trait or type of a component to retrieve. /// All components that have the specified type. + [Obsolete($"Prefer {nameof(GameObjects.EntityQuery<>)}")] IEnumerable EntityQuery(bool includePaused = false) where T: IComponent; /// @@ -564,6 +565,7 @@ EntityQueryEnumerator EntityQueryEnumeratorFirst required component. /// Second required component. /// The pairs of components from each entity that has the two required components. + [Obsolete($"Prefer {nameof(GameObjects.EntityQueryEnumerator<>)}")] IEnumerable<(TComp1, TComp2)> EntityQuery(bool includePaused = false) where TComp1 : IComponent where TComp2 : IComponent; @@ -575,6 +577,7 @@ EntityQueryEnumerator EntityQueryEnumeratorSecond required component. /// Third required component. /// The pairs of components from each entity that has the three required components. + [Obsolete($"Prefer {nameof(GameObjects.EntityQueryEnumerator<>)}")] IEnumerable<(TComp1, TComp2, TComp3)> EntityQuery(bool includePaused = false) where TComp1 : IComponent where TComp2 : IComponent @@ -588,6 +591,7 @@ EntityQueryEnumerator EntityQueryEnumeratorThird required component. /// Fourth required component. /// The pairs of components from each entity that has the four required components. + [Obsolete($"Prefer {nameof(GameObjects.EntityQueryEnumerator<>)}")] IEnumerable<(TComp1, TComp2, TComp3, TComp4)> EntityQuery(bool includePaused = false) where TComp1 : IComponent where TComp2 : IComponent From 96be0d68f254f5778c2bf95c7c60e2d49e29ee10 Mon Sep 17 00:00:00 2001 From: kaylie Date: Tue, 10 Mar 2026 17:03:53 +0100 Subject: [PATCH 27/77] Explicit ToList --- Robust.Shared/GameObjects/EntityQuery.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Robust.Shared/GameObjects/EntityQuery.cs b/Robust.Shared/GameObjects/EntityQuery.cs index bf4565e4bcd..ea18ef9dc3b 100644 --- a/Robust.Shared/GameObjects/EntityQuery.cs +++ b/Robust.Shared/GameObjects/EntityQuery.cs @@ -446,4 +446,15 @@ public void Dispose() _traitDictEnumerator.Dispose(); } } + + // I expect this one in particular to get used a lot 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; + } } From 4d0cf9e61575db3a99f2407e7d0815e83a9b8571 Mon Sep 17 00:00:00 2001 From: kaylie Date: Tue, 10 Mar 2026 17:07:59 +0100 Subject: [PATCH 28/77] Stop inlining everything or so help me. --- Robust.Shared/GameObjects/EntityQuery.cs | 27 ++++++++++-------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/Robust.Shared/GameObjects/EntityQuery.cs b/Robust.Shared/GameObjects/EntityQuery.cs index ea18ef9dc3b..80821f9c0d1 100644 --- a/Robust.Shared/GameObjects/EntityQuery.cs +++ b/Robust.Shared/GameObjects/EntityQuery.cs @@ -106,7 +106,6 @@ internal EntityQuery(EntityQuery derived, bool enumeratePaused) /// /// EntitySystem.Comp<T>(EntityUid) /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] [Pure] public TComp1 GetComponent(EntityUid uid) { @@ -117,7 +116,7 @@ public TComp1 GetComponent(EntityUid uid) } /// - [MethodImpl(MethodImplOptions.AggressiveInlining), Pure] + [Pure] public Entity Get(EntityUid uid) { if (_traitDict.TryGetValue(uid, out var comp) && !comp.Deleted) @@ -142,7 +141,6 @@ public Entity Get(EntityUid uid) /// /// EntitySystem.TryComp<T>(EntityUid, out T?) /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] [Pure] public bool TryGetComponent([NotNullWhen(true)] EntityUid? uid, [NotNullWhen(true)] out TComp1? component) { @@ -156,7 +154,6 @@ public bool TryGetComponent([NotNullWhen(true)] EntityUid? uid, [NotNullWhen(tru } /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] [Pure] public bool TryGetComponent(EntityUid uid, [NotNullWhen(true)] out TComp1? component) { @@ -171,13 +168,11 @@ public bool TryGetComponent(EntityUid uid, [NotNullWhen(true)] out TComp1? compo } /// - [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); @@ -194,17 +189,14 @@ public bool TryComp([NotNullWhen(true)] EntityUid? uid, [NotNullWhen(true)] out /// /// 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) { @@ -212,7 +204,6 @@ public bool HasComponent(EntityUid uid) } /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] [Pure] public bool HasComponent([NotNullWhen(true)] EntityUid? uid) { @@ -227,7 +218,6 @@ public bool HasComponent([NotNullWhen(true)] EntityUid? uid) /// /// 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) @@ -255,7 +245,6 @@ public bool Resolve(EntityUid uid, [NotNullWhen(true)] ref TComp1? component, bo /// /// 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); @@ -266,7 +255,6 @@ public bool Resolve(ref Entity entity, bool logMissing = true) /// /// The entity to do the lookup on. /// The component, if it exists. - [MethodImpl(MethodImplOptions.AggressiveInlining)] [Pure] public TComp1? CompOrNull(EntityUid uid) { @@ -277,7 +265,6 @@ public bool Resolve(ref Entity entity, bool logMissing = true) } /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] [Pure] public TComp1 Comp(EntityUid uid) { @@ -396,11 +383,15 @@ 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) @@ -422,6 +413,10 @@ public bool MoveNext() 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; @@ -439,7 +434,7 @@ public void Reset() Entity IEnumerator>.Current => Current; - object? IEnumerator.Current => Current; + object IEnumerator.Current => Current; public void Dispose() { @@ -447,7 +442,7 @@ public void Dispose() } } - // I expect this one in particular to get used a lot so.. optimize it :) + // I expect this one in particular to get used a bit more than most so.. optimize it :) /// public List> ToList() { From a9c17d4053ab45bc998b7f1715221a49e74a4b64 Mon Sep 17 00:00:00 2001 From: kaylie Date: Tue, 10 Mar 2026 17:12:40 +0100 Subject: [PATCH 29/77] docs --- Robust.Shared/GameObjects/EntityQuery.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Robust.Shared/GameObjects/EntityQuery.cs b/Robust.Shared/GameObjects/EntityQuery.cs index 80821f9c0d1..9f56124f1d7 100644 --- a/Robust.Shared/GameObjects/EntityQuery.cs +++ b/Robust.Shared/GameObjects/EntityQuery.cs @@ -44,6 +44,7 @@ namespace Robust.Shared.GameObjects; /// /// EntitySystem.GetEntityQuery() /// EntityManager.GetEntityQuery() +[PublicAPI] public readonly struct EntityQuery : IEnumerable> where TComp1 : IComponent { @@ -55,8 +56,11 @@ namespace Robust.Shared.GameObjects; /// /// Returns an entity query that will include paused entities when enumerated. - /// You shouldn't cache this, please. /// + /// + /// 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 @@ -86,7 +90,7 @@ internal EntityQuery(EntityManager entMan, Dictionary tra /// /// Internal constructor used for . /// - internal EntityQuery(EntityQuery derived, bool enumeratePaused) + private EntityQuery(EntityQuery derived, bool enumeratePaused) { _entMan = derived._entMan; _traitDict = derived._traitDict; From 6b697bcccfa0108b851cc6b351c3d526f68c0eef Mon Sep 17 00:00:00 2001 From: kaylie Date: Tue, 10 Mar 2026 18:40:25 +0100 Subject: [PATCH 30/77] Dynamic entity query. For internal use first. --- .../GameObjects/DynamicEntityQuery.cs | 277 ++++++++++++++++++ 1 file changed, 277 insertions(+) create mode 100644 Robust.Shared/GameObjects/DynamicEntityQuery.cs diff --git a/Robust.Shared/GameObjects/DynamicEntityQuery.cs b/Robust.Shared/GameObjects/DynamicEntityQuery.cs new file mode 100644 index 00000000000..c7f073355a5 --- /dev/null +++ b/Robust.Shared/GameObjects/DynamicEntityQuery.cs @@ -0,0 +1,277 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Robust.Shared.GameObjects; + +/// +/// An internal, typeless version of entity queries. +/// This isn't enumerable, but works for an arbitrary set of components. +/// +public readonly struct DynamicEntityQuery +{ + /// + /// Information on a query item, describing how to handle it. + /// + internal readonly struct QueryEntry + { + public readonly Dictionary Dict; + + public readonly QueryFlags Flags; + + public QueryEntry(Dictionary dict, QueryFlags flags) + { + Dict = dict; + 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 & QueryEntry.QueryFlags.Without) != 0)) + && (entryRef.Flags & QueryEntry.QueryFlags.Optional) == 0) + return false; + + // 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 & QueryEntry.QueryFlags.Without) != 0)) + && (entryRef.Flags & QueryEntry.QueryFlags.Optional) == 0) + return false; + + // 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 & QueryEntry.QueryFlags.Without) != 0)) + && (entryRef.Flags & QueryEntry.QueryFlags.Optional) == 0) + return false; + + // 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 + } + + /// + /// The custom enumerator for dynamic queries. + /// This is intended to be an implementation detail for other queries. + /// + public struct Enumerator + { + // ReSharper disable once CollectionNeverUpdated.Local + private static readonly Dictionary EmptyDict = new(); + + private readonly DynamicEntityQuery _owner; + private Dictionary.Enumerator _lead; + + internal Enumerator(DynamicEntityQuery owner) + { + QueryEntry.QueryFlags flags; + _owner = owner; + + if (_owner._entries.Length == 0) + { + flags = QueryEntry.QueryFlags.None; + } + else + { + flags = _owner._entries[0].Flags; + } + + if (flags != QueryEntry.QueryFlags.None) + { + throw new NotSupportedException( + "Query enumerators do not support optional or excluded first components."); + } + + Reset(); + } + + /// + /// 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); + + ref var spanEntry = ref MemoryMarshal.GetReference(output); + + ent = EntityUid.Invalid; + while (true) + { + if (!_lead.MoveNext()) + return false; + + ent = _lead.Current.Key; + spanEntry = _lead.Current.Value; + + if (spanEntry.Deleted) + continue; // Nevermind, move along. + + // 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 = EmptyDict.GetEnumerator(); + } + else + { + _lead = _owner._entries[0].Dict.GetEnumerator(); + } + } + } + + [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}"); + } +} From b7347a4ea8721d31470cf6bc15e207dc36f00daf Mon Sep 17 00:00:00 2001 From: kaylie Date: Tue, 10 Mar 2026 18:45:45 +0100 Subject: [PATCH 31/77] paused checks. --- Robust.Shared/GameObjects/DynamicEntityQuery.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Robust.Shared/GameObjects/DynamicEntityQuery.cs b/Robust.Shared/GameObjects/DynamicEntityQuery.cs index c7f073355a5..02b839c10fd 100644 --- a/Robust.Shared/GameObjects/DynamicEntityQuery.cs +++ b/Robust.Shared/GameObjects/DynamicEntityQuery.cs @@ -195,12 +195,14 @@ public struct Enumerator private static readonly Dictionary EmptyDict = new(); private readonly DynamicEntityQuery _owner; + private readonly bool _checkPaused; private Dictionary.Enumerator _lead; - internal Enumerator(DynamicEntityQuery owner) + internal Enumerator(DynamicEntityQuery owner, bool checkPaused) { QueryEntry.QueryFlags flags; _owner = owner; + _checkPaused = checkPaused; if (_owner._entries.Length == 0) { @@ -231,7 +233,9 @@ public bool MoveNext(out EntityUid ent, in Span output) if (output.Length != _owner.OutputCount) ThrowBadLength( _owner.OutputCount, output.Length); - ref var spanEntry = ref MemoryMarshal.GetReference(output); + // 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) @@ -239,9 +243,14 @@ public bool MoveNext(out EntityUid ent, in Span output) if (!_lead.MoveNext()) return false; + ref var spanEntry = ref span; + ent = _lead.Current.Key; spanEntry = _lead.Current.Value; + if (_checkPaused && meta[ent].EntityPaused) + continue; // Oops, paused. + if (spanEntry.Deleted) continue; // Nevermind, move along. From 5b7d9f4a3413a1a057d79d8cf8c72c5d14f2c80a Mon Sep 17 00:00:00 2001 From: kaylie Date: Tue, 10 Mar 2026 21:34:36 +0100 Subject: [PATCH 32/77] smal refactor --- .../GameObjects/DynamicEntityQuery.cs | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/Robust.Shared/GameObjects/DynamicEntityQuery.cs b/Robust.Shared/GameObjects/DynamicEntityQuery.cs index 02b839c10fd..0c0b54a3cc3 100644 --- a/Robust.Shared/GameObjects/DynamicEntityQuery.cs +++ b/Robust.Shared/GameObjects/DynamicEntityQuery.cs @@ -26,23 +26,23 @@ public QueryEntry(Dictionary dict, QueryFlags flags) Dict = dict; 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, - } + [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; @@ -92,8 +92,8 @@ public bool TryGet(EntityUid ent, in Span output) { 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 & QueryEntry.QueryFlags.Without) != 0)) - && (entryRef.Flags & QueryEntry.QueryFlags.Optional) == 0) + if ((exists ^ ((entryRef.Flags & QueryFlags.Without) != 0)) + && (entryRef.Flags & QueryFlags.Optional) == 0) return false; // Increment our index.. @@ -130,8 +130,8 @@ public bool Matches(EntityUid ent) 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 & QueryEntry.QueryFlags.Without) != 0)) - && (entryRef.Flags & QueryEntry.QueryFlags.Optional) == 0) + if ((exists ^ ((entryRef.Flags & QueryFlags.Without) != 0)) + && (entryRef.Flags & QueryFlags.Optional) == 0) return false; // and increment our index into the tails array, too. @@ -170,8 +170,8 @@ private bool TryGetAfterFirst(EntityUid ent, ref IComponent? spanEntry) { 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 & QueryEntry.QueryFlags.Without) != 0)) - && (entryRef.Flags & QueryEntry.QueryFlags.Optional) == 0) + if ((exists ^ ((entryRef.Flags & QueryFlags.Without) != 0)) + && (entryRef.Flags & QueryFlags.Optional) == 0) return false; // Increment our index.. @@ -200,20 +200,20 @@ public struct Enumerator internal Enumerator(DynamicEntityQuery owner, bool checkPaused) { - QueryEntry.QueryFlags flags; + QueryFlags flags; _owner = owner; _checkPaused = checkPaused; if (_owner._entries.Length == 0) { - flags = QueryEntry.QueryFlags.None; + flags = QueryFlags.None; } else { flags = _owner._entries[0].Flags; } - if (flags != QueryEntry.QueryFlags.None) + if (flags != QueryFlags.None) { throw new NotSupportedException( "Query enumerators do not support optional or excluded first components."); From 9069facc8b1daa226eb031c8a104842355a33702 Mon Sep 17 00:00:00 2001 From: kaylie Date: Tue, 10 Mar 2026 23:23:56 +0100 Subject: [PATCH 33/77] Extra EntityQuerys for 2-4 components. --- Robust.Shared/GameObjects/Docs.xml | 37 +++ .../GameObjects/DynamicEntityQuery.cs | 90 ++++++- .../GameObjects/EntityManager.Components.cs | 21 +- .../GameObjects/EntityManager.Queries.cs | 65 +++++ Robust.Shared/GameObjects/EntityQuery2.cs | 223 ++++++++++++++++ Robust.Shared/GameObjects/EntityQuery3.cs | 231 +++++++++++++++++ Robust.Shared/GameObjects/EntityQuery4.cs | 237 ++++++++++++++++++ .../GameObjects/IEntityManager.Components.cs | 22 +- .../GameObjects/IEntityManager.Queries.cs | 62 +++++ 9 files changed, 943 insertions(+), 45 deletions(-) create mode 100644 Robust.Shared/GameObjects/EntityManager.Queries.cs create mode 100644 Robust.Shared/GameObjects/EntityQuery2.cs create mode 100644 Robust.Shared/GameObjects/EntityQuery3.cs create mode 100644 Robust.Shared/GameObjects/EntityQuery4.cs create mode 100644 Robust.Shared/GameObjects/IEntityManager.Queries.cs diff --git a/Robust.Shared/GameObjects/Docs.xml b/Robust.Shared/GameObjects/Docs.xml index 6eaf60eb8b2..0bcce0751b3 100644 --- a/Robust.Shared/GameObjects/Docs.xml +++ b/Robust.Shared/GameObjects/Docs.xml @@ -11,4 +11,41 @@ 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 index 0c0b54a3cc3..bd30dd4211a 100644 --- a/Robust.Shared/GameObjects/DynamicEntityQuery.cs +++ b/Robust.Shared/GameObjects/DynamicEntityQuery.cs @@ -15,17 +15,11 @@ public readonly struct DynamicEntityQuery /// /// Information on a query item, describing how to handle it. /// - internal readonly struct QueryEntry + internal readonly struct QueryEntry(Dictionary dict, QueryFlags flags) { - public readonly Dictionary Dict; + public readonly Dictionary Dict = dict; - public readonly QueryFlags Flags; - - public QueryEntry(Dictionary dict, QueryFlags flags) - { - Dict = dict; - Flags = flags; - } + public readonly QueryFlags Flags = flags; } [Flags] @@ -46,7 +40,7 @@ public enum QueryFlags } private readonly QueryEntry[] _entries; - private readonly Dictionary _metaData; + private readonly Dictionary _metaData; /// /// The number of components this query is set up to emit. @@ -56,7 +50,7 @@ public enum QueryFlags /// public int OutputCount => _entries.Length; - internal DynamicEntityQuery(QueryEntry[] entries, Dictionary metaData) + internal DynamicEntityQuery(QueryEntry[] entries, Dictionary metaData) { _entries = entries; _metaData = metaData; @@ -96,6 +90,64 @@ public bool TryGet(EntityUid ent, in Span output) && (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); @@ -134,6 +186,9 @@ public bool Matches(EntityUid ent) && (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); } @@ -174,6 +229,9 @@ private bool TryGetAfterFirst(EntityUid ent, ref IComponent? spanEntry) && (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); @@ -185,6 +243,11 @@ private bool TryGetAfterFirst(EntityUid ent, ref IComponent? spanEntry) 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. @@ -248,12 +311,15 @@ public bool MoveNext(out EntityUid ent, in Span output) ent = _lead.Current.Key; spanEntry = _lead.Current.Value; - if (_checkPaused && meta[ent].EntityPaused) + 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); diff --git a/Robust.Shared/GameObjects/EntityManager.Components.cs b/Robust.Shared/GameObjects/EntityManager.Components.cs index 3d6a7b4f14c..39af0caf5d2 100644 --- a/Robust.Shared/GameObjects/EntityManager.Components.cs +++ b/Robust.Shared/GameObjects/EntityManager.Components.cs @@ -1185,25 +1185,6 @@ private T CopyComponentInternal(EntityUid source, EntityUid target, T sourceC return component; } - 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 IEnumerable GetComponents(EntityUid uid) { @@ -1843,6 +1824,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; @@ -1922,6 +1904,7 @@ public void Dispose() /// /// IEntityManager.EntityQueryEnumerator<TComp1, ...>() /// + [Obsolete($"Use {nameof(EntityQuery<>)} and generic {nameof(IEntityManager.GetEntityQuery)}")] public struct EntityQueryEnumerator : IDisposable where TComp1 : IComponent { diff --git a/Robust.Shared/GameObjects/EntityManager.Queries.cs b/Robust.Shared/GameObjects/EntityManager.Queries.cs new file mode 100644 index 00000000000..77a1461992a --- /dev/null +++ b/Robust.Shared/GameObjects/EntityManager.Queries.cs @@ -0,0 +1,65 @@ +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++) + { + entries[i] = new(_entTraitDict[userEntries[i].Item1], userEntries[i].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/EntityQuery2.cs b/Robust.Shared/GameObjects/EntityQuery2.cs new file mode 100644 index 00000000000..1fa5f67851c --- /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. + /// + [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..35d9af230d4 --- /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. + /// + [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..472b5d56b11 --- /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. + /// + [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/IEntityManager.Components.cs b/Robust.Shared/GameObjects/IEntityManager.Components.cs index 9525c53ca31..fce35b314c9 100644 --- a/Robust.Shared/GameObjects/IEntityManager.Components.cs +++ b/Robust.Shared/GameObjects/IEntityManager.Components.cs @@ -404,13 +404,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. @@ -503,18 +496,18 @@ bool TryCopyComponent( /// 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); /// - /// + /// /// public CompRegistryEntityEnumerator CompRegistryQueryEnumerator(ComponentRegistry registry); + [Obsolete($"Use {nameof(GameObjects.EntityQuery<>)}.All and non-generic {nameof(IEntityManager.GetEntityQuery)}")] AllEntityQueryEnumerator AllEntityQueryEnumerator(Type comp); + [Obsolete($"Use {nameof(GameObjects.EntityQuery<>)}.All and generic {nameof(IEntityManager.GetEntityQuery)}")] AllEntityQueryEnumerator AllEntityQueryEnumerator() where TComp1 : IComponent; @@ -533,6 +526,7 @@ AllEntityQueryEnumerator AllEntityQueryEnumerato where TComp3 : IComponent where TComp4 : IComponent; + [Obsolete($"Use {nameof(GameObjects.EntityQuery<>)} and generic {nameof(IEntityManager.GetEntityQuery)}")] EntityQueryEnumerator EntityQueryEnumerator() where TComp1 : IComponent; @@ -565,7 +559,7 @@ EntityQueryEnumerator EntityQueryEnumeratorFirst required component. /// Second required component. /// The pairs of components from each entity that has the two required components. - [Obsolete($"Prefer {nameof(GameObjects.EntityQueryEnumerator<>)}")] + [Obsolete($"Prefer {nameof(GameObjects.EntityQuery<>)}")] IEnumerable<(TComp1, TComp2)> EntityQuery(bool includePaused = false) where TComp1 : IComponent where TComp2 : IComponent; @@ -577,7 +571,7 @@ EntityQueryEnumerator EntityQueryEnumeratorSecond required component. /// Third required component. /// The pairs of components from each entity that has the three required components. - [Obsolete($"Prefer {nameof(GameObjects.EntityQueryEnumerator<>)}")] + [Obsolete($"Prefer {nameof(GameObjects.EntityQuery<>)}")] IEnumerable<(TComp1, TComp2, TComp3)> EntityQuery(bool includePaused = false) where TComp1 : IComponent where TComp2 : IComponent @@ -591,7 +585,7 @@ EntityQueryEnumerator EntityQueryEnumeratorThird required component. /// Fourth required component. /// The pairs of components from each entity that has the four required components. - [Obsolete($"Prefer {nameof(GameObjects.EntityQueryEnumerator<>)}")] + [Obsolete($"Prefer {nameof(GameObjects.EntityQuery<>)}")] IEnumerable<(TComp1, TComp2, TComp3, TComp4)> EntityQuery(bool includePaused = false) where TComp1 : IComponent where TComp2 : IComponent 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; +} From f3053dd18c74ab2d6587565a0ea93ec026c91a3a Mon Sep 17 00:00:00 2001 From: kaylie Date: Tue, 10 Mar 2026 23:25:48 +0100 Subject: [PATCH 34/77] my beloved Obsolete. --- Robust.Shared/GameObjects/IEntityManager.Components.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Robust.Shared/GameObjects/IEntityManager.Components.cs b/Robust.Shared/GameObjects/IEntityManager.Components.cs index fce35b314c9..b4ab19f94d6 100644 --- a/Robust.Shared/GameObjects/IEntityManager.Components.cs +++ b/Robust.Shared/GameObjects/IEntityManager.Components.cs @@ -511,15 +511,18 @@ bool TryCopyComponent( AllEntityQueryEnumerator AllEntityQueryEnumerator() where TComp1 : IComponent; + [Obsolete($"Use {nameof(GameObjects.EntityQuery<>)}.All and generic {nameof(IEntityManager.GetEntityQuery)}")] AllEntityQueryEnumerator AllEntityQueryEnumerator() where TComp1 : IComponent where TComp2 : IComponent; + [Obsolete($"Use {nameof(GameObjects.EntityQuery<>)}.All and generic {nameof(IEntityManager.GetEntityQuery)}")] AllEntityQueryEnumerator AllEntityQueryEnumerator() where TComp1 : IComponent where TComp2 : IComponent where TComp3 : IComponent; + [Obsolete($"Use {nameof(GameObjects.EntityQuery<>)}.All and generic {nameof(IEntityManager.GetEntityQuery)}")] AllEntityQueryEnumerator AllEntityQueryEnumerator() where TComp1 : IComponent where TComp2 : IComponent @@ -530,15 +533,18 @@ AllEntityQueryEnumerator AllEntityQueryEnumerato EntityQueryEnumerator EntityQueryEnumerator() where TComp1 : IComponent; + [Obsolete($"Use {nameof(GameObjects.EntityQuery<>)} and generic {nameof(IEntityManager.GetEntityQuery)}")] EntityQueryEnumerator EntityQueryEnumerator() where TComp1 : IComponent where TComp2 : IComponent; + [Obsolete($"Use {nameof(GameObjects.EntityQuery<>)} and generic {nameof(IEntityManager.GetEntityQuery)}")] EntityQueryEnumerator EntityQueryEnumerator() where TComp1 : IComponent where TComp2 : IComponent where TComp3 : IComponent; + [Obsolete($"Use {nameof(GameObjects.EntityQuery<>)} and generic {nameof(IEntityManager.GetEntityQuery)}")] EntityQueryEnumerator EntityQueryEnumerator() where TComp1 : IComponent where TComp2 : IComponent From cae52c98335f377238176598607061034718e66b Mon Sep 17 00:00:00 2001 From: kaylie Date: Tue, 10 Mar 2026 23:33:44 +0100 Subject: [PATCH 35/77] Tests. --- .../GameObjects/IEntityManagerTests.cs | 35 +++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/Robust.Shared.IntegrationTests/GameObjects/IEntityManagerTests.cs b/Robust.Shared.IntegrationTests/GameObjects/IEntityManagerTests.cs index d168c7fe1a2..31485882d15 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,38 @@ public void SpawnEntity_PrototypeTransform_Works() Assert.That(newEnt, Is.Not.EqualTo(EntityUid.Invalid)); } + /// + /// The entity prototype can define field on the TransformComponent, just like any other component. + /// + [Test] + 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] public void ComponentCount_Works() { From 19c9be43beb4a5ca3059ad50ab1a0bc79b5ee451 Mon Sep 17 00:00:00 2001 From: kaylie Date: Tue, 10 Mar 2026 23:46:47 +0100 Subject: [PATCH 36/77] more test. --- .../GameObjects/IEntityManagerTests.cs | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/Robust.Shared.IntegrationTests/GameObjects/IEntityManagerTests.cs b/Robust.Shared.IntegrationTests/GameObjects/IEntityManagerTests.cs index 31485882d15..666f3309bf0 100644 --- a/Robust.Shared.IntegrationTests/GameObjects/IEntityManagerTests.cs +++ b/Robust.Shared.IntegrationTests/GameObjects/IEntityManagerTests.cs @@ -64,6 +64,100 @@ public void EntityQuery3_Works() } } + [Test] + 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(); + + var queryAll = entMan.GetDynamicQuery( + (typeof(TransformComponent), DynamicEntityQuery.QueryFlags.None), + (typeof(MetaDataComponent), DynamicEntityQuery.QueryFlags.None) + ); + + var queryAllAndMaps = entMan.GetDynamicQuery( + (typeof(TransformComponent), DynamicEntityQuery.QueryFlags.None), + (typeof(MetaDataComponent), DynamicEntityQuery.QueryFlags.None), + (typeof(MapComponent), DynamicEntityQuery.QueryFlags.Optional) + ); + + 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; + 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)); + + 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)); + Assert.That(mapCount, NUnit.Framework.Is.EqualTo(2)); + } + + 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); + Assert.That(entMan.HasComponent(ent), NUnit.Framework.Is.False); + } + } + } + + + Assert.That(queryNotMapsCount, NUnit.Framework.Is.EqualTo(2)); + } + [Test] public void ComponentCount_Works() { From 8bdebf55bfa079d5377b65cda6621af9cb5613b5 Mon Sep 17 00:00:00 2001 From: kaylie Date: Tue, 10 Mar 2026 23:57:56 +0100 Subject: [PATCH 37/77] Add new EntityQuery types to IoC. --- .../GameObjects/EntitySystemManager.cs | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) 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) From cf97f19fc75bb7618f7450e464ad21670eb73aca Mon Sep 17 00:00:00 2001 From: kaylie Date: Wed, 11 Mar 2026 21:21:24 +0100 Subject: [PATCH 38/77] Release notes. --- RELEASE-NOTES.md | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 4c55457d3e2..c66d4802195 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -39,7 +39,18 @@ END TEMPLATE--> ### New features -*None yet* +- `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,7 +58,12 @@ END TEMPLATE--> ### Other -*None yet* +- `EntityQueryEnumerator`, `EntityQueryEnumerator`, `EntityQueryEnumerator`, + `EntityQueryEnumerator`, `AllEntityQueryEnumerator`, `AllEntityQueryEnumerator`, + `AllEntityQueryEnumerator`, `AllEntityQueryEnumerator`, + and `ComponentQueryEnumerator` are now obsolete. +- The EntityManager methods `EntityQuery`, `EntityQuery`, `EntityQuery`, + and `EntityQuery` are now obsolete. ### Internal From 319055c3142ab9b7ed0a5327fd9b28097d04c5d2 Mon Sep 17 00:00:00 2001 From: kaylie Date: Wed, 11 Mar 2026 21:50:57 +0100 Subject: [PATCH 39/77] Poke From ecd214091720fdf377dbf7090daf5d7d276c3797 Mon Sep 17 00:00:00 2001 From: kaylie Date: Thu, 12 Mar 2026 12:16:45 +0100 Subject: [PATCH 40/77] review. --- Robust.Shared/GameObjects/EntityQuery2.cs | 2 +- Robust.Shared/GameObjects/EntityQuery3.cs | 2 +- Robust.Shared/GameObjects/EntityQuery4.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Robust.Shared/GameObjects/EntityQuery2.cs b/Robust.Shared/GameObjects/EntityQuery2.cs index 1fa5f67851c..6bae70b0dad 100644 --- a/Robust.Shared/GameObjects/EntityQuery2.cs +++ b/Robust.Shared/GameObjects/EntityQuery2.cs @@ -175,7 +175,7 @@ IEnumerator IEnumerable.GetEnumerator() /// /// Inline storage for the components we're enumerating. - /// Basically just working around the fact you can't stackalloc the span. + /// Basically just working around the fact you can't stackalloc the span, error CS0208. /// [InlineArray(2)] private struct ComponentArray diff --git a/Robust.Shared/GameObjects/EntityQuery3.cs b/Robust.Shared/GameObjects/EntityQuery3.cs index 35d9af230d4..3e464e375fe 100644 --- a/Robust.Shared/GameObjects/EntityQuery3.cs +++ b/Robust.Shared/GameObjects/EntityQuery3.cs @@ -183,7 +183,7 @@ IEnumerator IEnumerable.GetEnumerator() /// /// Inline storage for the components we're enumerating. - /// Basically just working around the fact you can't stackalloc the span. + /// Basically just working around the fact you can't stackalloc the span, error CS0208. /// [InlineArray(3)] private struct ComponentArray diff --git a/Robust.Shared/GameObjects/EntityQuery4.cs b/Robust.Shared/GameObjects/EntityQuery4.cs index 472b5d56b11..1834c276ab7 100644 --- a/Robust.Shared/GameObjects/EntityQuery4.cs +++ b/Robust.Shared/GameObjects/EntityQuery4.cs @@ -189,7 +189,7 @@ IEnumerator IEnumerable.GetEnumerator() /// /// Inline storage for the components we're enumerating. - /// Basically just working around the fact you can't stackalloc the span. + /// Basically just working around the fact you can't stackalloc the span, error CS0208. /// [InlineArray(4)] private struct ComponentArray From e73f27498a04ddec3edd098a488de49b9a37e21a Mon Sep 17 00:00:00 2001 From: kaylie Date: Thu, 12 Mar 2026 13:04:41 +0100 Subject: [PATCH 41/77] cleanup + docs + proxies + obsoletions. --- RELEASE-NOTES.md | 4 +- .../GameObjects/IEntityManagerTests.cs | 27 ++++++--- .../GameObjects/DynamicEntityQuery.cs | 44 ++++++++++++-- .../GameObjects/EntityManager.Components.cs | 4 ++ .../GameObjects/EntityManager.Queries.cs | 22 +++++-- .../GameObjects/EntitySystem.Proxy.Queries.cs | 57 +++++++++++++++++++ .../GameObjects/EntitySystem.Proxy.cs | 36 ++++-------- .../GameObjects/IEntityManager.Components.cs | 33 ++++++----- .../Toolshed/Commands/Entities/WithCommand.cs | 2 +- 9 files changed, 170 insertions(+), 59 deletions(-) create mode 100644 Robust.Shared/GameObjects/EntitySystem.Proxy.Queries.cs diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index c66d4802195..7b01e58e960 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -63,7 +63,9 @@ END TEMPLATE--> `AllEntityQueryEnumerator`, `AllEntityQueryEnumerator`, and `ComponentQueryEnumerator` are now obsolete. - The EntityManager methods `EntityQuery`, `EntityQuery`, `EntityQuery`, - and `EntityQuery` are now obsolete. + and `EntityQuery`, `EntityQueryEnumerator`, `AllEntityQueryEnumerator`, + `ComponentQueryEnumerator`, `AllComponentsList`, `AllEntityUids`, `AllEntityUids`, `AllEntities`, and `AllComponents` + are now obsolete. ### Internal diff --git a/Robust.Shared.IntegrationTests/GameObjects/IEntityManagerTests.cs b/Robust.Shared.IntegrationTests/GameObjects/IEntityManagerTests.cs index 666f3309bf0..b24f3595cd4 100644 --- a/Robust.Shared.IntegrationTests/GameObjects/IEntityManagerTests.cs +++ b/Robust.Shared.IntegrationTests/GameObjects/IEntityManagerTests.cs @@ -32,10 +32,11 @@ public void SpawnEntity_PrototypeTransform_Works() Assert.That(newEnt, Is.Not.EqualTo(EntityUid.Invalid)); } - /// - /// The entity prototype can define field on the TransformComponent, just like any other component. - /// [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(); @@ -65,6 +66,10 @@ public void EntityQuery3_Works() } [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(); @@ -75,17 +80,20 @@ public void DynamicQueryTest_OptionalWithout() 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), @@ -100,6 +108,7 @@ public void DynamicQueryTest_OptionalWithout() 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()); @@ -107,7 +116,7 @@ public void DynamicQueryTest_OptionalWithout() } } - Assert.That(queryAllCount, NUnit.Framework.Is.EqualTo(4)); + Assert.That(queryAllCount, NUnit.Framework.Is.EqualTo(4), "Expected to iterate all entities"); var queryAllAndMapsEnum = queryAllAndMaps.GetEnumerator(false); var queryAllAndMapsCount = 0; @@ -132,8 +141,8 @@ public void DynamicQueryTest_OptionalWithout() using (Assert.EnterMultipleScope()) { - Assert.That(queryAllAndMapsCount, NUnit.Framework.Is.EqualTo(4)); - Assert.That(mapCount, NUnit.Framework.Is.EqualTo(2)); + 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); @@ -148,14 +157,14 @@ public void DynamicQueryTest_OptionalWithout() { Assert.That(buffer[0], NUnit.Framework.Is.TypeOf()); Assert.That(buffer[1], NUnit.Framework.Is.TypeOf()); - Assert.That(buffer[2], NUnit.Framework.Is.Null); - Assert.That(entMan.HasComponent(ent), NUnit.Framework.Is.False); + 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)); + Assert.That(queryNotMapsCount, NUnit.Framework.Is.EqualTo(2), "Expected to only iterate non-maps."); } [Test] diff --git a/Robust.Shared/GameObjects/DynamicEntityQuery.cs b/Robust.Shared/GameObjects/DynamicEntityQuery.cs index bd30dd4211a..db568bd7a76 100644 --- a/Robust.Shared/GameObjects/DynamicEntityQuery.cs +++ b/Robust.Shared/GameObjects/DynamicEntityQuery.cs @@ -7,9 +7,44 @@ namespace Robust.Shared.GameObjects; /// -/// An internal, typeless version of entity queries. -/// This isn't enumerable, but works for an arbitrary set of components. +/// +/// 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 { /// @@ -254,9 +289,6 @@ public Enumerator GetEnumerator(bool checkPaused) /// public struct Enumerator { - // ReSharper disable once CollectionNeverUpdated.Local - private static readonly Dictionary EmptyDict = new(); - private readonly DynamicEntityQuery _owner; private readonly bool _checkPaused; private Dictionary.Enumerator _lead; @@ -335,7 +367,7 @@ public void Reset() { if (_owner._entries.Length == 0) { - _lead = EmptyDict.GetEnumerator(); + _lead = _owner._metaData.GetEnumerator(); } else { diff --git a/Robust.Shared/GameObjects/EntityManager.Components.cs b/Robust.Shared/GameObjects/EntityManager.Components.cs index 39af0caf5d2..00a43810504 100644 --- a/Robust.Shared/GameObjects/EntityManager.Components.cs +++ b/Robust.Shared/GameObjects/EntityManager.Components.cs @@ -2250,6 +2250,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 { @@ -2299,6 +2300,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 @@ -2359,6 +2361,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 @@ -2433,6 +2436,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.Queries.cs b/Robust.Shared/GameObjects/EntityManager.Queries.cs index 77a1461992a..64ce17f5972 100644 --- a/Robust.Shared/GameObjects/EntityManager.Queries.cs +++ b/Robust.Shared/GameObjects/EntityManager.Queries.cs @@ -5,7 +5,8 @@ namespace Robust.Shared.GameObjects; public partial class EntityManager { - public EntityQuery GetEntityQuery() where TComp1 : IComponent + public EntityQuery GetEntityQuery() + where TComp1 : IComponent { DebugTools.Assert(_entTraitArray.Length > CompIdx.ArrayIndex(), $"Unknown component: {typeof(TComp1).Name}"); @@ -30,13 +31,17 @@ public DynamicEntityQuery GetDynamicQuery(params (Type, DynamicEntityQuery.Query for (var i = 0; i < userEntries.Length; i++) { - entries[i] = new(_entTraitDict[userEntries[i].Item1], userEntries[i].Item2); + 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 + public EntityQuery GetEntityQuery() + where TComp1 : IComponent + where TComp2 : IComponent { var dyQuery = GetDynamicQuery((typeof(TComp1), DynamicEntityQuery.QueryFlags.None), (typeof(TComp2), DynamicEntityQuery.QueryFlags.None)); @@ -44,7 +49,10 @@ public EntityQuery GetEntityQuery() where TComp1 return new(dyQuery, this); } - public EntityQuery GetEntityQuery() where TComp1 : IComponent where TComp2 : IComponent where TComp3 : IComponent + 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), @@ -53,7 +61,11 @@ public EntityQuery GetEntityQuery GetEntityQuery() where TComp1 : IComponent where TComp2 : IComponent where TComp3 : IComponent where TComp4 : IComponent + 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), 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..928ea37d83b 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 diff --git a/Robust.Shared/GameObjects/IEntityManager.Components.cs b/Robust.Shared/GameObjects/IEntityManager.Components.cs index b4ab19f94d6..b7bdd4a90b7 100644 --- a/Robust.Shared/GameObjects/IEntityManager.Components.cs +++ b/Robust.Shared/GameObjects/IEntityManager.Components.cs @@ -464,36 +464,42 @@ 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.")] @@ -504,47 +510,47 @@ bool TryCopyComponent( ///
public CompRegistryEntityEnumerator CompRegistryQueryEnumerator(ComponentRegistry registry); - [Obsolete($"Use {nameof(GameObjects.EntityQuery<>)}.All and non-generic {nameof(IEntityManager.GetEntityQuery)}")] + [Obsolete($"Prefer generic {nameof(GetEntityQuery)} and dependencies on {nameof(GameObjects.EntityQuery<>)}, using the All property")] AllEntityQueryEnumerator AllEntityQueryEnumerator(Type comp); - [Obsolete($"Use {nameof(GameObjects.EntityQuery<>)}.All and generic {nameof(IEntityManager.GetEntityQuery)}")] + [Obsolete($"Prefer generic {nameof(GetEntityQuery)} and dependencies on {nameof(GameObjects.EntityQuery<>)}, using the All property")] AllEntityQueryEnumerator AllEntityQueryEnumerator() where TComp1 : IComponent; - [Obsolete($"Use {nameof(GameObjects.EntityQuery<>)}.All and generic {nameof(IEntityManager.GetEntityQuery)}")] + [Obsolete($"Prefer generic {nameof(GetEntityQuery)} and dependencies on {nameof(GameObjects.EntityQuery<>)}, using the All property")] AllEntityQueryEnumerator AllEntityQueryEnumerator() where TComp1 : IComponent where TComp2 : IComponent; - [Obsolete($"Use {nameof(GameObjects.EntityQuery<>)}.All and generic {nameof(IEntityManager.GetEntityQuery)}")] + [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($"Use {nameof(GameObjects.EntityQuery<>)}.All and generic {nameof(IEntityManager.GetEntityQuery)}")] + [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($"Use {nameof(GameObjects.EntityQuery<>)} and generic {nameof(IEntityManager.GetEntityQuery)}")] + [Obsolete($"Prefer generic {nameof(GetEntityQuery)} and dependencies on {nameof(GameObjects.EntityQuery<>)}")] EntityQueryEnumerator EntityQueryEnumerator() where TComp1 : IComponent; - [Obsolete($"Use {nameof(GameObjects.EntityQuery<>)} and generic {nameof(IEntityManager.GetEntityQuery)}")] + [Obsolete($"Prefer generic {nameof(GetEntityQuery)} and dependencies on {nameof(GameObjects.EntityQuery<>)}")] EntityQueryEnumerator EntityQueryEnumerator() where TComp1 : IComponent where TComp2 : IComponent; - [Obsolete($"Use {nameof(GameObjects.EntityQuery<>)} and generic {nameof(IEntityManager.GetEntityQuery)}")] + [Obsolete($"Prefer generic {nameof(GetEntityQuery)} and dependencies on {nameof(GameObjects.EntityQuery<>)}")] EntityQueryEnumerator EntityQueryEnumerator() where TComp1 : IComponent where TComp2 : IComponent where TComp3 : IComponent; - [Obsolete($"Use {nameof(GameObjects.EntityQuery<>)} and generic {nameof(IEntityManager.GetEntityQuery)}")] + [Obsolete($"Prefer generic {nameof(GetEntityQuery)} and dependencies on {nameof(GameObjects.EntityQuery<>)}")] EntityQueryEnumerator EntityQueryEnumerator() where TComp1 : IComponent where TComp2 : IComponent @@ -556,7 +562,7 @@ EntityQueryEnumerator EntityQueryEnumerator /// A trait or type of a component to retrieve. /// All components that have the specified type. - [Obsolete($"Prefer {nameof(GameObjects.EntityQuery<>)}")] + [Obsolete($"Prefer {nameof(GetEntityQuery)} and dependencies on {nameof(GameObjects.EntityQuery<>)}")] IEnumerable EntityQuery(bool includePaused = false) where T: IComponent; ///
@@ -565,7 +571,7 @@ EntityQueryEnumerator EntityQueryEnumeratorFirst required component. /// Second required component. /// The pairs of components from each entity that has the two required components. - [Obsolete($"Prefer {nameof(GameObjects.EntityQuery<>)}")] + [Obsolete($"Prefer {nameof(GetEntityQuery)} and dependencies on {nameof(GameObjects.EntityQuery<>)}")] IEnumerable<(TComp1, TComp2)> EntityQuery(bool includePaused = false) where TComp1 : IComponent where TComp2 : IComponent; @@ -577,7 +583,7 @@ EntityQueryEnumerator EntityQueryEnumeratorSecond required component. /// Third required component. /// The pairs of components from each entity that has the three required components. - [Obsolete($"Prefer {nameof(GameObjects.EntityQuery<>)}")] + [Obsolete($"Prefer {nameof(GetEntityQuery)} and dependencies on {nameof(GameObjects.EntityQuery<>)}")] IEnumerable<(TComp1, TComp2, TComp3)> EntityQuery(bool includePaused = false) where TComp1 : IComponent where TComp2 : IComponent @@ -591,7 +597,7 @@ EntityQueryEnumerator EntityQueryEnumeratorThird required component. /// Fourth required component. /// The pairs of components from each entity that has the four required components. - [Obsolete($"Prefer {nameof(GameObjects.EntityQuery<>)}")] + [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 @@ -604,6 +610,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/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)); } From 98bc5937df1fb248e614d08fc244ae6aafd1fcae Mon Sep 17 00:00:00 2001 From: kaylie Date: Thu, 12 Mar 2026 13:11:31 +0100 Subject: [PATCH 42/77] Add a net to catch misuse of DynamicEntityQuery. --- .../GameObjects/DynamicEntityQuery.cs | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/Robust.Shared/GameObjects/DynamicEntityQuery.cs b/Robust.Shared/GameObjects/DynamicEntityQuery.cs index db568bd7a76..1c00bc6cd8b 100644 --- a/Robust.Shared/GameObjects/DynamicEntityQuery.cs +++ b/Robust.Shared/GameObjects/DynamicEntityQuery.cs @@ -1,6 +1,8 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -292,6 +294,12 @@ 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. + private readonly Dictionary.Enumerator[] _mines; +#endif internal Enumerator(DynamicEntityQuery owner, bool checkPaused) { @@ -314,9 +322,23 @@ internal Enumerator(DynamicEntityQuery owner, bool checkPaused) "Query enumerators do not support optional or excluded first components."); } +#if DEBUG + _mines = _owner._entries.Select(x => x.Dict.GetEnumerator()).ToArray(); +#endif + Reset(); } +#if DEBUG + private void StepOnMines() + { + foreach (var mine in _mines) + { + ((IEnumerator)mine).Reset(); + } + } +#endif + /// /// Attempts to find the next entity in the query iterator. /// @@ -328,6 +350,10 @@ 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; @@ -373,6 +399,10 @@ public void Reset() { _lead = _owner._entries[0].Dict.GetEnumerator(); } + +#if DEBUG + StepOnMines(); +#endif } } From fdb1c6ad42b25a606ad620fcdef743b8dd9d0fd9 Mon Sep 17 00:00:00 2001 From: kaylie Date: Thu, 12 Mar 2026 13:12:44 +0100 Subject: [PATCH 43/77] justification --- Robust.Shared/GameObjects/DynamicEntityQuery.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Robust.Shared/GameObjects/DynamicEntityQuery.cs b/Robust.Shared/GameObjects/DynamicEntityQuery.cs index 1c00bc6cd8b..06987b1ea29 100644 --- a/Robust.Shared/GameObjects/DynamicEntityQuery.cs +++ b/Robust.Shared/GameObjects/DynamicEntityQuery.cs @@ -297,7 +297,8 @@ public struct Enumerator #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. + // 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 From cdf842fedac05d36b32b20f563f1a13fbb011f2c Mon Sep 17 00:00:00 2001 From: kaylie Date: Thu, 12 Mar 2026 13:19:45 +0100 Subject: [PATCH 44/77] Friendlier exception + catch edgecase. --- .../GameObjects/DynamicEntityQuery.cs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/Robust.Shared/GameObjects/DynamicEntityQuery.cs b/Robust.Shared/GameObjects/DynamicEntityQuery.cs index 06987b1ea29..d42b8b09281 100644 --- a/Robust.Shared/GameObjects/DynamicEntityQuery.cs +++ b/Robust.Shared/GameObjects/DynamicEntityQuery.cs @@ -324,7 +324,10 @@ internal Enumerator(DynamicEntityQuery owner, bool checkPaused) } #if DEBUG - _mines = _owner._entries.Select(x => x.Dict.GetEnumerator()).ToArray(); + if (_owner._entries.Length > 0) + _mines = _owner._entries.Select(x => x.Dict.GetEnumerator()).ToArray(); + else + _mines = [_owner._metaData.GetEnumerator()]; #endif Reset(); @@ -333,9 +336,18 @@ internal Enumerator(DynamicEntityQuery owner, bool checkPaused) #if DEBUG private void StepOnMines() { - foreach (var mine in _mines) + try + { + foreach (var mine in _mines) + { + ((IEnumerator)mine).Reset(); + } + } + catch (InvalidOperationException versionError) { - ((IEnumerator)mine).Reset(); + 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 From 8b56561066db8afd91d1a9e99c1269fdd07bc971 Mon Sep 17 00:00:00 2001 From: kaylie Date: Thu, 12 Mar 2026 13:36:59 +0100 Subject: [PATCH 45/77] Poke From 98b9b1883337f6ec1dc87651822a1c9756e2bc52 Mon Sep 17 00:00:00 2001 From: kaylie Date: Sun, 15 Mar 2026 20:06:48 +0100 Subject: [PATCH 46/77] Start on CommandBuffer. --- .../CommandConstructionTests.cs | 42 ++++++++ .../CommandBuffers/CommandBuffer.cs | 91 ++++++++++++++++++ .../CommandBufferEntry.QueuedAction.cs | 85 ++++++++++++++++ .../CommandBuffers/CommandBufferEntry.cs | 96 +++++++++++++++++++ .../EntityManager.CommandBuffers.cs | 41 ++++++++ Robust.Shared/GameObjects/EntityManager.cs | 4 + .../IEntityManager.CommandBuffers.cs | 18 ++++ Robust.Shared/GameObjects/IEntityManager.cs | 2 +- .../GameObjects/IRemoteEntityManager.cs | 22 +++++ 9 files changed, 400 insertions(+), 1 deletion(-) create mode 100644 Robust.Shared.IntegrationTests/GameObjects/CommandBuffers/CommandConstructionTests.cs create mode 100644 Robust.Shared/GameObjects/CommandBuffers/CommandBuffer.cs create mode 100644 Robust.Shared/GameObjects/CommandBuffers/CommandBufferEntry.QueuedAction.cs create mode 100644 Robust.Shared/GameObjects/CommandBuffers/CommandBufferEntry.cs create mode 100644 Robust.Shared/GameObjects/EntityManager.CommandBuffers.cs create mode 100644 Robust.Shared/GameObjects/IEntityManager.CommandBuffers.cs create mode 100644 Robust.Shared/GameObjects/IRemoteEntityManager.cs 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/GameObjects/CommandBuffers/CommandBuffer.cs b/Robust.Shared/GameObjects/CommandBuffers/CommandBuffer.cs new file mode 100644 index 00000000000..cbf267a11b9 --- /dev/null +++ b/Robust.Shared/GameObjects/CommandBuffers/CommandBuffer.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Robust.Shared.Collections; +using Robust.Shared.IoC; + +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. +/// +/// +public sealed class CommandBuffer : IDisposable +{ + /// + /// The maximum capacity a command buffer is allowed to have when returned. + /// + private const int MaximumReturnCapacity = 128; + + private readonly IEntityManager _entMan; + + private ValueList _entries = []; + 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(); + } + + private ref CommandBufferEntry NextEntry() + { + _entries.Add(default); + + return ref _entries[^1]; + } + + /// + /// 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. + public void InvokeAction(Action action, T context) + where T : class + { + ref var entry = ref NextEntry(); + CommandBufferEntry.QueuedActionT(action, context, out entry); + } + + /// + /// 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 void InvokeAction(Action action, T context, EntityUid target) + where T : class + { + ref var entry = ref NextEntry(); + CommandBufferEntry.QueuedActionTEnt(action, context, target, out entry); + } + + /// + /// 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..f85883cd404 --- /dev/null +++ b/Robust.Shared/GameObjects/CommandBuffers/CommandBufferEntry.cs @@ -0,0 +1,96 @@ +using System; +using System.Reflection; +using System.Runtime.InteropServices; +using Robust.Shared.Utility; + +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. +/// +/// +[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; + + public CmdKind Kind => (CmdKind)(Command & 0xFF); + + 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 perf sensitive. + 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; + } + + /// + /// The kind of command in an entry. + /// + public enum CmdKind : byte + { + /// + /// An invalid command. + /// + Invalid = 0, + + /// + /// A command for running an Action<T>. + /// + /// unused Field1; + /// Action<object> Field2; + /// object Field3; + /// + /// + QueuedActionT, + + /// + /// A command for running an Action<T, EntityUid>. + /// + /// EntityUid Field1; + /// Action<T, EntityUid> Field2; + /// T Field3; + /// + /// + QueuedActionTEnt, + + /// + /// Handles deleting an entity. + /// + /// EntityUid Field1; + /// unused Field2; + /// unused Field3; + /// + /// + DeleteEntity, + } +} diff --git a/Robust.Shared/GameObjects/EntityManager.CommandBuffers.cs b/Robust.Shared/GameObjects/EntityManager.CommandBuffers.cs new file mode 100644 index 00000000000..a6d49072cc6 --- /dev/null +++ b/Robust.Shared/GameObjects/EntityManager.CommandBuffers.cs @@ -0,0 +1,41 @@ +using Microsoft.Extensions.ObjectPool; +using Robust.Shared.GameObjects.CommandBuffers; +using Robust.Shared.IoC; + +namespace Robust.Shared.GameObjects; + +public abstract partial class EntityManager +{ + public DefaultObjectPool CommandBufferPool { get; private set; } = default!; + public void ApplyCommandBuffer(CommandBuffer buffer) + { + throw new System.NotImplementedException(); + } + + private sealed class CommandBufferPolicy(IDependencyCollection collection) : IPooledObjectPolicy + { + public CommandBuffer Create() + { + return new CommandBuffer(collection); + } + + public bool Return(CommandBuffer obj) + { + if (obj.Capacity > 128) + return false; // Get rid of it, too chonky. + + obj.Clear(); + return true; + } + } + + public CommandBuffer GetCommandBuffer() + { + throw new System.NotImplementedException(); + } + + EntityUid IRemoteEntityManager.GetUnusedEntityUid() + { + return GenerateEntityUid(); + } +} diff --git a/Robust.Shared/GameObjects/EntityManager.cs b/Robust.Shared/GameObjects/EntityManager.cs index 4c8e1d5e84a..f61320f3dd4 100644 --- a/Robust.Shared/GameObjects/EntityManager.cs +++ b/Robust.Shared/GameObjects/EntityManager.cs @@ -8,6 +8,7 @@ using Robust.Shared.Console; using Robust.Shared.Containers; using Robust.Shared.GameStates; +using Robust.Shared.IoC; using Robust.Shared.Log; using Robust.Shared.Map; using Robust.Shared.Map.Components; @@ -35,6 +36,7 @@ public abstract partial class EntityManager : IEntityManager { #region Dependencies + [IoC.Dependency] private readonly IDependencyCollection _dependencyCollection = default!; [IoC.Dependency] protected readonly IPrototypeManager PrototypeManager = default!; [IoC.Dependency] protected readonly ILogManager LogManager = default!; [IoC.Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; @@ -142,6 +144,8 @@ public virtual void Initialize() if (Initialized) throw new InvalidOperationException("Initialize() called multiple times"); + CommandBufferPool = new(new CommandBufferPolicy(_dependencyCollection), 64); + EventBusInternal = new EntityEventBus(this, _reflection); InitializeComponents(); diff --git a/Robust.Shared/GameObjects/IEntityManager.CommandBuffers.cs b/Robust.Shared/GameObjects/IEntityManager.CommandBuffers.cs new file mode 100644 index 00000000000..280dcb794e9 --- /dev/null +++ b/Robust.Shared/GameObjects/IEntityManager.CommandBuffers.cs @@ -0,0 +1,18 @@ +using Microsoft.Extensions.ObjectPool; +using Robust.Shared.GameObjects.CommandBuffers; + +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); +} 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..c7930107261 --- /dev/null +++ b/Robust.Shared/GameObjects/IRemoteEntityManager.cs @@ -0,0 +1,22 @@ +using Robust.Shared.GameObjects.CommandBuffers; + +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(); + + /// + /// 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(); +} + From 0f1a86780589346640ff1d12cd99061d073b52d7 Mon Sep 17 00:00:00 2001 From: kaylie Date: Sun, 15 Mar 2026 21:10:16 +0100 Subject: [PATCH 47/77] scaffolding. --- .../CommandBuffer.InvokeAction.cs | 37 +++++++ .../CommandBuffers/CommandBuffer.cs | 62 ++++++----- .../CommandBuffers/CommandBufferEntry.cs | 101 ++++++++++++++++-- .../EntityManager.CommandBuffers.cs | 2 + .../GameObjects/IRemoteEntityManager.cs | 1 - 5 files changed, 163 insertions(+), 40 deletions(-) create mode 100644 Robust.Shared/GameObjects/CommandBuffers/CommandBuffer.InvokeAction.cs diff --git a/Robust.Shared/GameObjects/CommandBuffers/CommandBuffer.InvokeAction.cs b/Robust.Shared/GameObjects/CommandBuffers/CommandBuffer.InvokeAction.cs new file mode 100644 index 00000000000..0a31f2dadb3 --- /dev/null +++ b/Robust.Shared/GameObjects/CommandBuffers/CommandBuffer.InvokeAction.cs @@ -0,0 +1,37 @@ +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. + public void InvokeAction(Action action, T context) + where T : class + { + CommandBufferEntry.QueuedActionT(action, context, out NextEntry()); + } + + /// + /// 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 void InvokeAction(Action action, T context, EntityUid target) + where T : class + { + CommandBufferEntry.QueuedActionTEnt(action, context, target, out NextEntry()); + } +} diff --git a/Robust.Shared/GameObjects/CommandBuffers/CommandBuffer.cs b/Robust.Shared/GameObjects/CommandBuffers/CommandBuffer.cs index cbf267a11b9..a9b55fbf061 100644 --- a/Robust.Shared/GameObjects/CommandBuffers/CommandBuffer.cs +++ b/Robust.Shared/GameObjects/CommandBuffers/CommandBuffer.cs @@ -1,27 +1,34 @@ using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; +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 class CommandBuffer : IDisposable +public sealed partial class CommandBuffer : IDisposable { - /// - /// The maximum capacity a command buffer is allowed to have when returned. - /// - private const int MaximumReturnCapacity = 128; - private readonly IEntityManager _entMan; + private readonly IPrototypeManager _protoMan; + /// + /// The underlying list of entries in the buffer. + /// private ValueList _entries = []; + /// + /// The capacity of the underlying entry collection, for object pooling usage. + /// internal int Capacity => _entries.Capacity; /// @@ -31,6 +38,7 @@ public sealed class CommandBuffer : IDisposable internal CommandBuffer(IDependencyCollection collection) { _entMan = collection.Resolve(); + _protoMan = collection.Resolve(); } private ref CommandBufferEntry NextEntry() @@ -41,36 +49,26 @@ private ref CommandBufferEntry NextEntry() } /// - /// Adds an Action<T> invocation to the buffer. + /// 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. /// - /// - /// 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. - public void InvokeAction(Action action, T context) - where T : class + /// The created sub-buffer. + public CommandBuffer CreateSubBuffer() { - ref var entry = ref NextEntry(); - CommandBufferEntry.QueuedActionT(action, context, out entry); + var subBuffer = _entMan.GetCommandBuffer(); + + CommandBufferEntry.SubBuffer(subBuffer, out NextEntry()); + + return subBuffer; } /// - /// Adds an Action<T, EntityUid> invocation to the buffer. + /// Adds an entity deletion 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 void InvokeAction(Action action, T context, EntityUid target) - where T : class + /// The entity to delete immediately. + public void DeleteEntity(EntityUid target) { - ref var entry = ref NextEntry(); - CommandBufferEntry.QueuedActionTEnt(action, context, target, out entry); + CommandBufferEntry.DeleteEntity(target, out NextEntry()); } /// diff --git a/Robust.Shared/GameObjects/CommandBuffers/CommandBufferEntry.cs b/Robust.Shared/GameObjects/CommandBuffers/CommandBufferEntry.cs index f85883cd404..cfc1c5b3473 100644 --- a/Robust.Shared/GameObjects/CommandBuffers/CommandBufferEntry.cs +++ b/Robust.Shared/GameObjects/CommandBuffers/CommandBufferEntry.cs @@ -14,6 +14,11 @@ namespace Robust.Shared.GameObjects.CommandBuffers; /// 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 @@ -26,7 +31,20 @@ internal partial struct CommandBufferEntry 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() { @@ -53,13 +71,26 @@ public static void DeleteEntity(EntityUid target, out CommandBufferEntry entry) 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; + } + /// /// The kind of command in an entry. /// public enum CmdKind : byte { /// - /// An invalid command. + /// An invalid command. Causes to throw. /// Invalid = 0, @@ -67,8 +98,8 @@ public enum CmdKind : byte /// A command for running an Action<T>. /// /// unused Field1; - /// Action<object> Field2; - /// object Field3; + /// Action<T> Action; + /// T Context; /// /// QueuedActionT, @@ -76,21 +107,77 @@ public enum CmdKind : byte /// /// A command for running an Action<T, EntityUid>. /// - /// EntityUid Field1; - /// Action<T, EntityUid> Field2; - /// T Field3; + /// 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 Field1; + /// EntityUid Target; /// unused Field2; /// unused Field3; /// /// DeleteEntity, + + /// + /// Handles spawning a map with a builder. + /// + /// MapId ReservedMapId; + /// MapEntityBuilder EntityBuilder; + /// unused Field3; + /// + /// + SpawnMap, + + /// + /// Handles spawning an entity with a builder. + /// + /// unused Field1; + /// EntityBuilder EntityBuilder; + /// unused Field3; + /// + /// + SpawnEntity, + + /// + /// Handles adding components to an entity. + /// The fields for this are a bit funny, it can either contain a List of components, or up + /// to two direct references to components. + /// + /// EntityUid Target; + /// IComponent | List<IComponent> Components; + /// IComponent? ExtraComponent; + /// + /// + AddComponents, + + /// + /// Handles removing components from an entity. + /// Similar to AddComponents, this either contains a List of Type, or up to two direct references to + /// Type. + /// + /// EntityUid Target; + /// Type | List<Type> Components; + /// Type? ExtraComponent; + /// + /// + RemoveComponents, + + } } diff --git a/Robust.Shared/GameObjects/EntityManager.CommandBuffers.cs b/Robust.Shared/GameObjects/EntityManager.CommandBuffers.cs index a6d49072cc6..39c4bf1cf69 100644 --- a/Robust.Shared/GameObjects/EntityManager.CommandBuffers.cs +++ b/Robust.Shared/GameObjects/EntityManager.CommandBuffers.cs @@ -27,6 +27,8 @@ public bool Return(CommandBuffer obj) obj.Clear(); return true; } + + } public CommandBuffer GetCommandBuffer() diff --git a/Robust.Shared/GameObjects/IRemoteEntityManager.cs b/Robust.Shared/GameObjects/IRemoteEntityManager.cs index c7930107261..2774b2c6663 100644 --- a/Robust.Shared/GameObjects/IRemoteEntityManager.cs +++ b/Robust.Shared/GameObjects/IRemoteEntityManager.cs @@ -19,4 +19,3 @@ public interface IRemoteEntityManager /// A completely unallocated, now reserved entity id. internal EntityUid GetUnusedEntityUid(); } - From 0cadbf825bc827f6858cfe94800cb8959f4902b1 Mon Sep 17 00:00:00 2001 From: kaylie Date: Mon, 16 Mar 2026 11:09:30 +0100 Subject: [PATCH 48/77] save more work. --- .../CommandBuffers/CommandBufferEntry.cs | 4 +- .../EntityBuilders/EntityBuilder.AddComp.cs | 209 ++++++++++++++++++ .../EntityBuilders/EntityBuilder.cs | 140 ++++++++++++ .../EntityManager.CommandBuffers.cs | 44 ++-- Robust.Shared/GameObjects/EntityManager.cs | 5 +- .../GameObjects/IEntityLoadContext.cs | 1 + .../IEntityManager.CommandBuffers.cs | 18 ++ .../GameObjects/IRemoteEntityManager.cs | 8 + 8 files changed, 411 insertions(+), 18 deletions(-) create mode 100644 Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.AddComp.cs create mode 100644 Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.cs diff --git a/Robust.Shared/GameObjects/CommandBuffers/CommandBufferEntry.cs b/Robust.Shared/GameObjects/CommandBuffers/CommandBufferEntry.cs index cfc1c5b3473..efb109f99bb 100644 --- a/Robust.Shared/GameObjects/CommandBuffers/CommandBufferEntry.cs +++ b/Robust.Shared/GameObjects/CommandBuffers/CommandBufferEntry.cs @@ -138,7 +138,7 @@ public enum CmdKind : byte /// Handles spawning a map with a builder. /// /// MapId ReservedMapId; - /// MapEntityBuilder EntityBuilder; + /// EntityBuilder EntityBuilder; /// unused Field3; /// /// @@ -177,7 +177,5 @@ public enum CmdKind : byte /// /// RemoveComponents, - - } } diff --git a/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.AddComp.cs b/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.AddComp.cs new file mode 100644 index 00000000000..9ae11636cc3 --- /dev/null +++ b/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.AddComp.cs @@ -0,0 +1,209 @@ +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() + { + if (!_entityComponents.TryAdd(typeof(T), _factory.GetComponent(CompIdx.Index()))) + { + throw new ArgumentException( + $"The component {_factory.GetComponentName()} already existed in the builder."); + } + + 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) + { + if (!_entityComponents.TryAdd(t, _factory.GetComponent(t))) + { + throw new ArgumentException( + $"The component {_factory.GetComponentName(t)} already existed in the builder."); + } + + 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 _)); + + 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(typeof(T), newComp)) + { + throw new ArgumentException( + $"The component {_factory.GetComponentName(component.GetType())} already existed in the builder."); + } + + 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.cs b/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.cs new file mode 100644 index 00000000000..4170ed9869e --- /dev/null +++ b/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using Robust.Shared.IoC; +using Robust.Shared.Map.Components; +using Robust.Shared.Prototypes; +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. +/// +/// +public sealed partial class EntityBuilder +{ + private readonly IComponentFactory _factory; + private readonly IPrototypeManager _protoMan; + private readonly ISerializationManager _serMan; + + private void Foo(IRemoteEntityManager entMan) + { + var builder = entMan.BlankEntityBuilder() + .AddComp() + .AddComp(new MapGridComponent + { + CanSplit = false + }); + } + + /// + /// The EntityUid we have reserved. + /// + private readonly EntityUid _reservedId; + + /// + /// The components we'll be adding to the entity we construct. + /// + private readonly Dictionary _entityComponents; + + internal EntityBuilder(IDependencyCollection collection, EntityUid reservedId) + { + _factory = collection.Resolve(); + _protoMan = collection.Resolve(); + _serMan = collection.Resolve(); + _reservedId = reservedId; + _entityComponents = new(); + } + + internal static EntityBuilder BlankEntity( + IDependencyCollection collection, + EntityUid reservedId, + IEntityLoadContext? loadContext) + { + var self = new EntityBuilder(collection, reservedId); + + self.InitializeMinimalEntity(loadContext); + + return self; + } + + /// + /// Creates the bare minimal spawnable entity with metadata and a transform. + /// + /// The load context to use, if any. + private void InitializeMinimalEntity(IEntityLoadContext? context = null) + { + if (context?.TryGetComponent(_factory, out MetaDataComponent? meta) ?? false) + { + CopyComp(meta, context as ISerializationContext); + } + else + { + AddComp(); + } + + if (context?.TryGetComponent(_factory, out TransformComponent? xform) ?? false) + { + CopyComp(xform, context as ISerializationContext); + } + else + { + AddComp(); + } + } + + /// + /// Initializes a command buffer from a prototype, doing most of entity setup aside from actually + /// constructing an entity to apply the components. + /// + /// The prototype to construct from. + /// The load context to use. + private void InitializeFromPrototype(EntProtoId entityProtoId, IEntityLoadContext? context) + { + var entityProto = _protoMan.Index(entityProtoId); + + var meta = new MetaDataComponent(); + + if (context?.TryGetComponent(_factory, out MetaDataComponent? overrideMeta) ?? false) + { + _serMan.CopyTo(overrideMeta, + ref meta, + context as ISerializationContext, + notNullableOverride: true); + } + + // Ensure we set up our metadata correctly, i.e. set the prototype so no explosions. + meta._entityPrototype = entityProto; + AddComp(meta); + + if (context?.TryGetComponent(_factory, out TransformComponent? xform) ?? false) + { + CopyComp(xform, context as ISerializationContext); + } + else + { + AddComp(); + } + + foreach (var component in entityProto.Components.Components()) + { + } + } + + /// + /// 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. + public void MutateComp(Action action, TContext context) + where TComp: IComponent, new() + { + var comp = _entityComponents[typeof(TComp)]; + + action(context, (TComp)comp); + } +} diff --git a/Robust.Shared/GameObjects/EntityManager.CommandBuffers.cs b/Robust.Shared/GameObjects/EntityManager.CommandBuffers.cs index 39c4bf1cf69..1c77266ed59 100644 --- a/Robust.Shared/GameObjects/EntityManager.CommandBuffers.cs +++ b/Robust.Shared/GameObjects/EntityManager.CommandBuffers.cs @@ -1,17 +1,47 @@ +using System; using Microsoft.Extensions.ObjectPool; using Robust.Shared.GameObjects.CommandBuffers; +using Robust.Shared.GameObjects.EntityBuilders; using Robust.Shared.IoC; namespace Robust.Shared.GameObjects; public abstract partial class EntityManager { - public DefaultObjectPool CommandBufferPool { get; private set; } = default!; + private DefaultObjectPool CommandBufferPool { get; set; } = default!; + + DefaultObjectPool IEntityManager.CommandBufferPool => CommandBufferPool; + public void ApplyCommandBuffer(CommandBuffer buffer) { throw new System.NotImplementedException(); } + public EntityUid ApplyEntityBuilder(EntityBuilder builder) + { + throw new NotImplementedException(); + } + + public void BulkApplyEntityBuilders(ReadOnlySpan builders) + { + throw new NotImplementedException(); + } + + public CommandBuffer GetCommandBuffer() + { + return CommandBufferPool.Get(); + } + + public EntityBuilder BlankEntityBuilder() + { + return EntityBuilder.BlankEntity(_dependencyCollection, GenerateEntityUid(), null); + } + + EntityUid IRemoteEntityManager.GetUnusedEntityUid() + { + return GenerateEntityUid(); + } + private sealed class CommandBufferPolicy(IDependencyCollection collection) : IPooledObjectPolicy { public CommandBuffer Create() @@ -27,17 +57,5 @@ public bool Return(CommandBuffer obj) obj.Clear(); return true; } - - - } - - public CommandBuffer GetCommandBuffer() - { - throw new System.NotImplementedException(); - } - - EntityUid IRemoteEntityManager.GetUnusedEntityUid() - { - return GenerateEntityUid(); } } diff --git a/Robust.Shared/GameObjects/EntityManager.cs b/Robust.Shared/GameObjects/EntityManager.cs index f61320f3dd4..d9f5c008c5a 100644 --- a/Robust.Shared/GameObjects/EntityManager.cs +++ b/Robust.Shared/GameObjects/EntityManager.cs @@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.CompilerServices; +using System.Threading; using Prometheus; using Robust.Shared.Console; using Robust.Shared.Containers; @@ -1118,13 +1119,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/IEntityLoadContext.cs b/Robust.Shared/GameObjects/IEntityLoadContext.cs index 54c3cf40e6d..33d7948b6dc 100644 --- a/Robust.Shared/GameObjects/IEntityLoadContext.cs +++ b/Robust.Shared/GameObjects/IEntityLoadContext.cs @@ -54,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 index 280dcb794e9..885f2eb520a 100644 --- a/Robust.Shared/GameObjects/IEntityManager.CommandBuffers.cs +++ b/Robust.Shared/GameObjects/IEntityManager.CommandBuffers.cs @@ -1,5 +1,7 @@ +using System; using Microsoft.Extensions.ObjectPool; using Robust.Shared.GameObjects.CommandBuffers; +using Robust.Shared.GameObjects.EntityBuilders; namespace Robust.Shared.GameObjects; @@ -15,4 +17,20 @@ public partial interface IEntityManager ///
/// The command buffer to apply to sim. public void ApplyCommandBuffer(CommandBuffer buffer); + + /// + /// Applies an entity builder to the simulation, constructing the entity within in full. + /// + /// The builder to apply. + /// The constructed entity. + public EntityUid ApplyEntityBuilder(EntityBuilder builder); + + /// + /// Runs through entity builders in phases, ala loading a map. + /// + /// + /// If you want to create your own map loading logic, this is pretty much how. + /// + /// The entity builders to allocate for. + public void BulkApplyEntityBuilders(ReadOnlySpan builders); } diff --git a/Robust.Shared/GameObjects/IRemoteEntityManager.cs b/Robust.Shared/GameObjects/IRemoteEntityManager.cs index 2774b2c6663..134fa6ac0d8 100644 --- a/Robust.Shared/GameObjects/IRemoteEntityManager.cs +++ b/Robust.Shared/GameObjects/IRemoteEntityManager.cs @@ -1,4 +1,5 @@ using Robust.Shared.GameObjects.CommandBuffers; +using Robust.Shared.GameObjects.EntityBuilders; namespace Robust.Shared.GameObjects; @@ -13,9 +14,16 @@ public interface IRemoteEntityManager ///
public CommandBuffer GetCommandBuffer(); + /// + /// Creates a new, blank entity builder. + /// + public EntityBuilder BlankEntityBuilder(); + /// /// 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(); + + } From ae024e1e40a57c0347d04a72768478f9d1e5c098 Mon Sep 17 00:00:00 2001 From: kaylie Date: Mon, 16 Mar 2026 15:18:06 +0100 Subject: [PATCH 49/77] Save more work. --- Robust.Shared/GameObjects/Docs.xml | 1 - .../GameObjects/EntityBuilders/Docs.xml | 9 + .../EntityBuilders/EntityBuilder.AddComp.cs | 24 ++- .../EntityBuilder.Initialize.cs | 55 +++++ .../EntityBuilders/EntityBuilder.Meta.cs | 46 +++++ .../EntityBuilders/EntityBuilder.cs | 190 +++++++++++------- .../EntityManager.CommandBuffers.cs | 5 +- Robust.Shared/GameObjects/EntityManager.cs | 31 +++ 8 files changed, 286 insertions(+), 75 deletions(-) create mode 100644 Robust.Shared/GameObjects/EntityBuilders/Docs.xml create mode 100644 Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.Initialize.cs create mode 100644 Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.Meta.cs diff --git a/Robust.Shared/GameObjects/Docs.xml b/Robust.Shared/GameObjects/Docs.xml index 0bcce0751b3..ec08f371290 100644 --- a/Robust.Shared/GameObjects/Docs.xml +++ b/Robust.Shared/GameObjects/Docs.xml @@ -46,6 +46,5 @@ EntitySystem.GetEntityQuery() EntityManager.GetEntityQuery() - 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 index 9ae11636cc3..0a386ceab2b 100644 --- a/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.AddComp.cs +++ b/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.AddComp.cs @@ -16,12 +16,18 @@ public sealed partial class EntityBuilder public EntityBuilder AddComp() where T : IComponent, new() { - if (!_entityComponents.TryAdd(typeof(T), _factory.GetComponent(CompIdx.Index()))) + 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; } @@ -33,12 +39,18 @@ public EntityBuilder AddComp() /// The builder, for chaining. public EntityBuilder AddComp(Type t) { - if (!_entityComponents.TryAdd(t, _factory.GetComponent(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; } @@ -71,6 +83,10 @@ public EntityBuilder AddComp(T component) $"The component {_factory.GetComponentName(component.GetType())} 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; } @@ -99,6 +115,10 @@ public EntityBuilder CopyComp(T component, ISerializationContext? context = n $"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; } diff --git a/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.Initialize.cs b/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.Initialize.cs new file mode 100644 index 00000000000..2fe8cff2047 --- /dev/null +++ b/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.Initialize.cs @@ -0,0 +1,55 @@ +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 load context to use, if any. + private void InitializeMinimalEntity(IEntityLoadContext? context = null) + { + MetaData = new MetaDataComponent(); + + if (context?.TryGetComponent(_factory, out MetaDataComponent? overrideMeta) ?? false) + { + _serMan.CopyTo(overrideMeta, + ref MetaData, + context as ISerializationContext, + notNullableOverride: true); + } + + AddComp(MetaData); + + Transform = new TransformComponent(); + + if (context?.TryGetComponent(_factory, out TransformComponent? overrideXform) ?? false) + { + _serMan.CopyTo(overrideXform, + ref Transform, + context as ISerializationContext, + notNullableOverride: true); + } + + AddComp(Transform); + } + + /// + /// Initializes a command buffer from a prototype, doing most of entity setup aside from actually + /// constructing an entity to apply the components. + /// + /// The prototype to construct from. + /// The load context to use. + private void InitializeFromPrototype(EntProtoId entityProtoId, IEntityLoadContext? context) + { + var entityProto = _protoMan.Index(entityProtoId); + + InitializeMinimalEntity(context); + + foreach (var component in entityProto.Components.Components()) + { + } + } +} diff --git a/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.Meta.cs b/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.Meta.cs new file mode 100644 index 00000000000..bf08a5c116e --- /dev/null +++ b/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.Meta.cs @@ -0,0 +1,46 @@ +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) + { + GetComp(out MetaDataComponent meta); + meta._entityName = newName; + return this; + } + + /// + /// Sets the name of the entity to the given localization string. + /// + public EntityBuilder NamedLoc(LocId newName) + { + GetComp(out MetaDataComponent meta); + meta._entityName = _locMan.GetString(newName); + return this; + } + + /// + /// Sets the description of the entity. + /// + public EntityBuilder Described(string newDesc) + { + GetComp(out MetaDataComponent meta); + meta._entityDescription = newDesc; + return this; + } + + /// + /// Sets the description of the entity to the given localization string. + /// + public EntityBuilder DescribedLoc(LocId newDesc) + { + GetComp(out MetaDataComponent meta); + meta._entityDescription = _locMan.GetString(newDesc); + return this; + } +} diff --git a/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.cs b/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.cs index 4170ed9869e..1b8b2a9f48b 100644 --- a/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.cs +++ b/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.cs @@ -1,9 +1,14 @@ using System; using System.Collections.Generic; +using System.Numerics; using Robust.Shared.IoC; +using Robust.Shared.Localization; +using Robust.Shared.Map; using Robust.Shared.Map.Components; +using Robust.Shared.Maths; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.Manager; +using Robust.Shared.Utility; namespace Robust.Shared.GameObjects.EntityBuilders; @@ -18,33 +23,36 @@ public sealed partial class EntityBuilder private readonly IComponentFactory _factory; private readonly IPrototypeManager _protoMan; private readonly ISerializationManager _serMan; - - private void Foo(IRemoteEntityManager entMan) - { - var builder = entMan.BlankEntityBuilder() - .AddComp() - .AddComp(new MapGridComponent - { - CanSplit = false - }); - } + private readonly ILocalizationManager _locMan; /// - /// The EntityUid we have reserved. + /// The EntityUid we have reserved. No entity exists here yet unless the builder has been + /// applied and consumed. /// - private readonly EntityUid _reservedId; + public readonly EntityUid ReservedEntity; /// /// The components we'll be adding to the entity we construct. /// private readonly Dictionary _entityComponents; - internal EntityBuilder(IDependencyCollection collection, EntityUid reservedId) + /// + /// 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. + /// + private MapCoordinates? _mapCoordinates; + + internal MetaDataComponent MetaData = default!; + internal TransformComponent Transform = default!; + + internal EntityBuilder(IDependencyCollection collection, EntityUid reservedEntity) { _factory = collection.Resolve(); _protoMan = collection.Resolve(); _serMan = collection.Resolve(); - _reservedId = reservedId; + _locMan = collection.Resolve(); + ReservedEntity = reservedEntity; _entityComponents = new(); } @@ -60,81 +68,121 @@ internal static EntityBuilder BlankEntity( return self; } + // wishing we had rust alloc-free lambdas rn. /// - /// Creates the bare minimal spawnable entity with metadata and a transform. + /// Mutates a given component in place using the provided, ideally static, action. /// - /// The load context to use, if any. - private void InitializeMinimalEntity(IEntityLoadContext? context = null) + /// 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() { - if (context?.TryGetComponent(_factory, out MetaDataComponent? meta) ?? false) - { - CopyComp(meta, context as ISerializationContext); - } - else - { - AddComp(); - } + var comp = _entityComponents[typeof(TComp)]; - if (context?.TryGetComponent(_factory, out TransformComponent? xform) ?? false) - { - CopyComp(xform, context as ISerializationContext); - } - else - { - AddComp(); - } + action(context, (TComp)comp); + + return this; } /// - /// Initializes a command buffer from a prototype, doing most of entity setup aside from actually - /// constructing an entity to apply the components. + /// Retrieves the given concretely typed component from the builder. /// - /// The prototype to construct from. - /// The load context to use. - private void InitializeFromPrototype(EntProtoId entityProtoId, IEntityLoadContext? context) + /// The retrieved component. + /// The type of component to retrieve. + /// The builder, for chaining. + public EntityBuilder GetComp(out TComp comp) { - var entityProto = _protoMan.Index(entityProtoId); - - var meta = new MetaDataComponent(); + comp = (TComp) _entityComponents[typeof(TComp)]; + return this; + } - if (context?.TryGetComponent(_factory, out MetaDataComponent? overrideMeta) ?? false) - { - _serMan.CopyTo(overrideMeta, - ref meta, - context as ISerializationContext, - notNullableOverride: true); - } + /// + /// 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. - // Ensure we set up our metadata correctly, i.e. set the prototype so no explosions. - meta._entityPrototype = entityProto; - AddComp(meta); + Transform._parent = parent; + Transform._localPosition = relativePos; - if (context?.TryGetComponent(_factory, out TransformComponent? xform) ?? false) - { - CopyComp(xform, context as ISerializationContext); - } - else - { - AddComp(); - } + if (rotation is not null) + Transform._localRotation = rotation.Value; - foreach (var component in entityProto.Components.Components()) - { - } + return this; } /// - /// Mutates a given component in place using the provided, ideally static, action. + /// Ensures the entity is spawned at the given map coordinate, automatically finding a parent. /// - /// The mutator to run - /// The context object for the action. - /// - /// The concrete type of the component. - public void MutateComp(Action action, TContext context) - where TComp: IComponent, new() + /// The coordinates to spawn at + /// + /// The builder, for chaining. + public EntityBuilder SpawnAt(MapCoordinates mapCoordinates) { - var comp = _entityComponents[typeof(TComp)]; + _mapCoordinates = mapCoordinates; - action(context, (TComp)comp); + if (Transform._parent != EntityUid.Invalid) + { + Transform._parent = EntityUid.Invalid; + Transform._localPosition = default; + } + + return this; + } + + private sealed class TestSystem : EntitySystem + { + public void Foo() + { + var parent = EntityManager.BlankEntityBuilder() + .NamedLoc("my-key-amogus") + .AddComp(); + + var child = EntityManager.BlankEntityBuilder() + .Named("child") + .ChildOf(parent.ReservedEntity) + .AddComp(new MapGridComponent + { + CanSplit = false, + }); + + // spawn 'em all like a map. Orderless, it'll figure it out. + EntityManager.BulkApplyEntityBuilders(new [] { parent, child }); + } } } diff --git a/Robust.Shared/GameObjects/EntityManager.CommandBuffers.cs b/Robust.Shared/GameObjects/EntityManager.CommandBuffers.cs index 1c77266ed59..e5af3d0dc90 100644 --- a/Robust.Shared/GameObjects/EntityManager.CommandBuffers.cs +++ b/Robust.Shared/GameObjects/EntityManager.CommandBuffers.cs @@ -19,7 +19,10 @@ public void ApplyCommandBuffer(CommandBuffer buffer) public EntityUid ApplyEntityBuilder(EntityBuilder builder) { - throw new NotImplementedException(); + var ent = builder.ReservedEntity; + // Doesn't allocate :) + BulkApplyEntityBuilders([builder]); + return ent; } public void BulkApplyEntityBuilders(ReadOnlySpan builders) diff --git a/Robust.Shared/GameObjects/EntityManager.cs b/Robust.Shared/GameObjects/EntityManager.cs index d9f5c008c5a..b53ecbba824 100644 --- a/Robust.Shared/GameObjects/EntityManager.cs +++ b/Robust.Shared/GameObjects/EntityManager.cs @@ -8,6 +8,7 @@ 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; @@ -955,6 +956,36 @@ 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); + + // Add our initial mandatory components. + AddComponentInternal(uid, builder.MetaData, _metaReg, false, true, builder.MetaData); + AddComponentInternal(uid, builder.Transform, false, true, builder.MetaData); + } + /// /// Allocates an entity and loads components but does not do initialization. /// From e4ede9ae7a765587d02169f0eb2fce51064c19ad Mon Sep 17 00:00:00 2001 From: kaylie Date: Mon, 16 Mar 2026 16:05:12 +0100 Subject: [PATCH 50/77] First tests, it works!! --- .../EntityBuilder/EntityBuilderTests.cs | 90 +++++++++++++++++++ .../EntityBuilders/EntityBuilder.AddComp.cs | 8 +- .../EntityBuilders/EntityBuilder.cs | 8 +- .../EntityManager.CommandBuffers.cs | 13 --- .../EntityManager.EntityBuilder.cs | 46 ++++++++++ Robust.Shared/GameObjects/EntityManager.cs | 13 ++- .../IEntityManager.CommandBuffers.cs | 6 +- .../SharedTransformSystem.Component.cs | 7 +- Robust.Shared/Prototypes/EntityPrototype.cs | 9 +- 9 files changed, 166 insertions(+), 34 deletions(-) create mode 100644 Robust.Shared.IntegrationTests/GameObjects/EntityBuilder/EntityBuilderTests.cs create mode 100644 Robust.Shared/GameObjects/EntityManager.EntityBuilder.cs diff --git a/Robust.Shared.IntegrationTests/GameObjects/EntityBuilder/EntityBuilderTests.cs b/Robust.Shared.IntegrationTests/GameObjects/EntityBuilder/EntityBuilderTests.cs new file mode 100644 index 00000000000..f8cfa4c60c3 --- /dev/null +++ b/Robust.Shared.IntegrationTests/GameObjects/EntityBuilder/EntityBuilderTests.cs @@ -0,0 +1,90 @@ +using System.Numerics; +using NUnit.Framework; +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: 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] + public void CreateEntity() + { + var builder = _entMan.BlankEntityBuilder() + .Named("Test entity") + .AddComp() + .AddComp(); + + var ent = _entMan.ApplyEntityBuilder(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] + public void CreateHierarchy() + { + var root = _entMan.BlankEntityBuilder() + .Named("Test entity") + .AddComp() + .AddComp(); + + var child1 = _entMan.BlankEntityBuilder() + .Named("Test child 1") + .ChildOf(root.ReservedEntity, new Vector2(-4, -4)) + .AddComp() + .AddComp(); + + var child2 = _entMan.BlankEntityBuilder() + .Named("Test child 2") + .ChildOf(root.ReservedEntity, new Vector2(4, 4)) + .AddComp() + .AddComp(); + + _entMan.BulkApplyEntityBuilders([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."); + } +} diff --git a/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.AddComp.cs b/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.AddComp.cs index 0a386ceab2b..8c728829832 100644 --- a/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.AddComp.cs +++ b/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.AddComp.cs @@ -18,7 +18,7 @@ public EntityBuilder AddComp() { var component = _factory.GetComponent(CompIdx.Index()); - if (!_entityComponents.TryAdd(typeof(T), component)) + if (!EntityComponents.TryAdd(typeof(T), component)) { throw new ArgumentException( $"The component {_factory.GetComponentName()} already existed in the builder."); @@ -41,7 +41,7 @@ public EntityBuilder AddComp(Type t) { var component = _factory.GetComponent(t); - if (!_entityComponents.TryAdd(t, component)) + if (!EntityComponents.TryAdd(t, component)) { throw new ArgumentException( $"The component {_factory.GetComponentName(t)} already existed in the builder."); @@ -77,7 +77,7 @@ public EntityBuilder AddComp(T component) DebugTools.AssertEqual(component.LifeStage, ComponentLifeStage.PreAdd); DebugTools.Assert(_factory.TryGetRegistration(component.GetType(), out _)); - if (!_entityComponents.TryAdd(component.GetType(), component)) + if (!EntityComponents.TryAdd(component.GetType(), component)) { throw new ArgumentException( $"The component {_factory.GetComponentName(component.GetType())} already existed in the builder."); @@ -109,7 +109,7 @@ public EntityBuilder CopyComp(T component, ISerializationContext? context = n _serMan.CopyTo(component, ref newComp, context, notNullableOverride: true); - if (!_entityComponents.TryAdd(typeof(T), newComp)) + if (!EntityComponents.TryAdd(typeof(T), newComp)) { throw new ArgumentException( $"The component {_factory.GetComponentName(component.GetType())} already existed in the builder."); diff --git a/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.cs b/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.cs index 1b8b2a9f48b..3a0d2d71349 100644 --- a/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.cs +++ b/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.cs @@ -34,7 +34,7 @@ public sealed partial class EntityBuilder /// /// The components we'll be adding to the entity we construct. /// - private readonly Dictionary _entityComponents; + internal readonly Dictionary EntityComponents; /// /// The coordinates to spawn the entity at, if any. @@ -53,7 +53,7 @@ internal EntityBuilder(IDependencyCollection collection, EntityUid reservedEntit _serMan = collection.Resolve(); _locMan = collection.Resolve(); ReservedEntity = reservedEntity; - _entityComponents = new(); + EntityComponents = new(); } internal static EntityBuilder BlankEntity( @@ -105,7 +105,7 @@ internal static EntityBuilder BlankEntity( public EntityBuilder MutateComp(Action action, TContext context) where TComp: IComponent, new() { - var comp = _entityComponents[typeof(TComp)]; + var comp = EntityComponents[typeof(TComp)]; action(context, (TComp)comp); @@ -120,7 +120,7 @@ public EntityBuilder MutateComp(Action action, /// The builder, for chaining. public EntityBuilder GetComp(out TComp comp) { - comp = (TComp) _entityComponents[typeof(TComp)]; + comp = (TComp) EntityComponents[typeof(TComp)]; return this; } diff --git a/Robust.Shared/GameObjects/EntityManager.CommandBuffers.cs b/Robust.Shared/GameObjects/EntityManager.CommandBuffers.cs index e5af3d0dc90..9abf6ca73b9 100644 --- a/Robust.Shared/GameObjects/EntityManager.CommandBuffers.cs +++ b/Robust.Shared/GameObjects/EntityManager.CommandBuffers.cs @@ -17,19 +17,6 @@ public void ApplyCommandBuffer(CommandBuffer buffer) throw new System.NotImplementedException(); } - public EntityUid ApplyEntityBuilder(EntityBuilder builder) - { - var ent = builder.ReservedEntity; - // Doesn't allocate :) - BulkApplyEntityBuilders([builder]); - return ent; - } - - public void BulkApplyEntityBuilders(ReadOnlySpan builders) - { - throw new NotImplementedException(); - } - public CommandBuffer GetCommandBuffer() { return CommandBufferPool.Get(); diff --git a/Robust.Shared/GameObjects/EntityManager.EntityBuilder.cs b/Robust.Shared/GameObjects/EntityManager.EntityBuilder.cs new file mode 100644 index 00000000000..4dcc4242d62 --- /dev/null +++ b/Robust.Shared/GameObjects/EntityManager.EntityBuilder.cs @@ -0,0 +1,46 @@ +using System; +using System.Runtime.CompilerServices; +using Robust.Shared.GameObjects.EntityBuilders; + +namespace Robust.Shared.GameObjects; + +public abstract partial class EntityManager +{ + public EntityUid ApplyEntityBuilder(EntityBuilder builder, bool mapInit = true) + { + var ent = builder.ReservedEntity; + // Doesn't allocate. Not that it matters, we're about to allocate a lot. + BulkApplyEntityBuilders([builder]); + return ent; + } + + public void BulkApplyEntityBuilders(ReadOnlySpan builders, bool mapInit = true) + { + // Create entities + ComponentAdd + foreach (var builder in builders) + { + AllocBuilderEntity(builder); + } + + // ComponentInit + foreach (var builder in builders) + { + InitializeEntity(builder.ReservedEntity, builder.MetaData); + } + + // ComponentStartup + foreach (var builder in builders) + { + StartEntity(builder.ReservedEntity); + } + + // MapInit + if (mapInit) + { + foreach (var builder in builders) + { + RunMapInit(builder.ReservedEntity, builder.MetaData); + } + } + } +} diff --git a/Robust.Shared/GameObjects/EntityManager.cs b/Robust.Shared/GameObjects/EntityManager.cs index b53ecbba824..1cbfee2ae90 100644 --- a/Robust.Shared/GameObjects/EntityManager.cs +++ b/Robust.Shared/GameObjects/EntityManager.cs @@ -981,9 +981,20 @@ private void AllocBuilderEntity(EntityBuilder builder) Entities.Add(uid); - // Add our initial mandatory components. + // 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); + } } /// diff --git a/Robust.Shared/GameObjects/IEntityManager.CommandBuffers.cs b/Robust.Shared/GameObjects/IEntityManager.CommandBuffers.cs index 885f2eb520a..c64e0bc0676 100644 --- a/Robust.Shared/GameObjects/IEntityManager.CommandBuffers.cs +++ b/Robust.Shared/GameObjects/IEntityManager.CommandBuffers.cs @@ -22,8 +22,9 @@ public partial interface IEntityManager /// Applies an entity builder to the simulation, constructing the entity within in full. /// /// The builder to apply. + /// Whether map init should be run for the built entities. /// The constructed entity. - public EntityUid ApplyEntityBuilder(EntityBuilder builder); + public EntityUid ApplyEntityBuilder(EntityBuilder builder, bool mapInit = true); /// /// Runs through entity builders in phases, ala loading a map. @@ -32,5 +33,6 @@ public partial interface IEntityManager /// If you want to create your own map loading logic, this is pretty much how. /// /// The entity builders to allocate for. - public void BulkApplyEntityBuilders(ReadOnlySpan builders); + /// Whether map init should be run for the built entities. + public void BulkApplyEntityBuilders(ReadOnlySpan builders, bool mapInit = true); } 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/EntityPrototype.cs b/Robust.Shared/Prototypes/EntityPrototype.cs index dec9da87776..436ec501a7b 100644 --- a/Robust.Shared/Prototypes/EntityPrototype.cs +++ b/Robust.Shared/Prototypes/EntityPrototype.cs @@ -224,7 +224,7 @@ internal static void LoadEntity( var fullData = context != null && context.TryGetComponent(factory, comp.GetType(), out var data) ? data : comp; - EnsureCompExistsAndDeserialize(entity, compReg, factory, entityManager, serManager, compReg.Name, fullData, ctx); + EnsureCompExistsAndDeserialize(entity, compReg, factory, entityManager, serManager, fullData, ctx); if (!fullData.NetSyncEnabled && compReg.NetID is {} netId) meta.NetComponents.Remove(netId); @@ -250,7 +250,7 @@ internal static void LoadEntity( } var compReg = factory.GetRegistration(name); - EnsureCompExistsAndDeserialize(entity, compReg, factory, entityManager, serManager, name, data, ctx); + EnsureCompExistsAndDeserialize(entity, compReg, factory, entityManager, serManager, data, ctx); } } } @@ -260,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(compReg); entityManager.AddComponent(entity, newComponent); component = newComponent; } @@ -277,7 +276,7 @@ public static void EnsureCompExistsAndDeserialize(EntityUid entity, return; } - map.CurrentComponent = compName; + map.CurrentComponent = compReg.Name; serManager.CopyTo(data, ref component, context, notNullableOverride: true); map.CurrentComponent = null; } From 181a24cee3b621a18ebf2f856d96873ddad75137 Mon Sep 17 00:00:00 2001 From: kaylie Date: Mon, 16 Mar 2026 16:44:50 +0100 Subject: [PATCH 51/77] Comment on order. --- .../GameObjects/EntityBuilder/EntityBuilderTests.cs | 2 ++ Robust.Shared/GameObjects/IEntityManager.CommandBuffers.cs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/Robust.Shared.IntegrationTests/GameObjects/EntityBuilder/EntityBuilderTests.cs b/Robust.Shared.IntegrationTests/GameObjects/EntityBuilder/EntityBuilderTests.cs index f8cfa4c60c3..a0dd2f0a521 100644 --- a/Robust.Shared.IntegrationTests/GameObjects/EntityBuilder/EntityBuilderTests.cs +++ b/Robust.Shared.IntegrationTests/GameObjects/EntityBuilder/EntityBuilderTests.cs @@ -78,6 +78,8 @@ public void CreateHierarchy() .AddComp() .AddComp(); + // Spawn the map and its children. + // Note: Currently order does matter. _entMan.BulkApplyEntityBuilders([root, child1, child2]); Assert.That(_entMan.GetComponent(root.ReservedEntity).MapId, diff --git a/Robust.Shared/GameObjects/IEntityManager.CommandBuffers.cs b/Robust.Shared/GameObjects/IEntityManager.CommandBuffers.cs index c64e0bc0676..bf0550afd81 100644 --- a/Robust.Shared/GameObjects/IEntityManager.CommandBuffers.cs +++ b/Robust.Shared/GameObjects/IEntityManager.CommandBuffers.cs @@ -31,6 +31,8 @@ public partial interface IEntityManager /// /// /// 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 allocate for. /// Whether map init should be run for the built entities. From 88b2aa93f6ce48cc3f442b944bf3d877edb5fb15 Mon Sep 17 00:00:00 2001 From: kaylie Date: Mon, 16 Mar 2026 17:03:41 +0100 Subject: [PATCH 52/77] More tests. --- .../EntityBuilder/EntityBuilderTests.cs | 25 ++++++++++-- .../EntityBuilders/EntityBuilder.AddComp.cs | 32 +++++++++++++++- .../EntityBuilder.Initialize.cs | 26 ++----------- .../EntityBuilders/EntityBuilder.cs | 38 ++++++++----------- .../EntityManager.CommandBuffers.cs | 9 ++++- .../GameObjects/IRemoteEntityManager.cs | 9 ++++- 6 files changed, 85 insertions(+), 54 deletions(-) diff --git a/Robust.Shared.IntegrationTests/GameObjects/EntityBuilder/EntityBuilderTests.cs b/Robust.Shared.IntegrationTests/GameObjects/EntityBuilder/EntityBuilderTests.cs index a0dd2f0a521..601b84806dc 100644 --- a/Robust.Shared.IntegrationTests/GameObjects/EntityBuilder/EntityBuilderTests.cs +++ b/Robust.Shared.IntegrationTests/GameObjects/EntityBuilder/EntityBuilderTests.cs @@ -45,9 +45,10 @@ public void Setup() } [Test] + [Description("Creates a few entities with blank EntityBuilders, ensuring component adds work.")] public void CreateEntity() { - var builder = _entMan.BlankEntityBuilder() + var builder = _entMan.EntityBuilder() .Named("Test entity") .AddComp() .AddComp(); @@ -59,20 +60,21 @@ public void CreateEntity() } [Test] + [Description("Creates a hierarchy of entities, a map and some grids, using blank entity builders.")] public void CreateHierarchy() { - var root = _entMan.BlankEntityBuilder() + var root = _entMan.EntityBuilder() .Named("Test entity") .AddComp() .AddComp(); - var child1 = _entMan.BlankEntityBuilder() + var child1 = _entMan.EntityBuilder() .Named("Test child 1") .ChildOf(root.ReservedEntity, new Vector2(-4, -4)) .AddComp() .AddComp(); - var child2 = _entMan.BlankEntityBuilder() + var child2 = _entMan.EntityBuilder() .Named("Test child 2") .ChildOf(root.ReservedEntity, new Vector2(4, 4)) .AddComp() @@ -89,4 +91,19 @@ public void CreateHierarchy() NUnit.Framework.Is.EquivalentTo([child1.ReservedEntity, child2.ReservedEntity]), "Expected the hierarchy we set up to be respected."); } + + [Test] + public void CreateFromPrototype() + { + var builder = _entMan.EntityBuilder(TestEnt1); + + _entMan.ApplyEntityBuilder(builder); + + 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)); + } } diff --git a/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.AddComp.cs b/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.AddComp.cs index 8c728829832..d1c557e5e07 100644 --- a/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.AddComp.cs +++ b/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.AddComp.cs @@ -109,7 +109,7 @@ public EntityBuilder CopyComp(T component, ISerializationContext? context = n _serMan.CopyTo(component, ref newComp, context, notNullableOverride: true); - if (!EntityComponents.TryAdd(typeof(T), newComp)) + if (!EntityComponents.TryAdd(component.GetType(), newComp)) { throw new ArgumentException( $"The component {_factory.GetComponentName(component.GetType())} already existed in the builder."); @@ -122,6 +122,36 @@ public EntityBuilder CopyComp(T component, ISerializationContext? context = n 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()); + + _serMan.CopyTo(component, ref comp, context, notNullableOverride: true); + + EntityComponents.Add(component.GetType(), comp); + +#pragma warning disable CS0618 // Type or member is obsolete + comp.Owner = ReservedEntity; +#pragma warning restore CS0618 // Type or member is obsolete + + return this; + } + + /// /// Directly adds the given set of components to the entity being built. /// diff --git a/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.Initialize.cs b/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.Initialize.cs index 2fe8cff2047..fd906f7304a 100644 --- a/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.Initialize.cs +++ b/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.Initialize.cs @@ -9,40 +9,21 @@ public sealed partial class EntityBuilder /// Creates the bare minimal spawnable entity with metadata and a transform. /// /// The load context to use, if any. - private void InitializeMinimalEntity(IEntityLoadContext? context = null) + private void InitializeMinimalEntity(ISerializationContext? context = null) { MetaData = new MetaDataComponent(); - - if (context?.TryGetComponent(_factory, out MetaDataComponent? overrideMeta) ?? false) - { - _serMan.CopyTo(overrideMeta, - ref MetaData, - context as ISerializationContext, - notNullableOverride: true); - } - AddComp(MetaData); Transform = new TransformComponent(); - - if (context?.TryGetComponent(_factory, out TransformComponent? overrideXform) ?? false) - { - _serMan.CopyTo(overrideXform, - ref Transform, - context as ISerializationContext, - notNullableOverride: true); - } - AddComp(Transform); } /// - /// Initializes a command buffer from a prototype, doing most of entity setup aside from actually - /// constructing an entity to apply the components. + /// Initializes a command buffer from a prototype, cloning all the components onto the new entity. /// /// The prototype to construct from. /// The load context to use. - private void InitializeFromPrototype(EntProtoId entityProtoId, IEntityLoadContext? context) + private void InitializeFromPrototype(EntProtoId entityProtoId, ISerializationContext? context = null) { var entityProto = _protoMan.Index(entityProtoId); @@ -50,6 +31,7 @@ private void InitializeFromPrototype(EntProtoId entityProtoId, IEntityLoadContex foreach (var component in entityProto.Components.Components()) { + EnsureCopyComp(component, context); } } } diff --git a/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.cs b/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.cs index 3a0d2d71349..c504a3480fc 100644 --- a/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.cs +++ b/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.cs @@ -59,11 +59,24 @@ internal EntityBuilder(IDependencyCollection collection, EntityUid reservedEntit internal static EntityBuilder BlankEntity( IDependencyCollection collection, EntityUid reservedId, - IEntityLoadContext? loadContext) + ISerializationContext? context) { var self = new EntityBuilder(collection, reservedId); - self.InitializeMinimalEntity(loadContext); + self.InitializeMinimalEntity(context); + + return self; + } + + internal static EntityBuilder PrototypedEntity( + IDependencyCollection collection, + EntityUid reservedId, + EntProtoId proto, + ISerializationContext? context) + { + var self = new EntityBuilder(collection, reservedId); + + self.InitializeFromPrototype(proto, context); return self; } @@ -164,25 +177,4 @@ public EntityBuilder SpawnAt(MapCoordinates mapCoordinates) return this; } - - private sealed class TestSystem : EntitySystem - { - public void Foo() - { - var parent = EntityManager.BlankEntityBuilder() - .NamedLoc("my-key-amogus") - .AddComp(); - - var child = EntityManager.BlankEntityBuilder() - .Named("child") - .ChildOf(parent.ReservedEntity) - .AddComp(new MapGridComponent - { - CanSplit = false, - }); - - // spawn 'em all like a map. Orderless, it'll figure it out. - EntityManager.BulkApplyEntityBuilders(new [] { parent, child }); - } - } } diff --git a/Robust.Shared/GameObjects/EntityManager.CommandBuffers.cs b/Robust.Shared/GameObjects/EntityManager.CommandBuffers.cs index 9abf6ca73b9..1dc78420b8a 100644 --- a/Robust.Shared/GameObjects/EntityManager.CommandBuffers.cs +++ b/Robust.Shared/GameObjects/EntityManager.CommandBuffers.cs @@ -3,6 +3,8 @@ 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; @@ -22,9 +24,12 @@ public CommandBuffer GetCommandBuffer() return CommandBufferPool.Get(); } - public EntityBuilder BlankEntityBuilder() + public EntityBuilder EntityBuilder(EntProtoId? protoId = null, ISerializationContext? context = null) { - return EntityBuilder.BlankEntity(_dependencyCollection, GenerateEntityUid(), null); + if (protoId is null) + return EntityBuilders.EntityBuilder.BlankEntity(_dependencyCollection, GenerateEntityUid(), context); + + return EntityBuilders.EntityBuilder.PrototypedEntity(_dependencyCollection, GenerateEntityUid(), protoId.Value, context); } EntityUid IRemoteEntityManager.GetUnusedEntityUid() diff --git a/Robust.Shared/GameObjects/IRemoteEntityManager.cs b/Robust.Shared/GameObjects/IRemoteEntityManager.cs index 134fa6ac0d8..8ad54e5ff98 100644 --- a/Robust.Shared/GameObjects/IRemoteEntityManager.cs +++ b/Robust.Shared/GameObjects/IRemoteEntityManager.cs @@ -1,5 +1,7 @@ using Robust.Shared.GameObjects.CommandBuffers; using Robust.Shared.GameObjects.EntityBuilders; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.Manager; namespace Robust.Shared.GameObjects; @@ -15,9 +17,12 @@ public interface IRemoteEntityManager public CommandBuffer GetCommandBuffer(); /// - /// Creates a new, blank entity builder. + /// Creates a new entity builder, optionally for an entity with the given prototype. /// - public EntityBuilder BlankEntityBuilder(); + /// 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. From 251e07c569b8a7bd39fade14e1abe34622dc9dc0 Mon Sep 17 00:00:00 2001 From: kaylie Date: Mon, 16 Mar 2026 17:19:05 +0100 Subject: [PATCH 53/77] found a bug, thanks tests. --- .../GameObjects/EntityBuilder/EntityBuilderTests.cs | 3 +++ .../GameObjects/EntityBuilders/EntityBuilder.AddComp.cs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Robust.Shared.IntegrationTests/GameObjects/EntityBuilder/EntityBuilderTests.cs b/Robust.Shared.IntegrationTests/GameObjects/EntityBuilder/EntityBuilderTests.cs index 601b84806dc..6c865d8fb5f 100644 --- a/Robust.Shared.IntegrationTests/GameObjects/EntityBuilder/EntityBuilderTests.cs +++ b/Robust.Shared.IntegrationTests/GameObjects/EntityBuilder/EntityBuilderTests.cs @@ -20,6 +20,9 @@ internal sealed class EntityBuilderTests : OurRobustUnitTest - type: entity id: {TestEnt1} components: + - type: MetaData # Test of new spawn logic.. + - type: Transform + noRot: true - type: Marker1 - type: Marker2 - type: Marker3 diff --git a/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.AddComp.cs b/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.AddComp.cs index d1c557e5e07..51acbebaa53 100644 --- a/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.AddComp.cs +++ b/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.AddComp.cs @@ -142,7 +142,7 @@ public EntityBuilder EnsureCopyComp(T component, ISerializationContext? conte _serMan.CopyTo(component, ref comp, context, notNullableOverride: true); - EntityComponents.Add(component.GetType(), comp); + EntityComponents.TryAdd(component.GetType(), comp); #pragma warning disable CS0618 // Type or member is obsolete comp.Owner = ReservedEntity; From 26fb67ed6085f520ebfcc2238a761375ef4dd64d Mon Sep 17 00:00:00 2001 From: kaylie Date: Mon, 16 Mar 2026 17:20:58 +0100 Subject: [PATCH 54/77] one more test case. --- .../GameObjects/EntityBuilder/EntityBuilderTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Robust.Shared.IntegrationTests/GameObjects/EntityBuilder/EntityBuilderTests.cs b/Robust.Shared.IntegrationTests/GameObjects/EntityBuilder/EntityBuilderTests.cs index 6c865d8fb5f..5c88067d8a5 100644 --- a/Robust.Shared.IntegrationTests/GameObjects/EntityBuilder/EntityBuilderTests.cs +++ b/Robust.Shared.IntegrationTests/GameObjects/EntityBuilder/EntityBuilderTests.cs @@ -108,5 +108,7 @@ public void CreateFromPrototype() 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."); } } From 2ddd4946a0bf50d6fa3163aac9bff1f2a17d1dc0 Mon Sep 17 00:00:00 2001 From: kaylie Date: Tue, 17 Mar 2026 00:01:07 +0100 Subject: [PATCH 55/77] awr --- .../EntityBuilder/EntityBuilderTests.cs | 23 +++-- .../CommandBuffers/CommandBuffer.cs | 11 ++- .../CommandBuffers/CommandBufferEntry.cs | 67 +++++++++---- .../EntityManager.ApplyCommandBuffer.cs | 93 +++++++++++++++++++ .../EntityManager.CommandBuffers.cs | 5 - 5 files changed, 160 insertions(+), 39 deletions(-) create mode 100644 Robust.Shared/GameObjects/EntityManager.ApplyCommandBuffer.cs diff --git a/Robust.Shared.IntegrationTests/GameObjects/EntityBuilder/EntityBuilderTests.cs b/Robust.Shared.IntegrationTests/GameObjects/EntityBuilder/EntityBuilderTests.cs index 5c88067d8a5..db9ba77b317 100644 --- a/Robust.Shared.IntegrationTests/GameObjects/EntityBuilder/EntityBuilderTests.cs +++ b/Robust.Shared.IntegrationTests/GameObjects/EntityBuilder/EntityBuilderTests.cs @@ -84,7 +84,7 @@ public void CreateHierarchy() .AddComp(); // Spawn the map and its children. - // Note: Currently order does matter. + // Note: Currently order does matter. I'd like to lift that requirement sometime, but it does. _entMan.BulkApplyEntityBuilders([root, child1, child2]); Assert.That(_entMan.GetComponent(root.ReservedEntity).MapId, @@ -96,19 +96,24 @@ public void CreateHierarchy() } [Test] + [Description("Ensure EntityBuilder successfully creates entities from prototypes.")] public void CreateFromPrototype() { var builder = _entMan.EntityBuilder(TestEnt1); _entMan.ApplyEntityBuilder(builder); - 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."); + 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/GameObjects/CommandBuffers/CommandBuffer.cs b/Robust.Shared/GameObjects/CommandBuffers/CommandBuffer.cs index a9b55fbf061..f3717dc8aa2 100644 --- a/Robust.Shared/GameObjects/CommandBuffers/CommandBuffer.cs +++ b/Robust.Shared/GameObjects/CommandBuffers/CommandBuffer.cs @@ -25,11 +25,12 @@ public sealed partial class CommandBuffer : IDisposable /// /// The underlying list of entries in the buffer. /// - private ValueList _entries = []; + internal ValueList Entries = []; + /// /// The capacity of the underlying entry collection, for object pooling usage. /// - internal int Capacity => _entries.Capacity; + internal int Capacity => Entries.Capacity; /// /// Construct a new command buffer. As these are meant to be pooled by entity manager, the constructor is @@ -43,9 +44,9 @@ internal CommandBuffer(IDependencyCollection collection) private ref CommandBufferEntry NextEntry() { - _entries.Add(default); + Entries.Add(default); - return ref _entries[^1]; + return ref Entries[^1]; } /// @@ -84,6 +85,6 @@ public void Dispose() /// internal void Clear() { - _entries.Clear(); + Entries.Clear(); } } diff --git a/Robust.Shared/GameObjects/CommandBuffers/CommandBufferEntry.cs b/Robust.Shared/GameObjects/CommandBuffers/CommandBufferEntry.cs index efb109f99bb..9eb20562598 100644 --- a/Robust.Shared/GameObjects/CommandBuffers/CommandBufferEntry.cs +++ b/Robust.Shared/GameObjects/CommandBuffers/CommandBufferEntry.cs @@ -1,7 +1,7 @@ using System; -using System.Reflection; +using System.Collections.Generic; using System.Runtime.InteropServices; -using Robust.Shared.Utility; +using Robust.Shared.GameObjects.EntityBuilders; namespace Robust.Shared.GameObjects.CommandBuffers; @@ -46,6 +46,7 @@ internal partial struct CommandBufferEntry /// 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 @@ -84,6 +85,32 @@ public static void SubBuffer(CommandBuffer subBuffer, out CommandBufferEntry ent 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(EntityBuilder[] 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. /// @@ -135,45 +162,45 @@ public enum CmdKind : byte DeleteEntity, /// - /// Handles spawning a map with a builder. + /// Handles spawning an entity with a builder. + /// Either an entity builder, or a list of entity builders. /// - /// MapId ReservedMapId; - /// EntityBuilder EntityBuilder; + /// unused Field1; + /// EntityBuilder | EntityBuilder[] EntityBuilder; /// unused Field3; /// /// - SpawnMap, + SpawnEntity, /// - /// Handles spawning an entity with a builder. + /// Handles adding components to an entity. + /// Either a component, or a list of components. /// - /// unused Field1; - /// EntityBuilder EntityBuilder; + /// EntityUid Target; + /// IComponent | IComponent[] Components; /// unused Field3; /// /// - SpawnEntity, + AddComponents, /// - /// Handles adding components to an entity. - /// The fields for this are a bit funny, it can either contain a List of components, or up - /// to two direct references to components. + /// Handles ensuring components on an entity. + /// Similar to AddComponents, this is either a Type, or a list of Types. /// /// EntityUid Target; - /// IComponent | List<IComponent> Components; - /// IComponent? ExtraComponent; + /// Type | Type[] Components; + /// unused Field3; /// /// - AddComponents, + EnsureComponents, /// /// Handles removing components from an entity. - /// Similar to AddComponents, this either contains a List of Type, or up to two direct references to - /// Type. + /// Similar to AddComponents, this is either a Type, or a list of Types. /// /// EntityUid Target; - /// Type | List<Type> Components; - /// Type? ExtraComponent; + /// Type | Type[] Components; + /// unused Field3; /// /// RemoveComponents, diff --git a/Robust.Shared/GameObjects/EntityManager.ApplyCommandBuffer.cs b/Robust.Shared/GameObjects/EntityManager.ApplyCommandBuffer.cs new file mode 100644 index 00000000000..a23923694d3 --- /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: + { + ApplyEntityBuilder(b); + break; + } + case EntityBuilder[] b: + { + BulkApplyEntityBuilders(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 index 1dc78420b8a..9783260e99c 100644 --- a/Robust.Shared/GameObjects/EntityManager.CommandBuffers.cs +++ b/Robust.Shared/GameObjects/EntityManager.CommandBuffers.cs @@ -14,11 +14,6 @@ public abstract partial class EntityManager DefaultObjectPool IEntityManager.CommandBufferPool => CommandBufferPool; - public void ApplyCommandBuffer(CommandBuffer buffer) - { - throw new System.NotImplementedException(); - } - public CommandBuffer GetCommandBuffer() { return CommandBufferPool.Get(); From 35504389a7dbb6b36b5fdb0979c2cbb7e7ebcaac Mon Sep 17 00:00:00 2001 From: kaylie Date: Tue, 17 Mar 2026 01:28:17 +0100 Subject: [PATCH 56/77] what do you mean we have tests relying on hardcoded entity ids. i appreciate catching this, i do not appreciate those tests. --- Robust.Shared/GameObjects/EntityManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Robust.Shared/GameObjects/EntityManager.cs b/Robust.Shared/GameObjects/EntityManager.cs index 1cbfee2ae90..ead5faabe7e 100644 --- a/Robust.Shared/GameObjects/EntityManager.cs +++ b/Robust.Shared/GameObjects/EntityManager.cs @@ -87,9 +87,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; From 21c414f8cb35f70efc0f161b418c8bf9e2174458 Mon Sep 17 00:00:00 2001 From: kaylie Date: Tue, 17 Mar 2026 01:31:59 +0100 Subject: [PATCH 57/77] Caught a possible threading bug. --- Robust.Client/GameObjects/ClientEntityManager.Network.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 From 447addd11c50fb67c27815c60f6ec397cdaae1a6 Mon Sep 17 00:00:00 2001 From: kaylie Date: Tue, 17 Mar 2026 11:11:21 +0100 Subject: [PATCH 58/77] Fixes for content-side testing. --- Robust.Client/ClientIoC.cs | 1 + .../RobustServerSimulation.cs | 1 + Robust.Server/ServerIoC.cs | 1 + .../EntityBuilders/EntityBuilder.AddComp.cs | 18 +++++++++--------- .../EntityBuilders/EntityBuilder.Initialize.cs | 7 +++++-- 5 files changed, 17 insertions(+), 11 deletions(-) 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.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/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/GameObjects/EntityBuilders/EntityBuilder.AddComp.cs b/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.AddComp.cs index 51acbebaa53..943458c51f1 100644 --- a/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.AddComp.cs +++ b/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.AddComp.cs @@ -34,7 +34,7 @@ public EntityBuilder AddComp() /// /// Adds a component to the entity being built. /// - /// The type of 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(Type t) @@ -77,16 +77,16 @@ public EntityBuilder AddComp(T component) 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."); } -#pragma warning disable CS0618 // Type or member is obsolete - component.Owner = ReservedEntity; -#pragma warning restore CS0618 // Type or member is obsolete - return this; } @@ -140,14 +140,14 @@ public EntityBuilder EnsureCopyComp(T component, ISerializationContext? conte if (!EntityComponents.TryGetValue(component.GetType(), out var comp)) comp = _factory.GetComponent(component.GetType()); - _serMan.CopyTo(component, ref comp, context, notNullableOverride: true); - - EntityComponents.TryAdd(component.GetType(), comp); - #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; } diff --git a/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.Initialize.cs b/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.Initialize.cs index fd906f7304a..6a842e6ebad 100644 --- a/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.Initialize.cs +++ b/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.Initialize.cs @@ -11,10 +11,13 @@ public sealed partial class EntityBuilder /// The load context to use, if any. private void InitializeMinimalEntity(ISerializationContext? context = null) { - MetaData = new MetaDataComponent(); +#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); - Transform = new TransformComponent(); + // This thing is so legacy it has a [Dependency] in it. + Transform = _factory.GetComponent(); AddComp(Transform); } From d8cf8364c603ef5d33e6520b54e9979263f60525 Mon Sep 17 00:00:00 2001 From: kaylie Date: Tue, 17 Mar 2026 11:42:12 +0100 Subject: [PATCH 59/77] More intuitive names for the EntityBuilder spawn methods. --- .../GameObjects/EntityBuilder/EntityBuilderTests.cs | 6 +++--- .../GameObjects/EntityBuilders/EntityBuilder.cs | 2 +- .../GameObjects/EntityManager.ApplyCommandBuffer.cs | 4 ++-- Robust.Shared/GameObjects/EntityManager.EntityBuilder.cs | 6 +++--- .../GameObjects/IEntityManager.CommandBuffers.cs | 9 +++++---- 5 files changed, 14 insertions(+), 13 deletions(-) diff --git a/Robust.Shared.IntegrationTests/GameObjects/EntityBuilder/EntityBuilderTests.cs b/Robust.Shared.IntegrationTests/GameObjects/EntityBuilder/EntityBuilderTests.cs index db9ba77b317..084f889c5db 100644 --- a/Robust.Shared.IntegrationTests/GameObjects/EntityBuilder/EntityBuilderTests.cs +++ b/Robust.Shared.IntegrationTests/GameObjects/EntityBuilder/EntityBuilderTests.cs @@ -56,7 +56,7 @@ public void CreateEntity() .AddComp() .AddComp(); - var ent = _entMan.ApplyEntityBuilder(builder); + 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"); @@ -85,7 +85,7 @@ public void CreateHierarchy() // Spawn the map and its children. // Note: Currently order does matter. I'd like to lift that requirement sometime, but it does. - _entMan.BulkApplyEntityBuilders([root, child1, child2]); + _entMan.SpawnBulk([root, child1, child2]); Assert.That(_entMan.GetComponent(root.ReservedEntity).MapId, NUnit.Framework.Is.Not.EqualTo(MapId.Nullspace)); @@ -101,7 +101,7 @@ public void CreateFromPrototype() { var builder = _entMan.EntityBuilder(TestEnt1); - _entMan.ApplyEntityBuilder(builder); + _entMan.Spawn(builder); using (Assert.EnterMultipleScope()) { diff --git a/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.cs b/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.cs index c504a3480fc..b21a936e108 100644 --- a/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.cs +++ b/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.cs @@ -165,7 +165,7 @@ public EntityBuilder ChildOf(EntityUid parent, Vector2 relativePos = default, An /// The coordinates to spawn at /// /// The builder, for chaining. - public EntityBuilder SpawnAt(MapCoordinates mapCoordinates) + public EntityBuilder LocatedAt(MapCoordinates mapCoordinates) { _mapCoordinates = mapCoordinates; diff --git a/Robust.Shared/GameObjects/EntityManager.ApplyCommandBuffer.cs b/Robust.Shared/GameObjects/EntityManager.ApplyCommandBuffer.cs index a23923694d3..7040c47329a 100644 --- a/Robust.Shared/GameObjects/EntityManager.ApplyCommandBuffer.cs +++ b/Robust.Shared/GameObjects/EntityManager.ApplyCommandBuffer.cs @@ -43,12 +43,12 @@ public void ApplyCommandBuffer(CommandBuffer buffer) { case EntityBuilder b: { - ApplyEntityBuilder(b); + Spawn(b); break; } case EntityBuilder[] b: { - BulkApplyEntityBuilders(b); + SpawnBulk(b); break; } } diff --git a/Robust.Shared/GameObjects/EntityManager.EntityBuilder.cs b/Robust.Shared/GameObjects/EntityManager.EntityBuilder.cs index 4dcc4242d62..2a7de0e5fdf 100644 --- a/Robust.Shared/GameObjects/EntityManager.EntityBuilder.cs +++ b/Robust.Shared/GameObjects/EntityManager.EntityBuilder.cs @@ -6,15 +6,15 @@ namespace Robust.Shared.GameObjects; public abstract partial class EntityManager { - public EntityUid ApplyEntityBuilder(EntityBuilder builder, bool mapInit = true) + public EntityUid Spawn(EntityBuilder builder, bool mapInit = true) { var ent = builder.ReservedEntity; // Doesn't allocate. Not that it matters, we're about to allocate a lot. - BulkApplyEntityBuilders([builder]); + SpawnBulk([builder]); return ent; } - public void BulkApplyEntityBuilders(ReadOnlySpan builders, bool mapInit = true) + public void SpawnBulk(ReadOnlySpan builders, bool mapInit = true) { // Create entities + ComponentAdd foreach (var builder in builders) diff --git a/Robust.Shared/GameObjects/IEntityManager.CommandBuffers.cs b/Robust.Shared/GameObjects/IEntityManager.CommandBuffers.cs index bf0550afd81..88e097bbe2e 100644 --- a/Robust.Shared/GameObjects/IEntityManager.CommandBuffers.cs +++ b/Robust.Shared/GameObjects/IEntityManager.CommandBuffers.cs @@ -19,15 +19,16 @@ public partial interface IEntityManager public void ApplyCommandBuffer(CommandBuffer buffer); /// - /// Applies an entity builder to the simulation, constructing the entity within in full. + /// Applies an entity builder to the simulation, spawning the entity it describes. /// /// The builder to apply. /// Whether map init should be run for the built entities. /// The constructed entity. - public EntityUid ApplyEntityBuilder(EntityBuilder builder, bool mapInit = true); + public EntityUid Spawn(EntityBuilder builder, bool mapInit = true); /// - /// Runs through entity builders in phases, ala loading a map. + /// 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. @@ -36,5 +37,5 @@ public partial interface IEntityManager /// /// The entity builders to allocate for. /// Whether map init should be run for the built entities. - public void BulkApplyEntityBuilders(ReadOnlySpan builders, bool mapInit = true); + public void SpawnBulk(ReadOnlySpan builders, bool mapInit = true); } From 028f511fc481315115c38d0df91d86adf58bb2dd Mon Sep 17 00:00:00 2001 From: kaylie Date: Wed, 18 Mar 2026 11:12:53 +0100 Subject: [PATCH 60/77] Add a new, failing test. --- .../EntityBuilder/EntityBuilderTests.cs | 39 +++++++++++++++++ .../EntityManager.EntityBuilder.cs | 42 +++++++++++++++++++ .../IEntityManager.CommandBuffers.cs | 31 +++++++++++++- 3 files changed, 110 insertions(+), 2 deletions(-) diff --git a/Robust.Shared.IntegrationTests/GameObjects/EntityBuilder/EntityBuilderTests.cs b/Robust.Shared.IntegrationTests/GameObjects/EntityBuilder/EntityBuilderTests.cs index 084f889c5db..9b904432192 100644 --- a/Robust.Shared.IntegrationTests/GameObjects/EntityBuilder/EntityBuilderTests.cs +++ b/Robust.Shared.IntegrationTests/GameObjects/EntityBuilder/EntityBuilderTests.cs @@ -95,6 +95,45 @@ public void CreateHierarchy() "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() + .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() diff --git a/Robust.Shared/GameObjects/EntityManager.EntityBuilder.cs b/Robust.Shared/GameObjects/EntityManager.EntityBuilder.cs index 2a7de0e5fdf..2f1e92a0157 100644 --- a/Robust.Shared/GameObjects/EntityManager.EntityBuilder.cs +++ b/Robust.Shared/GameObjects/EntityManager.EntityBuilder.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Runtime.CompilerServices; using Robust.Shared.GameObjects.EntityBuilders; @@ -43,4 +44,45 @@ public void SpawnBulk(ReadOnlySpan builders, bool mapInit = true) } } } + + public void SpawnBulkUnordered(Span builders, bool mapInit = true) + { + // 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/IEntityManager.CommandBuffers.cs b/Robust.Shared/GameObjects/IEntityManager.CommandBuffers.cs index 88e097bbe2e..7dbab8d7ea7 100644 --- a/Robust.Shared/GameObjects/IEntityManager.CommandBuffers.cs +++ b/Robust.Shared/GameObjects/IEntityManager.CommandBuffers.cs @@ -21,7 +21,7 @@ public partial interface IEntityManager /// /// Applies an entity builder to the simulation, spawning the entity it describes. /// - /// The builder to apply. + /// The entity builder to spawn into the world. /// Whether map init should be run for the built entities. /// The constructed entity. public EntityUid Spawn(EntityBuilder builder, bool mapInit = true); @@ -35,7 +35,34 @@ public partial interface IEntityManager /// 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 allocate for. + /// The entity builders to spawn into the world. /// Whether map init should be run for the built entities. + /// public void SpawnBulk(ReadOnlySpan builders, bool mapInit = 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 = true); } From 68369636b6c591a4d57dba05507b69a8ef35536b Mon Sep 17 00:00:00 2001 From: kaylie Date: Wed, 18 Mar 2026 11:13:23 +0100 Subject: [PATCH 61/77] It was grid traversal. --- .../GameObjects/EntityBuilder/EntityBuilderTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Robust.Shared.IntegrationTests/GameObjects/EntityBuilder/EntityBuilderTests.cs b/Robust.Shared.IntegrationTests/GameObjects/EntityBuilder/EntityBuilderTests.cs index 9b904432192..bc30e17c71c 100644 --- a/Robust.Shared.IntegrationTests/GameObjects/EntityBuilder/EntityBuilderTests.cs +++ b/Robust.Shared.IntegrationTests/GameObjects/EntityBuilder/EntityBuilderTests.cs @@ -107,7 +107,6 @@ public void CreateUnorderedHierarchy() var child1 = _entMan.EntityBuilder() .Named("Test child 1") .ChildOf(root.ReservedEntity, new Vector2(-4, -4)) - .AddComp() .AddComp(); var child2 = _entMan.EntityBuilder() From 86f7a5820bc2572e42a3230e8f6eb97662737bdb Mon Sep 17 00:00:00 2001 From: kaylie Date: Wed, 18 Mar 2026 14:02:14 +0100 Subject: [PATCH 62/77] More stuff. --- .../EntityBuilder/EntityBuilderTests.cs | 1 - .../CommandBuffer.InvokeAction.cs | 7 +- .../CommandBuffers/CommandBuffer.cs | 12 +- .../EntityBuilders/EntityBuilder.Meta.cs | 12 +- .../EntityBuilders/EntityBuilder.cs | 145 +++++++++++++++++- .../EntityManager.EntityBuilder.cs | 16 ++ Robust.Shared/GameObjects/EntityManager.cs | 2 + 7 files changed, 175 insertions(+), 20 deletions(-) diff --git a/Robust.Shared.IntegrationTests/GameObjects/EntityBuilder/EntityBuilderTests.cs b/Robust.Shared.IntegrationTests/GameObjects/EntityBuilder/EntityBuilderTests.cs index bc30e17c71c..aa976511a4f 100644 --- a/Robust.Shared.IntegrationTests/GameObjects/EntityBuilder/EntityBuilderTests.cs +++ b/Robust.Shared.IntegrationTests/GameObjects/EntityBuilder/EntityBuilderTests.cs @@ -80,7 +80,6 @@ public void CreateHierarchy() var child2 = _entMan.EntityBuilder() .Named("Test child 2") .ChildOf(root.ReservedEntity, new Vector2(4, 4)) - .AddComp() .AddComp(); // Spawn the map and its children. diff --git a/Robust.Shared/GameObjects/CommandBuffers/CommandBuffer.InvokeAction.cs b/Robust.Shared/GameObjects/CommandBuffers/CommandBuffer.InvokeAction.cs index 0a31f2dadb3..62b63bd3ee7 100644 --- a/Robust.Shared/GameObjects/CommandBuffers/CommandBuffer.InvokeAction.cs +++ b/Robust.Shared/GameObjects/CommandBuffers/CommandBuffer.InvokeAction.cs @@ -13,10 +13,12 @@ public sealed partial class CommandBuffer /// The action to invoke. /// The context object to invoke it with. /// The type of the context object. - public void InvokeAction(Action action, T context) + /// The command buffer, for chaining. + public CommandBuffer InvokeAction(Action action, T context) where T : class { CommandBufferEntry.QueuedActionT(action, context, out NextEntry()); + return this; } /// @@ -29,9 +31,10 @@ public void InvokeAction(Action action, T context) /// The context object to invoke it with. /// The entity to use as a target. /// The type of the context object. - public void InvokeAction(Action action, T context, EntityUid target) + 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 index f3717dc8aa2..5ee4efd09ed 100644 --- a/Robust.Shared/GameObjects/CommandBuffers/CommandBuffer.cs +++ b/Robust.Shared/GameObjects/CommandBuffers/CommandBuffer.cs @@ -53,23 +53,25 @@ private ref CommandBufferEntry NextEntry() /// 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 created sub-buffer. - public CommandBuffer CreateSubBuffer() + /// The command buffer, for chaining. + public CommandBuffer CreateSubBuffer(out CommandBuffer subBuffer) { - var subBuffer = _entMan.GetCommandBuffer(); + subBuffer = _entMan.GetCommandBuffer(); CommandBufferEntry.SubBuffer(subBuffer, out NextEntry()); - return subBuffer; + return this; } /// /// Adds an entity deletion to the buffer. /// /// The entity to delete immediately. - public void DeleteEntity(EntityUid target) + /// The command buffer, for chaining. + public CommandBuffer DeleteEntity(EntityUid target) { CommandBufferEntry.DeleteEntity(target, out NextEntry()); + return this; } /// diff --git a/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.Meta.cs b/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.Meta.cs index bf08a5c116e..75ad252cee8 100644 --- a/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.Meta.cs +++ b/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.Meta.cs @@ -9,8 +9,7 @@ public sealed partial class EntityBuilder /// public EntityBuilder Named(string newName) { - GetComp(out MetaDataComponent meta); - meta._entityName = newName; + MetaData._entityName = newName; return this; } @@ -19,8 +18,7 @@ public EntityBuilder Named(string newName) /// public EntityBuilder NamedLoc(LocId newName) { - GetComp(out MetaDataComponent meta); - meta._entityName = _locMan.GetString(newName); + MetaData._entityName = _locMan.GetString(newName); return this; } @@ -29,8 +27,7 @@ public EntityBuilder NamedLoc(LocId newName) /// public EntityBuilder Described(string newDesc) { - GetComp(out MetaDataComponent meta); - meta._entityDescription = newDesc; + MetaData._entityDescription = newDesc; return this; } @@ -39,8 +36,7 @@ public EntityBuilder Described(string newDesc) /// public EntityBuilder DescribedLoc(LocId newDesc) { - GetComp(out MetaDataComponent meta); - meta._entityDescription = _locMan.GetString(newDesc); + MetaData._entityDescription = _locMan.GetString(newDesc); return this; } } diff --git a/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.cs b/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.cs index b21a936e108..69c5672606a 100644 --- a/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.cs +++ b/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.cs @@ -1,14 +1,15 @@ 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.Map.Components; using Robust.Shared.Maths; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.Manager; -using Robust.Shared.Utility; namespace Robust.Shared.GameObjects.EntityBuilders; @@ -18,12 +19,14 @@ namespace Robust.Shared.GameObjects.EntityBuilders; /// Supports construction from a prototype, and from a fresh list of components. /// ///
+[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 @@ -43,8 +46,10 @@ public sealed partial class EntityBuilder /// private MapCoordinates? _mapCoordinates; - internal MetaDataComponent MetaData = default!; - internal TransformComponent Transform = default!; + 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) { @@ -52,10 +57,14 @@ internal EntityBuilder(IDependencyCollection collection, EntityUid reservedEntit _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, @@ -68,6 +77,9 @@ internal static EntityBuilder BlankEntity( return self; } + /// + /// Constructs an entity builder for the given prototype and reserved entity ID. + /// internal static EntityBuilder PrototypedEntity( IDependencyCollection collection, EntityUid reservedId, @@ -125,18 +137,112 @@ public EntityBuilder MutateComp(Action action, 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. /// @@ -177,4 +283,35 @@ public EntityBuilder LocatedAt(MapCoordinates mapCoordinates) 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/EntityManager.EntityBuilder.cs b/Robust.Shared/GameObjects/EntityManager.EntityBuilder.cs index 2f1e92a0157..f4b18a96bb4 100644 --- a/Robust.Shared/GameObjects/EntityManager.EntityBuilder.cs +++ b/Robust.Shared/GameObjects/EntityManager.EntityBuilder.cs @@ -43,6 +43,22 @@ public void SpawnBulk(ReadOnlySpan builders, bool mapInit = true) RunMapInit(builder.ReservedEntity, builder.MetaData); } } + +#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.PostInitCommands is {} commands) + ApplyCommandBuffer(commands); + } } public void SpawnBulkUnordered(Span builders, bool mapInit = true) diff --git a/Robust.Shared/GameObjects/EntityManager.cs b/Robust.Shared/GameObjects/EntityManager.cs index ead5faabe7e..3ef9051b158 100644 --- a/Robust.Shared/GameObjects/EntityManager.cs +++ b/Robust.Shared/GameObjects/EntityManager.cs @@ -20,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; @@ -39,6 +40,7 @@ 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!; From 9813ab2bef5ab350530ab5e432e40c6401e761e6 Mon Sep 17 00:00:00 2001 From: kaylie Date: Wed, 18 Mar 2026 15:30:27 +0100 Subject: [PATCH 63/77] Fix various IoCManager invocations that could explode on other threads. --- .../Components/Renderable/SpriteComponent.cs | 5 ++- .../Graphics/Shaders/ShaderPrototype.cs | 7 ++-- .../EntityBuilder/ClientEntityBuilderTests.cs | 41 +++++++++++++++++++ .../EntityBuilder/EntityBuilderTests.cs | 2 + Robust.Shared/Prototypes/EntityPrototype.cs | 4 +- Robust.Shared/Prototypes/PrototypeManager.cs | 4 ++ .../Serialization/ISerializationHooks.cs | 24 ++++++++++- .../Manager/SerializationManager.cs | 8 +++- 8 files changed, 84 insertions(+), 11 deletions(-) create mode 100644 Robust.Shared.IntegrationTests/GameObjects/EntityBuilder/ClientEntityBuilderTests.cs diff --git a/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs b/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs index d1dbd6587c6..802440e9240 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. - IoCManager.InjectDependencies(this); + collection.InjectDependencies(this); + if (!string.IsNullOrWhiteSpace(rsi)) { var rsiPath = TextureRoot / rsi; 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.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 index aa976511a4f..c4bb1fefa36 100644 --- a/Robust.Shared.IntegrationTests/GameObjects/EntityBuilder/EntityBuilderTests.cs +++ b/Robust.Shared.IntegrationTests/GameObjects/EntityBuilder/EntityBuilderTests.cs @@ -1,5 +1,7 @@ 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; diff --git a/Robust.Shared/Prototypes/EntityPrototype.cs b/Robust.Shared/Prototypes/EntityPrototype.cs index 436ec501a7b..b475a007891 100644 --- a/Robust.Shared/Prototypes/EntityPrototype.cs +++ b/Robust.Shared/Prototypes/EntityPrototype.cs @@ -163,9 +163,9 @@ public EntityPrototype() Components.AddComponentManual("MetaData", new MetaDataComponent()); } - void ISerializationHooks.AfterDeserialization() + void ISerializationHooks.AfterDeserialization(IDependencyCollection collection) { - _loc = IoCManager.Resolve(); + _loc = collection.Resolve(); } [Obsolete("Pass in IComponentFactory")] 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 } From 2d177046e742349f765bfe2f73133034b70cb6a2 Mon Sep 17 00:00:00 2001 From: kaylie Date: Wed, 18 Mar 2026 17:09:51 +0100 Subject: [PATCH 64/77] Start smoke testing EntityBuilder in Spawn(), tests fail rn. --- .../EntityBuilder.Initialize.cs | 20 ++++++++++++++++-- .../EntityBuilders/EntityBuilder.cs | 21 +++++++++++++++++++ .../EntityManager.EntityBuilder.cs | 17 ++++++++------- .../GameObjects/EntityManager.Spawn.cs | 13 +++++++++--- .../IEntityManager.CommandBuffers.cs | 10 ++++----- 5 files changed, 63 insertions(+), 18 deletions(-) diff --git a/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.Initialize.cs b/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.Initialize.cs index 6a842e6ebad..fb5e34ee8b1 100644 --- a/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.Initialize.cs +++ b/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.Initialize.cs @@ -8,7 +8,7 @@ public sealed partial class EntityBuilder /// /// Creates the bare minimal spawnable entity with metadata and a transform. /// - /// The load context to use, if any. + /// The serialization context to use, if any. private void InitializeMinimalEntity(ISerializationContext? context = null) { #pragma warning disable CS0618 // Type or member is obsolete @@ -25,7 +25,7 @@ private void InitializeMinimalEntity(ISerializationContext? context = null) /// Initializes a command buffer from a prototype, cloning all the components onto the new entity. /// /// The prototype to construct from. - /// The load context to use. + /// The serialization context to use, if any. private void InitializeFromPrototype(EntProtoId entityProtoId, ISerializationContext? context = null) { var entityProto = _protoMan.Index(entityProtoId); @@ -37,4 +37,20 @@ private void InitializeFromPrototype(EntProtoId entityProtoId, ISerializationCon 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.cs b/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.cs index 69c5672606a..b2c0951eff1 100644 --- a/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.cs +++ b/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.cs @@ -265,6 +265,27 @@ public EntityBuilder ChildOf(EntityUid parent, Vector2 relativePos = default, An 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. /// diff --git a/Robust.Shared/GameObjects/EntityManager.EntityBuilder.cs b/Robust.Shared/GameObjects/EntityManager.EntityBuilder.cs index f4b18a96bb4..070cb8f667e 100644 --- a/Robust.Shared/GameObjects/EntityManager.EntityBuilder.cs +++ b/Robust.Shared/GameObjects/EntityManager.EntityBuilder.cs @@ -7,7 +7,7 @@ namespace Robust.Shared.GameObjects; public abstract partial class EntityManager { - public EntityUid Spawn(EntityBuilder builder, bool mapInit = true) + 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. @@ -15,7 +15,7 @@ public EntityUid Spawn(EntityBuilder builder, bool mapInit = true) return ent; } - public void SpawnBulk(ReadOnlySpan builders, bool mapInit = true) + public void SpawnBulk(ReadOnlySpan builders, bool? mapInit = null) { // Create entities + ComponentAdd foreach (var builder in builders) @@ -35,15 +35,16 @@ public void SpawnBulk(ReadOnlySpan builders, bool mapInit = true) StartEntity(builder.ReservedEntity); } - // MapInit - if (mapInit) + foreach (var builder in builders) { - foreach (var builder in builders) - { + var doMapInit = mapInit; + doMapInit ??= _mapSystem.IsInitialized(TransformQuery.GetComponent(builder.ReservedEntity).MapID); + + if (doMapInit.Value) RunMapInit(builder.ReservedEntity, builder.MetaData); - } } + #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 @@ -61,7 +62,7 @@ public void SpawnBulk(ReadOnlySpan builders, bool mapInit = true) } } - public void SpawnBulkUnordered(Span builders, bool mapInit = true) + public void SpawnBulkUnordered(Span builders, bool? mapInit = null) { // Create an index of entity uids to builders, var index = new Dictionary(); diff --git a/Robust.Shared/GameObjects/EntityManager.Spawn.cs b/Robust.Shared/GameObjects/EntityManager.Spawn.cs index 94a86489b37..4c8a9a609a8 100644 --- a/Robust.Shared/GameObjects/EntityManager.Spawn.cs +++ b/Robust.Shared/GameObjects/EntityManager.Spawn.cs @@ -103,9 +103,16 @@ public virtual EntityUid SpawnAttachedTo(string? protoName, EntityCoordinates co if (!coordinates.IsValid(this)) throw new InvalidOperationException($"Tried to spawn entity {protoName} on invalid coordinates {coordinates}."); - var entity = CreateEntityUninitialized(protoName, coordinates, overrides, rotation); - InitializeAndStartEntity(entity, _xforms.GetMapId(coordinates)); - return entity; + // 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); } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/Robust.Shared/GameObjects/IEntityManager.CommandBuffers.cs b/Robust.Shared/GameObjects/IEntityManager.CommandBuffers.cs index 7dbab8d7ea7..3a62ad2e6bd 100644 --- a/Robust.Shared/GameObjects/IEntityManager.CommandBuffers.cs +++ b/Robust.Shared/GameObjects/IEntityManager.CommandBuffers.cs @@ -22,9 +22,9 @@ public partial interface IEntityManager /// 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. + /// 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 = true); + 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 @@ -36,9 +36,9 @@ public partial interface IEntityManager /// 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. + /// Whether map init should be run for the built entities, or automatically inferred if null. /// - public void SpawnBulk(ReadOnlySpan builders, bool mapInit = true); + public void SpawnBulk(ReadOnlySpan builders, bool? mapInit = null); /// /// Spawns the provided set of entity builders, in a manner much like loading a map does (with initialization @@ -64,5 +64,5 @@ public partial interface IEntityManager /// 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 = true); + public void SpawnBulkUnordered(Span builders, bool? mapInit = null); } From e9a685df4afce1e45427137fed3429c0e89a9f34 Mon Sep 17 00:00:00 2001 From: kaylie Date: Wed, 18 Mar 2026 17:17:28 +0100 Subject: [PATCH 65/77] Pass engine tests again. --- .../EntitySerialization/LifestageSerializationTest.cs | 2 +- .../GameObjects/EntityManager.EntityBuilder.cs | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) 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/GameObjects/EntityManager.EntityBuilder.cs b/Robust.Shared/GameObjects/EntityManager.EntityBuilder.cs index 070cb8f667e..68ffd3e9c8e 100644 --- a/Robust.Shared/GameObjects/EntityManager.EntityBuilder.cs +++ b/Robust.Shared/GameObjects/EntityManager.EntityBuilder.cs @@ -37,11 +37,20 @@ public void SpawnBulk(ReadOnlySpan builders, bool? mapInit = null foreach (var builder in builders) { + var xform = TransformQuery.GetComponent(builder.ReservedEntity); var doMapInit = mapInit; - doMapInit ??= _mapSystem.IsInitialized(TransformQuery.GetComponent(builder.ReservedEntity).MapID); + doMapInit ??= _mapSystem.IsInitialized(xform.MapID); if (doMapInit.Value) RunMapInit(builder.ReservedEntity, builder.MetaData); + + // 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 (xform.ParentUid.IsValid() && MetaQuery.Comp(xform.ParentUid).EntityPaused) + { + EntitySysManager.GetEntitySystem().SetEntityPaused(builder.ReservedEntity, true, builder.MetaData); + } } From 5a41233cad4ea375d42dff42292fd2502e550e46 Mon Sep 17 00:00:00 2001 From: kaylie Date: Wed, 18 Mar 2026 17:19:30 +0100 Subject: [PATCH 66/77] comments. --- Robust.Shared/GameObjects/EntityManager.EntityBuilder.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Robust.Shared/GameObjects/EntityManager.EntityBuilder.cs b/Robust.Shared/GameObjects/EntityManager.EntityBuilder.cs index 68ffd3e9c8e..111d69683b3 100644 --- a/Robust.Shared/GameObjects/EntityManager.EntityBuilder.cs +++ b/Robust.Shared/GameObjects/EntityManager.EntityBuilder.cs @@ -37,8 +37,10 @@ public void SpawnBulk(ReadOnlySpan builders, bool? mapInit = null foreach (var builder in builders) { + // MapInit is inherited, if we're on an initialized map we should also map init unless otherwise told. var xform = TransformQuery.GetComponent(builder.ReservedEntity); var doMapInit = mapInit; + // Replicate whatever map we're on if mapInit is null. doMapInit ??= _mapSystem.IsInitialized(xform.MapID); if (doMapInit.Value) From cb728869f85a08a0885bc7bfdc75225cf8bc2550 Mon Sep 17 00:00:00 2001 From: kaylie Date: Wed, 18 Mar 2026 17:42:15 +0100 Subject: [PATCH 67/77] start on fixing spritecomponent. --- .../Components/Renderable/SpriteComponent.cs | 62 ++----------------- .../GameObjects/EntitySystems/SpriteSystem.cs | 55 ++++++++++++++++ .../EntityBuilder.Initialize.cs | 2 + 3 files changed, 62 insertions(+), 57 deletions(-) diff --git a/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs b/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs index 802440e9240..ec3d5e82288 100644 --- a/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs +++ b/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs @@ -35,7 +35,7 @@ namespace Robust.Client.GameObjects { [RegisterComponent] - public sealed partial class SpriteComponent : Component, IComponentDebug, ISerializationHooks, IComponentTreeEntry, IAnimationProperties + public sealed partial class SpriteComponent : Component, IComponentDebug, IComponentTreeEntry, IAnimationProperties { public const string LogCategory = "go.comp.sprite"; @@ -166,11 +166,11 @@ public RSI? BaseRSI set => Sys.SetBaseRsi((Owner, this), value); } - [DataField("sprite", readOnly: true)] private string? rsi; - [DataField("layers", readOnly: true)] private List layerDatums = new(); + [DataField("sprite", readOnly: true)] internal string? rsi; + [DataField("layers", readOnly: true)] internal List layerDatums = new(); - [DataField(readOnly: true)] private string? state; - [DataField(readOnly: true)] private string? texture; + [DataField(readOnly: true)] internal string? state; + [DataField(readOnly: true)] internal string? texture; /// /// Should this entity show up in containers regardless of whether the container can show contents? @@ -243,58 +243,6 @@ public ShaderInstance? PostShader public ISpriteLayer this[object layerKey] => this[LayerMap[layerKey]]; public IEnumerable AllLayers => Layers; - void ISerializationHooks.AfterDeserialization(IDependencyCollection collection) - { - // Please somebody burn this to the ground. There is so much spaghetti. - // Why has no one answered my prayers. - - collection.InjectDependencies(this); - - if (!string.IsNullOrWhiteSpace(rsi)) - { - var rsiPath = TextureRoot / rsi; - if (resourceCache.TryGetResource(rsiPath, out RSIResource? resource)) - _baseRsi = resource.RSI; - else - Logger.ErrorS(LogCategory, "Unable to load RSI '{0}'.", rsiPath); - } - - if (layerDatums.Count == 0) - { - if (state != null || texture != null) - { - layerDatums.Insert(0, new PrototypeLayerData - { - TexturePath = string.IsNullOrWhiteSpace(texture) ? null : texture, - State = string.IsNullOrWhiteSpace(state) ? null : state, - Color = Color.White, - Scale = Vector2.One, - Visible = true, - RenderingStrategy = LayerRenderingStrategy.UseSpriteStrategy, - Cycle = false, - }); - state = null; - texture = null; - } - } - - if (layerDatums.Count != 0) - { - LayerMap.Clear(); - Layers.Clear(); - foreach (var datum in layerDatums) - { - var layer = new Layer((Owner, this), Layers.Count); - Layers.Add(layer); - LayerSetData(layer, datum); - } - - } - - BoundsDirty = true; - LocalMatrix = Matrix3Helpers.CreateTransform(in offset, in rotation, in scale); - } - /// /// If false, this will prevent any of this sprite's animated layers from looping their animation. /// This will set whenever any layer's animation finishes. diff --git a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.cs b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.cs index 33a4e3ae540..cb55dedbf1d 100644 --- a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.cs +++ b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.cs @@ -61,6 +61,7 @@ public override void Initialize() UpdatesAfter.Add(typeof(SpriteTreeSystem)); SubscribeLocalEvent(OnPrototypesReloaded); + SubscribeLocalEvent(OnAdd); SubscribeLocalEvent(OnInit); Subs.CVar(_cfg, CVars.RenderSpriteDirectionBias, OnBiasChanged, true); @@ -68,6 +69,60 @@ public override void Initialize() _query = GetEntityQuery(); } + private void OnAdd(Entity ent, ref ComponentAdd args) + { + // This is a direct, dirty port of the old ISerializationHook from spritecomponent. + // This is just to make EntityBuilders work. + // This does not answer prayers, but it does fix some of my problems. + + var c = ent.Comp; + IoCManager.InjectDependencies(c); + + if (!string.IsNullOrWhiteSpace(c.rsi)) + { + var rsiPath = TextureRoot / c.rsi; + if (_resourceCache.TryGetResource(rsiPath, out RSIResource? resource)) + c._baseRsi = resource.RSI; + else + Log.Error("Unable to load RSI '{0}'.", rsiPath); + } + + if (c.layerDatums.Count == 0) + { + if (c.state != null || c.texture != null) + { + c.layerDatums.Insert(0, new PrototypeLayerData + { + TexturePath = string.IsNullOrWhiteSpace(c.texture) ? null : c.texture, + State = string.IsNullOrWhiteSpace(c.state) ? null : c.state, + Color = Color.White, + Scale = Vector2.One, + Visible = true, + RenderingStrategy = LayerRenderingStrategy.UseSpriteStrategy, + Cycle = false, + }); + c.state = null; + c.texture = null; + } + } + + if (c.layerDatums.Count != 0) + { + c.LayerMap.Clear(); + c.Layers.Clear(); + foreach (var datum in c.layerDatums) + { + var layer = new Layer(ent, c.Layers.Count); + c.Layers.Add(layer); + c.LayerSetData(layer, datum); + } + + } + + c.BoundsDirty = true; + c.LocalMatrix = Matrix3Helpers.CreateTransform(in c.offset, in c.rotation, in c.scale); + } + public bool IsVisible(Layer layer) { return layer.Visible && layer.CopyToShaderParameters == null; diff --git a/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.Initialize.cs b/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.Initialize.cs index fb5e34ee8b1..24fc415c6e4 100644 --- a/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.Initialize.cs +++ b/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.Initialize.cs @@ -32,6 +32,8 @@ private void InitializeFromPrototype(EntProtoId entityProtoId, ISerializationCon InitializeMinimalEntity(context); + MetaData._entityPrototype = entityProto; + foreach (var component in entityProto.Components.Components()) { EnsureCopyComp(component, context); From 23e852fbf6e00e0873066439a27d3f892e50dc73 Mon Sep 17 00:00:00 2001 From: kaylie Date: Wed, 18 Mar 2026 18:13:02 +0100 Subject: [PATCH 68/77] Work on fixing this mess. --- Robust.Client/GameObjects/EntitySystems/SpriteSystem.cs | 3 +-- Robust.Shared/Containers/ContainerManagerComponent.cs | 5 +---- Robust.Shared/Containers/SharedContainerSystem.cs | 5 +++-- Robust.Shared/Prototypes/EntityPrototype.cs | 8 +++++++- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.cs b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.cs index cb55dedbf1d..a89547a23c6 100644 --- a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.cs +++ b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.cs @@ -114,9 +114,8 @@ private void OnAdd(Entity ent, ref ComponentAdd args) { var layer = new Layer(ent, c.Layers.Count); c.Layers.Add(layer); - c.LayerSetData(layer, datum); + LayerSetData(layer, datum); } - } c.BoundsDirty = true; diff --git a/Robust.Shared/Containers/ContainerManagerComponent.cs b/Robust.Shared/Containers/ContainerManagerComponent.cs index 9ce385dd1a6..ee59708b41a 100644 --- a/Robust.Shared/Containers/ContainerManagerComponent.cs +++ b/Robust.Shared/Containers/ContainerManagerComponent.cs @@ -27,10 +27,7 @@ public sealed partial class ContainerManagerComponent : Component, ISerializatio // Requires a custom serializer + copier to get rid of. Good luck void ISerializationHooks.AfterDeserialization() { - foreach (var (id, container) in Containers) - { - container.Init(default!, id, (Owner, this)); - } + } [Obsolete] diff --git a/Robust.Shared/Containers/SharedContainerSystem.cs b/Robust.Shared/Containers/SharedContainerSystem.cs index b86a0a999d9..447e8882137 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,7 +56,7 @@ public override void Initialize() TransformQuery = GetEntityQuery(); } - private void OnInit(Entity ent, ref ComponentInit args) + private void OnAdd(Entity ent, ref ComponentAdd args) { foreach (var (id, container) in ent.Comp.Containers) { @@ -64,6 +64,7 @@ private void OnInit(Entity ent, ref ComponentInit arg } } + private void OnContainerGetState(EntityUid uid, ContainerManagerComponent component, ref ComponentGetState args) { Dictionary containerSet = diff --git a/Robust.Shared/Prototypes/EntityPrototype.cs b/Robust.Shared/Prototypes/EntityPrototype.cs index b475a007891..350957aa3a2 100644 --- a/Robust.Shared/Prototypes/EntityPrototype.cs +++ b/Robust.Shared/Prototypes/EntityPrototype.cs @@ -263,22 +263,28 @@ public static void EnsureCompExistsAndDeserialize(EntityUid entity, IComponent data, ISerializationContext? context) { + var add = false; if (!entityManager.TryGetComponent(entity, compReg.Idx, out var component)) { var newComponent = factory.GetComponent(compReg); - entityManager.AddComponent(entity, newComponent); + add = true; component = newComponent; } if (context is not EntityDeserializer map) { serManager.CopyTo(data, ref component, context, notNullableOverride: true); + if (add) + entityManager.AddComponent(entity, component); return; } map.CurrentComponent = compReg.Name; serManager.CopyTo(data, ref component, context, notNullableOverride: true); map.CurrentComponent = null; + + if (add) + entityManager.AddComponent(entity, component); } public override string ToString() From 91dc893de81704494aa5adfafd4ac855246a046c Mon Sep 17 00:00:00 2001 From: kaylie Date: Wed, 18 Mar 2026 18:28:28 +0100 Subject: [PATCH 69/77] Add a warning to Resolve for some obvious misuse of Resolve, make CopySprite not break for this case. --- .../GameObjects/EntitySystems/SpriteSystem.Component.cs | 7 +++++++ Robust.Shared/GameObjects/EntitySystem.Resolve.cs | 3 +++ 2 files changed, 10 insertions(+) diff --git a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Component.cs b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Component.cs index f96f1bb9ede..db3974db037 100644 --- a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Component.cs +++ b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Component.cs @@ -54,6 +54,13 @@ 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. + target.Comp.rsi = source.Comp.rsi; + target.Comp.layerDatums = source.Comp.layerDatums; + target.Comp.state = source.Comp.state; + target.Comp.texture = source.Comp.texture; + target.Comp._baseRsi = source.Comp._baseRsi; target.Comp._bounds = source.Comp._bounds; target.Comp._visible = source.Comp._visible; diff --git a/Robust.Shared/GameObjects/EntitySystem.Resolve.cs b/Robust.Shared/GameObjects/EntitySystem.Resolve.cs index 589ae44037a..7e31b1c55ad 100644 --- a/Robust.Shared/GameObjects/EntitySystem.Resolve.cs +++ b/Robust.Shared/GameObjects/EntitySystem.Resolve.cs @@ -22,6 +22,9 @@ protected bool Resolve(EntityUid uid, [NotNullWhen(true)] ref TComp? comp { DebugTools.AssertOwner(uid, component); + if (component?.LifeStage == ComponentLifeStage.PreAdd) + Log.Warning($"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. This will be an error in the future."); + if (component != null && !component.Deleted) return true; From edf0c33e61c89cca8de1d0d883b36ba66be684ec Mon Sep 17 00:00:00 2001 From: kaylie Date: Wed, 18 Mar 2026 18:39:30 +0100 Subject: [PATCH 70/77] sigh --- .../EntitySystems/SpriteSystem.Component.cs | 14 ++++++++++---- Robust.Shared/GameObjects/EntitySystem.Resolve.cs | 3 +-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Component.cs b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Component.cs index db3974db037..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). /// @@ -56,10 +60,12 @@ public void CopySprite(Entity source, Entity // 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. - target.Comp.rsi = source.Comp.rsi; - target.Comp.layerDatums = source.Comp.layerDatums; - target.Comp.state = source.Comp.state; - target.Comp.texture = source.Comp.texture; + 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; diff --git a/Robust.Shared/GameObjects/EntitySystem.Resolve.cs b/Robust.Shared/GameObjects/EntitySystem.Resolve.cs index 7e31b1c55ad..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; @@ -23,7 +22,7 @@ protected bool Resolve(EntityUid uid, [NotNullWhen(true)] ref TComp? comp DebugTools.AssertOwner(uid, component); if (component?.LifeStage == ComponentLifeStage.PreAdd) - Log.Warning($"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. This will be an error in the future."); + 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; From 769b3d6915f92677c1db0f36732f44a8a6757f07 Mon Sep 17 00:00:00 2001 From: kaylie Date: Wed, 25 Mar 2026 14:16:57 -0700 Subject: [PATCH 71/77] Change out where we're using EntityBuilder by default, server only now. --- .../GameObjects/ServerEntityManager.cs | 54 +++++++++++++++++++ .../EntityBuilders/EntityBuilder.cs | 40 +++++++++++--- .../GameObjects/EntityManager.Spawn.cs | 25 +++------ Robust.Shared/GameObjects/EntityManager.cs | 18 +++++-- 4 files changed, 108 insertions(+), 29 deletions(-) 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.Shared/GameObjects/EntityBuilders/EntityBuilder.cs b/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.cs index b2c0951eff1..ecd5dd43075 100644 --- a/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.cs +++ b/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.cs @@ -9,6 +9,7 @@ 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; @@ -19,6 +20,27 @@ namespace Robust.Shared.GameObjects.EntityBuilders; /// 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 { @@ -44,7 +66,7 @@ public sealed partial class EntityBuilder /// Due to how map coordinates work, these have to be resolved at spawn time just before initializing /// the entities. /// - private MapCoordinates? _mapCoordinates; + internal MapCoordinates? MapCoordinates; public MetaDataComponent MetaData { get; private set; } = default!; public TransformComponent Transform { get; private set; } = default!; @@ -253,8 +275,8 @@ public bool TryComp(Type t, [NotNullWhen(true)] out IComponent? comp) /// 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. + if (MapCoordinates is not null) + MapCoordinates = null; // One or the other. Transform._parent = parent; Transform._localPosition = relativePos; @@ -274,8 +296,8 @@ public EntityBuilder ChildOf(EntityUid parent, Vector2 relativePos = default, An /// The builder, for chaining. public EntityBuilder ChildOf(EntityCoordinates coordinates, Angle? rotation = null) { - if (_mapCoordinates is not null) - _mapCoordinates = null; // One or the other. + if (MapCoordinates is not null) + MapCoordinates = null; // One or the other. Transform._parent = coordinates.EntityId; Transform._localPosition = coordinates.Position; @@ -290,11 +312,12 @@ public EntityBuilder ChildOf(EntityCoordinates coordinates, Angle? rotation = nu /// 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) + public EntityBuilder LocatedAt(MapCoordinates mapCoordinates, Angle? angle = null) { - _mapCoordinates = mapCoordinates; + MapCoordinates = mapCoordinates; if (Transform._parent != EntityUid.Invalid) { @@ -302,6 +325,9 @@ public EntityBuilder LocatedAt(MapCoordinates mapCoordinates) Transform._localPosition = default; } + if (angle is not null) + Transform._localRotation = angle.Value; + return this; } diff --git a/Robust.Shared/GameObjects/EntityManager.Spawn.cs b/Robust.Shared/GameObjects/EntityManager.Spawn.cs index 4c8a9a609a8..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]; @@ -103,20 +99,12 @@ public virtual EntityUid SpawnAttachedTo(string? protoName, EntityCoordinates co 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); + var entity = CreateEntityUninitialized(protoName, coordinates, overrides, rotation); + InitializeAndStartEntity(entity, _xforms.GetMapId(coordinates)); + 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); @@ -130,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 3ef9051b158..fde3bc0b5c0 100644 --- a/Robust.Shared/GameObjects/EntityManager.cs +++ b/Robust.Shared/GameObjects/EntityManager.cs @@ -357,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); @@ -382,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; } /// @@ -997,6 +1006,9 @@ private void AllocBuilderEntity(EntityBuilder builder) AddComponentInternal(uid, component, false, true, builder.MetaData); } + + if (builder.MapCoordinates is {} coords) + SetMapCoordinates(coords, builder.Transform._localRotation, uid, builder.Transform); } /// From ce41fac5b5659992985eda98b5ca86f348df6638 Mon Sep 17 00:00:00 2001 From: kaylie Date: Wed, 25 Mar 2026 15:46:30 -0700 Subject: [PATCH 72/77] awawa --- .../ThrowingEntityDeletion_Test.cs | 5 +- .../Containers/ContainerManagerComponent.cs | 12 +- .../SharedContainerSystem.Remove.cs | 2 - .../Containers/SharedContainerSystem.cs | 7 +- .../EntityBuilders/EntityBuilder.cs | 5 + .../GameObjects/EntityCreationException2.cs | 67 +++++++ .../EntityManager.EntityBuilder.cs | 172 +++++++++++++++--- .../IEntityManager.CommandBuffers.cs | 4 +- 8 files changed, 242 insertions(+), 32 deletions(-) create mode 100644 Robust.Shared/GameObjects/EntityCreationException2.cs 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.Shared/Containers/ContainerManagerComponent.cs b/Robust.Shared/Containers/ContainerManagerComponent.cs index ee59708b41a..93911bfae6a 100644 --- a/Robust.Shared/Containers/ContainerManagerComponent.cs +++ b/Robust.Shared/Containers/ContainerManagerComponent.cs @@ -24,10 +24,20 @@ public sealed partial class ContainerManagerComponent : Component, ISerializatio [DataField("containers")] 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) + { +#pragma warning disable CS0618 // Type or member is obsolete + container.Init(null!, id, (Owner, this)); +#pragma warning restore CS0618 // Type or member is obsolete + } } [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 447e8882137..b76fbd83075 100644 --- a/Robust.Shared/Containers/SharedContainerSystem.cs +++ b/Robust.Shared/Containers/SharedContainerSystem.cs @@ -56,15 +56,14 @@ public override void Initialize() TransformQuery = GetEntityQuery(); } - private void OnAdd(Entity ent, ref ComponentAdd 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)); } } - private void OnContainerGetState(EntityUid uid, ContainerManagerComponent component, ref ComponentGetState args) { Dictionary containerSet = diff --git a/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.cs b/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.cs index ecd5dd43075..3a979c89321 100644 --- a/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.cs +++ b/Robust.Shared/GameObjects/EntityBuilders/EntityBuilder.cs @@ -68,6 +68,11 @@ public sealed partial class EntityBuilder /// 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!; 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.EntityBuilder.cs b/Robust.Shared/GameObjects/EntityManager.EntityBuilder.cs index 111d69683b3..1e611837b1f 100644 --- a/Robust.Shared/GameObjects/EntityManager.EntityBuilder.cs +++ b/Robust.Shared/GameObjects/EntityManager.EntityBuilder.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Runtime.CompilerServices; using Robust.Shared.GameObjects.EntityBuilders; namespace Robust.Shared.GameObjects; @@ -11,51 +10,120 @@ 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. - SpawnBulk([builder]); + // 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) + 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) { - AllocBuilderEntity(builder); + try + { + AllocBuilderEntity(builder); + } + 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) { - InitializeEntity(builder.ReservedEntity, builder.MetaData); + 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) { - StartEntity(builder.ReservedEntity); + 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) { - // MapInit is inherited, if we're on an initialized map we should also map init unless otherwise told. - var xform = TransformQuery.GetComponent(builder.ReservedEntity); - var doMapInit = mapInit; - // Replicate whatever map we're on if mapInit is null. - doMapInit ??= _mapSystem.IsInitialized(xform.MapID); - - if (doMapInit.Value) - RunMapInit(builder.ReservedEntity, builder.MetaData); - - // 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 (xform.ParentUid.IsValid() && MetaQuery.Comp(xform.ParentUid).EntityPaused) + 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 xform = TransformQuery.GetComponent(builder.ReservedEntity); + var doMapInit = mapInit; + // Replicate whatever map we're on if mapInit is null. + doMapInit ??= _mapSystem.IsInitialized(xform.MapID); + + if (doMapInit.Value) + RunMapInit(builder.ReservedEntity, builder.MetaData); + + // 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 (xform.ParentUid.IsValid() && MetaQuery.Comp(xform.ParentUid).EntityPaused) + { + EntitySysManager.GetEntitySystem() + .SetEntityPaused(builder.ReservedEntity, true, builder.MetaData); + } + } + catch (Exception e) { - EntitySysManager.GetEntitySystem().SetEntityPaused(builder.ReservedEntity, true, builder.MetaData); + 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 @@ -68,9 +136,67 @@ public void SpawnBulk(ReadOnlySpan builders, bool? mapInit = null foreach (var builder in buildersShuffled) { - if (builder.PostInitCommands is {} commands) - ApplyCommandBuffer(commands); + 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) diff --git a/Robust.Shared/GameObjects/IEntityManager.CommandBuffers.cs b/Robust.Shared/GameObjects/IEntityManager.CommandBuffers.cs index 3a62ad2e6bd..5dc691599cd 100644 --- a/Robust.Shared/GameObjects/IEntityManager.CommandBuffers.cs +++ b/Robust.Shared/GameObjects/IEntityManager.CommandBuffers.cs @@ -37,8 +37,10 @@ public partial interface IEntityManager /// /// 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); + 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 From 3cb35bd08f6d31030baee6a6903afd29ba3108d3 Mon Sep 17 00:00:00 2001 From: kaylie Date: Wed, 25 Mar 2026 15:52:22 -0700 Subject: [PATCH 73/77] fix. --- .../Containers/ContainerManagerComponent.cs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/Robust.Shared/Containers/ContainerManagerComponent.cs b/Robust.Shared/Containers/ContainerManagerComponent.cs index 5cae97c8bbe..c296b5e5693 100644 --- a/Robust.Shared/Containers/ContainerManagerComponent.cs +++ b/Robust.Shared/Containers/ContainerManagerComponent.cs @@ -23,20 +23,19 @@ public sealed partial class ContainerManagerComponent : Component, ISerializatio [DataField] public Dictionary Containers = new(); - // Waiting on everything to use EntityBuilder, then it's gone. - void ISerializationHooks.AfterDeserialization() - { + // 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; + if (Owner == EntityUid.Invalid) + return; #pragma warning restore CS0618 // Type or member is obsolete - foreach (var (id, container) in Containers) - { + foreach (var (id, container) in Containers) + { #pragma warning disable CS0618 // Type or member is obsolete - container.Init(null!, id, (Owner, this)); + container.Init(null!, id, (Owner, this)); #pragma warning restore CS0618 // Type or member is obsolete - } } } From 9459245abc0e5096a88063619b32811ac5d0059e Mon Sep 17 00:00:00 2001 From: kaylie Date: Wed, 25 Mar 2026 15:58:37 -0700 Subject: [PATCH 74/77] Un "fix" spritecomponent. It's just too much for now. --- .../Components/Renderable/SpriteComponent.cs | 62 +++++++++++++++++-- .../GameObjects/EntitySystems/SpriteSystem.cs | 54 ---------------- 2 files changed, 57 insertions(+), 59 deletions(-) diff --git a/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs b/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs index ec3d5e82288..a52d8d0e35d 100644 --- a/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs +++ b/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs @@ -35,7 +35,7 @@ namespace Robust.Client.GameObjects { [RegisterComponent] - public sealed partial class SpriteComponent : Component, IComponentDebug, IComponentTreeEntry, IAnimationProperties + public sealed partial class SpriteComponent : Component, IComponentDebug, ISerializationHooks, IComponentTreeEntry, IAnimationProperties { public const string LogCategory = "go.comp.sprite"; @@ -166,11 +166,11 @@ public RSI? BaseRSI set => Sys.SetBaseRsi((Owner, this), value); } - [DataField("sprite", readOnly: true)] internal string? rsi; - [DataField("layers", readOnly: true)] internal List layerDatums = new(); + [DataField("sprite", readOnly: true)] private string? rsi; + [DataField("layers", readOnly: true)] private List layerDatums = new(); - [DataField(readOnly: true)] internal string? state; - [DataField(readOnly: true)] internal string? texture; + [DataField(readOnly: true)] private string? state; + [DataField(readOnly: true)] private string? texture; /// /// Should this entity show up in containers regardless of whether the container can show contents? @@ -243,6 +243,58 @@ public ShaderInstance? PostShader public ISpriteLayer this[object layerKey] => this[LayerMap[layerKey]]; public IEnumerable AllLayers => Layers; + 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. + + collection.InjectDependencies(this); + if (!string.IsNullOrWhiteSpace(rsi)) + { + var rsiPath = TextureRoot / rsi; + if (resourceCache.TryGetResource(rsiPath, out RSIResource? resource)) + _baseRsi = resource.RSI; + else + Logger.ErrorS(LogCategory, "Unable to load RSI '{0}'.", rsiPath); + } + + if (layerDatums.Count == 0) + { + if (state != null || texture != null) + { + layerDatums.Insert(0, new PrototypeLayerData + { + TexturePath = string.IsNullOrWhiteSpace(texture) ? null : texture, + State = string.IsNullOrWhiteSpace(state) ? null : state, + Color = Color.White, + Scale = Vector2.One, + Visible = true, + RenderingStrategy = LayerRenderingStrategy.UseSpriteStrategy, + Cycle = false, + }); + state = null; + texture = null; + } + } + + if (layerDatums.Count != 0) + { + LayerMap.Clear(); + Layers.Clear(); + foreach (var datum in layerDatums) + { + var layer = new Layer((Owner, this), Layers.Count); + Layers.Add(layer); + LayerSetData(layer, datum); + } + + } + + BoundsDirty = true; + LocalMatrix = Matrix3Helpers.CreateTransform(in offset, in rotation, in scale); + } + /// /// If false, this will prevent any of this sprite's animated layers from looping their animation. /// This will set whenever any layer's animation finishes. diff --git a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.cs b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.cs index a89547a23c6..33a4e3ae540 100644 --- a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.cs +++ b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.cs @@ -61,7 +61,6 @@ public override void Initialize() UpdatesAfter.Add(typeof(SpriteTreeSystem)); SubscribeLocalEvent(OnPrototypesReloaded); - SubscribeLocalEvent(OnAdd); SubscribeLocalEvent(OnInit); Subs.CVar(_cfg, CVars.RenderSpriteDirectionBias, OnBiasChanged, true); @@ -69,59 +68,6 @@ public override void Initialize() _query = GetEntityQuery(); } - private void OnAdd(Entity ent, ref ComponentAdd args) - { - // This is a direct, dirty port of the old ISerializationHook from spritecomponent. - // This is just to make EntityBuilders work. - // This does not answer prayers, but it does fix some of my problems. - - var c = ent.Comp; - IoCManager.InjectDependencies(c); - - if (!string.IsNullOrWhiteSpace(c.rsi)) - { - var rsiPath = TextureRoot / c.rsi; - if (_resourceCache.TryGetResource(rsiPath, out RSIResource? resource)) - c._baseRsi = resource.RSI; - else - Log.Error("Unable to load RSI '{0}'.", rsiPath); - } - - if (c.layerDatums.Count == 0) - { - if (c.state != null || c.texture != null) - { - c.layerDatums.Insert(0, new PrototypeLayerData - { - TexturePath = string.IsNullOrWhiteSpace(c.texture) ? null : c.texture, - State = string.IsNullOrWhiteSpace(c.state) ? null : c.state, - Color = Color.White, - Scale = Vector2.One, - Visible = true, - RenderingStrategy = LayerRenderingStrategy.UseSpriteStrategy, - Cycle = false, - }); - c.state = null; - c.texture = null; - } - } - - if (c.layerDatums.Count != 0) - { - c.LayerMap.Clear(); - c.Layers.Clear(); - foreach (var datum in c.layerDatums) - { - var layer = new Layer(ent, c.Layers.Count); - c.Layers.Add(layer); - LayerSetData(layer, datum); - } - } - - c.BoundsDirty = true; - c.LocalMatrix = Matrix3Helpers.CreateTransform(in c.offset, in c.rotation, in c.scale); - } - public bool IsVisible(Layer layer) { return layer.Visible && layer.CopyToShaderParameters == null; From fd8eff57f804742bf283823b00541bc293bf3c87 Mon Sep 17 00:00:00 2001 From: kaylie Date: Wed, 25 Mar 2026 16:27:07 -0700 Subject: [PATCH 75/77] Change back some init logic we don't use in EntityBuilder. --- Robust.Shared/Prototypes/EntityPrototype.cs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/Robust.Shared/Prototypes/EntityPrototype.cs b/Robust.Shared/Prototypes/EntityPrototype.cs index 350957aa3a2..686ff79e978 100644 --- a/Robust.Shared/Prototypes/EntityPrototype.cs +++ b/Robust.Shared/Prototypes/EntityPrototype.cs @@ -263,28 +263,22 @@ public static void EnsureCompExistsAndDeserialize(EntityUid entity, IComponent data, ISerializationContext? context) { - var add = false; if (!entityManager.TryGetComponent(entity, compReg.Idx, out var component)) { - var newComponent = factory.GetComponent(compReg); - add = true; + var newComponent = factory.GetComponent(data.GetType()); + entityManager.AddComponent(entity, newComponent); component = newComponent; } if (context is not EntityDeserializer map) { serManager.CopyTo(data, ref component, context, notNullableOverride: true); - if (add) - entityManager.AddComponent(entity, component); return; } - map.CurrentComponent = compReg.Name; + map.CurrentComponent = factory.GetComponentName(data.GetType()); serManager.CopyTo(data, ref component, context, notNullableOverride: true); map.CurrentComponent = null; - - if (add) - entityManager.AddComponent(entity, component); } public override string ToString() From c6c46cad37641e48a21f1200ae18b5b32057bca6 Mon Sep 17 00:00:00 2001 From: kaylie Date: Wed, 25 Mar 2026 17:13:34 -0700 Subject: [PATCH 76/77] Tweak CommandBuffers. --- .../GameObjects/CommandBuffers/CommandBufferEntry.cs | 7 ++++--- Robust.Shared/GameObjects/EntityManager.CommandBuffers.cs | 4 ++-- Robust.Shared/GameObjects/EntityManager.cs | 3 ++- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Robust.Shared/GameObjects/CommandBuffers/CommandBufferEntry.cs b/Robust.Shared/GameObjects/CommandBuffers/CommandBufferEntry.cs index 9eb20562598..3cf9fe6bcf0 100644 --- a/Robust.Shared/GameObjects/CommandBuffers/CommandBufferEntry.cs +++ b/Robust.Shared/GameObjects/CommandBuffers/CommandBufferEntry.cs @@ -50,7 +50,8 @@ internal partial struct CommandBufferEntry 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 perf sensitive. + // 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( @@ -103,7 +104,7 @@ public static void SpawnEntity(EntityBuilder builder, out CommandBufferEntry ent /// /// The builders to apply. /// The location to place the new entry within. - public static void SpawnEntities(EntityBuilder[] builder, out CommandBufferEntry entry) + public static void SpawnEntities(List builder, out CommandBufferEntry entry) { entry.Command = (long)CmdKind.SpawnEntity; entry.Field1 = 0; @@ -166,7 +167,7 @@ public enum CmdKind : byte /// Either an entity builder, or a list of entity builders. /// /// unused Field1; - /// EntityBuilder | EntityBuilder[] EntityBuilder; + /// EntityBuilder | List<EntityBuilder> EntityBuilder; /// unused Field3; /// /// diff --git a/Robust.Shared/GameObjects/EntityManager.CommandBuffers.cs b/Robust.Shared/GameObjects/EntityManager.CommandBuffers.cs index 9783260e99c..3fc04cccde4 100644 --- a/Robust.Shared/GameObjects/EntityManager.CommandBuffers.cs +++ b/Robust.Shared/GameObjects/EntityManager.CommandBuffers.cs @@ -41,8 +41,8 @@ public CommandBuffer Create() public bool Return(CommandBuffer obj) { - if (obj.Capacity > 128) - return false; // Get rid of it, too chonky. + 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.cs b/Robust.Shared/GameObjects/EntityManager.cs index eb7a45501a4..feb6e4ac0be 100644 --- a/Robust.Shared/GameObjects/EntityManager.cs +++ b/Robust.Shared/GameObjects/EntityManager.cs @@ -147,7 +147,8 @@ public virtual void Initialize() if (Initialized) throw new InvalidOperationException("Initialize() called multiple times"); - CommandBufferPool = new(new CommandBufferPolicy(_dependencyCollection), 64); + // This can store at most 8MiB worth of command buffers. + CommandBufferPool = new(new CommandBufferPolicy(_dependencyCollection), 256); EventBusInternal = new EntityEventBus(this, _reflection); From 13ae6f647da2c642ea352f2881d5cff16be77ee5 Mon Sep 17 00:00:00 2001 From: kaylie Date: Wed, 25 Mar 2026 17:22:42 -0700 Subject: [PATCH 77/77] Cleanup. --- .../EntityManager.EntityBuilder.cs | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/Robust.Shared/GameObjects/EntityManager.EntityBuilder.cs b/Robust.Shared/GameObjects/EntityManager.EntityBuilder.cs index 1e611837b1f..e262dc9ca92 100644 --- a/Robust.Shared/GameObjects/EntityManager.EntityBuilder.cs +++ b/Robust.Shared/GameObjects/EntityManager.EntityBuilder.cs @@ -26,6 +26,15 @@ public void SpawnBulk(ReadOnlySpan builders, bool? mapInit = null 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) { @@ -97,22 +106,12 @@ public void SpawnBulk(ReadOnlySpan builders, bool? mapInit = null try { // MapInit is inherited, if we're on an initialized map we should also map init unless otherwise told. - var xform = TransformQuery.GetComponent(builder.ReservedEntity); var doMapInit = mapInit; // Replicate whatever map we're on if mapInit is null. - doMapInit ??= _mapSystem.IsInitialized(xform.MapID); + doMapInit ??= _mapSystem.IsInitialized(builder.Transform.MapID); if (doMapInit.Value) RunMapInit(builder.ReservedEntity, builder.MetaData); - - // 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 (xform.ParentUid.IsValid() && MetaQuery.Comp(xform.ParentUid).EntityPaused) - { - EntitySysManager.GetEntitySystem() - .SetEntityPaused(builder.ReservedEntity, true, builder.MetaData); - } } catch (Exception e) {