Split up test project

Robust.UnitTesting was both ALL tests for RT, and also API surface for content tests.

Tests are now split into separate projects as appropriate, and the API side has also been split off.
This commit is contained in:
PJB3005
2025-12-16 01:36:53 +01:00
parent 095c5f58d9
commit 788e9386fd
284 changed files with 849 additions and 595 deletions

View File

@@ -0,0 +1,110 @@
using System;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
namespace Robust.UnitTesting.Shared.GameObjects
{
[TestFixture]
[TestOf(typeof(ComponentFactory))]
internal sealed partial class ComponentFactory_Tests : OurRobustUnitTest
{
private const string TestComponentName = "A";
private const string LowercaseTestComponentName = "a";
private const string NonexistentComponentName = "B";
protected override Type[]? ExtraComponents => new[] {typeof(TestComponent)};
[Test]
public void GetComponentAvailabilityTest()
{
var componentFactory = IoCManager.Resolve<IComponentFactory>();
// Should not exist
Assert.That(componentFactory.GetComponentAvailability(NonexistentComponentName), Is.EqualTo(ComponentAvailability.Unknown));
Assert.That(componentFactory.GetComponentAvailability(NonexistentComponentName, true), Is.EqualTo(ComponentAvailability.Unknown));
// Normal casing, do not ignore case, should exist
Assert.That(componentFactory.GetComponentAvailability(TestComponentName), Is.EqualTo(ComponentAvailability.Available));
// Normal casing, ignore case, should exist
Assert.That(componentFactory.GetComponentAvailability(TestComponentName, true), Is.EqualTo(ComponentAvailability.Available));
// Lower casing, do not ignore case, should not exist
Assert.That(componentFactory.GetComponentAvailability(LowercaseTestComponentName), Is.EqualTo(ComponentAvailability.Unknown));
// Lower casing, ignore case, should exist
Assert.That(componentFactory.GetComponentAvailability(LowercaseTestComponentName, true), Is.EqualTo(ComponentAvailability.Available));
}
[Test]
public void GetComponentTest()
{
var componentFactory = IoCManager.Resolve<IComponentFactory>();
// Should not exist
Assert.Throws<UnknownComponentException>(() => componentFactory.GetComponent(NonexistentComponentName));
Assert.Throws<UnknownComponentException>(() => componentFactory.GetComponent(NonexistentComponentName, true));
// Normal casing, do not ignore case, should exist
Assert.That(componentFactory.GetComponent(TestComponentName), Is.InstanceOf<TestComponent>());
// Normal casing, ignore case, should exist
Assert.That(componentFactory.GetComponent(TestComponentName, true), Is.InstanceOf<TestComponent>());
// Lower casing, do not ignore case, should not exist
Assert.Throws<UnknownComponentException>(() => componentFactory.GetComponent(LowercaseTestComponentName));
// Lower casing, ignore case, should exist
Assert.That(componentFactory.GetComponent(LowercaseTestComponentName, true), Is.InstanceOf<TestComponent>());
}
[Test]
public void GetRegistrationTest()
{
var componentFactory = IoCManager.Resolve<IComponentFactory>();
// Should not exist
Assert.Throws<UnknownComponentException>(() => componentFactory.GetRegistration(NonexistentComponentName));
Assert.Throws<UnknownComponentException>(() => componentFactory.GetRegistration(NonexistentComponentName, true));
// Normal casing, do not ignore case, should exist
Assert.DoesNotThrow(() => componentFactory.GetRegistration(TestComponentName));
// Normal casing, ignore case, should exist
Assert.DoesNotThrow(() => componentFactory.GetRegistration(TestComponentName, true));
// Lower casing, do not ignore case, should not exist
Assert.Throws<UnknownComponentException>(() => componentFactory.GetRegistration(LowercaseTestComponentName));
// Lower casing, ignore case, should exist
Assert.DoesNotThrow(() => componentFactory.GetRegistration(LowercaseTestComponentName, true));
}
[Test]
public void TryGetRegistrationTest()
{
var componentFactory = IoCManager.Resolve<IComponentFactory>();
// Should not exist
Assert.That(componentFactory.TryGetRegistration(NonexistentComponentName, out _), Is.False);
Assert.That(componentFactory.TryGetRegistration(NonexistentComponentName, out _, true), Is.False);
// Normal casing, do not ignore case, should exist
Assert.That(componentFactory.TryGetRegistration(TestComponentName, out _));
// Normal casing, ignore case, should exist
Assert.That(componentFactory.TryGetRegistration(TestComponentName, out _, true));
// Lower casing, do not ignore case, should not exist
Assert.That(componentFactory.TryGetRegistration(LowercaseTestComponentName, out _), Is.False);
// Lower casing, ignore case, should exist
Assert.That(componentFactory.TryGetRegistration(LowercaseTestComponentName, out _, true));
}
[ComponentProtoName(TestComponentName)]
private sealed partial class TestComponent : Component
{
}
}
}

View File

@@ -0,0 +1,370 @@
using System.Linq;
using System.Numerics;
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Client.GameObjects;
using Robust.Client.Timing;
using Robust.Server.Player;
using Robust.Shared.Containers;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.UnitTesting.Shared.GameObjects
{
internal sealed class ContainerTests : RobustIntegrationTest
{
/// <summary>
/// Tests container states with children that do not exist on the client
/// and tests that said children are added to the container when they do arrive on the client.
/// </summary>
/// <returns></returns>
[Test]
public async Task TestContainerNonexistantItems()
{
var server = StartServer();
var client = StartClient();
await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync());
var cEntManager = client.ResolveDependency<IEntityManager>();
var clientNetManager = client.ResolveDependency<IClientNetManager>();
var sEntManager = server.ResolveDependency<IEntityManager>();
var sPlayerManager = server.ResolveDependency<IPlayerManager>();
Assert.DoesNotThrow(() => client.SetConnectTarget(server));
client.Post(() =>
{
clientNetManager.ClientConnect(null!, 0, null!);
});
for (int i = 0; i < 10; i++)
{
await server.WaitRunTicks(1);
await client.WaitRunTicks(1);
}
// Setup
var mapId = MapId.Nullspace;
var mapPos = MapCoordinates.Nullspace;
EntityUid entityUid = default!;
var cContainerSys = cEntManager.System<ContainerSystem>();
var sContainerSys = sEntManager.System<SharedContainerSystem>();
var sMetadataSys = sEntManager.System<MetaDataSystem>();
await server.WaitAssertion(() =>
{
sEntManager.System<SharedMapSystem>().CreateMap(out mapId);
mapPos = new MapCoordinates(new Vector2(0, 0), mapId);
entityUid = sEntManager.SpawnEntity(null, mapPos);
sMetadataSys.SetEntityName(entityUid, "Container");
sContainerSys.EnsureContainer<Container>(entityUid, "dummy");
// Setup PVS
sEntManager.AddComponent<EyeComponent>(entityUid);
var player = sPlayerManager.Sessions.First();
server.PlayerMan.SetAttachedEntity(player, entityUid);
sPlayerManager.JoinGame(player);
});
for (int i = 0; i < 10; i++)
{
await server.WaitRunTicks(1);
await client.WaitRunTicks(1);
}
EntityUid itemUid = default!;
await server.WaitAssertion(() =>
{
itemUid = sEntManager.SpawnEntity(null, mapPos);
sMetadataSys.SetEntityName(itemUid, "Item");
var container = sContainerSys.EnsureContainer<Container>(entityUid, "dummy");
Assert.That(sContainerSys.Insert(itemUid, container));
// Modify visibility layer so that the item does not get sent ot the player
sEntManager.System<SharedVisibilitySystem>().AddLayer(itemUid, 10 );
});
// Needs minimum 4 to sync to client because buffer size is 3
await server.WaitRunTicks(4);
await client.WaitRunTicks(10);
EntityUid cEntityUid = default!;
await client.WaitAssertion(() =>
{
cEntityUid = client.EntMan.GetEntity(server.EntMan.GetNetEntity(entityUid));
if (!cEntManager.TryGetComponent<ContainerManagerComponent>(cEntityUid, out var containerManagerComp))
{
Assert.Fail();
return;
}
var container = cContainerSys.GetContainer(cEntityUid, "dummy", containerManagerComp);
Assert.That(container.ContainedEntities.Count, Is.EqualTo(0));
Assert.That(container.ExpectedEntities.Count, Is.EqualTo(1));
Assert.That(cContainerSys.ExpectedEntities.ContainsKey(sEntManager.GetNetEntity(itemUid)));
Assert.That(cContainerSys.ExpectedEntities.Count, Is.EqualTo(1));
});
await server.WaitAssertion(() =>
{
// Modify visibility layer so it now gets sent to the client
sEntManager.System<SharedVisibilitySystem>().RemoveLayer(itemUid, 10 );
});
await server.WaitRunTicks(1);
await client.WaitRunTicks(4);
await client.WaitAssertion(() =>
{
if (!cEntManager.TryGetComponent<ContainerManagerComponent>(cEntityUid, out var containerManagerComp))
{
Assert.Fail();
return;
}
var container = cContainerSys.GetContainer(cEntityUid, "dummy", containerManagerComp);
Assert.That(container.ContainedEntities.Count, Is.EqualTo(1));
Assert.That(container.ExpectedEntities.Count, Is.EqualTo(0));
Assert.That(!cContainerSys.ExpectedEntities.ContainsKey(sEntManager.GetNetEntity(itemUid)));
Assert.That(cContainerSys.ExpectedEntities, Is.Empty);
});
await client.WaitPost(() => clientNetManager.ClientDisconnect(""));
await server.WaitRunTicks(5);
await client.WaitRunTicks(5);
}
/// <summary>
/// Tests container states with children that do not exist on the client
/// and that if those children are deleted that they get properly removed from the expected entities list.
/// </summary>
/// <returns></returns>
[Test]
public async Task TestContainerExpectedEntityDeleted()
{
var server = StartServer();
var client = StartClient();
await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync());
var cEntManager = client.ResolveDependency<IEntityManager>();
var clientTime = client.ResolveDependency<IClientGameTiming>();
var clientNetManager = client.ResolveDependency<IClientNetManager>();
var sMapManager = server.ResolveDependency<IMapManager>();
var sEntManager = server.ResolveDependency<IEntityManager>();
var sPlayerManager = server.ResolveDependency<IPlayerManager>();
var serverTime = server.ResolveDependency<IGameTiming>();
Assert.DoesNotThrow(() => client.SetConnectTarget(server));
await client.WaitPost(() =>
{
clientNetManager.ClientConnect(null!, 0, null!);
});
for (int i = 0; i < 10; i++)
{
await server.WaitRunTicks(1);
await client.WaitRunTicks(1);
}
// Setup
MapId mapId;
var mapPos = MapCoordinates.Nullspace;
EntityUid sEntityUid = default!;
EntityUid sItemUid = default!;
NetEntity netEnt = default;
var cContainerSys = cEntManager.System<ContainerSystem>();
var sContainerSys = sEntManager.System<SharedContainerSystem>();
var sMetadataSys = sEntManager.System<MetaDataSystem>();
await server.WaitAssertion(() =>
{
sEntManager.System<SharedMapSystem>().CreateMap(out mapId);
mapPos = new MapCoordinates(new Vector2(0, 0), mapId);
sEntityUid = sEntManager.SpawnEntity(null, mapPos);
sMetadataSys.SetEntityName(sEntityUid, "Container");
sContainerSys.EnsureContainer<Container>(sEntityUid, "dummy");
// Setup PVS
sEntManager.AddComponent<EyeComponent>(sEntityUid);
var player = sPlayerManager.Sessions.First();
server.PlayerMan.SetAttachedEntity(player, sEntityUid);
sPlayerManager.JoinGame(player);
});
for (int i = 0; i < 10; i++)
{
await server.WaitRunTicks(1);
await client.WaitRunTicks(1);
}
await server.WaitAssertion(() =>
{
sItemUid = sEntManager.SpawnEntity(null, mapPos);
netEnt = sEntManager.GetNetEntity(sItemUid);
sMetadataSys.SetEntityName(sItemUid, "Item");
var container = sContainerSys.GetContainer(sEntityUid, "dummy");
sContainerSys.Insert(sItemUid, container);
// Modify visibility layer so that the item does not get sent ot the player
sEntManager.System<SharedVisibilitySystem>().AddLayer(sItemUid, 10 );
});
await server.WaitRunTicks(1);
while (clientTime.LastRealTick < serverTime.CurTick - 1)
{
await client.WaitRunTicks(1);
}
var cUid = cEntManager.GetEntity(sEntManager.GetNetEntity(sEntityUid));
await client.WaitAssertion(() =>
{
if (!cEntManager.TryGetComponent<ContainerManagerComponent>(cUid, out var containerManagerComp))
{
Assert.Fail();
return;
}
var container = cContainerSys.GetContainer(cUid, "dummy", containerManagerComp);
Assert.That(container.ContainedEntities.Count, Is.EqualTo(0));
Assert.That(container.ExpectedEntities.Count, Is.EqualTo(1));
Assert.That(cContainerSys.ExpectedEntities.ContainsKey(netEnt));
Assert.That(cContainerSys.ExpectedEntities.Count, Is.EqualTo(1));
});
await server.WaitAssertion(() =>
{
// If possible it'd be best to only have the DeleteEntity, but right now
// the entity deleted event is not played on the client if the entity does not exist on the client.
if (sEntManager.EntityExists(sItemUid)
// && itemUid.TryGetContainer(out var container))
&& sContainerSys.TryGetContainingContainer(sItemUid, out var container))
{
sContainerSys.Remove(sItemUid, container, force: true);
}
sEntManager.DeleteEntity(sItemUid);
});
await server.WaitRunTicks(1);
await client.WaitRunTicks(4);
await client.WaitAssertion(() =>
{
if (!cEntManager.TryGetComponent<ContainerManagerComponent>(cUid, out var containerManagerComp))
{
Assert.Fail();
return;
}
var container = cContainerSys.GetContainer(cUid, "dummy", containerManagerComp);
Assert.That(container.ContainedEntities.Count, Is.EqualTo(0));
Assert.That(container.ExpectedEntities.Count, Is.EqualTo(0));
Assert.That(!cContainerSys.ExpectedEntities.ContainsKey(netEnt));
Assert.That(cContainerSys.ExpectedEntities.Count, Is.EqualTo(0));
});
await client.WaitPost(() => clientNetManager.ClientDisconnect(""));
await server.WaitRunTicks(5);
await client.WaitRunTicks(5);
}
/// <summary>
/// Sets up a new container, initializes map, saves the map, then loads it again on another map. The contained entity should still
/// be inside the container.
/// </summary>
[Test]
public async Task Container_DeserializeGrid_IsStillContained()
{
var server = StartServer();
await Task.WhenAll(server.WaitIdleAsync());
var sEntManager = server.ResolveDependency<IEntityManager>();
var mapSys = sEntManager.System<SharedMapSystem>();
var sContainerSys = sEntManager.System<SharedContainerSystem>();
var sMetadataSys = sEntManager.System<MetaDataSystem>();
var path = new ResPath("container_test.yml");
await server.WaitAssertion(() =>
{
// build the map
sEntManager.System<SharedMapSystem>().CreateMap(out var mapIdOne);
Assert.That(mapSys.IsInitialized(mapIdOne), Is.True);
var containerEnt = sEntManager.SpawnEntity(null, new MapCoordinates(1, 1, mapIdOne));
sMetadataSys.SetEntityName(containerEnt, "ContainerEnt");
var containeeEnt = sEntManager.SpawnEntity(null, new MapCoordinates(2, 2, mapIdOne));
sMetadataSys.SetEntityName(containeeEnt, "ContaineeEnt");
var container = sContainerSys.MakeContainer<Container>(containerEnt, "testContainer");
container.OccludesLight = true;
container.ShowContents = true;
sContainerSys.Insert(containeeEnt, container);
// save the map
var mapLoader = sEntManager.EntitySysManager.GetEntitySystem<MapLoaderSystem>();
Assert.That(mapLoader.TrySaveMap(mapIdOne, path));
mapSys.DeleteMap(mapIdOne);
});
// A few moments later...
await server.WaitRunTicks(10);
await server.WaitAssertion(() =>
{
var mapLoader = sEntManager.System<MapLoaderSystem>();
// load the map
Assert.That(mapLoader.TryLoadMap(path, out var map, out _));
Assert.That(mapSys.IsInitialized(map), Is.True); // Map Initialize-ness is saved in the map file.
});
await server.WaitRunTicks(1);
await server.WaitAssertion(() =>
{
// verify container
Entity<ContainerManagerComponent> container = default;
var query = sEntManager.EntityQueryEnumerator<ContainerManagerComponent>();
while (query.MoveNext(out var uid, out var containerComp))
{
container = (uid, containerComp);
}
var containerEnt = container.Owner;
Assert.That(container.Comp, Is.Not.Null);
Assert.That(sEntManager.GetComponent<MetaDataComponent>(containerEnt).EntityName, Is.EqualTo("ContainerEnt"));
Assert.That(container.Comp!.Containers.ContainsKey("testContainer"));
var baseContainer = sContainerSys.GetContainer(containerEnt, "testContainer", container.Comp);
Assert.That(baseContainer.ContainedEntities, Has.Count.EqualTo(1));
var containeeEnt = baseContainer.ContainedEntities[0];
Assert.That(sEntManager.GetComponent<MetaDataComponent>(containeeEnt).EntityName, Is.EqualTo("ContaineeEnt"));
});
}
}
}

