From 82aa5f3323b2c0bac67e07ee83326a8e28010af9 Mon Sep 17 00:00:00 2001 From: kaylie Date: Tue, 10 Mar 2026 16:42:24 +0100 Subject: [PATCH 01/21] 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 02/21] 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 03/21] 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 04/21] 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 05/21] 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 06/21] 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 07/21] 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 08/21] 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 09/21] 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 10/21] 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 11/21] 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 12/21] 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 13/21] 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 14/21] 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 15/21] Poke From ecd214091720fdf377dbf7090daf5d7d276c3797 Mon Sep 17 00:00:00 2001 From: kaylie Date: Thu, 12 Mar 2026 12:16:45 +0100 Subject: [PATCH 16/21] 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 17/21] 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 18/21] 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 19/21] 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 20/21] 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 21/21] Poke