From a03048270bd46fa26af14f5edaf1bb9e261a8718 Mon Sep 17 00:00:00 2001 From: kaylie Date: Mon, 9 Mar 2026 20:11:22 +0100 Subject: [PATCH 01/11] Implement the singleton methods. --- .../GameObjects/EntityManager.Singleton.cs | 536 ++++++++++++++++++ 1 file changed, 536 insertions(+) create mode 100644 Robust.Shared/GameObjects/EntityManager.Singleton.cs diff --git a/Robust.Shared/GameObjects/EntityManager.Singleton.cs b/Robust.Shared/GameObjects/EntityManager.Singleton.cs new file mode 100644 index 00000000000..4dba56b9835 --- /dev/null +++ b/Robust.Shared/GameObjects/EntityManager.Singleton.cs @@ -0,0 +1,536 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; + +namespace Robust.Shared.GameObjects; + +public abstract partial class EntityManager +{ + // REMARK: No API that allows you to use these queries without them throwing over non-uniqueness should be added. + // It's a pretty simple, natural error condition and the game *should* yell about it. + + /// + /// Gets the sole entity with the given component. + /// + /// The component to look for as a tag. + /// The singleton entity. + /// Thrown when multiple entities match the request. + /// Thrown when no entities match the request. + public Entity Single() + where TComp1: IComponent + { + var index = _entTraitArray[CompIdx.ArrayIndex()]; + + if (index.Keys.FirstOrNull() is { } ent && index.Count == 1) + { + return new Entity(ent, (TComp1)index[ent]); + } + + if (index.Count > 1) + { + throw new NonUniqueSingletonException(index.Keys.ToArray(), typeof(TComp1)); + } + else + { + // 0. + throw new MatchNotFoundException(typeof(TComp1)); + } + } + + /// + /// Gets the sole entity with the given components. + /// + /// The first component to look for as a tag. + /// The second component to look for as a tag. + /// The singleton entity. + /// Thrown when multiple entities match the request. + /// Thrown when no entities match the request. + /// + /// The least common component should be put in for query performance. + /// + public Entity Single() + where TComp1: IComponent + where TComp2: IComponent + { + var query = EntityQueryEnumerator(); + + if (!query.MoveNext(out var ent, out var comp1, out var comp2)) + throw new MatchNotFoundException(typeof(TComp1), typeof(TComp2)); + + if (query.MoveNext(out var ent2, out _, out _)) + { + var list = new List { ent, ent2 }; + + while (query.MoveNext(out var ent3, out _, out _)) + { + list.Add(ent3); + } + + throw new NonUniqueSingletonException(list.ToArray(), typeof(TComp1), typeof(TComp2)); + } + + return new(ent, comp1, comp2); + } + + /// + /// Gets the sole entity with the given components. + /// + /// The first component to look for as a tag. + /// The second component to look for as a tag. + /// The third component to look for as a tag. + /// The singleton entity. + /// Thrown when multiple entities match the request. + /// Thrown when no entities match the request. + /// + /// The least common component should be put in for query performance. + /// + public Entity Single() + where TComp1: IComponent + where TComp2: IComponent + where TComp3: IComponent + { + var query = EntityQueryEnumerator(); + + if (!query.MoveNext(out var ent, out var comp1, out var comp2, out var comp3)) + throw new MatchNotFoundException(typeof(TComp1), typeof(TComp2), typeof(TComp3)); + + if (query.MoveNext(out var ent2, out _, out _, out _)) + { + var list = new List { ent, ent2 }; + + while (query.MoveNext(out var ent3, out _, out _, out _)) + { + list.Add(ent3); + } + + throw new NonUniqueSingletonException(list.ToArray(), typeof(TComp1), typeof(TComp2), typeof(TComp3)); + } + + return new(ent, comp1, comp2, comp3); + } + + /// + /// Gets the sole entity with the given components. + /// + /// The first component to look for as a tag. + /// The second component to look for as a tag. + /// The third component to look for as a tag. + /// The third component to look for as a tag. + /// The singleton entity. + /// Thrown when multiple entities match the request. + /// Thrown when no entities match the request. + /// + /// The least common component should be put in for query performance. + /// + public Entity Single() + where TComp1: IComponent + where TComp2: IComponent + where TComp3: IComponent + where TComp4: IComponent + { + var query = EntityQueryEnumerator(); + + if (!query.MoveNext(out var ent, out var comp1, out var comp2, out var comp3, out var comp4)) + throw new MatchNotFoundException(typeof(TComp1), typeof(TComp2), typeof(TComp3), typeof(TComp4)); + + if (query.MoveNext(out var ent2, out _, out _, out _, out _)) + { + var list = new List { ent, ent2 }; + + while (query.MoveNext(out var ent3, out _, out _, out _, out _)) + { + list.Add(ent3); + } + + throw new NonUniqueSingletonException(list.ToArray(), typeof(TComp1), typeof(TComp2), typeof(TComp3), typeof(TComp4)); + } + + return new(ent, comp1, comp2, comp3, comp4); + } + + /// + /// Gets the sole entity with the given component, if it exists. Still throws if there's more than one. + /// + /// The singleton entity, if any. + /// The component to look for as a tag. + /// Thrown when multiple entities match the request. + /// Success. + public bool TrySingle([NotNullWhen(true)] out Entity? entity) + where TComp1: IComponent + { + var index = _entTraitArray[CompIdx.ArrayIndex()]; + + if (index.Keys.FirstOrNull() is { } ent && index.Count == 1) + { + entity = new Entity(ent, (TComp1)index[ent]); + return true; + } + else + { + entity = null; + } + + if (index.Count > 1) + { + throw new NonUniqueSingletonException(index.Keys.ToArray(), typeof(TComp1)); + } + + return false; + } + + /// + /// Gets the sole entity with the given components, if one exists, or returns if one does not. + /// + /// The singleton entity, if any. + /// The first component to look for as a tag. + /// The second component to look for as a tag. + /// Success. + /// Thrown when multiple entities match the request. + /// + /// The least common component should be put in for query performance. + /// + public bool TrySingle([NotNullWhen(true)] out Entity? entity) + where TComp1: IComponent + where TComp2: IComponent + { + var query = EntityQueryEnumerator(); + + if (!query.MoveNext(out var ent, out var comp1, out var comp2)) + { + entity = null; + return false; + } + + if (query.MoveNext(out var ent2, out _, out _)) + { + var list = new List { ent, ent2 }; + + while (query.MoveNext(out var ent3, out _, out _)) + { + list.Add(ent3); + } + + throw new NonUniqueSingletonException(list.ToArray(), typeof(TComp1), typeof(TComp2)); + } + + entity = new(ent, comp1, comp2); + return true; + } + + /// + /// Gets the sole entity with the given components, if one exists, or returns if one does not. + /// + /// The singleton entity, if any. + /// The first component to look for as a tag. + /// The second component to look for as a tag. + /// The third component to look for as a tag. + /// Success. + /// Thrown when multiple entities match the request. + /// + /// The least common component should be put in for query performance. + /// + public bool TrySingle([NotNullWhen(true)] out Entity? entity) + where TComp1: IComponent + where TComp2: IComponent + where TComp3: IComponent + { + var query = EntityQueryEnumerator(); + + if (!query.MoveNext(out var ent, out var comp1, out var comp2, out var comp3)) + { + entity = null; + return false; + } + + if (query.MoveNext(out var ent2, out _, out _, out _)) + { + var list = new List { ent, ent2 }; + + while (query.MoveNext(out var ent3, out _, out _, out _)) + { + list.Add(ent3); + } + + throw new NonUniqueSingletonException(list.ToArray(), typeof(TComp1), typeof(TComp2), typeof(TComp3)); + } + + entity = new(ent, comp1, comp2, comp3); + return true; + } + + /// + /// Gets the sole entity with the given components, if one exists, or returns if one does not. + /// + /// The singleton entity, if any. + /// The first component to look for as a tag. + /// The second component to look for as a tag. + /// The third component to look for as a tag. + /// The fourth component to look for as a tag. + /// Success. + /// Thrown when multiple entities match the request. + /// + /// The least common component should be put in for query performance. + /// + public bool TrySingle([NotNullWhen(true)] out Entity? entity) + where TComp1: IComponent + where TComp2: IComponent + where TComp3: IComponent + where TComp4: IComponent + { + var query = EntityQueryEnumerator(); + + if (!query.MoveNext(out var ent, out var comp1, out var comp2, out var comp3, out var comp4)) + { + entity = null; + return false; + } + + if (query.MoveNext(out var ent2, out _, out _, out _, out _)) + { + var list = new List { ent, ent2 }; + + while (query.MoveNext(out var ent3, out _, out _, out _, out _)) + { + list.Add(ent3); + } + + throw new NonUniqueSingletonException(list.ToArray(), typeof(TComp1), typeof(TComp2), typeof(TComp3), typeof(TComp4)); + } + + entity = new(ent, comp1, comp2, comp3, comp4); + return true; + } + + /// + /// Gets the sole entity with the given component, or spawns it at the given location if it doesn't exist. + /// + /// The fallback prototype to spawn on failure. + /// The location to spawn the singleton. Nullspace works. + /// The component to look for as a tag. + /// Thrown when multiple entities match the request. + /// Thrown when no entities match the request after spawning the fallback. + /// The singleton entity. + /// + /// This does not return the entity it spawns, it tries to look it up, so if spawning that entity violates + /// singleton status (or it lacks the necessary component) this will throw immediately after instead of later + /// on next call. + /// + /// This will also still throw if the spawned entity doesn't match at all. + /// + public Entity SingleOrSpawn(EntProtoId fallback, MapCoordinates location) + where TComp1: IComponent + { + if (TrySingle(out var ent)) + return ent.Value; + + Spawn(fallback, location); + + return Single(); + } + + /// + /// Gets the sole entity with the given component, or spawns it at the given location if it doesn't exist. + /// + /// The fallback prototype to spawn on failure. + /// The location to spawn the singleton. Nullspace works. + /// The first component to look for as a tag. + /// The second component to look for as a tag. + /// Thrown when multiple entities match the request. + /// Thrown when no entities match the request after spawning the fallback. + /// The singleton entity. + /// + /// This does not return the entity it spawns, it tries to look it up, so if spawning that entity violates + /// singleton status (or it lacks the necessary component) this will throw immediately after instead of later + /// on next call. + /// + /// This will also still throw if the spawned entity doesn't match at all. + /// + public Entity SingleOrSpawn(EntProtoId fallback, MapCoordinates location) + where TComp1: IComponent + where TComp2: IComponent + { + if (TrySingle(out var ent)) + return ent.Value; + + Spawn(fallback, location); + + return Single(); + } + + /// + /// Gets the sole entity with the given component, or spawns it at the given location if it doesn't exist. + /// + /// The fallback prototype to spawn on failure. + /// The location to spawn the singleton. Nullspace works. + /// The first component to look for as a tag. + /// The second component to look for as a tag. + /// The third component to look for as a tag. + /// Thrown when multiple entities match the request. + /// Thrown when no entities match the request after spawning the fallback. + /// The singleton entity. + /// + /// This does not return the entity it spawns, it tries to look it up, so if spawning that entity violates + /// singleton status (or it lacks the necessary component) this will throw immediately after instead of later + /// on next call. + /// + /// This will also still throw if the spawned entity doesn't match at all. + /// + public Entity SingleOrSpawn(EntProtoId fallback, MapCoordinates location) + where TComp1: IComponent + where TComp2: IComponent + where TComp3: IComponent + { + if (TrySingle(out var ent)) + return ent.Value; + + Spawn(fallback, location); + + return Single(); + } + + /// + /// Gets the sole entity with the given component, or spawns it at the given location if it doesn't exist. + /// + /// The fallback prototype to spawn on failure. + /// The location to spawn the singleton. Nullspace works. + /// The first component to look for as a tag. + /// The second component to look for as a tag. + /// The third component to look for as a tag. + /// The fourth component to look for as a tag. + /// Thrown when multiple entities match the request. + /// Thrown when no entities match the request after spawning the fallback. + /// The singleton entity. + /// + /// This does not return the entity it spawns, it tries to look it up, so if spawning that entity violates + /// singleton status (or it lacks the necessary component) this will throw immediately after instead of later + /// on next call. + /// + /// This will also still throw if the spawned entity doesn't match at all. + /// + public Entity SingleOrSpawn(EntProtoId fallback, MapCoordinates location) + where TComp1: IComponent + where TComp2: IComponent + where TComp3: IComponent + where TComp4: IComponent + { + if (TrySingle(out var ent)) + return ent.Value; + + Spawn(fallback, location); + + return Single(); + } + + /// + /// Gets the sole entity with the given component, or spawns it at the given location if it doesn't exist. + /// + /// The action to call on fallback, should no match exist. + /// The component to look for as a tag. + /// Thrown when multiple entities match the request. + /// Thrown when no entities match the request after calling fallback. + /// The singleton entity. + public Entity SingleOrInit(Action fallback) + where TComp1: IComponent + { + if (TrySingle(out var ent)) + return ent.Value; + + fallback(); + + return Single(); + } + + /// + /// Gets the sole entity with the given component, or spawns it at the given location if it doesn't exist. + /// + /// The action to call on fallback, should no match exist. + /// The first component to look for as a tag. + /// The second component to look for as a tag. + /// Thrown when multiple entities match the request. + /// Thrown when no entities match the request after calling fallback. + /// The singleton entity. + public Entity SingleOrInit(Action fallback) + where TComp1: IComponent + where TComp2: IComponent + { + if (TrySingle(out var ent)) + return ent.Value; + + fallback(); + + return Single(); + } + + /// + /// Gets the sole entity with the given component, or spawns it at the given location if it doesn't exist. + /// + /// The action to call on fallback, should no match exist. + /// The first component to look for as a tag. + /// The second component to look for as a tag. + /// The third component to look for as a tag. + /// Thrown when multiple entities match the request. + /// Thrown when no entities match the request after calling fallback. + /// The singleton entity. + public Entity SingleOrInit(Action fallback) + where TComp1: IComponent + where TComp2: IComponent + where TComp3: IComponent + { + if (TrySingle(out var ent)) + return ent.Value; + + fallback(); + + return Single(); + } + + /// + /// Gets the sole entity with the given component, or spawns it at the given location if it doesn't exist. + /// + /// The action to call on fallback, should no match exist. + /// The first component to look for as a tag. + /// The second component to look for as a tag. + /// The third component to look for as a tag. + /// The fourth component to look for as a tag. + /// Thrown when multiple entities match the request. + /// Thrown when no entities match the request after calling fallback. + /// The singleton entity. + public Entity SingleOrInit(Action fallback) + where TComp1: IComponent + where TComp2: IComponent + where TComp3: IComponent + where TComp4: IComponent + { + if (TrySingle(out var ent)) + return ent.Value; + + fallback(); + + return Single(); + } +} + +/// +/// Exception for when and co cannot find a unique match. +/// +/// The set of matching entities. +/// The set of components you tried to match over. +public sealed class NonUniqueSingletonException(EntityUid[] matches, params Type[] components) : Exception +{ + public override string Message => + $"Expected precisely one entity to match the component set {string.Join(", ", components)}, but found {matches.Length}: {string.Join(", ", matches)}"; +} + +/// +/// Exception for when and co cannot find any match. +/// +/// The set of components you tried to match over. +public sealed class MatchNotFoundException(params Type[] components) : Exception +{ + public override string Message => + $"Expected precisely one entity to match the component set {string.Join(", ", components)}, but found none."; +} From b51a1f01830d5a6a6527c3a60e4728e2b4e95bfa Mon Sep 17 00:00:00 2001 From: kaylie Date: Mon, 9 Mar 2026 21:10:45 +0100 Subject: [PATCH 02/11] Fix API + Tests. --- .../EntityManagerSingletonTests.cs | 119 ++++++++ .../GameObjects/EntityManager.Singleton.cs | 194 ------------ .../GameObjects/IEntityManager.Single.cs | 280 ++++++++++++++++++ 3 files changed, 399 insertions(+), 194 deletions(-) create mode 100644 Robust.Shared.IntegrationTests/GameObjects/EntityManagerSingletonTests.cs create mode 100644 Robust.Shared/GameObjects/IEntityManager.Single.cs diff --git a/Robust.Shared.IntegrationTests/GameObjects/EntityManagerSingletonTests.cs b/Robust.Shared.IntegrationTests/GameObjects/EntityManagerSingletonTests.cs new file mode 100644 index 00000000000..7f78b59d8dd --- /dev/null +++ b/Robust.Shared.IntegrationTests/GameObjects/EntityManagerSingletonTests.cs @@ -0,0 +1,119 @@ +using NUnit.Framework; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.Manager; + +namespace Robust.UnitTesting.Shared.GameObjects; + +[TestFixture, TestOf(typeof(EntityManager))] +internal sealed class EntityManagerSingletonTests : OurRobustUnitTest +{ + private const string TestSingleton1 = "T_TestSingleton1"; + + private const string Prototypes = $""" + - type: entity + id: {TestSingleton1} + components: + - type: Marker1 + - type: Marker2 + - type: Marker3 + - type: Marker4 + """; + + protected override Type[]? ExtraComponents => + [ + typeof(Marker1Component), typeof(Marker2Component), typeof(Marker3Component), typeof(Marker4Component) + ]; + + private PrototypeManager _protoMan = default!; + private IEntityManager _entMan = default!; + + [OneTimeSetUp] + public void Setup() + { + IoCManager.Resolve().Initialize(); + + _protoMan = (PrototypeManager) IoCManager.Resolve(); + _protoMan.RegisterKind(typeof(EntityPrototype), typeof(EntityCategoryPrototype)); + _protoMan.LoadString(Prototypes); + _protoMan.ResolveResults(); + + _entMan = IoCManager.Resolve(); + } + + [Test] + public void SingleEntity() + { + var mySingle = _entMan.Spawn(TestSingleton1); + + var single1 = _entMan.Single(); + var single2 = _entMan.Single(); + var single3 = _entMan.Single(); + var single4 = _entMan.Single(); + + Assert.That(mySingle, NUnit.Framework.Is.EqualTo(single1.Owner)); + Assert.That(mySingle, NUnit.Framework.Is.EqualTo(single2.Owner)); + Assert.That(mySingle, NUnit.Framework.Is.EqualTo(single3.Owner)); + Assert.That(mySingle, NUnit.Framework.Is.EqualTo(single4.Owner)); + + var bonusSingle = _entMan.Spawn(TestSingleton1); + + using (Assert.EnterMultipleScope()) + { + Assert.Throws(() => _entMan.Single()); + Assert.Throws(() => _entMan.Single()); + Assert.Throws(() => + _entMan.Single()); + Assert.Throws(() => + _entMan.Single()); + Assert.Throws(() => _entMan.TrySingle(out _)); + Assert.Throws(() => + _entMan.TrySingle(out _)); + Assert.Throws(() => + _entMan.TrySingle(out _)); + Assert.Throws(() => + _entMan.TrySingle(out _)); + } + + _entMan.DeleteEntity(bonusSingle); + _entMan.DeleteEntity(mySingle); + + using (Assert.EnterMultipleScope()) + { + Assert.Throws(() => _entMan.Single()); + Assert.Throws(() => _entMan.Single()); + Assert.Throws(() => + _entMan.Single()); + Assert.Throws(() => + _entMan.Single()); + Assert.That(_entMan.TrySingle(out _), NUnit.Framework.Is.False); + Assert.That(_entMan.TrySingle(out _), NUnit.Framework.Is.False); + Assert.That(_entMan.TrySingle(out _), + NUnit.Framework.Is.False); + Assert.That( + _entMan.TrySingle(out _), + NUnit.Framework.Is.False); + } + } +} + +internal sealed partial class Marker1Component : Component +{ + +} + +internal sealed partial class Marker2Component : Component +{ + +} + +internal sealed partial class Marker3Component : Component +{ + +} + +internal sealed partial class Marker4Component : Component +{ + +} diff --git a/Robust.Shared/GameObjects/EntityManager.Singleton.cs b/Robust.Shared/GameObjects/EntityManager.Singleton.cs index 4dba56b9835..4a1cf538f7d 100644 --- a/Robust.Shared/GameObjects/EntityManager.Singleton.cs +++ b/Robust.Shared/GameObjects/EntityManager.Singleton.cs @@ -13,13 +13,6 @@ public abstract partial class EntityManager // REMARK: No API that allows you to use these queries without them throwing over non-uniqueness should be added. // It's a pretty simple, natural error condition and the game *should* yell about it. - /// - /// Gets the sole entity with the given component. - /// - /// The component to look for as a tag. - /// The singleton entity. - /// Thrown when multiple entities match the request. - /// Thrown when no entities match the request. public Entity Single() where TComp1: IComponent { @@ -41,17 +34,6 @@ public Entity Single() } } - /// - /// Gets the sole entity with the given components. - /// - /// The first component to look for as a tag. - /// The second component to look for as a tag. - /// The singleton entity. - /// Thrown when multiple entities match the request. - /// Thrown when no entities match the request. - /// - /// The least common component should be put in for query performance. - /// public Entity Single() where TComp1: IComponent where TComp2: IComponent @@ -76,18 +58,6 @@ public Entity Single() return new(ent, comp1, comp2); } - /// - /// Gets the sole entity with the given components. - /// - /// The first component to look for as a tag. - /// The second component to look for as a tag. - /// The third component to look for as a tag. - /// The singleton entity. - /// Thrown when multiple entities match the request. - /// Thrown when no entities match the request. - /// - /// The least common component should be put in for query performance. - /// public Entity Single() where TComp1: IComponent where TComp2: IComponent @@ -113,19 +83,6 @@ public Entity Single() return new(ent, comp1, comp2, comp3); } - /// - /// Gets the sole entity with the given components. - /// - /// The first component to look for as a tag. - /// The second component to look for as a tag. - /// The third component to look for as a tag. - /// The third component to look for as a tag. - /// The singleton entity. - /// Thrown when multiple entities match the request. - /// Thrown when no entities match the request. - /// - /// The least common component should be put in for query performance. - /// public Entity Single() where TComp1: IComponent where TComp2: IComponent @@ -152,13 +109,6 @@ public Entity Single - /// Gets the sole entity with the given component, if it exists. Still throws if there's more than one. - /// - /// The singleton entity, if any. - /// The component to look for as a tag. - /// Thrown when multiple entities match the request. - /// Success. public bool TrySingle([NotNullWhen(true)] out Entity? entity) where TComp1: IComponent { @@ -182,17 +132,6 @@ public bool TrySingle([NotNullWhen(true)] out Entity? entity) return false; } - /// - /// Gets the sole entity with the given components, if one exists, or returns if one does not. - /// - /// The singleton entity, if any. - /// The first component to look for as a tag. - /// The second component to look for as a tag. - /// Success. - /// Thrown when multiple entities match the request. - /// - /// The least common component should be put in for query performance. - /// public bool TrySingle([NotNullWhen(true)] out Entity? entity) where TComp1: IComponent where TComp2: IComponent @@ -221,18 +160,6 @@ public bool TrySingle([NotNullWhen(true)] out Entity - /// Gets the sole entity with the given components, if one exists, or returns if one does not. - /// - /// The singleton entity, if any. - /// The first component to look for as a tag. - /// The second component to look for as a tag. - /// The third component to look for as a tag. - /// Success. - /// Thrown when multiple entities match the request. - /// - /// The least common component should be put in for query performance. - /// public bool TrySingle([NotNullWhen(true)] out Entity? entity) where TComp1: IComponent where TComp2: IComponent @@ -262,19 +189,6 @@ public bool TrySingle([NotNullWhen(true)] out Entity - /// Gets the sole entity with the given components, if one exists, or returns if one does not. - /// - /// The singleton entity, if any. - /// The first component to look for as a tag. - /// The second component to look for as a tag. - /// The third component to look for as a tag. - /// The fourth component to look for as a tag. - /// Success. - /// Thrown when multiple entities match the request. - /// - /// The least common component should be put in for query performance. - /// public bool TrySingle([NotNullWhen(true)] out Entity? entity) where TComp1: IComponent where TComp2: IComponent @@ -305,22 +219,6 @@ public bool TrySingle([NotNullWhen(true)] out En return true; } - /// - /// Gets the sole entity with the given component, or spawns it at the given location if it doesn't exist. - /// - /// The fallback prototype to spawn on failure. - /// The location to spawn the singleton. Nullspace works. - /// The component to look for as a tag. - /// Thrown when multiple entities match the request. - /// Thrown when no entities match the request after spawning the fallback. - /// The singleton entity. - /// - /// This does not return the entity it spawns, it tries to look it up, so if spawning that entity violates - /// singleton status (or it lacks the necessary component) this will throw immediately after instead of later - /// on next call. - /// - /// This will also still throw if the spawned entity doesn't match at all. - /// public Entity SingleOrSpawn(EntProtoId fallback, MapCoordinates location) where TComp1: IComponent { @@ -332,23 +230,6 @@ public Entity SingleOrSpawn(EntProtoId fallback, MapCoordinates return Single(); } - /// - /// Gets the sole entity with the given component, or spawns it at the given location if it doesn't exist. - /// - /// The fallback prototype to spawn on failure. - /// The location to spawn the singleton. Nullspace works. - /// The first component to look for as a tag. - /// The second component to look for as a tag. - /// Thrown when multiple entities match the request. - /// Thrown when no entities match the request after spawning the fallback. - /// The singleton entity. - /// - /// This does not return the entity it spawns, it tries to look it up, so if spawning that entity violates - /// singleton status (or it lacks the necessary component) this will throw immediately after instead of later - /// on next call. - /// - /// This will also still throw if the spawned entity doesn't match at all. - /// public Entity SingleOrSpawn(EntProtoId fallback, MapCoordinates location) where TComp1: IComponent where TComp2: IComponent @@ -361,24 +242,6 @@ public Entity SingleOrSpawn(EntProtoId fallback, return Single(); } - /// - /// Gets the sole entity with the given component, or spawns it at the given location if it doesn't exist. - /// - /// The fallback prototype to spawn on failure. - /// The location to spawn the singleton. Nullspace works. - /// The first component to look for as a tag. - /// The second component to look for as a tag. - /// The third component to look for as a tag. - /// Thrown when multiple entities match the request. - /// Thrown when no entities match the request after spawning the fallback. - /// The singleton entity. - /// - /// This does not return the entity it spawns, it tries to look it up, so if spawning that entity violates - /// singleton status (or it lacks the necessary component) this will throw immediately after instead of later - /// on next call. - /// - /// This will also still throw if the spawned entity doesn't match at all. - /// public Entity SingleOrSpawn(EntProtoId fallback, MapCoordinates location) where TComp1: IComponent where TComp2: IComponent @@ -392,25 +255,6 @@ public Entity SingleOrSpawn(EntP return Single(); } - /// - /// Gets the sole entity with the given component, or spawns it at the given location if it doesn't exist. - /// - /// The fallback prototype to spawn on failure. - /// The location to spawn the singleton. Nullspace works. - /// The first component to look for as a tag. - /// The second component to look for as a tag. - /// The third component to look for as a tag. - /// The fourth component to look for as a tag. - /// Thrown when multiple entities match the request. - /// Thrown when no entities match the request after spawning the fallback. - /// The singleton entity. - /// - /// This does not return the entity it spawns, it tries to look it up, so if spawning that entity violates - /// singleton status (or it lacks the necessary component) this will throw immediately after instead of later - /// on next call. - /// - /// This will also still throw if the spawned entity doesn't match at all. - /// public Entity SingleOrSpawn(EntProtoId fallback, MapCoordinates location) where TComp1: IComponent where TComp2: IComponent @@ -425,14 +269,6 @@ public Entity SingleOrSpawn(); } - /// - /// Gets the sole entity with the given component, or spawns it at the given location if it doesn't exist. - /// - /// The action to call on fallback, should no match exist. - /// The component to look for as a tag. - /// Thrown when multiple entities match the request. - /// Thrown when no entities match the request after calling fallback. - /// The singleton entity. public Entity SingleOrInit(Action fallback) where TComp1: IComponent { @@ -444,15 +280,6 @@ public Entity SingleOrInit(Action fallback) return Single(); } - /// - /// Gets the sole entity with the given component, or spawns it at the given location if it doesn't exist. - /// - /// The action to call on fallback, should no match exist. - /// The first component to look for as a tag. - /// The second component to look for as a tag. - /// Thrown when multiple entities match the request. - /// Thrown when no entities match the request after calling fallback. - /// The singleton entity. public Entity SingleOrInit(Action fallback) where TComp1: IComponent where TComp2: IComponent @@ -465,16 +292,6 @@ public Entity SingleOrInit(Action fallback) return Single(); } - /// - /// Gets the sole entity with the given component, or spawns it at the given location if it doesn't exist. - /// - /// The action to call on fallback, should no match exist. - /// The first component to look for as a tag. - /// The second component to look for as a tag. - /// The third component to look for as a tag. - /// Thrown when multiple entities match the request. - /// Thrown when no entities match the request after calling fallback. - /// The singleton entity. public Entity SingleOrInit(Action fallback) where TComp1: IComponent where TComp2: IComponent @@ -488,17 +305,6 @@ public Entity SingleOrInit(Actio return Single(); } - /// - /// Gets the sole entity with the given component, or spawns it at the given location if it doesn't exist. - /// - /// The action to call on fallback, should no match exist. - /// The first component to look for as a tag. - /// The second component to look for as a tag. - /// The third component to look for as a tag. - /// The fourth component to look for as a tag. - /// Thrown when multiple entities match the request. - /// Thrown when no entities match the request after calling fallback. - /// The singleton entity. public Entity SingleOrInit(Action fallback) where TComp1: IComponent where TComp2: IComponent diff --git a/Robust.Shared/GameObjects/IEntityManager.Single.cs b/Robust.Shared/GameObjects/IEntityManager.Single.cs new file mode 100644 index 00000000000..44c5370808b --- /dev/null +++ b/Robust.Shared/GameObjects/IEntityManager.Single.cs @@ -0,0 +1,280 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; + +namespace Robust.Shared.GameObjects; + +public partial interface IEntityManager +{ + /// + /// Gets the sole entity with the given component. + /// + /// The component to look for as a tag. + /// The singleton entity. + /// Thrown when multiple entities match the request. + /// Thrown when no entities match the request. + public Entity Single() + where TComp1 : IComponent; + + /// + /// Gets the sole entity with the given components. + /// + /// The first component to look for as a tag. + /// The second component to look for as a tag. + /// The singleton entity. + /// Thrown when multiple entities match the request. + /// Thrown when no entities match the request. + /// + /// The least common component should be put in for query performance. + /// + public Entity Single() + where TComp1 : IComponent + where TComp2 : IComponent; + + /// + /// Gets the sole entity with the given components. + /// + /// The first component to look for as a tag. + /// The second component to look for as a tag. + /// The third component to look for as a tag. + /// The singleton entity. + /// Thrown when multiple entities match the request. + /// Thrown when no entities match the request. + /// + /// The least common component should be put in for query performance. + /// + public Entity Single() + where TComp1 : IComponent + where TComp2 : IComponent + where TComp3 : IComponent; + + /// + /// Gets the sole entity with the given components. + /// + /// The first component to look for as a tag. + /// The second component to look for as a tag. + /// The third component to look for as a tag. + /// The third component to look for as a tag. + /// The singleton entity. + /// Thrown when multiple entities match the request. + /// Thrown when no entities match the request. + /// + /// The least common component should be put in for query performance. + /// + public Entity Single() + where TComp1 : IComponent + where TComp2 : IComponent + where TComp3 : IComponent + where TComp4 : IComponent; + + /// + /// Gets the sole entity with the given component, if it exists. Still throws if there's more than one. + /// + /// The singleton entity, if any. + /// The component to look for as a tag. + /// Thrown when multiple entities match the request. + /// Success. + public bool TrySingle([NotNullWhen(true)] out Entity? entity) + where TComp1 : IComponent; + + /// + /// Gets the sole entity with the given components, if one exists, or returns if one does not. + /// + /// The singleton entity, if any. + /// The first component to look for as a tag. + /// The second component to look for as a tag. + /// Success. + /// Thrown when multiple entities match the request. + /// + /// The least common component should be put in for query performance. + /// + public bool TrySingle([NotNullWhen(true)] out Entity? entity) + where TComp1 : IComponent + where TComp2 : IComponent; + + /// + /// Gets the sole entity with the given components, if one exists, or returns if one does not. + /// + /// The singleton entity, if any. + /// The first component to look for as a tag. + /// The second component to look for as a tag. + /// The third component to look for as a tag. + /// Success. + /// Thrown when multiple entities match the request. + /// + /// The least common component should be put in for query performance. + /// + public bool TrySingle([NotNullWhen(true)] out Entity? entity) + where TComp1 : IComponent + where TComp2 : IComponent + where TComp3 : IComponent; + + /// + /// Gets the sole entity with the given components, if one exists, or returns if one does not. + /// + /// The singleton entity, if any. + /// The first component to look for as a tag. + /// The second component to look for as a tag. + /// The third component to look for as a tag. + /// The fourth component to look for as a tag. + /// Success. + /// Thrown when multiple entities match the request. + /// + /// The least common component should be put in for query performance. + /// + public bool TrySingle( + [NotNullWhen(true)] out Entity? entity) + where TComp1 : IComponent + where TComp2 : IComponent + where TComp3 : IComponent + where TComp4 : IComponent; + + /// + /// Gets the sole entity with the given component, or spawns it at the given location if it doesn't exist. + /// + /// The fallback prototype to spawn on failure. + /// The location to spawn the singleton. Nullspace works. + /// The component to look for as a tag. + /// Thrown when multiple entities match the request. + /// Thrown when no entities match the request after spawning the fallback. + /// The singleton entity. + /// + /// This does not return the entity it spawns, it tries to look it up, so if spawning that entity violates + /// singleton status (or it lacks the necessary component) this will throw immediately after instead of later + /// on next call. + /// + /// This will also still throw if the spawned entity doesn't match at all. + /// + public Entity SingleOrSpawn(EntProtoId fallback, MapCoordinates location) + where TComp1 : IComponent; + + /// + /// Gets the sole entity with the given component, or spawns it at the given location if it doesn't exist. + /// + /// The fallback prototype to spawn on failure. + /// The location to spawn the singleton. Nullspace works. + /// The first component to look for as a tag. + /// The second component to look for as a tag. + /// Thrown when multiple entities match the request. + /// Thrown when no entities match the request after spawning the fallback. + /// The singleton entity. + /// + /// This does not return the entity it spawns, it tries to look it up, so if spawning that entity violates + /// singleton status (or it lacks the necessary component) this will throw immediately after instead of later + /// on next call. + /// + /// This will also still throw if the spawned entity doesn't match at all. + /// + public Entity SingleOrSpawn(EntProtoId fallback, MapCoordinates location) + where TComp1 : IComponent + where TComp2 : IComponent; + + /// + /// Gets the sole entity with the given component, or spawns it at the given location if it doesn't exist. + /// + /// The fallback prototype to spawn on failure. + /// The location to spawn the singleton. Nullspace works. + /// The first component to look for as a tag. + /// The second component to look for as a tag. + /// The third component to look for as a tag. + /// Thrown when multiple entities match the request. + /// Thrown when no entities match the request after spawning the fallback. + /// The singleton entity. + /// + /// This does not return the entity it spawns, it tries to look it up, so if spawning that entity violates + /// singleton status (or it lacks the necessary component) this will throw immediately after instead of later + /// on next call. + /// + /// This will also still throw if the spawned entity doesn't match at all. + /// + public Entity SingleOrSpawn( + EntProtoId fallback, + MapCoordinates location) + where TComp1 : IComponent + where TComp2 : IComponent + where TComp3 : IComponent; + + /// + /// Gets the sole entity with the given component, or spawns it at the given location if it doesn't exist. + /// + /// The fallback prototype to spawn on failure. + /// The location to spawn the singleton. Nullspace works. + /// The first component to look for as a tag. + /// The second component to look for as a tag. + /// The third component to look for as a tag. + /// The fourth component to look for as a tag. + /// Thrown when multiple entities match the request. + /// Thrown when no entities match the request after spawning the fallback. + /// The singleton entity. + /// + /// This does not return the entity it spawns, it tries to look it up, so if spawning that entity violates + /// singleton status (or it lacks the necessary component) this will throw immediately after instead of later + /// on next call. + /// + /// This will also still throw if the spawned entity doesn't match at all. + /// + public Entity SingleOrSpawn( + EntProtoId fallback, + MapCoordinates location) + where TComp1 : IComponent + where TComp2 : IComponent + where TComp3 : IComponent + where TComp4 : IComponent; + + /// + /// Gets the sole entity with the given component, or spawns it at the given location if it doesn't exist. + /// + /// The action to call on fallback, should no match exist. + /// The component to look for as a tag. + /// Thrown when multiple entities match the request. + /// Thrown when no entities match the request after calling fallback. + /// The singleton entity. + public Entity SingleOrInit(Action fallback) + where TComp1 : IComponent; + + /// + /// Gets the sole entity with the given component, or spawns it at the given location if it doesn't exist. + /// + /// The action to call on fallback, should no match exist. + /// The first component to look for as a tag. + /// The second component to look for as a tag. + /// Thrown when multiple entities match the request. + /// Thrown when no entities match the request after calling fallback. + /// The singleton entity. + public Entity SingleOrInit(Action fallback) + where TComp1 : IComponent + where TComp2 : IComponent; + + /// + /// Gets the sole entity with the given component, or spawns it at the given location if it doesn't exist. + /// + /// The action to call on fallback, should no match exist. + /// The first component to look for as a tag. + /// The second component to look for as a tag. + /// The third component to look for as a tag. + /// Thrown when multiple entities match the request. + /// Thrown when no entities match the request after calling fallback. + /// The singleton entity. + public Entity SingleOrInit(Action fallback) + where TComp1 : IComponent + where TComp2 : IComponent + where TComp3 : IComponent; + + /// + /// Gets the sole entity with the given component, or spawns it at the given location if it doesn't exist. + /// + /// The action to call on fallback, should no match exist. + /// The first component to look for as a tag. + /// The second component to look for as a tag. + /// The third component to look for as a tag. + /// The fourth component to look for as a tag. + /// Thrown when multiple entities match the request. + /// Thrown when no entities match the request after calling fallback. + /// The singleton entity. + public Entity SingleOrInit(Action fallback) + where TComp1 : IComponent + where TComp2 : IComponent + where TComp3 : IComponent + where TComp4 : IComponent; +} From 47ad5497f0b02a57153f2f442ae1a98a5bcccb3c Mon Sep 17 00:00:00 2001 From: kaylie Date: Mon, 9 Mar 2026 21:11:18 +0100 Subject: [PATCH 03/11] Move remark. --- Robust.Shared/GameObjects/EntityManager.Singleton.cs | 3 --- Robust.Shared/GameObjects/IEntityManager.Single.cs | 3 +++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Robust.Shared/GameObjects/EntityManager.Singleton.cs b/Robust.Shared/GameObjects/EntityManager.Singleton.cs index 4a1cf538f7d..135b728077a 100644 --- a/Robust.Shared/GameObjects/EntityManager.Singleton.cs +++ b/Robust.Shared/GameObjects/EntityManager.Singleton.cs @@ -10,9 +10,6 @@ namespace Robust.Shared.GameObjects; public abstract partial class EntityManager { - // REMARK: No API that allows you to use these queries without them throwing over non-uniqueness should be added. - // It's a pretty simple, natural error condition and the game *should* yell about it. - public Entity Single() where TComp1: IComponent { diff --git a/Robust.Shared/GameObjects/IEntityManager.Single.cs b/Robust.Shared/GameObjects/IEntityManager.Single.cs index 44c5370808b..412bc1ace80 100644 --- a/Robust.Shared/GameObjects/IEntityManager.Single.cs +++ b/Robust.Shared/GameObjects/IEntityManager.Single.cs @@ -7,6 +7,9 @@ namespace Robust.Shared.GameObjects; public partial interface IEntityManager { + // REMARK: No API that allows you to use these queries without them throwing over non-uniqueness should be added. + // It's a pretty simple, natural error condition and the game *should* yell about it. + /// /// Gets the sole entity with the given component. /// From 222f28d5cc7335e71a63ac9ff805bfa1eedff145 Mon Sep 17 00:00:00 2001 From: kaylie Date: Mon, 9 Mar 2026 21:25:19 +0100 Subject: [PATCH 04/11] Typo fix + Proxies. --- .../GameObjects/EntitySystem.Proxy.cs | 160 ++++++++++++++++++ .../GameObjects/IEntityManager.Single.cs | 2 +- 2 files changed, 161 insertions(+), 1 deletion(-) diff --git a/Robust.Shared/GameObjects/EntitySystem.Proxy.cs b/Robust.Shared/GameObjects/EntitySystem.Proxy.cs index 06b852ceaf5..7793c657afa 100644 --- a/Robust.Shared/GameObjects/EntitySystem.Proxy.cs +++ b/Robust.Shared/GameObjects/EntitySystem.Proxy.cs @@ -1783,4 +1783,164 @@ protected NetCoordinates[] GetNetCoordinatesArray(EntityCoordinates[] entities) } #endregion + + #region Singletons + + /// + [ProxyFor(typeof(EntityManager))] + public Entity Single() + where TComp1: IComponent + { + return EntityManager.Single(); + } + + /// + [ProxyFor(typeof(EntityManager))] + public Entity Single() + where TComp1: IComponent + where TComp2: IComponent + { + return EntityManager.Single(); + } + + /// + [ProxyFor(typeof(EntityManager))] + public Entity Single() + where TComp1: IComponent + where TComp2: IComponent + where TComp3: IComponent + { + return EntityManager.Single(); + } + + /// + [ProxyFor(typeof(EntityManager))] + public Entity Single() + where TComp1: IComponent + where TComp2: IComponent + where TComp3: IComponent + where TComp4: IComponent + { + return EntityManager.Single(); + } + + /// + [ProxyFor(typeof(EntityManager))] + public bool TrySingle([NotNullWhen(true)] out Entity? entity) + where TComp1 : IComponent + { + return EntityManager.TrySingle(out entity); + } + + /// + [ProxyFor(typeof(EntityManager))] + public bool TrySingle([NotNullWhen(true)] out Entity? entity) + where TComp1 : IComponent + where TComp2 : IComponent + { + return EntityManager.TrySingle(out entity); + } + + /// + [ProxyFor(typeof(EntityManager))] + public bool TrySingle([NotNullWhen(true)] out Entity? entity) + where TComp1 : IComponent + where TComp2 : IComponent + where TComp3 : IComponent + { + return EntityManager.TrySingle(out entity); + } + + /// + [ProxyFor(typeof(EntityManager))] + public bool TrySingle( + [NotNullWhen(true)] out Entity? entity) + where TComp1 : IComponent + where TComp2 : IComponent + where TComp3 : IComponent + where TComp4 : IComponent + { + return EntityManager.TrySingle(out entity); + } + + /// + [ProxyFor(typeof(EntityManager))] + public Entity SingleOrSpawn(EntProtoId fallback, MapCoordinates location) + where TComp1 : IComponent + { + return EntityManager.SingleOrSpawn(fallback, location); + } + + /// + [ProxyFor(typeof(EntityManager))] + public Entity SingleOrSpawn(EntProtoId fallback, MapCoordinates location) + where TComp1 : IComponent + where TComp2 : IComponent + { + return EntityManager.SingleOrSpawn(fallback, location); + } + + /// + [ProxyFor(typeof(EntityManager))] + public Entity SingleOrSpawn( + EntProtoId fallback, + MapCoordinates location) + where TComp1 : IComponent + where TComp2 : IComponent + where TComp3 : IComponent + { + return EntityManager.SingleOrSpawn(fallback, location); + } + + /// + [ProxyFor(typeof(EntityManager))] + public Entity SingleOrSpawn( + EntProtoId fallback, + MapCoordinates location) + where TComp1 : IComponent + where TComp2 : IComponent + where TComp3 : IComponent + where TComp4 : IComponent + { + return EntityManager.SingleOrSpawn(fallback, location); + } + + /// + [ProxyFor(typeof(EntityManager))] + public Entity SingleOrInit(Action fallback) + where TComp1 : IComponent + { + return EntityManager.SingleOrInit(fallback); + } + + /// + [ProxyFor(typeof(EntityManager))] + public Entity SingleOrInit(Action fallback) + where TComp1 : IComponent + where TComp2 : IComponent + { + return EntityManager.SingleOrInit(fallback); + } + + /// + [ProxyFor(typeof(EntityManager))] + public Entity SingleOrInit(Action fallback) + where TComp1 : IComponent + where TComp2 : IComponent + where TComp3 : IComponent + { + return EntityManager.SingleOrInit(fallback); + } + + /// + [ProxyFor(typeof(EntityManager))] + public Entity SingleOrInit(Action fallback) + where TComp1 : IComponent + where TComp2 : IComponent + where TComp3 : IComponent + where TComp4 : IComponent + { + return EntityManager.SingleOrInit(fallback); + } + #endregion } diff --git a/Robust.Shared/GameObjects/IEntityManager.Single.cs b/Robust.Shared/GameObjects/IEntityManager.Single.cs index 412bc1ace80..8734f041f12 100644 --- a/Robust.Shared/GameObjects/IEntityManager.Single.cs +++ b/Robust.Shared/GameObjects/IEntityManager.Single.cs @@ -58,7 +58,7 @@ public Entity Single() /// The first component to look for as a tag. /// The second component to look for as a tag. /// The third component to look for as a tag. - /// The third component to look for as a tag. + /// The fourth component to look for as a tag. /// The singleton entity. /// Thrown when multiple entities match the request. /// Thrown when no entities match the request. From b6987193db2b68aed0ecc116c514418b4337fa43 Mon Sep 17 00:00:00 2001 From: kaylie Date: Tue, 10 Mar 2026 13:45:23 +0100 Subject: [PATCH 05/11] Pause remarks. --- Robust.Shared/GameObjects/Docs.xml | 19 +++++++ .../GameObjects/IEntityManager.Single.cs | 54 ++++++++----------- 2 files changed, 42 insertions(+), 31 deletions(-) diff --git a/Robust.Shared/GameObjects/Docs.xml b/Robust.Shared/GameObjects/Docs.xml index 6eaf60eb8b2..a1203da58a8 100644 --- a/Robust.Shared/GameObjects/Docs.xml +++ b/Robust.Shared/GameObjects/Docs.xml @@ -11,4 +11,23 @@ This is also preferable if you may have already looked up the component, saving on lookup time. + + + + This does not return the entity it spawns, it tries to look it up, so if spawning that entity violates + singleton status (or it lacks the necessary component) this will throw immediately after instead of later + on next call. + + This will also still throw if the spawned entity doesn't match at all. + + + + + + + This API does not obey . + If you need to "pause" singleton entities, use an extra marker component that you remove when "pausing" them. + + + diff --git a/Robust.Shared/GameObjects/IEntityManager.Single.cs b/Robust.Shared/GameObjects/IEntityManager.Single.cs index 8734f041f12..fad78a9042c 100644 --- a/Robust.Shared/GameObjects/IEntityManager.Single.cs +++ b/Robust.Shared/GameObjects/IEntityManager.Single.cs @@ -17,6 +17,7 @@ public partial interface IEntityManager /// The singleton entity. /// Thrown when multiple entities match the request. /// Thrown when no entities match the request. + /// public Entity Single() where TComp1 : IComponent; @@ -29,8 +30,9 @@ public Entity Single() /// Thrown when multiple entities match the request. /// Thrown when no entities match the request. /// - /// The least common component should be put in for query performance. + /// The least common component should be put in for query performance. /// + /// public Entity Single() where TComp1 : IComponent where TComp2 : IComponent; @@ -45,8 +47,9 @@ public Entity Single() /// Thrown when multiple entities match the request. /// Thrown when no entities match the request. /// - /// The least common component should be put in for query performance. + /// The least common component should be put in for query performance. /// + /// public Entity Single() where TComp1 : IComponent where TComp2 : IComponent @@ -63,8 +66,9 @@ public Entity Single() /// Thrown when multiple entities match the request. /// Thrown when no entities match the request. /// - /// The least common component should be put in for query performance. + /// The least common component should be put in for query performance. /// + /// public Entity Single() where TComp1 : IComponent where TComp2 : IComponent @@ -78,6 +82,7 @@ public Entity SingleThe component to look for as a tag. /// Thrown when multiple entities match the request. /// Success. + /// public bool TrySingle([NotNullWhen(true)] out Entity? entity) where TComp1 : IComponent; @@ -92,6 +97,7 @@ public bool TrySingle([NotNullWhen(true)] out Entity? entity) /// /// The least common component should be put in for query performance. /// + /// public bool TrySingle([NotNullWhen(true)] out Entity? entity) where TComp1 : IComponent where TComp2 : IComponent; @@ -108,6 +114,7 @@ public bool TrySingle([NotNullWhen(true)] out Entity /// The least common component should be put in for query performance. /// + /// public bool TrySingle([NotNullWhen(true)] out Entity? entity) where TComp1 : IComponent where TComp2 : IComponent @@ -126,6 +133,7 @@ public bool TrySingle([NotNullWhen(true)] out Entity /// The least common component should be put in for query performance. /// + /// public bool TrySingle( [NotNullWhen(true)] out Entity? entity) where TComp1 : IComponent @@ -142,13 +150,8 @@ public bool TrySingle( /// Thrown when multiple entities match the request. /// Thrown when no entities match the request after spawning the fallback. /// The singleton entity. - /// - /// This does not return the entity it spawns, it tries to look it up, so if spawning that entity violates - /// singleton status (or it lacks the necessary component) this will throw immediately after instead of later - /// on next call. - /// - /// This will also still throw if the spawned entity doesn't match at all. - /// + /// + /// public Entity SingleOrSpawn(EntProtoId fallback, MapCoordinates location) where TComp1 : IComponent; @@ -162,13 +165,8 @@ public Entity SingleOrSpawn(EntProtoId fallback, MapCoordinates /// Thrown when multiple entities match the request. /// Thrown when no entities match the request after spawning the fallback. /// The singleton entity. - /// - /// This does not return the entity it spawns, it tries to look it up, so if spawning that entity violates - /// singleton status (or it lacks the necessary component) this will throw immediately after instead of later - /// on next call. - /// - /// This will also still throw if the spawned entity doesn't match at all. - /// + /// + /// public Entity SingleOrSpawn(EntProtoId fallback, MapCoordinates location) where TComp1 : IComponent where TComp2 : IComponent; @@ -184,13 +182,8 @@ public Entity SingleOrSpawn(EntProtoId fallback, /// Thrown when multiple entities match the request. /// Thrown when no entities match the request after spawning the fallback. /// The singleton entity. - /// - /// This does not return the entity it spawns, it tries to look it up, so if spawning that entity violates - /// singleton status (or it lacks the necessary component) this will throw immediately after instead of later - /// on next call. - /// - /// This will also still throw if the spawned entity doesn't match at all. - /// + /// + /// public Entity SingleOrSpawn( EntProtoId fallback, MapCoordinates location) @@ -210,13 +203,8 @@ public Entity SingleOrSpawn( /// Thrown when multiple entities match the request. /// Thrown when no entities match the request after spawning the fallback. /// The singleton entity. - /// - /// This does not return the entity it spawns, it tries to look it up, so if spawning that entity violates - /// singleton status (or it lacks the necessary component) this will throw immediately after instead of later - /// on next call. - /// - /// This will also still throw if the spawned entity doesn't match at all. - /// + /// + /// public Entity SingleOrSpawn( EntProtoId fallback, MapCoordinates location) @@ -233,6 +221,7 @@ public Entity SingleOrSpawnThrown when multiple entities match the request. /// Thrown when no entities match the request after calling fallback. /// The singleton entity. + /// public Entity SingleOrInit(Action fallback) where TComp1 : IComponent; @@ -245,6 +234,7 @@ public Entity SingleOrInit(Action fallback) /// Thrown when multiple entities match the request. /// Thrown when no entities match the request after calling fallback. /// The singleton entity. + /// public Entity SingleOrInit(Action fallback) where TComp1 : IComponent where TComp2 : IComponent; @@ -259,6 +249,7 @@ public Entity SingleOrInit(Action fallback) /// Thrown when multiple entities match the request. /// Thrown when no entities match the request after calling fallback. /// The singleton entity. + /// public Entity SingleOrInit(Action fallback) where TComp1 : IComponent where TComp2 : IComponent @@ -275,6 +266,7 @@ public Entity SingleOrInit(Actio /// Thrown when multiple entities match the request. /// Thrown when no entities match the request after calling fallback. /// The singleton entity. + /// public Entity SingleOrInit(Action fallback) where TComp1 : IComponent where TComp2 : IComponent From c4d11799b9eb54d54e3e25fc1179c118802b49cc Mon Sep 17 00:00:00 2001 From: kaylie Date: Tue, 10 Mar 2026 14:13:52 +0100 Subject: [PATCH 06/11] No, really, we don't obey paused! --- Robust.Shared/GameObjects/EntityManager.Singleton.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Robust.Shared/GameObjects/EntityManager.Singleton.cs b/Robust.Shared/GameObjects/EntityManager.Singleton.cs index 135b728077a..552a6b4941f 100644 --- a/Robust.Shared/GameObjects/EntityManager.Singleton.cs +++ b/Robust.Shared/GameObjects/EntityManager.Singleton.cs @@ -35,7 +35,7 @@ public Entity Single() where TComp1: IComponent where TComp2: IComponent { - var query = EntityQueryEnumerator(); + var query = AllEntityQueryEnumerator(); if (!query.MoveNext(out var ent, out var comp1, out var comp2)) throw new MatchNotFoundException(typeof(TComp1), typeof(TComp2)); @@ -60,7 +60,7 @@ public Entity Single() where TComp2: IComponent where TComp3: IComponent { - var query = EntityQueryEnumerator(); + var query = AllEntityQueryEnumerator(); if (!query.MoveNext(out var ent, out var comp1, out var comp2, out var comp3)) throw new MatchNotFoundException(typeof(TComp1), typeof(TComp2), typeof(TComp3)); @@ -86,7 +86,7 @@ public Entity Single(); + var query = AllEntityQueryEnumerator(); if (!query.MoveNext(out var ent, out var comp1, out var comp2, out var comp3, out var comp4)) throw new MatchNotFoundException(typeof(TComp1), typeof(TComp2), typeof(TComp3), typeof(TComp4)); @@ -133,7 +133,7 @@ public bool TrySingle([NotNullWhen(true)] out Entity(); + var query = AllEntityQueryEnumerator(); if (!query.MoveNext(out var ent, out var comp1, out var comp2)) { @@ -162,7 +162,7 @@ public bool TrySingle([NotNullWhen(true)] out Entity(); + var query = AllEntityQueryEnumerator(); if (!query.MoveNext(out var ent, out var comp1, out var comp2, out var comp3)) { @@ -192,7 +192,7 @@ public bool TrySingle([NotNullWhen(true)] out En where TComp3: IComponent where TComp4: IComponent { - var query = EntityQueryEnumerator(); + var query = AllEntityQueryEnumerator(); if (!query.MoveNext(out var ent, out var comp1, out var comp2, out var comp3, out var comp4)) { From 34298d01ba13681d02ca85dbce39146b08497503 Mon Sep 17 00:00:00 2001 From: kaylie Date: Wed, 11 Mar 2026 21:42:40 +0100 Subject: [PATCH 07/11] Release notes. --- RELEASE-NOTES.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 4c55457d3e2..ae6ee351877 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -39,7 +39,9 @@ END TEMPLATE--> ### New features -*None yet* +- `IEntityManager` has a new family of 1-4 component methods for working with *singleton entities*, entities which only + one instance of exists at a time for state management. Consult `IEntityManager.Single.cs` and the documentation for + details. ### Bugfixes From b6f1a84d1bbd14de3d452295cfd07976256262dc Mon Sep 17 00:00:00 2001 From: kaylie Date: Thu, 12 Mar 2026 11:10:47 +0100 Subject: [PATCH 08/11] testdocs. --- .../GameObjects/EntityManagerSingletonTests.cs | 18 +++++++++++++----- ...er.Singleton.cs => EntityManager.Single.cs} | 0 2 files changed, 13 insertions(+), 5 deletions(-) rename Robust.Shared/GameObjects/{EntityManager.Singleton.cs => EntityManager.Single.cs} (100%) diff --git a/Robust.Shared.IntegrationTests/GameObjects/EntityManagerSingletonTests.cs b/Robust.Shared.IntegrationTests/GameObjects/EntityManagerSingletonTests.cs index 7f78b59d8dd..501f0c7451d 100644 --- a/Robust.Shared.IntegrationTests/GameObjects/EntityManagerSingletonTests.cs +++ b/Robust.Shared.IntegrationTests/GameObjects/EntityManagerSingletonTests.cs @@ -6,7 +6,7 @@ namespace Robust.UnitTesting.Shared.GameObjects; -[TestFixture, TestOf(typeof(EntityManager))] +[TestOf(typeof(EntityManager))] internal sealed class EntityManagerSingletonTests : OurRobustUnitTest { private const string TestSingleton1 = "T_TestSingleton1"; @@ -43,6 +43,11 @@ public void Setup() } [Test] + [Description(""" + Creates an entity with marker components 1-4, and ensures increasingly constrained Single<>() queries match it. + Then creates a second entity, and ensures queries fail. + Then deletes both, and ensures Single fails while TrySingle is fine. + """)] public void SingleEntity() { var mySingle = _entMan.Spawn(TestSingleton1); @@ -52,10 +57,13 @@ public void SingleEntity() var single3 = _entMan.Single(); var single4 = _entMan.Single(); - Assert.That(mySingle, NUnit.Framework.Is.EqualTo(single1.Owner)); - Assert.That(mySingle, NUnit.Framework.Is.EqualTo(single2.Owner)); - Assert.That(mySingle, NUnit.Framework.Is.EqualTo(single3.Owner)); - Assert.That(mySingle, NUnit.Framework.Is.EqualTo(single4.Owner)); + using (Assert.EnterMultipleScope()) + { + Assert.That(mySingle, NUnit.Framework.Is.EqualTo(single1.Owner)); + Assert.That(mySingle, NUnit.Framework.Is.EqualTo(single2.Owner)); + Assert.That(mySingle, NUnit.Framework.Is.EqualTo(single3.Owner)); + Assert.That(mySingle, NUnit.Framework.Is.EqualTo(single4.Owner)); + } var bonusSingle = _entMan.Spawn(TestSingleton1); diff --git a/Robust.Shared/GameObjects/EntityManager.Singleton.cs b/Robust.Shared/GameObjects/EntityManager.Single.cs similarity index 100% rename from Robust.Shared/GameObjects/EntityManager.Singleton.cs rename to Robust.Shared/GameObjects/EntityManager.Single.cs From 91ad9f766fd180f6db27862b31f8055941cbf2e4 Mon Sep 17 00:00:00 2001 From: kaylie Date: Thu, 12 Mar 2026 11:56:50 +0100 Subject: [PATCH 09/11] Poke From 745e797065f1e377b6dab7365a0f77f1005fdf11 Mon Sep 17 00:00:00 2001 From: kaylie Date: Fri, 13 Mar 2026 11:18:47 +0100 Subject: [PATCH 10/11] The API design axe. --- .../GameObjects/EntityManager.Single.cs | 100 ------------- .../GameObjects/EntitySystem.Proxy.cs | 80 ----------- .../GameObjects/IEntityManager.Single.cs | 132 ------------------ 3 files changed, 312 deletions(-) diff --git a/Robust.Shared/GameObjects/EntityManager.Single.cs b/Robust.Shared/GameObjects/EntityManager.Single.cs index 552a6b4941f..4480d98517c 100644 --- a/Robust.Shared/GameObjects/EntityManager.Single.cs +++ b/Robust.Shared/GameObjects/EntityManager.Single.cs @@ -215,106 +215,6 @@ public bool TrySingle([NotNullWhen(true)] out En entity = new(ent, comp1, comp2, comp3, comp4); return true; } - - public Entity SingleOrSpawn(EntProtoId fallback, MapCoordinates location) - where TComp1: IComponent - { - if (TrySingle(out var ent)) - return ent.Value; - - Spawn(fallback, location); - - return Single(); - } - - public Entity SingleOrSpawn(EntProtoId fallback, MapCoordinates location) - where TComp1: IComponent - where TComp2: IComponent - { - if (TrySingle(out var ent)) - return ent.Value; - - Spawn(fallback, location); - - return Single(); - } - - public Entity SingleOrSpawn(EntProtoId fallback, MapCoordinates location) - where TComp1: IComponent - where TComp2: IComponent - where TComp3: IComponent - { - if (TrySingle(out var ent)) - return ent.Value; - - Spawn(fallback, location); - - return Single(); - } - - public Entity SingleOrSpawn(EntProtoId fallback, MapCoordinates location) - where TComp1: IComponent - where TComp2: IComponent - where TComp3: IComponent - where TComp4: IComponent - { - if (TrySingle(out var ent)) - return ent.Value; - - Spawn(fallback, location); - - return Single(); - } - - public Entity SingleOrInit(Action fallback) - where TComp1: IComponent - { - if (TrySingle(out var ent)) - return ent.Value; - - fallback(); - - return Single(); - } - - public Entity SingleOrInit(Action fallback) - where TComp1: IComponent - where TComp2: IComponent - { - if (TrySingle(out var ent)) - return ent.Value; - - fallback(); - - return Single(); - } - - public Entity SingleOrInit(Action fallback) - where TComp1: IComponent - where TComp2: IComponent - where TComp3: IComponent - { - if (TrySingle(out var ent)) - return ent.Value; - - fallback(); - - return Single(); - } - - public Entity SingleOrInit(Action fallback) - where TComp1: IComponent - where TComp2: IComponent - where TComp3: IComponent - where TComp4: IComponent - { - if (TrySingle(out var ent)) - return ent.Value; - - fallback(); - - return Single(); - } } /// diff --git a/Robust.Shared/GameObjects/EntitySystem.Proxy.cs b/Robust.Shared/GameObjects/EntitySystem.Proxy.cs index 7793c657afa..8a264e68720 100644 --- a/Robust.Shared/GameObjects/EntitySystem.Proxy.cs +++ b/Robust.Shared/GameObjects/EntitySystem.Proxy.cs @@ -1862,85 +1862,5 @@ public bool TrySingle( { return EntityManager.TrySingle(out entity); } - - /// - [ProxyFor(typeof(EntityManager))] - public Entity SingleOrSpawn(EntProtoId fallback, MapCoordinates location) - where TComp1 : IComponent - { - return EntityManager.SingleOrSpawn(fallback, location); - } - - /// - [ProxyFor(typeof(EntityManager))] - public Entity SingleOrSpawn(EntProtoId fallback, MapCoordinates location) - where TComp1 : IComponent - where TComp2 : IComponent - { - return EntityManager.SingleOrSpawn(fallback, location); - } - - /// - [ProxyFor(typeof(EntityManager))] - public Entity SingleOrSpawn( - EntProtoId fallback, - MapCoordinates location) - where TComp1 : IComponent - where TComp2 : IComponent - where TComp3 : IComponent - { - return EntityManager.SingleOrSpawn(fallback, location); - } - - /// - [ProxyFor(typeof(EntityManager))] - public Entity SingleOrSpawn( - EntProtoId fallback, - MapCoordinates location) - where TComp1 : IComponent - where TComp2 : IComponent - where TComp3 : IComponent - where TComp4 : IComponent - { - return EntityManager.SingleOrSpawn(fallback, location); - } - - /// - [ProxyFor(typeof(EntityManager))] - public Entity SingleOrInit(Action fallback) - where TComp1 : IComponent - { - return EntityManager.SingleOrInit(fallback); - } - - /// - [ProxyFor(typeof(EntityManager))] - public Entity SingleOrInit(Action fallback) - where TComp1 : IComponent - where TComp2 : IComponent - { - return EntityManager.SingleOrInit(fallback); - } - - /// - [ProxyFor(typeof(EntityManager))] - public Entity SingleOrInit(Action fallback) - where TComp1 : IComponent - where TComp2 : IComponent - where TComp3 : IComponent - { - return EntityManager.SingleOrInit(fallback); - } - - /// - [ProxyFor(typeof(EntityManager))] - public Entity SingleOrInit(Action fallback) - where TComp1 : IComponent - where TComp2 : IComponent - where TComp3 : IComponent - where TComp4 : IComponent - { - return EntityManager.SingleOrInit(fallback); - } #endregion } diff --git a/Robust.Shared/GameObjects/IEntityManager.Single.cs b/Robust.Shared/GameObjects/IEntityManager.Single.cs index fad78a9042c..d6adf396827 100644 --- a/Robust.Shared/GameObjects/IEntityManager.Single.cs +++ b/Robust.Shared/GameObjects/IEntityManager.Single.cs @@ -140,136 +140,4 @@ public bool TrySingle( where TComp2 : IComponent where TComp3 : IComponent where TComp4 : IComponent; - - /// - /// Gets the sole entity with the given component, or spawns it at the given location if it doesn't exist. - /// - /// The fallback prototype to spawn on failure. - /// The location to spawn the singleton. Nullspace works. - /// The component to look for as a tag. - /// Thrown when multiple entities match the request. - /// Thrown when no entities match the request after spawning the fallback. - /// The singleton entity. - /// - /// - public Entity SingleOrSpawn(EntProtoId fallback, MapCoordinates location) - where TComp1 : IComponent; - - /// - /// Gets the sole entity with the given component, or spawns it at the given location if it doesn't exist. - /// - /// The fallback prototype to spawn on failure. - /// The location to spawn the singleton. Nullspace works. - /// The first component to look for as a tag. - /// The second component to look for as a tag. - /// Thrown when multiple entities match the request. - /// Thrown when no entities match the request after spawning the fallback. - /// The singleton entity. - /// - /// - public Entity SingleOrSpawn(EntProtoId fallback, MapCoordinates location) - where TComp1 : IComponent - where TComp2 : IComponent; - - /// - /// Gets the sole entity with the given component, or spawns it at the given location if it doesn't exist. - /// - /// The fallback prototype to spawn on failure. - /// The location to spawn the singleton. Nullspace works. - /// The first component to look for as a tag. - /// The second component to look for as a tag. - /// The third component to look for as a tag. - /// Thrown when multiple entities match the request. - /// Thrown when no entities match the request after spawning the fallback. - /// The singleton entity. - /// - /// - public Entity SingleOrSpawn( - EntProtoId fallback, - MapCoordinates location) - where TComp1 : IComponent - where TComp2 : IComponent - where TComp3 : IComponent; - - /// - /// Gets the sole entity with the given component, or spawns it at the given location if it doesn't exist. - /// - /// The fallback prototype to spawn on failure. - /// The location to spawn the singleton. Nullspace works. - /// The first component to look for as a tag. - /// The second component to look for as a tag. - /// The third component to look for as a tag. - /// The fourth component to look for as a tag. - /// Thrown when multiple entities match the request. - /// Thrown when no entities match the request after spawning the fallback. - /// The singleton entity. - /// - /// - public Entity SingleOrSpawn( - EntProtoId fallback, - MapCoordinates location) - where TComp1 : IComponent - where TComp2 : IComponent - where TComp3 : IComponent - where TComp4 : IComponent; - - /// - /// Gets the sole entity with the given component, or spawns it at the given location if it doesn't exist. - /// - /// The action to call on fallback, should no match exist. - /// The component to look for as a tag. - /// Thrown when multiple entities match the request. - /// Thrown when no entities match the request after calling fallback. - /// The singleton entity. - /// - public Entity SingleOrInit(Action fallback) - where TComp1 : IComponent; - - /// - /// Gets the sole entity with the given component, or spawns it at the given location if it doesn't exist. - /// - /// The action to call on fallback, should no match exist. - /// The first component to look for as a tag. - /// The second component to look for as a tag. - /// Thrown when multiple entities match the request. - /// Thrown when no entities match the request after calling fallback. - /// The singleton entity. - /// - public Entity SingleOrInit(Action fallback) - where TComp1 : IComponent - where TComp2 : IComponent; - - /// - /// Gets the sole entity with the given component, or spawns it at the given location if it doesn't exist. - /// - /// The action to call on fallback, should no match exist. - /// The first component to look for as a tag. - /// The second component to look for as a tag. - /// The third component to look for as a tag. - /// Thrown when multiple entities match the request. - /// Thrown when no entities match the request after calling fallback. - /// The singleton entity. - /// - public Entity SingleOrInit(Action fallback) - where TComp1 : IComponent - where TComp2 : IComponent - where TComp3 : IComponent; - - /// - /// Gets the sole entity with the given component, or spawns it at the given location if it doesn't exist. - /// - /// The action to call on fallback, should no match exist. - /// The first component to look for as a tag. - /// The second component to look for as a tag. - /// The third component to look for as a tag. - /// The fourth component to look for as a tag. - /// Thrown when multiple entities match the request. - /// Thrown when no entities match the request after calling fallback. - /// The singleton entity. - /// - public Entity SingleOrInit(Action fallback) - where TComp1 : IComponent - where TComp2 : IComponent - where TComp3 : IComponent - where TComp4 : IComponent; } From b9a67111294f17f54779a8cbe2e49bc82f0d06e4 Mon Sep 17 00:00:00 2001 From: Moony Date: Thu, 12 Mar 2026 22:18:02 +0100 Subject: [PATCH 11/11] Update Robust.Shared.IntegrationTests/GameObjects/EntityManagerSingletonTests.cs Co-authored-by: pathetic meowmeow --- .../EntityManagerSingletonTests.cs | 23 ++++--------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/Robust.Shared.IntegrationTests/GameObjects/EntityManagerSingletonTests.cs b/Robust.Shared.IntegrationTests/GameObjects/EntityManagerSingletonTests.cs index 501f0c7451d..8290dcfa438 100644 --- a/Robust.Shared.IntegrationTests/GameObjects/EntityManagerSingletonTests.cs +++ b/Robust.Shared.IntegrationTests/GameObjects/EntityManagerSingletonTests.cs @@ -106,22 +106,7 @@ public void SingleEntity() } } -internal sealed partial class Marker1Component : Component -{ - -} - -internal sealed partial class Marker2Component : Component -{ - -} - -internal sealed partial class Marker3Component : Component -{ - -} - -internal sealed partial class Marker4Component : Component -{ - -} +internal sealed partial class Marker1Component : Component; +internal sealed partial class Marker2Component : Component; +internal sealed partial class Marker3Component : Component; +internal sealed partial class Marker4Component : Component;