View File

@@ -0,0 +1,145 @@
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Reflection;
namespace Robust.UnitTesting.Shared.GameObjects;
internal sealed partial class DeferredEntityDeletionTest : RobustIntegrationTest
{
// This test ensures that deferred deletion can be used while handling events without issue, and that deleting an
// entity after deferring component removal doesn't cause any issues.
[Test]
public async Task TestDeferredEntityDeletion()
{
var options = new ServerIntegrationOptions();
options.Pool = false;
options.BeforeRegisterComponents += () =>
{
var fact = IoCManager.Resolve<IComponentFactory>();
fact.RegisterClass<DeferredDeletionTestComponent>();
fact.RegisterClass<OtherDeferredDeletionTestComponent>();
};
options.BeforeStart += () =>
{
var sysMan = IoCManager.Resolve<IEntitySystemManager>();
sysMan.LoadExtraSystemType<DeferredDeletionTestSystem>();
sysMan.LoadExtraSystemType<OtherDeferredDeletionTestSystem>();
};
var server = StartServer(options);
await server.WaitIdleAsync();
EntityUid uid1 = default, uid2 = default, uid3 = default, uid4 = default;
DeferredDeletionTestComponent comp1 = default!, comp2 = default!, comp3 = default!, comp4 = default!;
IEntityManager entMan = default!;
await server.WaitAssertion(() =>
{
var mapMan = IoCManager.Resolve<IMapManager>();
entMan = IoCManager.Resolve<IEntityManager>();
var sys = entMan.EntitySysManager.GetEntitySystem<DeferredDeletionTestSystem>();
uid1 = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
uid2 = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
uid3 = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
uid4 = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
comp1 = entMan.AddComponent<DeferredDeletionTestComponent>(uid1);
comp2 = entMan.AddComponent<DeferredDeletionTestComponent>(uid2);
comp3 = entMan.AddComponent<DeferredDeletionTestComponent>(uid3);
comp4 = entMan.AddComponent<DeferredDeletionTestComponent>(uid4);
entMan.AddComponent<OtherDeferredDeletionTestComponent>(uid1);
entMan.AddComponent<OtherDeferredDeletionTestComponent>(uid2);
entMan.AddComponent<OtherDeferredDeletionTestComponent>(uid3);
entMan.AddComponent<OtherDeferredDeletionTestComponent>(uid4);
});
await server.WaitRunTicks(1);
// first: test that deferring deletion while handling events doesn't cause issues
await server.WaitAssertion(() =>
{
Assert.That(comp1.Running);
var ev = new DeferredDeletionTestEvent();
entMan.EventBus.RaiseLocalEvent(uid1, ev);
Assert.That(comp1.LifeStage == ComponentLifeStage.Stopped);
});
await server.WaitRunTicks(1);
Assert.That(comp1.LifeStage == ComponentLifeStage.Deleted);
// next check that entity deletion doesn't cause issues:
await server.WaitAssertion(() =>
{
var ev = new DeferredDeletionTestEvent();
entMan.EventBus.RaiseLocalEvent(uid2, ev);
entMan.EventBus.RaiseLocalEvent(uid3, ev);
entMan.EventBus.RaiseLocalEvent(uid4, ev);
entMan.DeleteEntity(uid2);
entMan.QueueDeleteEntity(uid3);
entMan.TryQueueDeleteEntity(uid4);
Assert.That(entMan.Deleted(uid2));
Assert.That(!entMan.Deleted(uid3));
Assert.That(!entMan.Deleted(uid4));
Assert.That(comp2.LifeStage == ComponentLifeStage.Deleted);
Assert.That(comp3.LifeStage == ComponentLifeStage.Stopped);
Assert.That(comp4.LifeStage == ComponentLifeStage.Stopped);
});
await server.WaitRunTicks(1);
Assert.That(comp3.LifeStage == ComponentLifeStage.Deleted);
Assert.That(comp4.LifeStage == ComponentLifeStage.Deleted);
Assert.That(entMan.Deleted(uid3));
Assert.That(entMan.Deleted(uid4));
await server.WaitIdleAsync();
}
[Reflect(false)]
private sealed class DeferredDeletionTestSystem : EntitySystem
{
public override void Initialize()
{
SubscribeLocalEvent<DeferredDeletionTestComponent, DeferredDeletionTestEvent>(OnTestEvent);
}
private void OnTestEvent(EntityUid uid, DeferredDeletionTestComponent component, DeferredDeletionTestEvent args)
{
// remove both this component, and some other component that this entity has that also subscribes to this event.
RemCompDeferred<DeferredDeletionTestComponent>(uid);
RemCompDeferred<OtherDeferredDeletionTestComponent>(uid);
}
}
[Reflect(false)]
private sealed class OtherDeferredDeletionTestSystem : EntitySystem
{
public override void Initialize() => SubscribeLocalEvent<OtherDeferredDeletionTestComponent, DeferredDeletionTestEvent>(OnTestEvent);
private void OnTestEvent(EntityUid uid, OtherDeferredDeletionTestComponent component, DeferredDeletionTestEvent args)
{
// remove both this component, and some other component that this entity has that also subscribes to this event.
RemCompDeferred<DeferredDeletionTestComponent>(uid);
RemCompDeferred<OtherDeferredDeletionTestComponent>(uid);
}
}
[RegisterComponent]
[Reflect(false)]
private sealed partial class DeferredDeletionTestComponent : Component
{
}
[Reflect(false)]
private sealed partial class OtherDeferredDeletionTestComponent : Component
{
}
private sealed class DeferredDeletionTestEvent
{
}
}

View File

@@ -0,0 +1,270 @@
using System.Collections.Generic;
using Moq;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.Reflection;
using Robust.UnitTesting.Server;
namespace Robust.UnitTesting.Shared.GameObjects
{
internal sealed partial class EntityEventBusTests
{
[Test]
public void SubscribeCompEvent()
{
var (bus, sim, entUid, compInstance) = EntFactory();
var compFactory = sim.Resolve<IComponentFactory>();
// Subscribe
int calledCount = 0;
bus.SubscribeLocalEvent<MetaDataComponent, TestEvent>(HandleTestEvent);
bus.LockSubscriptions();
// add a component to the system
bus.OnEntityAdded(entUid);
var reg = compFactory.GetRegistration(CompIdx.Index<MetaDataComponent>());
bus.OnComponentAdded(new AddedComponentEventArgs(new ComponentEventArgs(compInstance, entUid), reg));
// Raise
var evntArgs = new TestEvent(5);
bus.RaiseLocalEvent(entUid, evntArgs, true);
// Assert
Assert.That(calledCount, Is.EqualTo(1));
void HandleTestEvent(EntityUid uid, MetaDataComponent component, TestEvent args)
{
calledCount++;
Assert.That(uid, Is.EqualTo(entUid));
Assert.That(component, Is.EqualTo(compInstance));
Assert.That(args.TestNumber, Is.EqualTo(5));
}
}
[Test]
public void UnsubscribeCompEvent()
{
var (bus, sim, entUid, compInstance) = EntFactory();
var compFactory = sim.Resolve<IComponentFactory>();
// Subscribe
int calledCount = 0;
bus.SubscribeLocalEvent<MetaDataComponent, TestEvent>(HandleTestEvent);
bus.UnsubscribeLocalEvent<MetaDataComponent, TestEvent>();
bus.LockSubscriptions();
// add a component to the system
bus.OnEntityAdded(entUid);
var reg = compFactory.GetRegistration(CompIdx.Index<MetaDataComponent>());
bus.OnComponentAdded(new AddedComponentEventArgs(new ComponentEventArgs(compInstance, entUid), reg));
// Raise
var evntArgs = new TestEvent(5);
bus.RaiseLocalEvent(entUid, evntArgs, true);
// Assert
Assert.That(calledCount, Is.EqualTo(0));
void HandleTestEvent(EntityUid uid, MetaDataComponent component, TestEvent args)
{
calledCount++;
}
}
[Test]
public void SubscribeCompLifeEvent()
{
var (bus, sim, entUid, compInstance) = EntFactory();
var entMan = sim.Resolve<EntityManager>();
var fact = sim.Resolve<IComponentFactory>();
// Subscribe
int calledCount = 0;
bus.SubscribeLocalEvent<MetaDataComponent, ComponentInit>(HandleTestEvent);
bus.LockSubscriptions();
// Raise
bus.RaiseComponentEvent(entUid, compInstance, new ComponentInit());
// Assert
Assert.That(calledCount, Is.EqualTo(1));
void HandleTestEvent(EntityUid uid, MetaDataComponent component, ComponentInit args)
{
calledCount++;
Assert.That(uid, Is.EqualTo(entUid));
Assert.That(component, Is.EqualTo(compInstance));
}
}
[Test]
public void CompEventOrdered()
{
var sim = RobustServerSimulation
.NewSimulation()
.RegisterComponents(f =>
{
f.RegisterClass<OrderAComponent>();
f.RegisterClass<OrderBComponent>();
f.RegisterClass<OrderCComponent>();
})
.InitializeInstance();
var entMan = sim.Resolve<EntityManager>();
var entUid = entMan.Spawn();
var instA = entMan.AddComponent<OrderAComponent>(entUid);
var instB = entMan.AddComponent<OrderBComponent>(entUid);
var instC = entMan.AddComponent<OrderCComponent>(entUid);
var bus = entMan.EventBusInternal;
bus.ClearSubscriptions();
var fact = sim.Resolve<IComponentFactory>();
// Subscribe
var a = false;
var b = false;
var c = false;
void HandlerA(EntityUid uid, Component comp, TestEvent ev)
{
Assert.That(b, Is.False, "A should run before B");
Assert.That(c, Is.False, "A should run before C");
a = true;
}
void HandlerB(EntityUid uid, Component comp, TestEvent ev)
{
Assert.That(c, Is.True, "B should run after C");
b = true;
}
void HandlerC(EntityUid uid, Component comp, TestEvent ev) => c = true;
bus.SubscribeLocalEvent<OrderAComponent, TestEvent>(HandlerA, typeof(OrderAComponent), before: new []{typeof(OrderBComponent), typeof(OrderCComponent)});
bus.SubscribeLocalEvent<OrderBComponent, TestEvent>(HandlerB, typeof(OrderBComponent), after: new []{typeof(OrderCComponent)});
bus.SubscribeLocalEvent<OrderCComponent, TestEvent>(HandlerC, typeof(OrderCComponent));
bus.LockSubscriptions();
// add a component to the system
bus.OnEntityAdded(entUid);
var regA = fact.GetRegistration(CompIdx.Index<OrderAComponent>());
var regB = fact.GetRegistration(CompIdx.Index<OrderBComponent>());
var regC = fact.GetRegistration(CompIdx.Index<OrderCComponent>());
bus.OnComponentAdded(new AddedComponentEventArgs(new ComponentEventArgs(instA, entUid), regA));
bus.OnComponentAdded(new AddedComponentEventArgs(new ComponentEventArgs(instB, entUid), regB));
bus.OnComponentAdded(new AddedComponentEventArgs(new ComponentEventArgs(instC, entUid), regC));
// Raise
var evntArgs = new TestEvent(5);
bus.RaiseLocalEvent(entUid, evntArgs, true);
// Assert
Assert.That(a, Is.True, "A did not fire");
Assert.That(b, Is.True, "B did not fire");
Assert.That(c, Is.True, "C did not fire");
}
[Test]
public void CompEventLoop()
{
var sim = RobustServerSimulation
.NewSimulation()
.RegisterComponents(f =>
{
f.RegisterClass<OrderAComponent>();
f.RegisterClass<OrderBComponent>();
})
.InitializeInstance();
var entMan = sim.Resolve<EntityManager>();
var entUid = entMan.Spawn();
var instA = entMan.AddComponent<OrderAComponent>(entUid);
var instB = entMan.AddComponent<OrderBComponent>(entUid);
var bus = entMan.EventBusInternal;
bus.ClearSubscriptions();
var fact = sim.Resolve<IComponentFactory>();
var regA = fact.GetRegistration(CompIdx.Index<OrderAComponent>());
var regB = fact.GetRegistration(CompIdx.Index<OrderBComponent>());
var handlerACount = 0;
void HandlerA(EntityUid uid, Component comp, TestEvent ev)
{
Assert.That(handlerACount, Is.EqualTo(0));
handlerACount++;
// add and then remove component B
bus.OnComponentRemoved(new RemovedComponentEventArgs(new ComponentEventArgs(instB, entUid), false, default!, CompIdx.Index<OrderBComponent>()));
bus.OnComponentAdded(new AddedComponentEventArgs(new ComponentEventArgs(instB, entUid), regB));
}
var handlerBCount = 0;
void HandlerB(EntityUid uid, Component comp, TestEvent ev)
{
Assert.That(handlerBCount, Is.EqualTo(0));
handlerBCount++;
// add and then remove component A
bus.OnComponentRemoved(new RemovedComponentEventArgs(new ComponentEventArgs(instA, entUid), false, default!, CompIdx.Index<OrderAComponent>()));
bus.OnComponentAdded(new AddedComponentEventArgs(new ComponentEventArgs(instA, entUid), regA));
}
bus.SubscribeLocalEvent<OrderAComponent, TestEvent>(HandlerA, typeof(OrderAComponent));
bus.SubscribeLocalEvent<OrderBComponent, TestEvent>(HandlerB, typeof(OrderBComponent));
bus.LockSubscriptions();
// add a component to the system
bus.OnEntityAdded(entUid);
bus.OnComponentAdded(new AddedComponentEventArgs(new ComponentEventArgs(instA, entUid), regA));
bus.OnComponentAdded(new AddedComponentEventArgs(new ComponentEventArgs(instB, entUid), regB));
// Event subscriptions currently use a linked list.
// Currently expect event subscriptions to be raised in order: handlerB -> handlerA
// If a component gets removed and added again, it gets moved back to the front of the linked list.
// I.e., adding and then removing compA changes the linked list order: handlerA -> handlerB
//
// This could in principle cause the event raising code to enter an infinite loop.
// Adding and removing a comp in an event handler may seem silly but:
// - it doesn't have to be the same component if you had a chain of three or more components
// - some event handlers raise other events and can lead to convoluted chains of interactions that might inadvertently trigger something like this.
// Raise
bus.RaiseLocalEvent(entUid, new TestEvent(0));
// Assert
Assert.That(handlerACount, Is.LessThanOrEqualTo(1));
Assert.That(handlerBCount, Is.LessThanOrEqualTo(1));
Assert.That(handlerACount+handlerBCount, Is.GreaterThan(0));
}
private sealed partial class DummyComponent : Component
{
}
private sealed partial class OrderAComponent : Component
{
}
private sealed partial class OrderBComponent : Component
{
}
private sealed partial class OrderCComponent : Component
{
}
private sealed class TestEvent : EntityEventArgs
{
public int TestNumber { get; }
public TestEvent(int testNumber)
{
TestNumber = testNumber;
}
}
}
}

View File

@@ -0,0 +1,75 @@
using System.Collections.Generic;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Reflection;
using Robust.UnitTesting.Server;
namespace Robust.UnitTesting.Shared.GameObjects;
internal sealed partial class EntityEventBusTests
{
// Explanation of what bug this is testing:
// Because event ordering is keyed on system type, we have a problem.
// If you register to a directed event like FooEvent twice for different components,
// you now have different subscriptions with the same key.
//
// To trigger this, at least one subscription to this event (possibly another system entirely)
// needs to demand some ordering calculation to happen.
[Test]
public void TestDifferentComponentsOrderedSameKeySub()
{
var simulation = RobustServerSimulation
.NewSimulation()
.RegisterEntitySystems(factory =>
{
factory.LoadExtraSystemType<DifferentComponentsSameKeySubSystem>();
factory.LoadExtraSystemType<DifferentComponentsSameKeySubSystem2>();
})
.RegisterComponents(factory => factory.RegisterClass<FooComponent>())
.InitializeInstance();
var map = simulation.CreateMap().MapId;
var entity = simulation.SpawnEntity(null, new MapCoordinates(0, 0, map));
simulation.Resolve<IEntityManager>().AddComponent<FooComponent>(entity);
var foo = new FooEvent();
simulation.Resolve<IEntityManager>().EventBus.RaiseLocalEvent(entity, foo, true);
Assert.That(foo.EventOrder, Is.EquivalentTo(new[]{"Foo", "Transform", "Metadata"}).Or.EquivalentTo(new[]{"Foo", "Metadata", "Transform"}));
}
[Reflect(false)]
private sealed class DifferentComponentsSameKeySubSystem : EntitySystem
{
public override void Initialize()
{
SubscribeLocalEvent<TransformComponent, FooEvent>((_, _, e) => { e.EventOrder.Add("Transform"); });
SubscribeLocalEvent<MetaDataComponent, FooEvent>((_, _, e) => { e.EventOrder.Add("Metadata"); });
}
}
[Reflect(false)]
private sealed class DifferentComponentsSameKeySubSystem2 : EntitySystem
{
public override void Initialize()
{
SubscribeLocalEvent<FooComponent, FooEvent>(
(_, _, e) => e.EventOrder.Add("Foo"),
before: new[] {typeof(DifferentComponentsSameKeySubSystem)});
}
}
[Reflect(false)]
private sealed partial class FooComponent : Component
{
}
private sealed class FooEvent
{
public List<string> EventOrder = new();
}
}

View File

@@ -0,0 +1,149 @@
using System;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.Reflection;
using Robust.UnitTesting.Server;
namespace Robust.UnitTesting.Shared.GameObjects
{
[TestFixture]
internal sealed partial class EntityEventBusTests
{
[Test]
public void SubscribeCompRefBroadcastEvent()
{
// Arrange.
var simulation = RobustServerSimulation
.NewSimulation()
.RegisterEntitySystems(factory => factory.LoadExtraSystemType<SubscribeCompRefBroadcastSystem>())
.InitializeInstance();
var ev = new TestStructEvent() {TestNumber = 5};
simulation.Resolve<IEntityManager>().EventBus.RaiseEvent(EventSource.Local, ref ev);
Assert.That(ev.TestNumber, Is.EqualTo(15));
}
[Reflect(false)]
internal sealed class SubscribeCompRefBroadcastSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<TestStructEvent>(OnTestEvent);
}
private void OnTestEvent(ref TestStructEvent ev)
{
Assert.That(ev.TestNumber, Is.EqualTo(5));
ev.TestNumber += 10;
}
}
[Test]
public void SubscriptionNoMixedRefValueBroadcastEvent()
{
// Arrange.
var simulation = RobustServerSimulation
.NewSimulation()
.RegisterEntitySystems(factory =>
factory.LoadExtraSystemType<SubscriptionNoMixedRefValueBroadcastEventSystem>());
// Act. No mixed ref and value subscriptions are allowed.
Assert.Throws(typeof(InvalidOperationException), () => simulation.InitializeInstance());
}
[Reflect(false)]
private sealed class SubscriptionNoMixedRefValueBroadcastEventSystem : EntitySystem
{
public override void Initialize()
{
// The below is not allowed, as you're subscribing by-ref and by-value to the same event...
#pragma warning disable RA0013
SubscribeLocalEvent<TestStructEvent>(MyRefHandler);
SubscribeLocalEvent<TestStructEvent>(MyValueHandler);
#pragma warning restore RA0013
}
private void MyValueHandler(TestStructEvent args) { }
private void MyRefHandler(ref TestStructEvent args) { }
}
[Test]
public void SortedBroadcastRefEvents()
{
// Arrange.
var simulation = RobustServerSimulation
.NewSimulation()
.RegisterEntitySystems(factory =>
{
factory.LoadExtraSystemType<BroadcastOrderASystem>();
factory.LoadExtraSystemType<BroadcastOrderBSystem>();
factory.LoadExtraSystemType<BroadcastOrderCSystem>();
})
.InitializeInstance();
// Act.
var testEvent = new TestStructEvent {TestNumber = 5};
var eventBus = simulation.Resolve<IEntityManager>().EventBus;
eventBus.RaiseEvent(EventSource.Local, ref testEvent);
// Check that the entity systems changed the value correctly
Assert.That(testEvent.TestNumber, Is.EqualTo(15));
}
[Reflect(false)]
private sealed class BroadcastOrderASystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<TestStructEvent>(OnA, new[]{typeof(BroadcastOrderBSystem)}, new[]{typeof(BroadcastOrderCSystem)});
}
private void OnA(ref TestStructEvent args)
{
// Second handler being ran.
Assert.That(args.TestNumber, Is.EqualTo(0));
args.TestNumber = 10;
}
}
[Reflect(false)]
private sealed class BroadcastOrderBSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<TestStructEvent>(OnB, null, new []{typeof(BroadcastOrderASystem)});
}
private void OnB(ref TestStructEvent args)
{
// Last handler being ran.
Assert.That(args.TestNumber, Is.EqualTo(10));
args.TestNumber = 15;
}
}
[Reflect(false)]
private sealed class BroadcastOrderCSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<TestStructEvent>(OnC);
}
private void OnC(ref TestStructEvent args)
{
// First handler being ran.
Assert.That(args.TestNumber, Is.EqualTo(5));
args.TestNumber = 0;
}
}
}
}

View File

@@ -0,0 +1,166 @@
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Reflection;
using Robust.UnitTesting.Server;
namespace Robust.UnitTesting.Shared.GameObjects
{
internal sealed partial class EntityEventBusTests
{
[Test]
public void SubscribeCompRefDirectedEvent()
{
// Arrange.
var simulation = RobustServerSimulation
.NewSimulation()
.RegisterComponents(factory => factory.RegisterClass<DummyComponent>())
.RegisterEntitySystems(factory => factory.LoadExtraSystemType<SubscribeCompRefDirectedEventSystem>())
.InitializeInstance();
var map = simulation.CreateMap().MapId;
var entity = simulation.SpawnEntity(null, new MapCoordinates(0, 0, map));
IoCManager.Resolve<IEntityManager>().AddComponent<DummyComponent>(entity);
// Act.
var testEvent = new TestStructEvent {TestNumber = 5};
var eventBus = simulation.Resolve<IEntityManager>().EventBus;
eventBus.RaiseLocalEvent(entity, ref testEvent, true);
// Check that the entity system changed the value correctly
Assert.That(testEvent.TestNumber, Is.EqualTo(10));
}
[Reflect(false)]
private sealed class SubscribeCompRefDirectedEventSystem : EntitySystem
{
public override void Initialize()
{
SubscribeLocalEvent<DummyComponent, TestStructEvent>(MyRefHandler);
}
private void MyRefHandler(EntityUid uid, DummyComponent component, ref TestStructEvent args)
{
Assert.That(args.TestNumber, Is.EqualTo(5));
args.TestNumber = 10;
}
}
[Reflect(false)]
private sealed class SubscriptionNoMixedRefValueDirectedEventSystem : EntitySystem
{
public override void Initialize()
{
// The below is not allowed, as you're subscribing by-ref and by-value to the same event...
SubscribeLocalEvent<DummyComponent, TestStructEvent>(MyRefHandler);
#pragma warning disable RA0013
SubscribeLocalEvent<DummyTwoComponent, TestStructEvent>(MyValueHandler);
#pragma warning restore RA0013
}
private void MyValueHandler(EntityUid uid, DummyTwoComponent component, TestStructEvent args) { }
private void MyRefHandler(EntityUid uid, DummyComponent component, ref TestStructEvent args) { }
}
[Test]
public void SortedDirectedRefEvents()
{
// Arrange.
var simulation = RobustServerSimulation
.NewSimulation()
.RegisterComponents(factory =>
{
factory.RegisterClass<OrderAComponent>();
factory.RegisterClass<OrderBComponent>();
factory.RegisterClass<OrderCComponent>();
})
.RegisterEntitySystems(factory =>
{
factory.LoadExtraSystemType<OrderASystem>();
factory.LoadExtraSystemType<OrderBSystem>();
factory.LoadExtraSystemType<OrderCSystem>();
})
.InitializeInstance();
var map = simulation.CreateMap().MapId;
var entity = simulation.SpawnEntity(null, new MapCoordinates(0, 0, map));
IoCManager.Resolve<IEntityManager>().AddComponent<OrderAComponent>(entity);
IoCManager.Resolve<IEntityManager>().AddComponent<OrderBComponent>(entity);
IoCManager.Resolve<IEntityManager>().AddComponent<OrderCComponent>(entity);
// Act.
var testEvent = new TestStructEvent {TestNumber = 5};
var eventBus = simulation.Resolve<IEntityManager>().EventBus;
eventBus.RaiseLocalEvent(entity, ref testEvent, true);
// Check that the entity systems changed the value correctly
Assert.That(testEvent.TestNumber, Is.EqualTo(15));
}
[Reflect(false)]
private sealed class OrderASystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<OrderAComponent, TestStructEvent>(OnA, new[]{typeof(OrderBSystem)}, new[]{typeof(OrderCSystem)});
}
private void OnA(EntityUid uid, OrderAComponent component, ref TestStructEvent args)
{
// Second handler being ran.
Assert.That(args.TestNumber, Is.EqualTo(0));
args.TestNumber = 10;
}
}
[Reflect(false)]
private sealed class OrderBSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<OrderBComponent, TestStructEvent>(OnB, null, new []{typeof(OrderASystem)});
}
private void OnB(EntityUid uid, OrderBComponent component, ref TestStructEvent args)
{
// Last handler being ran.
Assert.That(args.TestNumber, Is.EqualTo(10));
args.TestNumber = 15;
}
}
[Reflect(false)]
private sealed class OrderCSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<OrderCComponent, TestStructEvent>(OnC);
}
private void OnC(EntityUid uid, OrderCComponent component, ref TestStructEvent args)
{
// First handler being ran.
Assert.That(args.TestNumber, Is.EqualTo(5));
args.TestNumber = 0;
}
}
private sealed partial class DummyTwoComponent : Component
{
}
[ByRefEvent]
private struct TestStructEvent
{
public int TestNumber;
}
}
}

View File

@@ -0,0 +1,14 @@
using System;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Reflection;
using Robust.UnitTesting.Server;
namespace Robust.UnitTesting.Shared.GameObjects
{
internal sealed partial class EntityEventBusTests
{
}
}

View File

@@ -0,0 +1,508 @@
using System;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.UnitTesting.Server;
namespace Robust.UnitTesting.Shared.GameObjects
{
[TestFixture, Parallelizable, TestOf(typeof(EntityEventBus))]
internal sealed partial class EntityEventBusTests
{
private static (EntityEventBus Bus, ISimulation Sim, EntityUid Uid, MetaDataComponent Comp) EntFactory()
{
var sim = RobustServerSimulation.NewSimulation().InitializeInstance();
var entMan = sim.Resolve<EntityManager>();
var uid = entMan.Spawn();
var comp = entMan.MetaQuery.Comp(uid);
var bus = entMan.EventBusInternal;
bus.ClearSubscriptions();
return (bus, sim, uid, comp);
}
private static EntityEventBus BusFactory()
{
return EntFactory().Bus;
}
/// <summary>
/// Trying to subscribe a null handler causes a <see cref="ArgumentNullException"/> to be thrown.
/// </summary>
[Test]
public void SubscribeEvent_NullHandler_NullArgumentException()
{
// Arrange
var bus = BusFactory();
var subscriber = new TestEventSubscriber();
// Act
void Code() => bus.SubscribeEvent(EventSource.Local, subscriber, (EntityEventHandler<TestEventArgs>) null!);
//Assert
Assert.Throws<ArgumentNullException>(Code);
}
/// <summary>
/// Trying to subscribe with a null subscriber causes a <see cref="ArgumentNullException"/> to be thrown.
/// </summary>
[Test]
public void SubscribeEvent_NullSubscriber_NullArgumentException()
{
// Arrange
var bus = BusFactory();
// Act
void Code() => bus.SubscribeEvent<TestEventArgs>(EventSource.Local, null!, ev => {});
//Assert: this should do nothing
Assert.Throws<ArgumentNullException>(Code);
}
/// <summary>
/// Duplicate Event subscriptions are not allowed.
/// </summary>
[Test]
public void SubscribeEvent_DuplicateSubscription_Invalid()
{
// Arrange
var bus = BusFactory();
var subscriber = new TestEventSubscriber();
int delegateCallCount = 0;
void Handler(TestEventArgs ev) => delegateCallCount++;
// 2 subscriptions 1 handler
bus.SubscribeEvent<TestEventArgs>(EventSource.Local, subscriber, Handler);
Assert.Throws<InvalidOperationException>(() => bus.SubscribeEvent<TestEventArgs>(EventSource.Local, subscriber, Handler));
}
// TODO if ever duplicate events are allowed, re-enable these tests.
/*
/// <summary>
/// Unlike C# events, the set of event handler delegates is unique.
/// Subscribing the same delegate multiple times will only call the handler once.
/// </summary>
[Test]
public void SubscribeEvent_DuplicateSubscription_RaisedOnce()
{
// Arrange
var bus = BusFactory();
var subscriber = new TestEventSubscriber();
int delegateCallCount = 0;
void Handler(TestEventArgs ev) => delegateCallCount++;
// 2 subscriptions 1 handler
bus.SubscribeEvent<TestEventArgs>(EventSource.Local, subscriber, Handler);
bus.SubscribeEvent<TestEventArgs>(EventSource.Local, subscriber, Handler);
// Act
bus.RaiseEvent(EventSource.Local, new TestEventArgs());
//Assert
Assert.That(delegateCallCount, Is.EqualTo(1));
}
/// <summary>
/// Subscribing two different delegates to a single event type causes both events
/// to be raised in an indeterminate order.
/// </summary>
[Test]
public void SubscribeEvent_MultipleDelegates_BothRaised()
{
// Arrange
var bus = BusFactory();
var subscriber = new TestEventSubscriber();
int delFooCount = 0;
int delBarCount = 0;
bus.SubscribeEvent<TestEventArgs>(EventSource.Local, subscriber, ev => delFooCount++);
bus.SubscribeEvent<TestEventArgs>(EventSource.Local, subscriber, ev => delBarCount++);
// Act
bus.RaiseEvent(EventSource.Local, new TestEventArgs());
// Assert
Assert.That(delFooCount, Is.EqualTo(1));
Assert.That(delBarCount, Is.EqualTo(1));
}
*/
/// <summary>
/// A subscriber's handlers are properly called only when the specified event type is raised.
/// </summary>
[Test]
public void SubscribeEvent_MultipleSubscriptions_IndividuallyCalled()
{
// Arrange
var bus = BusFactory();
var subscriber = new TestEventSubscriber();
int delFooCount = 0;
int delBarCount = 0;
bus.SubscribeEvent<TestEventArgs>(EventSource.Local, subscriber, ev => delFooCount++);
bus.SubscribeEvent<TestEventTwoArgs>(EventSource.Local, subscriber, ev => delBarCount++);
bus.LockSubscriptions();
// Act & Assert
bus.RaiseEvent(EventSource.Local, new TestEventArgs());
Assert.That(delFooCount, Is.EqualTo(1));
Assert.That(delBarCount, Is.EqualTo(0));
delFooCount = delBarCount = 0;
bus.RaiseEvent(EventSource.Local, new TestEventTwoArgs());
Assert.That(delFooCount, Is.EqualTo(0));
Assert.That(delBarCount, Is.EqualTo(1));
}
/// <summary>
/// Trying to subscribe with <see cref="EventSource.None"/> makes no sense and causes
/// a <see cref="ArgumentOutOfRangeException"/> to be thrown.
/// </summary>
[Test]
public void SubscribeEvent_SourceNone_ArgOutOfRange()
{
// Arrange
var bus = BusFactory();
var subscriber = new TestEventSubscriber();
void TestEventHandler(TestEventArgs args) { }
// Act
void Code() => bus.SubscribeEvent(EventSource.None, subscriber, (EntityEventHandler<TestEventArgs>)TestEventHandler);
//Assert
Assert.Throws<ArgumentOutOfRangeException>(Code);
}
/// <summary>
/// Unsubscribing a handler twice does nothing.
/// </summary>
[Test]
public void UnsubscribeEvent_DoubleUnsubscribe_Nop()
{
// Arrange
var bus = BusFactory();
var subscriber = new TestEventSubscriber();
void Handler(TestEventArgs ev) { }
bus.SubscribeEvent<TestEventArgs>(EventSource.Local, subscriber, Handler);
bus.UnsubscribeEvent<TestEventArgs>(EventSource.Local, subscriber);
bus.LockSubscriptions();
// Act
bus.UnsubscribeEvent<TestEventArgs>(EventSource.Local, subscriber);
// Assert: Does not throw
}
/// <summary>
/// Unsubscribing a handler that was never subscribed in the first place does nothing.
/// </summary>
[Test]
public void UnsubscribeEvent_NoSubscription_Nop()
{
// Arrange
var bus = BusFactory();
var subscriber = new TestEventSubscriber();
// Act
bus.UnsubscribeEvent<TestEventArgs>(EventSource.Local, subscriber);
// Assert: Does not throw
}
/// <summary>
/// Trying to unsubscribe with a null subscriber causes a <see cref="ArgumentNullException"/> to be thrown.
/// </summary>
[Test]
public void UnsubscribeEvent_NullSubscriber_NullArgumentException()
{
// Arrange
var bus = BusFactory();
// Act
void Code() => bus.UnsubscribeEvent<TestEventArgs>(EventSource.Local, null!);
// Assert
Assert.Throws<ArgumentNullException>(Code);
}
/// <summary>
/// An event cannot be subscribed to with <see cref="EventSource.None"/>, so trying to unsubscribe
/// with an <see cref="EventSource.None"/> causes a <see cref="ArgumentOutOfRangeException"/> to be thrown.
/// </summary>
[Test]
public void UnsubscribeEvent_SourceNone_ArgOutOfRange()
{
// Arrange
var bus = BusFactory();
var subscriber = new TestEventSubscriber();
// Act
void Code() => bus.UnsubscribeEvent<TestEventArgs>(EventSource.None, subscriber);
// Assert
Assert.Throws<ArgumentOutOfRangeException>(Code);
}
/// <summary>
/// Raising an event with no handlers subscribed to it does nothing.
/// </summary>
[Test]
public void RaiseEvent_NoSubscriptions_Nop()
{
// Arrange
var bus = BusFactory();
var subscriber = new TestEventSubscriber();
int delCalledCount = 0;
bus.SubscribeEvent<TestEventTwoArgs>(EventSource.Local, subscriber, ev => delCalledCount++);
bus.LockSubscriptions();
// Act
bus.RaiseEvent(EventSource.Local, new TestEventArgs());
// Assert
Assert.That(delCalledCount, Is.EqualTo(0));
}
/// <summary>
/// Raising an event when a handler has been unsubscribed no longer calls the handler.
/// </summary>
[Test]
public void RaiseEvent_Unsubscribed_Nop()
{
// Arrange
var bus = BusFactory();
var subscriber = new TestEventSubscriber();
int delCallCount = 0;
void Handler(TestEventArgs ev) => delCallCount++;
bus.SubscribeEvent<TestEventArgs>(EventSource.Local, subscriber, Handler);
bus.UnsubscribeEvent<TestEventArgs>(EventSource.Local, subscriber);
bus.LockSubscriptions();
// Act
bus.RaiseEvent(EventSource.Local, new TestEventArgs());
// Assert
Assert.That(delCallCount, Is.EqualTo(0));
}
/// <summary>
/// Trying to raise an event with <see cref="EventSource.None"/> makes no sense and causes
/// a <see cref="ArgumentOutOfRangeException"/> to be thrown.
/// </summary>
[Test]
public void RaiseEvent_SourceNone_ArgOutOfRange()
{
// Arrange
var bus = BusFactory();
// Act
void Code() => bus.RaiseEvent(EventSource.None, new TestEventArgs());
// Assert
Assert.Throws<ArgumentOutOfRangeException>(Code);
}
/// <summary>
/// Trying to unsubscribe all of a null subscriber's events causes a <see cref="ArgumentNullException"/> to be thrown.
/// </summary>
[Test]
public void UnsubscribeEvents_NullSubscriber_NullArgumentException()
{
// Arrange
var bus = BusFactory();
// Act
void Code() => bus.UnsubscribeEvents(null!);
// Assert
Assert.Throws<ArgumentNullException>(Code);
}
/// <summary>
/// Unsubscribing a subscriber with no subscriptions does nothing.
/// </summary>
[Test]
public void UnsubscribeEvents_NoSubscriptions_Nop()
{
// Arrange
var bus = BusFactory();
var subscriber = new TestEventSubscriber();
// Act
bus.UnsubscribeEvents(subscriber);
// Assert: no exception
}
/// <summary>
/// The subscriber's handlers are not raised after they are unsubscribed.
/// </summary>
[Test]
public void UnsubscribeEvents_UnsubscribedHandler_Nop()
{
// Arrange
var bus = BusFactory();
var subscriber = new TestEventSubscriber();
int delCallCount = 0;
void Handler(TestEventArgs ev) => delCallCount++;
bus.SubscribeEvent<TestEventArgs>(EventSource.Local, subscriber, Handler);
bus.UnsubscribeEvents(subscriber);
bus.LockSubscriptions();
// Act
bus.RaiseEvent(EventSource.Local, new TestEventArgs());
// Assert
Assert.That(delCallCount, Is.EqualTo(0));
}
/// <summary>
/// Trying to queue a null event causes a <see cref="ArgumentNullException"/> to be thrown.
/// </summary>
[Test]
public void QueueEvent_NullEvent_ArgumentNullException()
{
// Arrange
var bus = BusFactory();
// Act
void Code() => bus.QueueEvent(EventSource.Local, null!);
// Assert
Assert.Throws<ArgumentNullException>(Code);
}
/// <summary>
/// Queuing an event does not immediately raise the event unless the queue is processed.
/// </summary>
[Test]
public void QueueEvent_EventQueued_DoesNotImmediatelyRaise()
{
// Arrange
var bus = BusFactory();
var subscriber = new TestEventSubscriber();
int delCallCount = 0;
void Handler(TestEventArgs ev) => delCallCount++;
bus.SubscribeEvent<TestEventArgs>(EventSource.Local, subscriber, Handler);
// Act
bus.QueueEvent(EventSource.Local, new TestEventArgs());
// Assert
Assert.That(delCallCount, Is.EqualTo(0));
}
/// <summary>
/// Trying to queue an event with <see cref="EventSource.None"/> makes no sense and causes
/// a <see cref="ArgumentOutOfRangeException"/> to be thrown.
/// </summary>
[Test]
public void QueueEvent_SourceNone_ArgOutOfRange()
{
// Arrange
var bus = BusFactory();
// Act
void Code() => bus.QueueEvent(EventSource.None, new TestEventArgs());
// Assert
Assert.Throws<ArgumentOutOfRangeException>(Code);
}
/// <summary>
/// Queued events are raised when the queue is processed.
/// </summary>
[Test]
public void ProcessQueue_EventQueued_HandlerRaised()
{
// Arrange
var bus = BusFactory();
var subscriber = new TestEventSubscriber();
int delCallCount = 0;
void Handler(TestEventArgs ev) => delCallCount++;
bus.SubscribeEvent<TestEventArgs>(EventSource.Local, subscriber, Handler);
bus.LockSubscriptions();
bus.QueueEvent(EventSource.Local, new TestEventArgs());
// Act
bus.ProcessEventQueue();
// Assert
Assert.That(delCallCount, Is.EqualTo(1));
}
[Test]
public void RaiseEvent_Ordered()
{
// Arrange
var bus = BusFactory();
// Expected order is A -> C -> B
var a = false;
var b = false;
var c = false;
void HandlerA(TestEventArgs ev)
{
Assert.That(b, Is.False, "A should run before B");
Assert.That(c, Is.False, "A should run before C");
a = true;
}
void HandlerB(TestEventArgs ev)
{
Assert.That(c, Is.True, "B should run after C");
b = true;
}
void HandlerC(TestEventArgs ev) => c = true;
bus.SubscribeEvent<TestEventArgs>(EventSource.Local, new SubA(), HandlerA, typeof(SubA), before: new []{typeof(SubB), typeof(SubC)});
bus.SubscribeEvent<TestEventArgs>(EventSource.Local, new SubB(), HandlerB, typeof(SubB), after: new []{typeof(SubC)});
bus.SubscribeEvent<TestEventArgs>(EventSource.Local, new SubC(), HandlerC, typeof(SubC));
bus.LockSubscriptions();
// Act
bus.RaiseEvent(EventSource.Local, new TestEventArgs());
// Assert
Assert.That(a, Is.True, "A did not fire");
Assert.That(b, Is.True, "B did not fire");
Assert.That(c, Is.True, "C did not fire");
}
internal sealed class SubA : IEntityEventSubscriber
{
}
internal sealed class SubB : IEntityEventSubscriber
{
}
internal sealed class SubC : IEntityEventSubscriber
{
}
}
internal sealed class TestEventSubscriber : IEntityEventSubscriber { }
internal sealed class TestEventArgs : EntityEventArgs { }
internal sealed class TestEventTwoArgs : EntityEventArgs { }
}

View File

@@ -0,0 +1,168 @@
using System.Numerics;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.UnitTesting.Server;
namespace Robust.UnitTesting.Shared.GameObjects;
[TestFixture]
internal sealed partial class EntityManagerCopyTests
{
[Test]
public void CopyComponentGeneric()
{
var instant = RobustServerSimulation.NewSimulation();
instant.RegisterComponents(fac =>
{
fac.RegisterClass<AComponent>();
});
var sim = instant.InitializeInstance();
var entManager = sim.Resolve<IEntityManager>();
var mapSystem = entManager.System<SharedMapSystem>();
mapSystem.CreateMap(out var mapId);
var original = entManager.Spawn(null, new MapCoordinates(Vector2.Zero, mapId));
var comp = entManager.AddComponent<AComponent>(original);
Assert.That(comp.Value, Is.EqualTo(false));
comp.Value = true;
var target = entManager.Spawn(null, new MapCoordinates(Vector2.Zero, mapId));
Assert.That(!entManager.HasComponent<AComponent>(target));
var targetComp = entManager.CopyComponent(original, target, comp);
Assert.That(entManager.GetComponent<AComponent>(target), Is.EqualTo(targetComp));
Assert.That(targetComp.Value, Is.EqualTo(comp.Value));
Assert.That(!ReferenceEquals(comp, targetComp));
}
[Test]
public void CopyComponentNonGeneric()
{
var instant = RobustServerSimulation.NewSimulation();
instant.RegisterComponents(fac =>
{
fac.RegisterClass<AComponent>();
});
var sim = instant.InitializeInstance();
var entManager = sim.Resolve<IEntityManager>();
var mapSystem = entManager.System<SharedMapSystem>();
mapSystem.CreateMap(out var mapId);
var original = entManager.Spawn(null, new MapCoordinates(Vector2.Zero, mapId));
var comp = entManager.AddComponent<AComponent>(original);
Assert.That(comp.Value, Is.EqualTo(false));
comp.Value = true;
var target = entManager.Spawn(null, new MapCoordinates(Vector2.Zero, mapId));
Assert.That(!entManager.HasComponent<AComponent>(target));
var targetComp = entManager.CopyComponent(original, target, (IComponent) comp);
Assert.That(entManager.GetComponent<AComponent>(target), Is.EqualTo(targetComp));
Assert.That(((AComponent) targetComp).Value, Is.EqualTo(comp.Value));
Assert.That(!ReferenceEquals(comp, targetComp));
}
[Test]
public void CopyComponentMultiple()
{
var instant = RobustServerSimulation.NewSimulation();
instant.RegisterComponents(fac =>
{
fac.RegisterClass<AComponent>();
fac.RegisterClass<BComponent>();
});
var sim = instant.InitializeInstance();
var entManager = sim.Resolve<IEntityManager>();
var mapSystem = entManager.System<SharedMapSystem>();
mapSystem.CreateMap(out var mapId);
var original = entManager.Spawn(null, new MapCoordinates(Vector2.Zero, mapId));
var comp = entManager.AddComponent<AComponent>(original);
var comp2 = entManager.AddComponent<BComponent>(original);
Assert.That(comp.Value, Is.EqualTo(false));
comp.Value = true;
var target = entManager.Spawn(null, new MapCoordinates(Vector2.Zero, mapId));
Assert.That(!entManager.HasComponent<AComponent>(target));
entManager.CopyComponents(original, target, null, comp, comp2);
var targetComp = entManager.GetComponent<AComponent>(target);
var targetComp2 = entManager.GetComponent<BComponent>(target);
Assert.That(entManager.GetComponent<AComponent>(target), Is.EqualTo(targetComp));
Assert.That(targetComp.Value, Is.EqualTo(comp.Value));
Assert.That(entManager.GetComponent<BComponent>(target), Is.EqualTo(targetComp2));
Assert.That(targetComp2.Value, Is.EqualTo(comp2.Value));
Assert.That(!ReferenceEquals(comp, targetComp));
Assert.That(!ReferenceEquals(comp2, targetComp2));
}
[Test]
public void CopyComponentMultipleViaTry()
{
var instant = RobustServerSimulation.NewSimulation();
instant.RegisterComponents(fac =>
{
fac.RegisterClass<AComponent>();
fac.RegisterClass<BComponent>();
});
var sim = instant.InitializeInstance();
var entManager = sim.Resolve<IEntityManager>();
var mapSystem = entManager.System<SharedMapSystem>();
mapSystem.CreateMap(out var mapId);
var original = entManager.Spawn(null, new MapCoordinates(Vector2.Zero, mapId));
var comp = entManager.AddComponent<AComponent>(original);
var comp2 = entManager.AddComponent<BComponent>(original);
Assert.That(comp.Value, Is.EqualTo(false));
comp.Value = true;
var target = entManager.Spawn(null, new MapCoordinates(Vector2.Zero, mapId));
Assert.That(!entManager.HasComponent<AComponent>(target));
entManager.TryCopyComponents(original, target, null, comp.GetType(), comp2.GetType());
var targetComp = entManager.GetComponent<AComponent>(target);
var targetComp2 = entManager.GetComponent<BComponent>(target);
Assert.That(entManager.GetComponent<AComponent>(target), Is.EqualTo(targetComp));
Assert.That(targetComp.Value, Is.EqualTo(comp.Value));
Assert.That(entManager.GetComponent<BComponent>(target), Is.EqualTo(targetComp2));
Assert.That(targetComp2.Value, Is.EqualTo(comp2.Value));
Assert.That(!ReferenceEquals(comp, targetComp));
Assert.That(!ReferenceEquals(comp2, targetComp2));
}
[DataDefinition]
private sealed partial class AComponent : Component
{
[DataField]
public bool Value = false;
}
[DataDefinition]
private sealed partial class BComponent : Component
{
[DataField]
public bool Value = false;
}
}

View File

@@ -0,0 +1,373 @@
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager;
using Robust.UnitTesting.Server;
namespace Robust.UnitTesting.Shared.GameObjects
{
[TestFixture, Parallelizable ,TestOf(typeof(EntityManager))]
internal sealed partial class EntityManager_Components_Tests
{
private const string DummyLoadId = "DummyLoad";
private const string DummyLoad = $@"
- type: entity
id: {DummyLoadId}
name: weh
components:
- type: Joint
- type: Physics
";
[Test]
public void AddRegistryComponentTest()
{
var sim = RobustServerSimulation
.NewSimulation()
.RegisterPrototypes(fac => fac.LoadString(DummyLoad))
.InitializeInstance();
var entMan = sim.Resolve<IEntityManager>();
var protoManager = sim.Resolve<IPrototypeManager>();
var map = sim.CreateMap().Uid;
var coords = new EntityCoordinates(map, default);
var entity = entMan.SpawnEntity(null, coords);
Assert.That(!entMan.HasComponent<PhysicsComponent>(entity));
var proto = protoManager.Index<EntityPrototype>(DummyLoadId);
entMan.AddComponents(entity, proto);
Assert.Multiple(() =>
{
Assert.That(entMan.HasComponent<JointComponent>(entity));
Assert.That(entMan.HasComponent<PhysicsComponent>(entity));
});
}
[Test]
public void RemoveRegistryComponentTest()
{
var sim = RobustServerSimulation
.NewSimulation()
.RegisterPrototypes(fac => fac.LoadString(DummyLoad))
.InitializeInstance();
var entMan = sim.Resolve<IEntityManager>();
var protoManager = sim.Resolve<IPrototypeManager>();
var map = sim.CreateMap().Uid;
var coords = new EntityCoordinates(map, default);
var entity = entMan.SpawnEntity(DummyLoadId, coords);
var proto = protoManager.Index<EntityPrototype>(DummyLoadId);
entMan.RemoveComponents(entity, proto);
Assert.Multiple(() =>
{
Assert.That(!entMan.HasComponent<JointComponent>(entity));
Assert.That(!entMan.HasComponent<PhysicsComponent>(entity));
});
}
[Test]
public void AddComponentTest()
{
// Arrange
var (sim, coords) = SimulationFactory();
var entMan = sim.Resolve<IEntityManager>();
var entity = entMan.SpawnEntity(null, coords);
var component = new DummyComponent();
// Act
entMan.AddComponent(entity, component);
// Assert
var result = entMan.GetComponent<DummyComponent>(entity);
Assert.That(result, Is.EqualTo(component));
}
[Test]
public void AddComponentOverwriteTest()
{
// Arrange
var (sim, coords) = SimulationFactory();
var entMan = sim.Resolve<IEntityManager>();
var entity = entMan.SpawnEntity(null, coords);
var component = new DummyComponent();
// Act
entMan.AddComponent(entity, component, true);
// Assert
var result = entMan.GetComponent<DummyComponent>(entity);
Assert.That(result, Is.EqualTo(component));
}
[Test]
public void AddComponent_ExistingDeleted()
{
// Arrange
var (sim, coords) = SimulationFactory();
var entMan = sim.Resolve<IEntityManager>();
var entity = entMan.SpawnEntity(null, coords);
var firstComp = new DummyComponent();
entMan.AddComponent(entity, firstComp);
entMan.RemoveComponent<DummyComponent>(entity);
var secondComp = new DummyComponent();
// Act
entMan.AddComponent(entity, secondComp);
// Assert
var result = entMan.GetComponent<DummyComponent>(entity);
Assert.That(result, Is.EqualTo(secondComp));
}
[Test]
public void HasComponentTest()
{
// Arrange
var (sim, coords) = SimulationFactory();
var entMan = sim.Resolve<IEntityManager>();
var entity = entMan.SpawnEntity(null, coords);
entMan.AddComponent<DummyComponent>(entity);
// Act
var result = entMan.HasComponent<DummyComponent>(entity);
// Assert
Assert.That(result, Is.True);
}
[Test]
public void HasComponentNoGenericTest()
{
// Arrange
var (sim, coords) = SimulationFactory();
var entMan = sim.Resolve<IEntityManager>();
var entity = entMan.SpawnEntity(null, coords);
entMan.AddComponent<DummyComponent>(entity);
// Act
var result = entMan.HasComponent(entity, typeof(DummyComponent));
// Assert
Assert.That(result, Is.True);
}
[Test]
public void HasNetComponentTest()
{
// Arrange
var (sim, coords) = SimulationFactory();
var factory = sim.Resolve<IComponentFactory>();
var netId = factory.GetRegistration<DummyComponent>().NetID!;
var entMan = sim.Resolve<IEntityManager>();
var entity = entMan.SpawnEntity(null, coords);
entMan.AddComponent<DummyComponent>(entity);
// Act
var result = entMan.HasComponent(entity, netId.Value);
// Assert
Assert.That(result, Is.True);
}
[Test]
public void GetNetComponentTest()
{
// Arrange
var (sim, coords) = SimulationFactory();
var factory = sim.Resolve<IComponentFactory>();
var netId = factory.GetRegistration<DummyComponent>().NetID!;
var entMan = sim.Resolve<IEntityManager>();
var entity = entMan.SpawnEntity(null, coords);
var component = entMan.AddComponent<DummyComponent>(entity);
// Act
var result = entMan.GetComponent(entity, netId.Value);
// Assert
Assert.That(result, Is.EqualTo(component));
}
[Test]
public void TryGetComponentTest()
{
// Arrange
var (sim, coords) = SimulationFactory();
var entMan = sim.Resolve<IEntityManager>();
var entity = entMan.SpawnEntity(null, coords);
var component = entMan.AddComponent<DummyComponent>(entity);
// Act
var result = entMan.TryGetComponent<DummyComponent>(entity, out var comp);
// Assert
Assert.That(result, Is.True);
Assert.That(comp, Is.EqualTo(component));
}
[Test]
public void TryGetNetComponentTest()
{
// Arrange
var (sim, coords) = SimulationFactory();
var factory = sim.Resolve<IComponentFactory>();
var netId = factory.GetRegistration<DummyComponent>().NetID!;
var entMan = sim.Resolve<IEntityManager>();
var entity = entMan.SpawnEntity(null, coords);
var component = entMan.AddComponent<DummyComponent>(entity);
// Act
var result = entMan.TryGetComponent(entity, netId.Value, out var comp);
// Assert
Assert.That(result, Is.True);
Assert.That(comp, Is.EqualTo(component));
}
[Test]
public void RemoveComponentTest()
{
// Arrange
var (sim, coords) = SimulationFactory();
var entMan = sim.Resolve<IEntityManager>();
var entity = entMan.SpawnEntity(null, coords);
var component = entMan.AddComponent<DummyComponent>(entity);
// Act
entMan.RemoveComponent<DummyComponent>(entity);
entMan.CullRemovedComponents();
// Assert
Assert.That(entMan.HasComponent(entity, component.GetType()), Is.False);
}
[Test]
public void EnsureQueuedComponentDeletion()
{
var (sim, coords) = SimulationFactory();
var entMan = sim.Resolve<IEntityManager>();
var entity = entMan.SpawnEntity(null, coords);
var component = entMan.AddComponent<DummyComponent>(entity);
Assert.That(component.LifeStage, Is.LessThanOrEqualTo(ComponentLifeStage.Running));
entMan.RemoveComponentDeferred(entity, component);
Assert.That(component.LifeStage, Is.EqualTo(ComponentLifeStage.Stopped));
Assert.That(entMan.EnsureComponent<DummyComponent>(entity, out var comp2), Is.False);
Assert.That(comp2.LifeStage, Is.LessThanOrEqualTo(ComponentLifeStage.Running));
Assert.That(component.LifeStage, Is.EqualTo(ComponentLifeStage.Deleted));
}
[Test]
public void RemoveNetComponentTest()
{
// Arrange
var (sim, coords) = SimulationFactory();
var factory = sim.Resolve<IComponentFactory>();
var netId = factory.GetRegistration<DummyComponent>().NetID!;
var entMan = sim.Resolve<IEntityManager>();
var entity = entMan.SpawnEntity(null, coords);
var component = entMan.AddComponent<DummyComponent>(entity);
// Act
entMan.RemoveComponent(entity, netId.Value);
entMan.CullRemovedComponents();
// Assert
Assert.That(entMan.HasComponent(entity, component.GetType()), Is.False);
}
[Test]
public void GetComponentsTest()
{
// Arrange
var (sim, coords) = SimulationFactory();
var entMan = sim.Resolve<IEntityManager>();
var entity = entMan.SpawnEntity(null, coords);
var component = entMan.AddComponent<DummyComponent>(entity);
// Act
var result = entMan.GetComponents<DummyComponent>(entity);
// Assert
var list = result.ToList();
Assert.That(list.Count, Is.EqualTo(1));
Assert.That(list[0], Is.EqualTo(component));
}
[Test]
public void GetAllComponentsTest()
{
// Arrange
var (sim, coords) = SimulationFactory();
var entMan = sim.Resolve<IEntityManager>();
var entity = entMan.SpawnEntity(null, coords);
var component = entMan.AddComponent<DummyComponent>(entity);
// Act
var result = entMan.EntityQuery<DummyComponent>(true);
// Assert
var list = result.ToList();
Assert.That(list.Count, Is.EqualTo(1));
Assert.That(list[0], Is.EqualTo(component));
}
[Test]
public void GetAllComponentInstances()
{
// Arrange
var (sim, coords) = SimulationFactory();
var entMan = sim.Resolve<IEntityManager>();
var fac = sim.Resolve<IComponentFactory>();
var entity = entMan.SpawnEntity(null, coords);
var component = entMan.AddComponent<DummyComponent>(entity);
// Act
var result = entMan.GetComponents(entity);
// Assert
var list = result.Where(c=>fac.GetComponentName(c.GetType()) == "Dummy").ToList();
Assert.That(list.Count, Is.EqualTo(1));
Assert.That(list[0], Is.EqualTo(component));
}
private static (ISimulation, EntityCoordinates) SimulationFactory()
{
var sim = RobustServerSimulation
.NewSimulation()
.RegisterComponents(factory => factory.RegisterClass<DummyComponent>())
.InitializeInstance();
var map = sim.CreateMap().Uid;
var coords = new EntityCoordinates(map, default);
return (sim, coords);
}
[NetworkedComponent()]
private sealed partial class DummyComponent : Component, ICompType1, ICompType2
{
}
private interface ICompType1 { }
private interface ICompType2 { }
}
}

View File

@@ -0,0 +1,85 @@
using System;
using System.IO;
using Moq;
using NUnit.Framework;
using Robust.Server.Configuration;
using Robust.Server.Reflection;
using Robust.Server.Serialization;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Network;
using Robust.Shared.Profiling;
using Robust.Shared.Reflection;
using Robust.Shared.Replays;
using Robust.Shared.Serialization;
using Robust.Shared.Timing;
namespace Robust.UnitTesting.Shared.GameObjects
{
[TestFixture, Serializable]
sealed class EntityState_Tests
{
/// <summary>
/// Used to measure the size of <see cref="object"/>s in bytes. This is not actually a test,
/// but a useful benchmark tool, so i'm leaving it here.
/// </summary>
[Test]
public void ComponentChangedSerialized()
{
var container = new DependencyCollection();
container.Register<ILogManager, LogManager>();
container.Register<IConfigurationManager, ServerNetConfigurationManager>();
container.Register<IConfigurationManagerInternal, ServerNetConfigurationManager>();
container.Register<INetManager, NetManager>();
container.Register<IHWId, DummyHWId>();
container.Register<IReflectionManager, ServerReflectionManager>();
container.Register<IRobustSerializer, ServerRobustSerializer>();
container.Register<IRobustMappedStringSerializer, RobustMappedStringSerializer>();
container.Register<IAuthManager, AuthManager>();
container.Register<IGameTiming, GameTiming>();
container.Register<ProfManager, ProfManager>();
container.Register<HttpClientHolder>();
container.RegisterInstance<IReplayRecordingManager>(new Mock<IReplayRecordingManager>().Object);
container.BuildGraph();
var cfg = container.Resolve<IConfigurationManagerInternal>();
cfg.Initialize(true);
cfg.LoadCVarsFromAssembly(typeof(IConfigurationManager).Assembly);
container.Resolve<IReflectionManager>().LoadAssemblies(AppDomain.CurrentDomain.GetAssemblyByName("Robust.Shared"));
IoCManager.InitThread(container, replaceExisting: true);
cfg.LoadCVarsFromAssembly(typeof(IConfigurationManager).Assembly); // Robust.Shared
container.Resolve<INetManager>().Initialize(true);
var serializer = container.Resolve<IRobustSerializer>();
serializer.Initialize();
IoCManager.Resolve<IRobustMappedStringSerializer>().LockStrings();
byte[] array;
using(var stream = new MemoryStream())
{
var payload = new EntityState(
new NetEntity(64),
new []
{
new ComponentChange(0, new MapGridComponentDeltaState(16, chunkData: null, default), default)
}, default);
serializer.Serialize(stream, payload);
array = stream.ToArray();
}
IoCManager.Clear();
Assert.Pass($"Size in Bytes: {array.Length.ToString()}");
}
}
}

View File

@@ -0,0 +1,137 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Moq;
using NUnit.Framework;
using Robust.Server.Configuration;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.Exceptions;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Network;
using Robust.Shared.Profiling;
using Robust.Shared.Reflection;
using Robust.Shared.Replays;
using Robust.Shared.Timing;
namespace Robust.UnitTesting.Shared.GameObjects
{
[TestFixture]
[TestOf(typeof(EntitySystemManager))]
internal sealed class EntitySystemManagerOrderTest
{
private sealed class Counter
{
public int X;
}
[Reflect(false)]
private abstract class TestSystemBase : IEntitySystem
{
public Counter? Counter;
public int LastUpdate;
public virtual IEnumerable<Type> UpdatesAfter => Enumerable.Empty<Type>();
public virtual IEnumerable<Type> UpdatesBefore => Enumerable.Empty<Type>();
public bool UpdatesOutsidePrediction => true;
public void Initialize() { }
public void Shutdown() { }
public void Update(float frameTime)
{
LastUpdate = Counter!.X++;
}
public void FrameUpdate(float frameTime) { }
}
// Expected update order is is A -> D -> C -> B
[Reflect(false)]
private sealed class TestSystemA : TestSystemBase
{
}
[Reflect(false)]
private sealed class TestSystemB : TestSystemBase
{
public override IEnumerable<Type> UpdatesAfter => new[] {typeof(TestSystemA)};
}
[Reflect(false)]
private sealed class TestSystemC : TestSystemBase
{
public override IEnumerable<Type> UpdatesBefore => new[] {typeof(TestSystemB)};
}
[Reflect(false)]
private sealed class TestSystemD : TestSystemBase
{
public override IEnumerable<Type> UpdatesAfter => new[] {typeof(TestSystemA)};
public override IEnumerable<Type> UpdatesBefore => new[] {typeof(TestSystemC)};
}
[Test]
public void Test()
{
var deps = new DependencyCollection();
deps.Register<IRuntimeLog, RuntimeLog>();
deps.Register<ILogManager, LogManager>();
deps.Register<IGameTiming, GameTiming>();
deps.RegisterInstance<INetManager>(new Mock<INetManager>().Object);
deps.Register<IConfigurationManager, ServerNetConfigurationManager>();
deps.Register<IServerNetConfigurationManager, ServerNetConfigurationManager>();
deps.Register<ProfManager, ProfManager>();
deps.Register<IDynamicTypeFactory, DynamicTypeFactory>();
deps.Register<IDynamicTypeFactoryInternal, DynamicTypeFactory>();
deps.RegisterInstance<IModLoader>(new Mock<IModLoader>().Object);
deps.Register<IEntitySystemManager, EntitySystemManager>();
deps.RegisterInstance<IEntityManager>(new Mock<IEntityManager>().Object);
// WHEN WILL THE SUFFERING END
deps.RegisterInstance<IReplayRecordingManager>(new Mock<IReplayRecordingManager>().Object);
var reflectionMock = new Mock<IReflectionManager>();
reflectionMock.Setup(p => p.GetAllChildren<IEntitySystem>(false))
.Returns(new[]
{
typeof(TestSystemA),
typeof(TestSystemB),
typeof(TestSystemC),
typeof(TestSystemD),
});
deps.RegisterInstance<IReflectionManager>(reflectionMock.Object);
deps.BuildGraph();
IoCManager.InitThread(deps, true);
var systems = deps.Resolve<IEntitySystemManager>();
systems.Initialize();
var counter = new Counter();
systems.GetEntitySystem<TestSystemA>().Counter = counter;
systems.GetEntitySystem<TestSystemB>().Counter = counter;
systems.GetEntitySystem<TestSystemC>().Counter = counter;
systems.GetEntitySystem<TestSystemD>().Counter = counter;
systems.TickUpdate(1, noPredictions: false);
Assert.That(counter.X, Is.EqualTo(4));
Assert.That(systems.GetEntitySystem<TestSystemA>().LastUpdate, Is.EqualTo(0));
Assert.That(systems.GetEntitySystem<TestSystemB>().LastUpdate, Is.EqualTo(3));
Assert.That(systems.GetEntitySystem<TestSystemC>().LastUpdate, Is.EqualTo(2));
Assert.That(systems.GetEntitySystem<TestSystemD>().LastUpdate, Is.EqualTo(1));
}
[TearDown]
public void TearDown()
{
IoCManager.Clear();
}
}
}

View File

@@ -0,0 +1,107 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using Robust.Shared.Analyzers;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.IoC.Exceptions;
namespace Robust.UnitTesting.Shared.GameObjects
{
[TestFixture, TestOf(typeof(EntitySystemManager))]
internal sealed class EntitySystemManager_Tests: OurRobustUnitTest
{
public abstract class ESystemBase : IEntitySystem
{
public virtual IEnumerable<Type> UpdatesAfter => Enumerable.Empty<Type>();
public virtual IEnumerable<Type> UpdatesBefore => Enumerable.Empty<Type>();
public bool UpdatesOutsidePrediction => true;
public void Initialize() { }
public void Shutdown() { }
public void Update(float frameTime) { }
public void FrameUpdate(float frameTime) { }
}
[Virtual]
public class ESystemA : ESystemBase { }
internal sealed class ESystemC : ESystemA { }
public abstract class ESystemBase2 : ESystemBase { }
internal sealed class ESystemB : ESystemBase2 { }
internal sealed class ESystemDepA : ESystemBase
{
[Dependency] public readonly ESystemDepB ESystemDepB = default!;
}
internal sealed class ESystemDepB : ESystemBase
{
[Dependency] public readonly ESystemDepA ESystemDepA = default!;
}
/*
ESystemBase (Abstract)
- ESystemA
- ESystemC
- EsystemBase2 (Abstract)
- ESystemB
*/
[OneTimeSetUp]
public void Setup()
{
var syssy = IoCManager.Resolve<IEntitySystemManager>();
syssy.Clear();
syssy.LoadExtraSystemType<ESystemA>();
syssy.LoadExtraSystemType<ESystemB>();
syssy.LoadExtraSystemType<ESystemC>();
syssy.LoadExtraSystemType<ESystemDepA>();
syssy.LoadExtraSystemType<ESystemDepB>();
syssy.Initialize(false);
}
[Test]
public void GetsByTypeOrSupertype()
{
var esm = IoCManager.Resolve<IEntitySystemManager>();
// getting type by the exact type should work fine
Assert.That(esm.GetEntitySystem<ESystemB>(), Is.TypeOf<ESystemB>());
// getting type by an abstract supertype should work fine
// because there are no other subtypes of that supertype it would conflict with
// it should return the only concrete subtype
Assert.That(esm.GetEntitySystem<ESystemBase2>(), Is.TypeOf<ESystemB>());
// getting ESystemA type by its exact type should work fine,
// even though EsystemC is a subtype - it should return an instance of ESystemA
var esysA = esm.GetEntitySystem<ESystemA>();
Assert.That(esysA, Is.TypeOf<ESystemA>());
Assert.That(esysA, Is.Not.TypeOf<ESystemC>());
var esysC = esm.GetEntitySystem<ESystemC>();
Assert.That(esysC, Is.TypeOf<ESystemC>());
// this should not work - it's abstract and there are multiple
// concrete subtypes
Assert.Throws<UnregisteredTypeException>(() =>
{
esm.GetEntitySystem<ESystemBase>();
});
}
[Test]
public void DependencyTest()
{
var esm = IoCManager.Resolve<IEntitySystemManager>();
var sysA = esm.GetEntitySystem<ESystemDepA>();
var sysB = esm.GetEntitySystem<ESystemDepB>();
Assert.That(sysA.ESystemDepB, Is.EqualTo(sysB));
Assert.That(sysB.ESystemDepA, Is.EqualTo(sysA));
}
}
}

View File

@@ -0,0 +1,226 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Robust.UnitTesting.Shared.GameObjects;
internal sealed class GenericEntityPrint
{
//[Test]
public void Print()
{
// Using the test framework for things it was not meant for is my passion
//
// Its pretty fucked that just occasionally running this manually is so much easier than setting up a porper
// source generator.
var i = 8;
IEnumerable<string> Generics(int n, bool nullable, bool forceIncludeNumber = false)
{
for (var j = 1; j <= n; j++)
{
var jStr = n == 1 && !forceIncludeNumber ? string.Empty : j.ToString();
yield return $"T{jStr}{(nullable ? "?" : string.Empty)}";
}
}
IEnumerable<string> PartiallyNullableGenerics(int n, int notNullCount)
{
bool nullable;
for (var j = 1; j <= n; j++)
{
nullable = j > notNullCount;
var jStr = n == 1 ? string.Empty : j.ToString();
yield return $"T{jStr}{(nullable ? "?" : string.Empty)}";
}
}
var structs = new StringBuilder();
var constraints = new StringBuilder();
var fields = new StringBuilder();
var parameters = new StringBuilder();
var asserts = new StringBuilder();
var assignments = new StringBuilder();
var tupleParameters = new StringBuilder();
var tupleAccess = new StringBuilder();
var entityAccess = new StringBuilder();
var selfAccess = new StringBuilder();
var entityNumberedAccess = new StringBuilder();
var defaults = new StringBuilder();
var compOperators = new StringBuilder();
var deConstructorParameters = new StringBuilder();
var deConstructorAccess = new StringBuilder();
var partialTupleCasts = new StringBuilder();
var partialEntityCasts = new StringBuilder();
var entitySubCast = new StringBuilder();
var castRegion = new StringBuilder();
for (var j = 1; j <= i; j++)
{
constraints.Clear();
fields.Clear();
parameters.Clear();
asserts.Clear();
assignments.Clear();
tupleParameters.Clear();
tupleAccess.Clear();
entityAccess.Clear();
selfAccess.Clear();
entityNumberedAccess.Clear();
defaults.Clear();
compOperators.Clear();
deConstructorParameters.Clear();
deConstructorAccess.Clear();
partialTupleCasts.Clear();
partialEntityCasts.Clear();
entitySubCast.Clear();
castRegion.Clear();
var generics = string.Join(", ", Generics(j, false));
var nullableGenerics = string.Join(", ", Generics(j, true));
for (var k = 1; k <= j; k++)
{
var kStr = j == 1 ? string.Empty : k.ToString();
fields.AppendLine($" public T{kStr} Comp{kStr};");
constraints.Append($"where T{kStr} : IComponent? ");
parameters.Append($", T{kStr} comp{kStr}");
asserts.AppendLine($" DebugTools.AssertOwner(owner, comp{kStr});");
assignments.AppendLine($" Comp{kStr} = comp{kStr};");
tupleParameters.Append($", T{kStr} Comp{kStr}");
tupleAccess.Append($", tuple.Comp{kStr}");
var suffix = (j >= 2 && k == 1) ? string.Empty : kStr;
var prefix = (j >= 2 && k == 2) ? "1" : string.Empty;
entityAccess.Append($"{prefix}, ent.Comp{suffix}");
selfAccess.Append($"{prefix}, Comp{suffix}");
entityNumberedAccess.Append($", ent.Comp{kStr}");
defaults.Append(", default");
compOperators.AppendLine($$"""
public static implicit operator T{{kStr}}(Entity<{{generics}}> ent)
{
return ent.Comp{{kStr}};
}
""");
deConstructorParameters.Append($", out T{kStr} comp{kStr}");
deConstructorAccess.AppendLine($" comp{kStr} = Comp{kStr};");
if (k == j)
continue;
// Cast a (EntityUid, T1) tuple to an Entity<T1, T2?>
// We could also casts for going from a (Uid, T2) tuple to a Entity<T1?, T2> but once we get to 4 or
// more components there are just too many combinations and I CBF writing the code to generate all those.
var partiallyNullableGenerics = string.Join(", ", PartiallyNullableGenerics(j, k));
var defaultArgs = string.Concat(Enumerable.Repeat(", default", j-k));
partialTupleCasts.Append($$"""
public static implicit operator Entity<{{partiallyNullableGenerics}}>((EntityUid Owner{{tupleParameters}}) tuple)
{
return new Entity<{{partiallyNullableGenerics}}>(tuple.Owner{{tupleAccess}}{{defaultArgs}});
}
""");
// Cast an Entity<T1> to an Entity<T1, T2?>
// As with the tuple casts, we could in principle generate more here.
var subGenerics = string.Join(", ", Generics(k, false, true));
partialEntityCasts.Append($$"""
public static implicit operator Entity<{{partiallyNullableGenerics}}>(Entity<{{subGenerics}}> ent)
{
return new Entity<{{partiallyNullableGenerics}}>(ent.Owner{{entityAccess}}{{defaultArgs}});
}
""");
// Cast an Entity<T1, T2> to an Entity<T1/2>
entitySubCast.Append($$"""
public static implicit operator Entity<{{subGenerics}}>(Entity<{{generics}}> ent)
{
return new Entity<{{subGenerics}}>(ent.Owner{{entityNumberedAccess}});
}
""");
}
if (j == 2)
{
castRegion.Append($$"""
{{partialTupleCasts.ToString().TrimEnd()}}
{{partialEntityCasts.ToString().TrimEnd()}}
{{entitySubCast.ToString().TrimEnd()}}
""");
}
else if (j > 2)
{
castRegion.Append($$"""
#region Partial Tuple Casts
{{partialTupleCasts}}
#endregion
#region Partial Entity Casts
{{partialEntityCasts}}
#endregion
#region Entity Sub casts
{{entitySubCast}}
#endregion
""");
}
structs.Append($$"""
[NotYamlSerializable]
public record struct Entity<{{generics}}> : IFluentEntityUid, IAsType<EntityUid>
{{constraints.ToString().TrimEnd()}}
{
public EntityUid Owner;
{{fields.ToString().TrimEnd()}}
readonly EntityUid IFluentEntityUid.FluentOwner => Owner;
public Entity(EntityUid owner{{parameters}})
{
{{asserts}}
Owner = owner;
{{assignments.ToString().TrimEnd()}}
}
public static implicit operator Entity<{{generics}}>((EntityUid Owner{{tupleParameters}}) tuple)
{
return new Entity<{{generics}}>(tuple.Owner{{tupleAccess}});
}
public static implicit operator Entity<{{nullableGenerics}}>(EntityUid owner)
{
return new Entity<{{nullableGenerics}}>(owner{{defaults}});
}
public static implicit operator EntityUid(Entity<{{generics}}> ent)
{
return ent.Owner;
}
{{compOperators.ToString().TrimEnd()}}
public readonly void Deconstruct(out EntityUid owner{{deConstructorParameters}})
{
owner = Owner;
{{deConstructorAccess.ToString().TrimEnd()}}
}
{{castRegion}}
public override readonly int GetHashCode() => Owner.GetHashCode();
public readonly Entity<{{nullableGenerics}}> AsNullable() => new(Owner{{selfAccess}});
public readonly EntityUid AsType() => Owner;
}
""");
}
Console.WriteLine(structs);
}
}

View File

@@ -0,0 +1,52 @@
using System.Numerics;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.UnitTesting.Server;
namespace Robust.UnitTesting.Shared.GameObjects
{
[TestFixture, Parallelizable]
sealed partial class EntityManagerTests
{
private static ISimulation SimulationFactory()
{
var sim = RobustServerSimulation
.NewSimulation()
.InitializeInstance();
return sim;
}
/// <summary>
/// The entity prototype can define field on the TransformComponent, just like any other component.
/// </summary>
[Test]
public void SpawnEntity_PrototypeTransform_Works()
{
var sim = SimulationFactory();
var map = sim.CreateMap().MapId;
var entMan = sim.Resolve<IEntityManager>();
var newEnt = entMan.SpawnEntity(null, new MapCoordinates(0, 0, map));
Assert.That(newEnt, Is.Not.EqualTo(EntityUid.Invalid));
}
[Test]
public void ComponentCount_Works()
{
var sim = RobustServerSimulation.NewSimulation().InitializeInstance();
var entManager = sim.Resolve<IEntityManager>();
var mapSystem = entManager.System<SharedMapSystem>();
Assert.That(entManager.Count<TransformComponent>(), Is.EqualTo(0));
var mapId = sim.CreateMap().MapId;
Assert.That(entManager.Count<TransformComponent>(), Is.EqualTo(1));
mapSystem.DeleteMap(mapId);
Assert.That(entManager.Count<TransformComponent>(), Is.EqualTo(0));
}
}
}

View File

@@ -0,0 +1,521 @@
using System.Linq;
using System.Numerics;
using NUnit.Framework;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Reflection;
using Robust.UnitTesting.Server;
namespace Robust.UnitTesting.Shared.GameObjects.Systems
{
[TestFixture, Parallelizable]
internal sealed partial class AnchoredSystemTests
{
private const string Prototypes = @"
- type: entity
name: anchoredEnt
id: anchoredEnt
components:
- type: Transform
anchored: true";
private static (ISimulation, Entity<MapGridComponent> grid, MapCoordinates, SharedTransformSystem xformSys, SharedMapSystem mapSys) SimulationFactory()
{
var sim = RobustServerSimulation
.NewSimulation()
.RegisterEntitySystems(f => f.LoadExtraSystemType<MoveEventTestSystem>())
.RegisterPrototypes(f=>
{
f.LoadString(Prototypes);
})
.InitializeInstance();
var mapManager = sim.Resolve<IMapManager>();
var testMapId = sim.CreateMap().MapId;
var coords = new MapCoordinates(new Vector2(7, 7), testMapId);
// Add grid 1, as the default grid to anchor things to.
var grid = mapManager.CreateGridEntity(testMapId);
return (sim, grid, coords, sim.System<SharedTransformSystem>(), sim.System<SharedMapSystem>());
}
// An entity is anchored to the tile it is over on the target grid.
// An entity is anchored by setting the flag on the transform.
// An anchored entity is defined as an entity with the TransformComponent.Anchored flag set.
// The Anchored field is used for serialization of anchored state.
// TODO: The grid SnapGrid functions are internal, expose the query functions to content.
// PhysicsComponent.BodyType is not able to be changed by content.
/// <summary>
/// When an entity is anchored to a grid tile, it's world position is centered on the tile.
/// Otherwise you can anchor an entity to a tile without the entity actually being on top of the tile.
/// This movement will trigger a MoveEvent.
/// </summary>
[Test]
public void OnAnchored_WorldPosition_TileCenter()
{
var (sim, grid, coordinates, xformSys, mapSys) = SimulationFactory();
// can only be anchored to a tile
mapSys.SetTile(grid, mapSys.TileIndicesFor(grid, coordinates), new Tile(1));
var ent1 = sim.SpawnEntity(null, coordinates); // this raises MoveEvent, subscribe after
// Act
sim.System<MoveEventTestSystem>().ResetCounters();
xformSys.AnchorEntity(ent1);
Assert.That(xformSys.GetWorldPosition(ent1), Is.EqualTo(new Vector2(7.5f, 7.5f))); // centered on tile
sim.System<MoveEventTestSystem>().AssertMoved(false);
}
[ComponentProtoName("AnchorOnInit")]
[Reflect(false)]
private sealed partial class AnchorOnInitComponent : Component;
[Reflect(false)]
private sealed class AnchorOnInitTestSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AnchorOnInitComponent, ComponentInit>((e, _, _) => Transform(e).Anchored = true);
}
}
[Reflect(false)]
internal sealed class MoveEventTestSystem : EntitySystem
{
[Dependency] private readonly SharedTransformSystem _transform = default!;
public override void Initialize()
{
base.Initialize();
_transform.OnGlobalMoveEvent += OnMove;
SubscribeLocalEvent<EntParentChangedMessage>(OnReparent);
}
public override void Shutdown()
{
base.Shutdown();
_transform.OnGlobalMoveEvent -= OnMove;
}
public bool FailOnMove;
public int MoveCounter;
public int ParentCounter;
private void OnMove(ref MoveEvent ev)
{
MoveCounter++;
if (FailOnMove)
Assert.Fail($"Move event was raised");
}
private void OnReparent(ref EntParentChangedMessage ev)
{
ParentCounter++;
if (FailOnMove)
Assert.Fail($"Move event was raised");
}
public void ResetCounters()
{
ParentCounter = 0;
MoveCounter = 0;
}
public void AssertMoved(bool parentChanged = true)
{
if (parentChanged)
Assert.That(ParentCounter, Is.EqualTo(1));
Assert.That(MoveCounter, Is.EqualTo(1));
}
}
/// <summary>
/// Ensures that if an entity gets added to lookups when anchored during init by some system.
/// </summary>
/// <remarks>
/// See space-wizards/RobustToolbox/issues/3444
/// </remarks>
[Test]
public void OnInitAnchored_AddedToLookup()
{
var sim = RobustServerSimulation
.NewSimulation()
.RegisterEntitySystems(f => f.LoadExtraSystemType<AnchorOnInitTestSystem>())
.RegisterComponents(f => f.RegisterClass<AnchorOnInitComponent>())
.InitializeInstance();
var mapSys = sim.System<SharedMapSystem>();
var entMan = sim.Resolve<IEntityManager>();
var mapMan = sim.Resolve<IMapManager>();
var mapId = sim.CreateMap().MapId;
var grid = mapMan.CreateGridEntity(mapId);
var coordinates = new MapCoordinates(new Vector2(7, 7), mapId);
var pos = mapSys.TileIndicesFor(grid, coordinates);
mapSys.SetTile(grid, pos, new Tile(1));
var ent1 = entMan.SpawnEntity(null, coordinates);
Assert.That(sim.Transform(ent1).Anchored, Is.False);
Assert.That(!mapSys.GetAnchoredEntities(grid, pos).Any());
entMan.DeleteEntity(ent1);
var ent2 = entMan.CreateEntityUninitialized(null, coordinates);
entMan.AddComponent<AnchorOnInitComponent>(ent2);
entMan.InitializeAndStartEntity(ent2);
Assert.That(sim.Transform(ent2).Anchored);
Assert.That(mapSys.GetAnchoredEntities(grid, pos).Count(), Is.EqualTo(1));
Assert.That(mapSys.GetAnchoredEntities(grid, pos).Contains(ent2));
}
/// <summary>
/// When an entity is anchored to a grid tile, it's parent is set to the grid.
/// </summary>
[Test]
public void OnAnchored_Parent_SetToGrid()
{
var (sim, grid, coordinates, xformSys, mapSys) = SimulationFactory();
// can only be anchored to a tile
mapSys.SetTile(grid, mapSys.TileIndicesFor(grid, coordinates), new Tile(1));
var traversal = sim.System<SharedGridTraversalSystem>();
traversal.Enabled = false;
var ent1 = sim.SpawnEntity(null, coordinates); // this raises MoveEvent, subscribe after
// Act
xformSys.AnchorEntity(ent1);
Assert.That(sim.Transform(ent1).ParentUid, Is.EqualTo(grid.Owner));
traversal.Enabled = true;
}
/// <summary>
/// Entities cannot be anchored to empty tiles. Attempting this is a no-op, and silently fails.
/// </summary>
[Test]
public void OnAnchored_EmptyTile_Nop()
{
var (sim, grid, coords, xformSys, mapSys) = SimulationFactory();
var ent1 = sim.SpawnEntity(null, coords);
var tileIndices = mapSys.TileIndicesFor(grid, sim.Transform(ent1).Coordinates);
mapSys.SetTile(grid, tileIndices, Tile.Empty);
// Act
xformSys.AnchorEntity(ent1);
Assert.That(mapSys.GetAnchoredEntities(grid, tileIndices).Count(), Is.EqualTo(0));
Assert.That(mapSys.GetTileRef(grid, tileIndices).Tile, Is.EqualTo(Tile.Empty));
}
/// <summary>
/// Entities can be anchored to any non-empty grid tile. A physics component is not required on either
/// the grid or the entity to anchor it.
/// </summary>
[Test]
public void OnAnchored_NonEmptyTile_Anchors()
{
var (sim, grid, coords, xformSys, mapSys) = SimulationFactory();
var ent1 = sim.SpawnEntity(null, coords);
var tileIndices = mapSys.TileIndicesFor(grid, sim.Transform(ent1).Coordinates);
mapSys.SetTile(grid, tileIndices, new Tile(1));
// Act
sim.Transform(ent1).Anchored = true;
Assert.That(mapSys.GetAnchoredEntities(grid, tileIndices).First(), Is.EqualTo(ent1));
Assert.That(mapSys.GetTileRef(grid, tileIndices).Tile, Is.Not.EqualTo(Tile.Empty));
Assert.That(sim.HasComp<PhysicsComponent>(ent1), Is.False);
var tempQualifier = grid.Owner;
Assert.That(sim.HasComp<PhysicsComponent>(tempQualifier), Is.True);
}
/// <summary>
/// Local position of an anchored entity cannot be changed (can still change world position via parent).
/// Writing to the property is a no-op and is silently ignored.
/// Because the position cannot be changed, MoveEvents are not raised when setting the property.
/// </summary>
[Test]
public void Anchored_SetPosition_Nop()
{
var (sim, grid, coordinates, xformSys, mapSys) = SimulationFactory();
// coordinates are already tile centered to prevent snapping and MoveEvent
coordinates = coordinates.Offset(new Vector2(0.5f, 0.5f));
// can only be anchored to a tile
mapSys.SetTile(grid, mapSys.TileIndicesFor(grid, coordinates), new Tile(1));
var ent1 = sim.SpawnEntity(null, coordinates); // this raises MoveEvent, subscribe after
sim.Transform(ent1).Anchored = true;
sim.System<MoveEventTestSystem>().FailOnMove = true;
// Act
sim.Transform(ent1).WorldPosition = new Vector2(99, 99);
sim.Transform(ent1).LocalPosition = new Vector2(99, 99);
Assert.That(xformSys.GetMapCoordinates(ent1), Is.EqualTo(coordinates));
sim.System<MoveEventTestSystem>().FailOnMove = false;
}
/// <summary>
/// Changing the parent of the entity un-anchors it.
/// </summary>
[Test]
public void Anchored_ChangeParent_Unanchors()
{
var (sim, grid, coordinates, xformSys, mapSys) = SimulationFactory();
var ent1 = sim.SpawnEntity(null, coordinates);
var tileIndices = mapSys.TileIndicesFor(grid, sim.Transform(ent1).Coordinates);
mapSys.SetTile(grid, tileIndices, new Tile(1));
xformSys.AnchorEntity(ent1);
// Act
xformSys.SetParent(ent1, mapSys.GetMap(coordinates.MapId));
Assert.That(sim.Transform(ent1).Anchored, Is.False);
Assert.That(mapSys.GetAnchoredEntities(grid, tileIndices).Count(), Is.EqualTo(0));
Assert.That(mapSys.GetTileRef(grid, tileIndices).Tile, Is.EqualTo(new Tile(1)));
}
/// <summary>
/// Setting the parent of an anchored entity to the same parent is a no-op (it will not be un-anchored).
/// This is an specific case to the base functionality of TransformComponent, where in general setting the same
/// parent is a no-op.
/// </summary>
[Test]
public void Anchored_SetParentSame_Nop()
{
var (sim, grid, coords, xformSys, mapSys) = SimulationFactory();
var entMan = sim.Resolve<IEntityManager>();
var ent1 = entMan.SpawnEntity(null, coords);
var tileIndices = mapSys.TileIndicesFor(grid, sim.Transform(ent1).Coordinates);
mapSys.SetTile(grid, tileIndices, new Tile(1));
sim.Transform(ent1).Anchored = true;
// Act
xformSys.SetParent(ent1, grid.Owner);
Assert.That(mapSys.GetAnchoredEntities(grid, tileIndices).First(), Is.EqualTo(ent1));
Assert.That(mapSys.GetTileRef(grid, tileIndices).Tile, Is.Not.EqualTo(Tile.Empty));
}
/// <summary>
/// If a tile is changed to a space tile, all entities anchored to that tile are unanchored.
/// </summary>
[Test]
public void Anchored_TileToSpace_Unanchors()
{
var (sim, grid, coords, xformSys, mapSys) = SimulationFactory();
var ent1 = sim.SpawnEntity(null, coords);
var tileIndices = mapSys.TileIndicesFor(grid, sim.Transform(ent1).Coordinates);
mapSys.SetTile(grid, tileIndices, new Tile(1));
mapSys.SetTile(grid, new Vector2i(100, 100), new Tile(1)); // Prevents the grid from being deleted when the Act happens
xformSys.AnchorEntity(ent1);
// Act
mapSys.SetTile(grid, tileIndices, Tile.Empty);
Assert.That(sim.Transform(ent1).Anchored, Is.False);
Assert.That(mapSys.GetAnchoredEntities(grid, tileIndices).Count(), Is.EqualTo(0));
Assert.That(mapSys.GetTileRef(grid, tileIndices).Tile, Is.EqualTo(Tile.Empty));
}
/// <summary>
/// Adding an anchored entity to a container un-anchors an entity. There should be no way to have an anchored entity
/// inside a container.
/// </summary>
/// <remarks>
/// The only way you can do this without changing the parent is to make the parent grid a ContainerManager, then add the anchored entity to it.
/// </remarks>
[Test]
public void Anchored_AddToContainer_Unanchors()
{
var (sim, grid, coords, xformSys, mapSys) = SimulationFactory();
var entMan = sim.Resolve<IEntityManager>();
var ent1 = sim.SpawnEntity(null, coords);
var tileIndices = mapSys.TileIndicesFor(grid, sim.Transform(ent1).Coordinates);
mapSys.SetTile(grid, tileIndices, new Tile(1));
xformSys.AnchorEntity(ent1);
// Act
// We purposefully use the grid as container so parent stays the same, reparent will unanchor
var containerSys = entMan.System<SharedContainerSystem>();
var containerMan = entMan.AddComponent<ContainerManagerComponent>(grid);
var container = containerSys.MakeContainer<Container>(grid, "TestContainer", containerMan);
containerSys.Insert(ent1, container);
Assert.That(sim.Transform(ent1).Anchored, Is.False);
Assert.That(mapSys.GetAnchoredEntities(grid, tileIndices).Count(), Is.EqualTo(0));
Assert.That(mapSys.GetTileRef(grid, tileIndices).Tile, Is.EqualTo(new Tile(1)));
Assert.That(container.ContainedEntities.Count, Is.EqualTo(1));
}
/// <summary>
/// Adding a physics component should poll TransformComponent.Anchored for the correct body type.
/// </summary>
[Test]
public void Anchored_AddPhysComp_IsStaticBody()
{
var (sim, grid, coords, xformSys, mapSys) = SimulationFactory();
var entMan = sim.Resolve<IEntityManager>();
var ent1 = sim.SpawnEntity(null, coords);
var tileIndices = mapSys.TileIndicesFor(grid, sim.Transform(ent1).Coordinates);
mapSys.SetTile(grid, tileIndices, new Tile(1));
xformSys.AnchorEntity(ent1);
// Act
// assumed default body is Dynamic
var physComp = entMan.AddComponent<PhysicsComponent>(ent1);
Assert.That(physComp.BodyType, Is.EqualTo(BodyType.Static));
}
/// <summary>
/// When an entity is anchored, it's physics body type is set to <see cref="BodyType.Static"/>.
/// </summary>
[Test]
public void OnAnchored_HasPhysicsComp_IsStaticBody()
{
var (sim, grid, coordinates, xformSys, mapSys) = SimulationFactory();
var entMan = sim.Resolve<IEntityManager>();
var physSystem = sim.System<SharedPhysicsSystem>();
// can only be anchored to a tile
mapSys.SetTile(grid, mapSys.TileIndicesFor(grid, coordinates), new Tile(1));
var ent1 = entMan.SpawnEntity(null, coordinates);
var physComp = entMan.AddComponent<PhysicsComponent>(ent1);
physSystem.SetBodyType(ent1, BodyType.Dynamic, body: physComp);
// Act
xformSys.AnchorEntity(ent1);
Assert.That(physComp.BodyType, Is.EqualTo(BodyType.Static));
}
/// <summary>
/// When an entity is unanchored, it's physics body type is set to <see cref="BodyType.Dynamic"/>.
/// </summary>
[Test]
public void OnUnanchored_HasPhysicsComp_IsDynamicBody()
{
var (sim, grid, coords, xformSys, mapSys) = SimulationFactory();
var entMan = sim.Resolve<IEntityManager>();
var ent1 = sim.SpawnEntity(null, coords);
var tileIndices = mapSys.TileIndicesFor(grid, sim.Transform(ent1).Coordinates);
mapSys.SetTile(grid, tileIndices, new Tile(1));
var physComp = entMan.AddComponent<PhysicsComponent>(ent1);
sim.Transform(ent1).Anchored = true;
// Act
xformSys.Unanchor(ent1);
Assert.That(physComp.BodyType, Is.EqualTo(BodyType.Dynamic));
}
/// <summary>
/// If an entity with an anchored prototype is spawned in an invalid location, the entity is unanchored.
/// </summary>
[Test]
public void SpawnAnchored_EmptyTile_Unanchors()
{
var (sim, grid, coords, _, mapSys) = SimulationFactory();
// Act
var ent1 = sim.SpawnEntity("anchoredEnt", coords);
var tileIndices = mapSys.TileIndicesFor(grid, sim.Transform(ent1).Coordinates);
Assert.That(mapSys.GetAnchoredEntities(grid, tileIndices).Count(), Is.EqualTo(0));
Assert.That(mapSys.GetTileRef(grid, tileIndices).Tile, Is.EqualTo(Tile.Empty));
Assert.That(sim.Transform(ent1).Anchored, Is.False);
}
/// <summary>
/// If an entity is inside a container, setting Anchored silently fails.
/// </summary>
[Test]
public void OnAnchored_InContainer_Nop()
{
var (sim, grid, coords, xformSys, mapSys) = SimulationFactory();
var entMan = sim.Resolve<IEntityManager>();
var ent1 = sim.SpawnEntity(null, coords);
var tileIndices = mapSys.TileIndicesFor(grid, sim.Transform(ent1).Coordinates);
mapSys.SetTile(grid, tileIndices, new Tile(1));
var containerSys = entMan.System<SharedContainerSystem>();
var containerMan = entMan.AddComponent<ContainerManagerComponent>(grid);
var container = containerSys.MakeContainer<Container>(grid, "TestContainer", containerMan);
containerSys.Insert(ent1, container);
// Act
xformSys.AnchorEntity(ent1);
Assert.That(sim.Transform(ent1).Anchored, Is.False);
Assert.That(mapSys.GetAnchoredEntities(grid, tileIndices).Count(), Is.EqualTo(0));
Assert.That(mapSys.GetTileRef(grid, tileIndices).Tile, Is.EqualTo(new Tile(1)));
Assert.That(container.ContainedEntities.Count, Is.EqualTo(1));
}
/// <summary>
/// Unanchoring an unanchored entity is a no-op.
/// </summary>
[Test]
public void Unanchored_Unanchor_Nop()
{
var (sim, grid, coordinates, xformSys, mapSys) = SimulationFactory();
// can only be anchored to a tile
mapSys.SetTile(grid, mapSys.TileIndicesFor(grid, coordinates), new Tile(1));
var traversal = sim.System<SharedGridTraversalSystem>();
traversal.Enabled = false;
var ent1 = sim.SpawnEntity(null, coordinates); // this raises MoveEvent, subscribe after
// Act
sim.System<MoveEventTestSystem>().FailOnMove = true;
xformSys.Unanchor(ent1);
Assert.That(sim.Transform(ent1).ParentUid, Is.EqualTo(grid.Owner));
sim.System<MoveEventTestSystem>().FailOnMove = false;
traversal.Enabled = true;
}
/// <summary>
/// Unanchoring an entity should leave it parented to the grid it was anchored to.
/// </summary>
[Test]
public void Anchored_Unanchored_ParentUnchanged()
{
var (sim, grid, coordinates, xformSys, mapSys) = SimulationFactory();
var entMan = sim.Resolve<IEntityManager>();
// can only be anchored to a tile
mapSys.SetTile(grid, mapSys.TileIndicesFor(grid, coordinates), new Tile(1));
var ent1 = entMan.SpawnEntity("anchoredEnt", mapSys.MapToGrid(grid, coordinates));
xformSys.Unanchor(ent1);
Assert.That(sim.Transform(ent1).ParentUid, Is.EqualTo(grid.Owner));
}
}
}

View File

@@ -0,0 +1,103 @@
using System.Numerics;
using NUnit.Framework;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.UnitTesting.Server;
namespace Robust.UnitTesting.Shared.GameObjects.Systems
{
[TestFixture, Parallelizable]
sealed class TransformSystemTests
{
private static ISimulation SimulationFactory()
{
var sim = RobustServerSimulation
.NewSimulation()
.RegisterEntitySystems(f => f.LoadExtraSystemType<AnchoredSystemTests.MoveEventTestSystem>())
.InitializeInstance();
return sim;
}
/// <summary>
/// When the local position of the transform changes, a MoveEvent is raised.
/// </summary>
[Test]
public void OnMove_LocalPosChanged_RaiseMoveEvent()
{
var sim = SimulationFactory();
var entMan = sim.Resolve<IEntityManager>();
var map = sim.CreateMap().MapId;
var ent1 = entMan.SpawnEntity(null, new MapCoordinates(Vector2.Zero, map));
entMan.System<AnchoredSystemTests.MoveEventTestSystem>().ResetCounters();
entMan.System<TransformSystem>().SetLocalPosition(ent1, Vector2.One);
entMan.System<AnchoredSystemTests.MoveEventTestSystem>().AssertMoved(false);
}
/// <summary>
/// Checks that the MoverCoordinates between parent and children is correct.
/// </summary>
[Test]
public void MoverCoordinatesCorrect()
{
var sim = SimulationFactory();
var entManager = sim.Resolve<IEntityManager>();
var xformSystem = sim.Resolve<IEntitySystemManager>().GetEntitySystem<SharedTransformSystem>();
var mapId = sim.CreateMap().MapId;
var parent = entManager.SpawnEntity(null, new MapCoordinates(Vector2.One, mapId));
var parentXform = entManager.GetComponent<TransformComponent>(parent);
Assert.That(parentXform.LocalPosition, Is.EqualTo(Vector2.One));
var child1 = entManager.SpawnEntity(null, new MapCoordinates(Vector2.One, mapId));
var child2 = entManager.SpawnEntity(null, new MapCoordinates(new Vector2(10f, 10f), mapId));
var child1Xform = entManager.GetComponent<TransformComponent>(child1);
var child2Xform = entManager.GetComponent<TransformComponent>(child2);
xformSystem.SetParent(child1, child1Xform, parent, parentXform: parentXform);
xformSystem.SetParent(child2, child2Xform, parent, parentXform: parentXform);
var mover1 = xformSystem.GetMoverCoordinates(child1, child1Xform);
var mover2 = xformSystem.GetMoverCoordinates(child2, child2Xform);
Assert.That(mover1.Position, Is.EqualTo(Vector2.One));
Assert.That(mover2.Position, Is.EqualTo(new Vector2(10f, 10f)));
var child3 = entManager.SpawnEntity(null, new MapCoordinates(Vector2.One, mapId));
var child3Xform = entManager.GetComponent<TransformComponent>(child3);
xformSystem.SetParent(child3, child3Xform, child2, parentXform: child2Xform);
Assert.That(xformSystem.GetMoverCoordinates(child3, child3Xform).Position, Is.EqualTo(Vector2.One));
}
/// <summary>
/// Asserts that when a transformcomponent is detached to null all of its children update their mapids.
/// </summary>
[Test]
public void DetachMapRecursive()
{
var sim = SimulationFactory();
var entManager = sim.Resolve<IEntityManager>();
var xformSystem = sim.Resolve<IEntitySystemManager>().GetEntitySystem<SharedTransformSystem>();
var mapId = sim.CreateMap().MapId;
var parent = entManager.SpawnEntity(null, new MapCoordinates(Vector2.One, mapId));
var parentXform = entManager.GetComponent<TransformComponent>(parent);
var child = entManager.SpawnEntity(null, new EntityCoordinates(parent, Vector2.Zero));
var childXform = entManager.GetComponent<TransformComponent>(child);
Assert.That(parentXform.MapID, Is.EqualTo(mapId));
Assert.That(childXform.MapID, Is.EqualTo(mapId));
xformSystem.DetachEntity(parent, parentXform);
Assert.That(parentXform.MapID, Is.EqualTo(MapId.Nullspace));
Assert.That(childXform.MapID, Is.EqualTo(MapId.Nullspace));
}
private sealed class Subscriber : IEntityEventSubscriber { }
}
}

View File

@@ -0,0 +1,76 @@
using System;
using System.Numerics;
using NUnit.Framework;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.UnitTesting.Server;
namespace Robust.UnitTesting.Shared.GameObjects
{
[TestFixture]
internal sealed class TransformComponent_Tests
{
/// <summary>
/// Verify that WorldPosition and WorldRotation return the same result as the faster helper method.
/// </summary>
[Test]
public void TestGetWorldMatches()
{
var server = RobustServerSimulation.NewSimulation().InitializeInstance();
var entManager = server.Resolve<IEntityManager>();
entManager.System<SharedMapSystem>().CreateMap(out var mapId);
var xform = entManager.System<TransformSystem>();
var ent1 = entManager.SpawnEntity(null, new MapCoordinates(Vector2.Zero, mapId));
var ent2 = entManager.SpawnEntity(null, new MapCoordinates(new Vector2(100f, 0f), mapId));
var xform1 = entManager.GetComponent<TransformComponent>(ent1);
var xform2 = entManager.GetComponent<TransformComponent>(ent2);
xform.SetParent(ent2, ent1);
xform1.LocalRotation = MathF.PI;
var (worldPos, worldRot, worldMatrix) = xform.GetWorldPositionRotationMatrix(xform2);
Assert.That(worldPos, Is.EqualTo(xform.GetWorldPosition(xform2)));
Assert.That(worldRot, Is.EqualTo(xform.GetWorldRotation(xform2)));
Assert.That(worldMatrix, Is.EqualTo(xform.GetWorldMatrix(xform2)));
var (_, _, invWorldMatrix) = xform.GetWorldPositionRotationInvMatrix(xform2);
Assert.That(invWorldMatrix, Is.EqualTo(xform.GetInvWorldMatrix(xform2)));
}
/// <summary>
/// Asserts that when AttachToGridOrMap is called the entity remains in the same position.
/// </summary>
[Test]
public void AttachToGridOrMap()
{
var server = RobustServerSimulation.NewSimulation().InitializeInstance();
var entManager = server.Resolve<IEntityManager>();
var mapManager = server.Resolve<IMapManager>();
var mapSystem = entManager.System<SharedMapSystem>();
var xformSystem = entManager.System<TransformSystem>();
mapSystem.CreateMap(out var mapId);
var grid = mapManager.CreateGridEntity(mapId);
mapSystem.SetTile(grid, new Vector2i(0, 0), new Tile(1));
xformSystem.SetLocalPosition(grid, new Vector2(0f, 100f));
var ent1 = entManager.SpawnEntity(null, new EntityCoordinates(grid, Vector2.One * grid.Comp.TileSize / 2));
var ent2 = entManager.SpawnEntity(null, new EntityCoordinates(ent1, Vector2.Zero));
var xform2 = entManager.GetComponent<TransformComponent>(ent2);
Assert.That(xformSystem.GetWorldPosition(ent2), Is.EqualTo(new Vector2(0.5f, 100.5f)));
xformSystem.AttachToGridOrMap(ent2);
Assert.That(xform2.LocalPosition, Is.EqualTo(Vector2.One * grid.Comp.TileSize / 2));
}
}
}