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,43 @@
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Shared.Configuration;
using Robust.Shared.IntegrationTests.Configuration;
using Robust.Shared.Log;
namespace Robust.UnitTesting.Shared.Configuration;
[Parallelizable(ParallelScope.All)]
[TestFixture]
[TestOf(typeof(ConfigurationManagerTest))]
internal sealed class ConfigurationIntegrationTest : RobustIntegrationTest
{
[Test]
public async Task TestSaveNoWarningServer()
{
using var server = StartServer(new ServerIntegrationOptions
{
FailureLogLevel = LogLevel.Warning
});
await server.WaitPost(() =>
{
// ReSharper disable once AccessToDisposedClosure
var cfg = server.Resolve<IConfigurationManager>();
cfg.SaveToFile();
});
}
[Test]
public async Task TestSaveNoWarningClient()
{
using var server = StartClient(new ClientIntegrationOptions
{
FailureLogLevel = LogLevel.Warning
});
await server.WaitPost(() =>
{
// ReSharper disable once AccessToDisposedClosure
var cfg = server.Resolve<IConfigurationManager>();
cfg.SaveToFile();
});
}
}

View File

@@ -0,0 +1,154 @@
using Moq;
using NUnit.Framework;
using Robust.Server.Configuration;
using Robust.Shared.Configuration;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Network;
using Robust.Shared.Replays;
using Robust.Shared.Timing;
namespace Robust.Shared.IntegrationTests.Configuration
{
[TestFixture]
[Parallelizable(ParallelScope.All)]
[TestOf(typeof(ConfigurationManager))]
internal sealed class ConfigurationManagerTest
{
[Test]
public void TestSubscribeUnsubscribe()
{
var mgr = MakeCfg();
mgr.RegisterCVar("foo.bar", 5);
var lastValue = 0;
var timesRan = 0;
void ValueChanged(int value)
{
timesRan += 1;
lastValue = value;
}
mgr.OnValueChanged<int>("foo.bar", ValueChanged);
mgr.SetCVar("foo.bar", 2);
Assert.That(timesRan, Is.EqualTo(1), "OnValueChanged did not run!");
Assert.That(lastValue, Is.EqualTo(2), "OnValueChanged value was wrong!");
mgr.UnsubValueChanged<int>("foo.bar", ValueChanged);
Assert.That(timesRan, Is.EqualTo(1), "UnsubValueChanged did not unsubscribe!");
}
[Test]
public void TestSubscribe_SubscribeMultipleThenUnsubscribe()
{
var mgr = MakeCfg();
mgr.RegisterCVar("foo.bar", 5);
var lastValueBar1 = 0;
var lastValueBar2 = 0;
var lastValueBar3 = 0;
var lastValueBar4 = 0;
var subscription = mgr.SubscribeMultiple()
.OnValueChanged<int>("foo.bar", value => lastValueBar1 = value)
.OnValueChanged<int>("foo.bar", value => lastValueBar2 = value)
.OnValueChanged<int>("foo.bar", value => lastValueBar3 = value)
.OnValueChanged<int>("foo.bar", value => lastValueBar4 = value);
mgr.SetCVar("foo.bar", 1);
Assert.That(lastValueBar1, Is.EqualTo(1), "OnValueChanged value was wrong!");
Assert.That(lastValueBar2, Is.EqualTo(1), "OnValueChanged value was wrong!");
Assert.That(lastValueBar3, Is.EqualTo(1), "OnValueChanged value was wrong!");
Assert.That(lastValueBar4, Is.EqualTo(1), "OnValueChanged value was wrong!");
subscription.Dispose();
mgr.SetCVar("foo.bar", 10);
Assert.That(lastValueBar1, Is.EqualTo(1), "OnValueChanged value was wrong!");
Assert.That(lastValueBar2, Is.EqualTo(1), "OnValueChanged value was wrong!");
Assert.That(lastValueBar3, Is.EqualTo(1), "OnValueChanged value was wrong!");
Assert.That(lastValueBar4, Is.EqualTo(1), "OnValueChanged value was wrong!");
}
[Test]
public void TestSubscribe_Unsubscribe()
{
var mgr = MakeCfg();
mgr.RegisterCVar("foo.bar", 5);
mgr.RegisterCVar("foo.foo", 2);
var lastValueBar = 0;
var lastValueFoo = 0;
var subscription = mgr.SubscribeMultiple()
.OnValueChanged<int>("foo.bar", value => lastValueBar = value)
.OnValueChanged<int>("foo.foo", value => lastValueFoo = value);
mgr.SetCVar("foo.bar", 1);
mgr.SetCVar("foo.foo", 3);
Assert.That(lastValueBar, Is.EqualTo(1), "OnValueChanged value was wrong!");
Assert.That(lastValueFoo, Is.EqualTo(3), "OnValueChanged value was wrong!");
subscription.Dispose();
mgr.SetCVar("foo.bar", 10);
mgr.SetCVar("foo.foo", 30);
Assert.That(lastValueBar, Is.EqualTo(1), "OnValueChanged value was wrong!");
Assert.That(lastValueFoo, Is.EqualTo(3), "OnValueChanged value was wrong!");
}
[Test]
public void TestOverrideDefaultValue()
{
var mgr = MakeCfg();
mgr.RegisterCVar("foo.bar", 5);
var value = 0;
mgr.OnValueChanged<int>("foo.bar", v => value = v);
// Change default value, this fires the value changed callback.
mgr.OverrideDefault("foo.bar", 10);
Assert.That(value, Is.EqualTo(10));
Assert.That(mgr.GetCVar<int>("foo.bar"), Is.EqualTo(10));
// Modify the cvar programmatically, also fires the callback.
mgr.SetCVar("foo.bar", 7);
Assert.That(value, Is.EqualTo(7));
Assert.That(mgr.GetCVar<int>("foo.bar"), Is.EqualTo(7));
// We have a value set now, so changing the default won't do anything.
mgr.OverrideDefault("foo.bar", 15);
Assert.That(value, Is.EqualTo(7));
Assert.That(mgr.GetCVar<int>("foo.bar"), Is.EqualTo(7));
}
private IConfigurationManager MakeCfg()
{
var collection = new DependencyCollection();
collection.RegisterInstance<IReplayRecordingManager>(new Mock<IReplayRecordingManager>().Object);
collection.RegisterInstance<INetManager>(new Mock<INetManager>().Object);
collection.Register<ConfigurationManager, ServerNetConfigurationManager>();
collection.Register<IServerNetConfigurationManager, ServerNetConfigurationManager>();
collection.Register<IGameTiming, GameTiming>();
collection.Register<ILogManager, LogManager>();
collection.BuildGraph();
return collection.Resolve<ConfigurationManager>();
}
}
}

View File

@@ -0,0 +1,126 @@
using NUnit.Framework;
using Robust.Shared.ContentPack;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Utility;
using Robust.UnitTesting.Shared;
namespace Robust.Shared.IntegrationTests.ContentPack
{
[TestFixture]
internal sealed class ResourceManagerTest : OurRobustUnitTest
{
private static Stream ZipStream => typeof(ResourceManagerTest).Assembly
.GetManifestResourceStream("Robust.Shared.IntegrationTests.ContentPack.ZipTest.zip")!;
private static readonly byte[] Data =
{
0x56, 0x75, 0x6c, 0x6b, 0x61, 0x6e, 0x20, 0x57, 0x68, 0x65, 0x6e
};
private static readonly string[] InvalidPaths =
{
// Reserved filenames like COM ports.
"/foo/COM1",
"COM2",
"/COM3/foo",
"COM4",
"COM5",
"COM6",
"COM7",
"COM8",
"COM9",
"LPT1",
"LPT2",
"LPT3",
"LPT4",
"LPT5",
"LPT6",
"LPT7",
"LPT8",
"LPT9",
"NUL",
"AUX",
"CON",
"PRN",
// ? is banned.
"/foo/bar?",
// * is banned.
"/foo*/baz",
// | is banned.
"/yay|bugs",
// : is banned.
"/yay: bugs",
// " is banned.
"/yay... \"bugs\"",
// \0 is banned.
"/\0",
// \x01-\x1f are banned.
"/\n",
};
[OneTimeSetUp]
public void Setup()
{
var componentFactory = IoCManager.Resolve<IComponentFactory>();
componentFactory.GenerateNetIds();
var stream = new MemoryStream(Data);
var resourceManager = IoCManager.Resolve<IResourceManagerInternal>();
resourceManager.MountStreamAt(stream, new ("/a/b/c.dat"));
}
[Test]
public void TestMountedStreamRead()
{
var resourceManager = IoCManager.Resolve<IResourceManagerInternal>();
using (var stream = resourceManager.ContentFileRead("/a/b/c.dat"))
{
Assert.That(stream.CopyToArray(), Is.EqualTo(Data));
}
}
[Test]
public void TestInvalidPaths([ValueSource(nameof(InvalidPaths))] string path)
{
Assert.That(ResourceManager.IsPathValid(new (path)), Is.False);
}
[Test]
public void TestInvalidPathsLowerCase([ValueSource(nameof(InvalidPaths))] string path)
{
Assert.That(ResourceManager.IsPathValid(new (path.ToLowerInvariant())), Is.False);
}
[Test]
public void TestZipRead()
{
var resourceManager = IoCManager.Resolve<IResourceManagerInternal>();
resourceManager.MountContentPack(ZipStream);
var stream = resourceManager.ContentFileRead("/foo.txt");
Assert.That(ReadString(stream), Is.EqualTo("Honk!! \n"));
}
[Test]
public void TestZipFind()
{
var resourceManager = IoCManager.Resolve<IResourceManagerInternal>();
resourceManager.MountContentPack(ZipStream);
var found = resourceManager.ContentFindFiles("/bar/");
Assert.That(found, Is.EquivalentTo(new[]
{
new ResPath("/bar/a.txt"),
new ResPath("/bar/b.txt"),
}));
}
private static string ReadString(Stream stream)
{
using var streamReader = new StreamReader(stream);
return streamReader.ReadToEnd();
}
}
}

Binary file not shown.

View File

@@ -0,0 +1,99 @@
using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Server.Player;
using Robust.Shared.Enums;
using Robust.Shared.IoC;
using Robust.Shared.Network;
namespace Robust.UnitTesting.Shared
{
[TestFixture]
internal sealed class EngineIntegrationTest_Test : RobustIntegrationTest
{
[Test]
public void ServerStartsCorrectlyTest()
{
ServerIntegrationInstance? server = null;
Assert.DoesNotThrow(() => server = StartServer());
Assert.That(server, Is.Not.Null);
}
[Test]
public void ClientStartsCorrectlyTest()
{
ClientIntegrationInstance? client = null;
Assert.DoesNotThrow(() => client = StartClient());
Assert.That(client, Is.Not.Null);
}
[Test]
public async Task ConsoleErrorsFailTest()
{
var server = StartServer();
var client = StartClient();
await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync());
// test missing commands
await client.WaitPost(() => Assert.Throws<AssertionException>(() => client.ConsoleHost.ExecuteCommand("aaaaaaaa")));
// test invalid commands / missing arguments
await client.WaitPost(() => Assert.Throws<AssertionException>(() => client.ConsoleHost.ExecuteCommand("cvar")));
// and repeat for the server
await server.WaitPost(() => Assert.Throws<AssertionException>(() => server.ConsoleHost.ExecuteCommand("aaaaaaaa")));
await server.WaitPost(() => Assert.Throws<AssertionException>(() => server.ConsoleHost.ExecuteCommand("cvar")));
}
[Test]
public async Task ServerClientPairConnectCorrectlyTest()
{
var server = StartServer();
var client = StartClient();
Assert.That(server, Is.Not.Null);
Assert.That(client, Is.Not.Null);
await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync());
// Connect client to the server...
var netMan = client.ResolveDependency<IClientNetManager>();
Assert.DoesNotThrow(() => client.SetConnectTarget(server));
client.Post(() => netMan.ClientConnect(null!, 0, null!));
// Run 10 synced ticks...
for (int i = 0; i < 10; i++)
{
await server.WaitRunTicks(1);
await client.WaitRunTicks(1);
}
await server.WaitAssertion(() =>
{
var playerManager = IoCManager.Resolve<IPlayerManager>();
// There must be a player connected.
Assert.That(playerManager.PlayerCount, Is.EqualTo(1));
// Get the only player...
var player = playerManager.Sessions.Single();
Assert.That(player.Status, Is.EqualTo(SessionStatus.Connected));
Assert.That(player.Channel.IsConnected, Is.True);
});
await client.WaitAssertion(() =>
{
var netManager = IoCManager.Resolve<IClientNetManager>();
Assert.That(netManager.IsConnected, Is.True);
Assert.That(netManager.ServerChannel, Is.Not.Null);
Assert.That(netManager.ServerChannel!.IsConnected, Is.True);
});
await client.WaitPost(() => netMan.ClientDisconnect(""));
await server.WaitRunTicks(5);
await client.WaitRunTicks(5);
}
}
}

View File

@@ -0,0 +1,389 @@
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
using Robust.UnitTesting.Server;
namespace Robust.UnitTesting.Shared
{
[TestFixture, TestOf(typeof(EntityLookupSystem))]
internal sealed class EntityLookupTest
{
private static readonly MapId MapId = new MapId(1);
private static readonly TestCaseData[] IntersectingCases = new[]
{
// Big offset
new TestCaseData(true, new MapCoordinates(new Vector2(10.5f, 10.5f), MapId), new MapCoordinates(new Vector2(10.5f, 10.5f), MapId), 0.25f, true),
};
private static readonly TestCaseData[] InRangeCases = new[]
{
new TestCaseData(true, new MapCoordinates(Vector2.One, MapId), new MapCoordinates(Vector2.Zero, MapId), 0.5f, false),
new TestCaseData(true, new MapCoordinates(new Vector2(10f, 10f), MapId), new MapCoordinates(new Vector2(9.5f, 9.5f), MapId), 0.5f, true),
// Close but no cigar
new TestCaseData(true, new MapCoordinates(new Vector2(10f, 10f), MapId), new MapCoordinates(new Vector2(9f, 9f), MapId), 0.5f, false),
// Large area so useboundsquery
new TestCaseData(true, new MapCoordinates(new Vector2(0f, 0f), MapId), new MapCoordinates(new Vector2(0f, 1000f), MapId), 999f, false),
new TestCaseData(true, new MapCoordinates(new Vector2(0f, 0f), MapId), new MapCoordinates(new Vector2(0f, 999f), MapId), 999f, true),
// NoFixturecases
new TestCaseData(false, new MapCoordinates(Vector2.One, MapId), new MapCoordinates(Vector2.Zero, MapId), 0.5f, false),
new TestCaseData(false, new MapCoordinates(new Vector2(10f, 10f), MapId), new MapCoordinates(new Vector2(9.5f, 9.5f), MapId), 0.5f, false),
// Close but no cigar
new TestCaseData(false, new MapCoordinates(new Vector2(10f, 10f), MapId), new MapCoordinates(new Vector2(9f, 9f), MapId), 0.5f, false),
};
// Remember this test data is relative.
private static readonly TestCaseData[] Box2Cases = new[]
{
new TestCaseData(true, new MapCoordinates(Vector2.One, MapId), Box2.UnitCentered, false),
new TestCaseData(true, new MapCoordinates(new Vector2(10f, 10f), MapId), Box2.UnitCentered, true),
};
private static readonly TestCaseData[] TileCases = new[]
{
new TestCaseData(true, new MapCoordinates(Vector2.One, MapId), Vector2i.Zero, false),
new TestCaseData(true, new MapCoordinates(new Vector2(10f, 10f), MapId), Vector2i.Zero, true),
// Need to make sure we don't pull out neighbor fixtures even if they barely touch our tile
new TestCaseData(true, new MapCoordinates(new Vector2(11f + 0.35f, 10f), MapId), Vector2i.Zero, false),
};
private EntityUid GetPhysicsEntity(IEntityManager entManager, MapCoordinates spawnPos)
{
var ent = entManager.SpawnEntity(null, spawnPos);
var physics = entManager.AddComponent<PhysicsComponent>(ent);
entManager.System<FixtureSystem>().TryCreateFixture(ent, new PhysShapeCircle(0.35f, Vector2.Zero), "fix1");
entManager.System<SharedPhysicsSystem>().SetCanCollide(ent, true, body: physics);
return ent;
}
private Entity<MapGridComponent> SetupGrid(MapId mapId, SharedMapSystem mapSystem, IEntityManager entManager, IMapManager mapManager)
{
var grid = mapManager.CreateGridEntity(mapId);
entManager.System<SharedTransformSystem>().SetLocalPosition(grid.Owner, new Vector2(10f, 10f));
mapSystem.SetTile(grid, Vector2i.Zero, new Tile(1));
return grid;
}
#region Entity
/*
* We double these tests just because these have slightly different codepaths at the moment.
*
*/
[Test, TestCaseSource(nameof(Box2Cases))]
public void TestEntityAnyIntersecting(bool physics, MapCoordinates spawnPos, Box2 queryBounds, bool result)
{
var sim = RobustServerSimulation.NewSimulation();
var server = sim.InitializeInstance();
var lookup = server.Resolve<IEntitySystemManager>().GetEntitySystem<EntityLookupSystem>();
var entManager = server.Resolve<IEntityManager>();
var mapManager = server.Resolve<IMapManager>();
var mapSystem = entManager.System<SharedMapSystem>();
mapSystem.CreateMap(spawnPos.MapId);
var grid = SetupGrid(spawnPos.MapId, mapSystem, entManager, mapManager);
if (physics)
GetPhysicsEntity(entManager, spawnPos);
else
entManager.Spawn(null, spawnPos);
var outcome = lookup.AnyLocalEntitiesIntersecting(grid.Owner, queryBounds, LookupFlags.All);
Assert.That(outcome, Is.EqualTo(result));
mapSystem.DeleteMap(spawnPos.MapId);
}
[Test, TestCaseSource(nameof(Box2Cases))]
public void TestEntityAnyLocalIntersecting(bool physics, MapCoordinates spawnPos, Box2 queryBounds, bool result)
{
var sim = RobustServerSimulation.NewSimulation();
var server = sim.InitializeInstance();
var lookup = server.Resolve<IEntitySystemManager>().GetEntitySystem<EntityLookupSystem>();
var entManager = server.Resolve<IEntityManager>();
var mapManager = server.Resolve<IMapManager>();
var mapSystem = entManager.System<SharedMapSystem>();
mapSystem.CreateMap(spawnPos.MapId);
var grid = SetupGrid(spawnPos.MapId, mapSystem, entManager, mapManager);
if (physics)
GetPhysicsEntity(entManager, spawnPos);
else
entManager.Spawn(null, spawnPos);
var outcome = lookup.AnyLocalEntitiesIntersecting(grid.Owner, queryBounds, LookupFlags.All);
Assert.That(outcome, Is.EqualTo(result));
mapSystem.DeleteMap(spawnPos.MapId);
}
/// <summary>
/// Tests Box2 local queries for a particular lookup ID.
/// </summary>
[Test, TestCaseSource(nameof(Box2Cases))]
public void TestEntityGridLocalIntersecting(bool physics, MapCoordinates spawnPos, Box2 queryBounds, bool result)
{
var sim = RobustServerSimulation.NewSimulation();
var server = sim.InitializeInstance();
var lookup = server.Resolve<IEntitySystemManager>().GetEntitySystem<EntityLookupSystem>();
var entManager = server.Resolve<IEntityManager>();
var mapManager = server.Resolve<IMapManager>();
var mapSystem = entManager.System<SharedMapSystem>();
mapSystem.CreateMap(spawnPos.MapId);
var grid = SetupGrid(spawnPos.MapId, mapSystem, entManager, mapManager);
if (physics)
GetPhysicsEntity(entManager, spawnPos);
else
entManager.Spawn(null, spawnPos);
var entities = new HashSet<Entity<TransformComponent>>();
lookup.GetLocalEntitiesIntersecting(grid.Owner, queryBounds, entities);
Assert.That(entities.Count > 0, Is.EqualTo(result));
mapSystem.DeleteMap(spawnPos.MapId);
}
/// <summary>
/// Tests Box2 local queries for a particular lookup ID.
/// </summary>
[Test, TestCaseSource(nameof(TileCases))]
public void TestEntityGridTileIntersecting(bool physics, MapCoordinates spawnPos, Vector2i queryTile, bool result)
{
var sim = RobustServerSimulation.NewSimulation();
var server = sim.InitializeInstance();
var lookup = server.Resolve<IEntitySystemManager>().GetEntitySystem<EntityLookupSystem>();
var entManager = server.Resolve<IEntityManager>();
var mapManager = server.Resolve<IMapManager>();
var mapSystem = entManager.System<SharedMapSystem>();
mapSystem.CreateMap(spawnPos.MapId);
var grid = SetupGrid(spawnPos.MapId, mapSystem, entManager, mapManager);
if (physics)
GetPhysicsEntity(entManager, spawnPos);
else
entManager.Spawn(null, spawnPos);
var entities = new HashSet<Entity<TransformComponent>>();
lookup.GetLocalEntitiesIntersecting(grid.Owner, queryTile, entities);
Assert.That(entities.Count > 0, Is.EqualTo(result));
mapSystem.DeleteMap(spawnPos.MapId);
}
#endregion
#region EntityUid
[Test, TestCaseSource(nameof(InRangeCases))]
public void TestMapInRange(bool physics, MapCoordinates spawnPos, MapCoordinates queryPos, float range, bool result)
{
var sim = RobustServerSimulation.NewSimulation();
var server = sim.InitializeInstance();
var lookup = server.Resolve<IEntitySystemManager>().GetEntitySystem<EntityLookupSystem>();
var entManager = server.Resolve<IEntityManager>();
var mapSystem = entManager.System<SharedMapSystem>();
entManager.System<SharedMapSystem>().CreateMap(spawnPos.MapId);
if (physics)
GetPhysicsEntity(entManager, spawnPos);
else
entManager.Spawn(null, spawnPos);
Assert.That(lookup.GetEntitiesInRange(queryPos.MapId, queryPos.Position, range).Count > 0, Is.EqualTo(result));
mapSystem.DeleteMap(spawnPos.MapId);
}
[Test, TestCaseSource(nameof(IntersectingCases))]
public void TestGridIntersecting(bool physics, MapCoordinates spawnPos, MapCoordinates queryPos, float range, bool result)
{
var sim = RobustServerSimulation.NewSimulation();
var server = sim.InitializeInstance();
var lookup = server.Resolve<IEntitySystemManager>().GetEntitySystem<EntityLookupSystem>();
var entManager = server.Resolve<IEntityManager>();
var mapManager = server.Resolve<IMapManager>();
var mapSystem = entManager.System<SharedMapSystem>();
mapSystem.CreateMap(spawnPos.MapId);
var grid = SetupGrid(spawnPos.MapId, mapSystem, entManager, mapManager);
if (physics)
GetPhysicsEntity(entManager, spawnPos);
else
entManager.Spawn(null, spawnPos);
_ = entManager.SpawnEntity(null, spawnPos);
var bounds = new Box2Rotated(Box2.CenteredAround(queryPos.Position, new Vector2(range, range)));
Assert.That(lookup.GetEntitiesIntersecting(queryPos.MapId, bounds).Count > 0, Is.EqualTo(result));
mapSystem.DeleteMap(spawnPos.MapId);
}
[Test, TestCaseSource(nameof(InRangeCases))]
public void TestGridInRange(bool physics, MapCoordinates spawnPos, MapCoordinates queryPos, float range, bool result)
{
var sim = RobustServerSimulation.NewSimulation();
var server = sim.InitializeInstance();
var lookup = server.Resolve<IEntitySystemManager>().GetEntitySystem<EntityLookupSystem>();
var entManager = server.Resolve<IEntityManager>();
var mapManager = server.Resolve<IMapManager>();
var mapSystem = entManager.System<SharedMapSystem>();
mapSystem.CreateMap(spawnPos.MapId);
var grid = SetupGrid(spawnPos.MapId, mapSystem, entManager, mapManager);
if (physics)
GetPhysicsEntity(entManager, spawnPos);
else
entManager.Spawn(null, spawnPos);
_ = entManager.SpawnEntity(null, spawnPos);
Assert.That(lookup.GetEntitiesInRange(queryPos.MapId, queryPos.Position, range).Count > 0, Is.EqualTo(result));
mapSystem.DeleteMap(spawnPos.MapId);
}
[Test, TestCaseSource(nameof(InRangeCases))]
public void TestMapNoFixtureInRange(bool physics, MapCoordinates spawnPos, MapCoordinates queryPos, float range, bool result)
{
var sim = RobustServerSimulation.NewSimulation();
var server = sim.InitializeInstance();
var lookup = server.Resolve<IEntitySystemManager>().GetEntitySystem<EntityLookupSystem>();
var entManager = server.Resolve<IEntityManager>();
var mapSystem = entManager.System<SharedMapSystem>();
entManager.System<SharedMapSystem>().CreateMap(spawnPos.MapId);
if (physics)
GetPhysicsEntity(entManager, spawnPos);
else
entManager.Spawn(null, spawnPos);
Assert.That(lookup.GetEntitiesInRange(queryPos.MapId, queryPos.Position, range).Count > 0, Is.EqualTo(result));
mapSystem.DeleteMap(spawnPos.MapId);
}
/// <summary>
/// Tests Box2 local queries for a particular lookup ID.
/// </summary>
[Test, TestCaseSource(nameof(Box2Cases))]
public void TestGridAnyIntersecting(bool physics, MapCoordinates spawnPos, Box2 queryBounds, bool result)
{
var sim = RobustServerSimulation.NewSimulation();
var server = sim.InitializeInstance();
var lookup = server.Resolve<IEntitySystemManager>().GetEntitySystem<EntityLookupSystem>();
var entManager = server.Resolve<IEntityManager>();
var mapManager = server.Resolve<IMapManager>();
var mapSystem = entManager.System<SharedMapSystem>();
mapSystem.CreateMap(spawnPos.MapId);
var grid = SetupGrid(spawnPos.MapId, mapSystem, entManager, mapManager);
if (physics)
GetPhysicsEntity(entManager, spawnPos);
else
entManager.Spawn(null, spawnPos);
var outcome = lookup.AnyLocalEntitiesIntersecting(grid.Owner, queryBounds, LookupFlags.All);
Assert.That(outcome, Is.EqualTo(result));
mapSystem.DeleteMap(spawnPos.MapId);
}
/// <summary>
/// Tests Box2 local queries for a particular lookup ID.
/// </summary>
[Test, TestCaseSource(nameof(Box2Cases))]
public void TestGridLocalIntersecting(bool physics, MapCoordinates spawnPos, Box2 queryBounds, bool result)
{
var sim = RobustServerSimulation.NewSimulation();
var server = sim.InitializeInstance();
var lookup = server.Resolve<IEntitySystemManager>().GetEntitySystem<EntityLookupSystem>();
var entManager = server.Resolve<IEntityManager>();
var mapManager = server.Resolve<IMapManager>();
var mapSystem = entManager.System<SharedMapSystem>();
mapSystem.CreateMap(spawnPos.MapId);
var grid = SetupGrid(spawnPos.MapId, mapSystem, entManager, mapManager);
if (physics)
GetPhysicsEntity(entManager, spawnPos);
else
entManager.Spawn(null, spawnPos);
var entities = new HashSet<EntityUid>();
lookup.GetLocalEntitiesIntersecting(grid.Owner, queryBounds, entities);
Assert.That(entities.Count > 0, Is.EqualTo(result));
mapSystem.DeleteMap(spawnPos.MapId);
}
#endregion
/// <summary>
/// Is the entity correctly removed / added to EntityLookup when anchored
/// </summary>
[Test]
public void TestAnchoring()
{
var sim = RobustServerSimulation.NewSimulation();
var server = sim.InitializeInstance();
var lookup = server.Resolve<IEntitySystemManager>().GetEntitySystem<EntityLookupSystem>();
var entManager = server.Resolve<IEntityManager>();
var mapManager = server.Resolve<IMapManager>();
var mapSystem = entManager.System<SharedMapSystem>();
var transformSystem = entManager.System<SharedTransformSystem>();
var mapId = server.CreateMap().MapId;
var grid = mapManager.CreateGridEntity(mapId);
var theMapSpotBeingUsed = new Box2(Vector2.Zero, Vector2.One);
mapSystem.SetTile(grid, new Vector2i(), new Tile(1));
Assert.That(lookup.GetEntitiesIntersecting(mapId, theMapSpotBeingUsed).ToList(), Is.Empty);
// Setup and check it actually worked
var dummy = entManager.SpawnEntity(null, new MapCoordinates(Vector2.Zero, mapId));
Assert.That(lookup.GetEntitiesIntersecting(mapId, theMapSpotBeingUsed).ToList(), Has.Count.EqualTo(1));
var xform = entManager.GetComponent<TransformComponent>(dummy);
// When anchoring it should still get returned.
transformSystem.AnchorEntity(dummy, xform);
Assert.That(xform.Anchored, Is.True);
Assert.That(lookup.GetEntitiesIntersecting(mapId, theMapSpotBeingUsed).ToList(), Has.Count.EqualTo(1));
transformSystem.Unanchor(dummy, xform);
Assert.That(xform.Anchored, Is.False);
Assert.That(lookup.GetEntitiesIntersecting(mapId, theMapSpotBeingUsed).ToList().Count, Is.EqualTo(1));
entManager.DeleteEntity(dummy);
entManager.DeleteEntity(grid);
mapSystem.DeleteMap(mapId);
}
}
}

View File

@@ -0,0 +1,143 @@
using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Utility;
using static Robust.UnitTesting.Shared.EntitySerialization.EntitySaveTestComponent;
namespace Robust.UnitTesting.Shared.EntitySerialization;
[TestFixture]
internal sealed class AlwaysPushSerializationTest : RobustIntegrationTest
{
private const string Prototype = @"
- type: entity
id: TestEntityCompositionParent
components:
- type: EntitySaveTest
list: [ 1, 2 ]
- type: entity
id: TestEntityCompositionChild
parent: TestEntityCompositionParent
components:
- type: EntitySaveTest
list: [ 3 , 4 ]
";
/// <summary>
/// This test checks that deserializing an entity with some component that has the
/// <see cref="AlwaysPushInheritanceAttribute"/> works as intended. Previously the attribute would cause the entity
/// prototype to **always** append it's contents to the loaded entity, effectively causing
/// the <see cref="TestAlwaysPushComponent.List"/> data-field to grow each time a map was loaded and saved.
/// </summary>
[Test]
[TestOf(typeof(AlwaysPushInheritanceAttribute))]
public async Task TestAlwaysPushSerialization()
{
var opts = new ServerIntegrationOptions
{
ExtraPrototypes = Prototype
};
var server = StartServer(opts);
await server.WaitIdleAsync();
var entMan = server.EntMan;
// Create a new map and spawn in some entities.
MapId mapId = default;
Entity<TransformComponent, EntitySaveTestComponent> parent1 = default;
Entity<TransformComponent, EntitySaveTestComponent> parent2 = default;
Entity<TransformComponent, EntitySaveTestComponent> parent3 = default;
Entity<TransformComponent, EntitySaveTestComponent> child1 = default;
Entity<TransformComponent, EntitySaveTestComponent> child2 = default;
Entity<TransformComponent, EntitySaveTestComponent> child3 = default;
var path = new ResPath($"{nameof(TestAlwaysPushSerialization)}.yml");
await server.WaitPost(() =>
{
server.System<SharedMapSystem>().CreateMap(out mapId);
var coords = new MapCoordinates(0, 0, mapId);
var parent1Uid = entMan.Spawn("TestEntityCompositionParent", coords);
var parent2Uid = entMan.Spawn("TestEntityCompositionParent", coords);
var parent3Uid = entMan.Spawn("TestEntityCompositionParent", coords);
var child1Uid = entMan.Spawn("TestEntityCompositionChild", coords);
var child2Uid = entMan.Spawn("TestEntityCompositionChild", coords);
var child3Uid = entMan.Spawn("TestEntityCompositionChild", coords);
parent1 = Get(parent1Uid, entMan);
parent2 = Get(parent2Uid, entMan);
parent3 = Get(parent3Uid, entMan);
child1 = Get(child1Uid, entMan);
child2 = Get(child2Uid, entMan);
child3 = Get(child3Uid, entMan);
});
// Assign a unique id to each entity (so they can be identified after saving & loading a map)
parent1.Comp2!.Id = nameof(parent1);
parent2.Comp2!.Id = nameof(parent2);
parent3.Comp2!.Id = nameof(parent3);
child1.Comp2!.Id = nameof(child1);
child2.Comp2!.Id = nameof(child2);
child3.Comp2!.Id = nameof(child3);
// The inheritance pushing for the prototypes should ensure that the parent & child prototype's lists were merged.
Assert.That(parent1.Comp2.List.SequenceEqual(new[] {1, 2}));
Assert.That(parent2.Comp2.List.SequenceEqual(new[] {1, 2}));
Assert.That(parent3.Comp2.List.SequenceEqual(new[] {1, 2}));
Assert.That(child1.Comp2.List.SequenceEqual(new[] {3, 4, 1, 2}));
Assert.That(child2.Comp2.List.SequenceEqual(new[] {3, 4, 1, 2}));
Assert.That(child3.Comp2.List.SequenceEqual(new[] {3, 4, 1, 2}));
// Modify data on some components.
parent2.Comp2.List.Add(-1);
child2.Comp2.List.Add(-1);
parent3.Comp2.List.RemoveAt(1);
child3.Comp2.List.RemoveAt(1);
Assert.That(parent1.Comp2.List.SequenceEqual(new[] {1, 2}));
Assert.That(parent2.Comp2.List.SequenceEqual(new[] {1, 2, -1}));
Assert.That(parent3.Comp2.List.SequenceEqual(new[] {1}));
Assert.That(child1.Comp2.List.SequenceEqual(new[] {3, 4, 1, 2}));
Assert.That(child2.Comp2.List.SequenceEqual(new[] {3, 4, 1, 2, -1}));
Assert.That(child3.Comp2.List.SequenceEqual(new[] {3, 1, 2}));
// Save map to yaml
var loader = server.System<MapLoaderSystem>();
var map = server.System<SharedMapSystem>();
Assert.That(loader.TrySaveMap(mapId, path));
// Delete the entities
await server.WaitPost(() => map.DeleteMap(mapId));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
// Load the map
await server.WaitPost(() =>
{
Assert.That(loader.TryLoadMap(path, out var ent, out _));
mapId = ent!.Value.Comp.MapId;
});
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(6));
// Find the deserialized entities
parent1 = Find(nameof(parent1), entMan);
parent2 = Find(nameof(parent2), entMan);
parent3 = Find(nameof(parent3), entMan);
child1 = Find(nameof(child1), entMan);
child2 = Find(nameof(child2), entMan);
child3 = Find(nameof(child3), entMan);
// Verify that the entity data has not changed.
Assert.That(parent1.Comp2.List.SequenceEqual(new[] {1, 2}));
Assert.That(parent2.Comp2.List.SequenceEqual(new[] {1, 2, -1}));
Assert.That(parent3.Comp2.List.SequenceEqual(new[] {1}));
Assert.That(child1.Comp2.List.SequenceEqual(new[] {3, 4, 1, 2}));
Assert.That(child2.Comp2.List.SequenceEqual(new[] {3, 4, 1, 2, -1}));
Assert.That(child3.Comp2.List.SequenceEqual(new[] {3, 1, 2}));
}
}

View File

@@ -0,0 +1,318 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Shared.EntitySerialization;
using Robust.Shared.EntitySerialization.Components;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using static Robust.UnitTesting.Shared.EntitySerialization.EntitySaveTestComponent;
namespace Robust.UnitTesting.Shared.EntitySerialization;
[TestFixture]
internal sealed partial class AutoIncludeSerializationTest : RobustIntegrationTest
{
private const string TestTileDefId = "a";
private const string TestPrototypes = $@"
- type: testTileDef
id: space
- type: testTileDef
id: {TestTileDefId}
";
[Test]
public async Task TestAutoIncludeSerialization()
{
var server = StartServer(new() { Pool = false, ExtraPrototypes = TestPrototypes }); // Pool=false due to TileDef registration
await server.WaitIdleAsync();
var entMan = server.EntMan;
var mapSys = server.System<SharedMapSystem>();
var loader = server.System<MapLoaderSystem>();
var mapMan = server.ResolveDependency<IMapManager>();
var tileMan = server.ResolveDependency<ITileDefinitionManager>();
var mapPath = new ResPath($"{nameof(AutoIncludeSerializationTest)}_map.yml");
var gridPath = new ResPath($"{nameof(AutoIncludeSerializationTest)}_grid.yml");
SerializationTestHelper.LoadTileDefs(server.ProtoMan, tileMan, "space");
var tDef = server.ProtoMan.Index<TileDef>(TestTileDefId);
// Create a map that contains an entity that references a nullspace entity.
MapId mapId = default;
Entity<TransformComponent, EntitySaveTestComponent> map = default;
Entity<TransformComponent, EntitySaveTestComponent> grid = default;
Entity<TransformComponent, EntitySaveTestComponent> onGrid = default;
Entity<TransformComponent, EntitySaveTestComponent> offGrid = default;
Entity<TransformComponent, EntitySaveTestComponent> nullSpace = default;
void AssertCount(int expected) => Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(expected));
await server.WaitPost(() =>
{
var mapUid = mapSys.CreateMap(out mapId);
var gridUid = mapMan.CreateGridEntity(mapId);
mapSys.SetTile(gridUid, Vector2i.Zero, new Tile(tDef.TileId));
var onGridUid = entMan.SpawnEntity(null, new EntityCoordinates(gridUid, 0.5f, 0.5f));
var offGridUid = entMan.SpawnEntity(null, new MapCoordinates(10f, 10f, mapId));
var nullSpaceUid = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
map = Get(mapUid, entMan);
grid = Get(gridUid, entMan);
onGrid = Get(onGridUid, entMan);
offGrid = Get(offGridUid, entMan);
nullSpace = Get(nullSpaceUid, entMan);
});
await server.WaitRunTicks(5);
Assert.That(map.Comp1!.ParentUid, Is.EqualTo(EntityUid.Invalid));
Assert.That(grid.Comp1!.ParentUid, Is.EqualTo(map.Owner));
Assert.That(onGrid.Comp1!.ParentUid, Is.EqualTo(grid.Owner));
Assert.That(offGrid.Comp1!.ParentUid, Is.EqualTo(map.Owner));
Assert.That(nullSpace.Comp1!.ParentUid, Is.EqualTo(EntityUid.Invalid));
// Assign unique ids.
map.Comp2!.Id = nameof(map);
grid.Comp2!.Id = nameof(grid);
onGrid.Comp2!.Id = nameof(onGrid);
offGrid.Comp2!.Id = nameof(offGrid);
nullSpace.Comp2!.Id = nameof(nullSpace);
// First simple map loading without any references to other entities.
// This will cause the null-space entity to be lost.
// Save the map, then delete all the entities.
AssertCount(5);
Assert.That(loader.TrySaveMap(mapId, mapPath));
Assert.That(loader.TrySaveGrid(grid, gridPath));
await server.WaitPost(() => mapSys.DeleteMap(mapId));
AssertCount(1);
await server.WaitPost(() => entMan.DeleteEntity(nullSpace));
AssertCount(0);
// Load up the file that only saved the grid and check that the expected entities exist.
await server.WaitPost(() => mapSys.CreateMap(out mapId));
await server.WaitAssertion(() => Assert.That(loader.TryLoadGrid(mapId, gridPath, out _)));
AssertCount(2);
grid = Find(nameof(grid), entMan);
onGrid = Find(nameof(onGrid), entMan);
Assert.That(onGrid.Comp1.ParentUid, Is.EqualTo(grid.Owner));
await server.WaitPost(() => mapSys.DeleteMap(mapId));
AssertCount(0);
// Load up the map, and check that the expected entities exist.
Entity<MapComponent>? loadedMap = default;
HashSet<Entity<MapGridComponent>>? loadedGrids = default!;
await server.WaitAssertion(() => Assert.That(loader.TryLoadMap(mapPath, out loadedMap, out loadedGrids)));
mapId = loadedMap!.Value.Comp.MapId;
Assert.That(loadedGrids, Has.Count.EqualTo(1));
AssertCount(4);
map = Find(nameof(map), entMan);
grid = Find(nameof(grid), entMan);
onGrid = Find(nameof(onGrid), entMan);
offGrid = Find(nameof(offGrid), entMan);
Assert.That(map.Comp1.ParentUid, Is.EqualTo(EntityUid.Invalid));
Assert.That(grid.Comp1.ParentUid, Is.EqualTo(map.Owner));
Assert.That(onGrid.Comp1.ParentUid, Is.EqualTo(grid.Owner));
Assert.That(offGrid.Comp1.ParentUid, Is.EqualTo(map.Owner));
// Re-spawn the nullspace entity
await server.WaitPost(() =>
{
var nullSpaceUid = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
nullSpace = Get(nullSpaceUid, entMan);
nullSpace.Comp2.Id = nameof(nullSpace);
});
// Repeat the previous saves, but with an entity that references the null-space entity.
onGrid.Comp2.Entity = nullSpace.Owner;
AssertCount(5);
Assert.That(loader.TrySaveMap(mapId, mapPath));
Assert.That(loader.TrySaveGrid(grid, gridPath));
await server.WaitPost(() => mapSys.DeleteMap(mapId));
AssertCount(1);
await server.WaitPost(() => entMan.DeleteEntity(nullSpace));
AssertCount(0);
// Load up the file that only saved the grid and check that the expected entities exist.
await server.WaitPost(() => mapSys.CreateMap(out mapId));
await server.WaitAssertion(() => Assert.That(loader.TryLoadGrid(mapId, gridPath, out _)));
AssertCount(3);
grid = Find(nameof(grid), entMan);
onGrid = Find(nameof(onGrid), entMan);
nullSpace = Find(nameof(nullSpace), entMan);
Assert.That(onGrid.Comp1.ParentUid, Is.EqualTo(grid.Owner));
Assert.That(onGrid.Comp2.Entity, Is.EqualTo(nullSpace.Owner));
Assert.That(nullSpace.Comp1.ParentUid, Is.EqualTo(EntityUid.Invalid));
await server.WaitPost(() => mapSys.DeleteMap(mapId));
AssertCount(1);
await server.WaitPost(() => entMan.DeleteEntity(nullSpace));
AssertCount(0);
// Load up the map, and check that the expected entities exist.
await server.WaitAssertion(() => Assert.That(loader.TryLoadMap(mapPath, out loadedMap, out loadedGrids)));
mapId = loadedMap!.Value.Comp.MapId;
Assert.That(loadedGrids, Has.Count.EqualTo(1));
AssertCount(5);
map = Find(nameof(map), entMan);
grid = Find(nameof(grid), entMan);
onGrid = Find(nameof(onGrid), entMan);
offGrid = Find(nameof(offGrid), entMan);
nullSpace = Find(nameof(nullSpace), entMan);
Assert.That(map.Comp1.ParentUid, Is.EqualTo(EntityUid.Invalid));
Assert.That(grid.Comp1.ParentUid, Is.EqualTo(map.Owner));
Assert.That(onGrid.Comp1.ParentUid, Is.EqualTo(grid.Owner));
Assert.That(offGrid.Comp1.ParentUid, Is.EqualTo(map.Owner));
Assert.That(onGrid.Comp2.Entity, Is.EqualTo(nullSpace.Owner));
Assert.That(nullSpace.Comp1.ParentUid, Is.EqualTo(EntityUid.Invalid));
// Check that attempting to save a reference to a non-null-space entity does not auto-include it.
Entity<TransformComponent, EntitySaveTestComponent> otherMap = default;
await server.WaitPost(() =>
{
var otherMapUid = mapSys.CreateMap();
otherMap = Get(otherMapUid, entMan);
otherMap.Comp2.Id = nameof(otherMap);
});
onGrid.Comp2.Entity = otherMap.Owner;
// By default it should log an error, but tests don't have a nice way to validate that an error was logged, so we'll just suppress it.
var opts = SerializationOptions.Default with {MissingEntityBehaviour = MissingEntityBehaviour.Ignore};
AssertCount(6);
Assert.That(loader.TrySaveMap(mapId, mapPath, opts));
Assert.That(loader.TrySaveGrid(grid, gridPath, opts));
await server.WaitPost(() => mapSys.DeleteMap(mapId));
await server.WaitPost(() => entMan.DeleteEntity(nullSpace));
await server.WaitPost(() => entMan.DeleteEntity(otherMap));
AssertCount(0);
// Check the grid file
await server.WaitPost(() => mapSys.CreateMap(out mapId));
var dOpts = DeserializationOptions.Default with {LogInvalidEntities = false};
await server.WaitAssertion(() => Assert.That(loader.TryLoadGrid(mapId, gridPath, out _, dOpts)));
AssertCount(2);
grid = Find(nameof(grid), entMan);
onGrid = Find(nameof(onGrid), entMan);
Assert.That(onGrid.Comp1.ParentUid, Is.EqualTo(grid.Owner));
await server.WaitPost(() => mapSys.DeleteMap(mapId));
AssertCount(0);
// Check the map file
await server.WaitAssertion(() => Assert.That(loader.TryLoadMap(mapPath, out loadedMap, out loadedGrids, dOpts)));
mapId = loadedMap!.Value.Comp.MapId;
Assert.That(loadedGrids, Has.Count.EqualTo(1));
AssertCount(4);
map = Find(nameof(map), entMan);
grid = Find(nameof(grid), entMan);
onGrid = Find(nameof(onGrid), entMan);
offGrid = Find(nameof(offGrid), entMan);
Assert.That(map.Comp1.ParentUid, Is.EqualTo(EntityUid.Invalid));
Assert.That(grid.Comp1.ParentUid, Is.EqualTo(map.Owner));
Assert.That(onGrid.Comp1.ParentUid, Is.EqualTo(grid.Owner));
Assert.That(offGrid.Comp1.ParentUid, Is.EqualTo(map.Owner));
// repeat the check, but this time with auto inclusion fully enabled.
Entity<TransformComponent, EntitySaveTestComponent> otherEnt = default;
await server.WaitPost(() =>
{
var otherMapUid = mapSys.CreateMap(out var otherMapId);
otherMap = Get(otherMapUid, entMan);
otherMap.Comp2.Id = nameof(otherMap);
var otherEntUid = entMan.SpawnEntity(null, new MapCoordinates(0, 0, otherMapId));
otherEnt = Get(otherEntUid, entMan);
otherEnt.Comp2.Id = nameof(otherEnt);
var nullSpaceUid = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
nullSpace = Get(nullSpaceUid, entMan);
nullSpace.Comp2.Id = nameof(nullSpace);
});
onGrid.Comp2.Entity = otherMap.Owner;
otherEnt.Comp2!.Entity = nullSpace;
AssertCount(7);
opts = opts with {MissingEntityBehaviour = MissingEntityBehaviour.AutoInclude};
Assert.That(loader.TrySaveGeneric(map.Owner, mapPath, out var cat, opts));
Assert.That(cat, Is.EqualTo(FileCategory.Unknown));
Assert.That(loader.TrySaveGeneric(grid.Owner, gridPath, out cat, opts));
Assert.That(cat, Is.EqualTo(FileCategory.Unknown));
await server.WaitPost(() => mapSys.DeleteMap(mapId));
await server.WaitPost(() => entMan.DeleteEntity(otherMap));
await server.WaitPost(() => entMan.DeleteEntity(nullSpace));
AssertCount(0);
// Check the grid file
await server.WaitPost(() => mapSys.CreateMap(out mapId));
var mapLoadOpts = MapLoadOptions.Default with
{
DeserializationOptions = DeserializationOptions.Default with {LogOrphanedGrids = false}
};
LoadResult? result = default;
await server.WaitAssertion(() => Assert.That(loader.TryLoadGeneric(gridPath, out result, mapLoadOpts)));
Assert.That(result!.Grids, Has.Count.EqualTo(1));
Assert.That(result.Orphans, Is.Empty); // Grid was orphaned, but was adopted after a new map was created
Assert.That(result.Maps, Has.Count.EqualTo(2));
Assert.That(result.NullspaceEntities, Has.Count.EqualTo(1));
Assert.That(entMan.Count<LoadedMapComponent>(), Is.EqualTo(1)); // auto-generated map isn't marked as "loaded"
AssertCount(5);
grid = Find(nameof(grid), entMan);
onGrid = Find(nameof(onGrid), entMan);
otherMap = Find(nameof(otherMap), entMan);
otherEnt = Find(nameof(otherEnt), entMan);
nullSpace = Find(nameof(nullSpace), entMan);
Assert.That(onGrid.Comp1.ParentUid, Is.EqualTo(grid.Owner));
Assert.That(otherEnt.Comp1.ParentUid, Is.EqualTo(otherMap.Owner));
Assert.That(otherMap.Comp1.ParentUid, Is.EqualTo(EntityUid.Invalid));
Assert.That(nullSpace.Comp1.ParentUid, Is.EqualTo(EntityUid.Invalid));
await server.WaitPost(() => entMan.DeleteEntity(otherMap));
await server.WaitPost(() => entMan.DeleteEntity(grid.Comp1.ParentUid));
await server.WaitPost(() => entMan.DeleteEntity(nullSpace));
await server.WaitPost(() => mapSys.DeleteMap(mapId));
AssertCount(0);
// Check the map file
await server.WaitAssertion(() => Assert.That(loader.TryLoadGeneric(mapPath, out result)));
Assert.That(result.Orphans, Is.Empty);
Assert.That(result.NullspaceEntities, Has.Count.EqualTo(1));
Assert.That(result.Grids, Has.Count.EqualTo(1));
Assert.That(result.Maps, Has.Count.EqualTo(2));
Assert.That(entMan.Count<LoadedMapComponent>(), Is.EqualTo(2));
AssertCount(7);
map = Find(nameof(map), entMan);
grid = Find(nameof(grid), entMan);
onGrid = Find(nameof(onGrid), entMan);
offGrid = Find(nameof(offGrid), entMan);
otherMap = Find(nameof(otherMap), entMan);
otherEnt = Find(nameof(otherEnt), entMan);
nullSpace = Find(nameof(nullSpace), entMan);
Assert.That(map.Comp1.ParentUid, Is.EqualTo(EntityUid.Invalid));
Assert.That(grid.Comp1.ParentUid, Is.EqualTo(map.Owner));
Assert.That(onGrid.Comp1.ParentUid, Is.EqualTo(grid.Owner));
Assert.That(offGrid.Comp1.ParentUid, Is.EqualTo(map.Owner));
Assert.That(otherEnt.Comp1.ParentUid, Is.EqualTo(otherMap.Owner));
Assert.That(otherMap.Comp1.ParentUid, Is.EqualTo(EntityUid.Invalid));
Assert.That(nullSpace.Comp1.ParentUid, Is.EqualTo(EntityUid.Invalid));
await server.WaitPost(() => entMan.DeleteEntity(map));
await server.WaitPost(() => entMan.DeleteEntity(otherMap));
await server.WaitPost(() => entMan.DeleteEntity(nullSpace));
AssertCount(0);
Assert.That(entMan.Count<LoadedMapComponent>(), Is.EqualTo(0));
Assert.That(entMan.Count<MapComponent>(), Is.EqualTo(0));
}
}

View File

@@ -0,0 +1,450 @@
using System.Text;
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Shared.ContentPack;
using Robust.Shared.EntitySerialization;
using Robust.Shared.EntitySerialization.Components;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Utility;
using static Robust.UnitTesting.Shared.EntitySerialization.EntitySaveTestComponent;
namespace Robust.UnitTesting.Shared.EntitySerialization;
[TestFixture]
internal sealed partial class BackwardsCompatibilityTest
{
/// <summary>
/// Check that v3 maps can be loaded. This simply tries to load a file and doesn't do a lot of extra validation.
/// </summary>
/// <remarks>
/// The file was pilfered from content integration tests ("floor3x3.yml") and modified slightly.
/// See also the comments around <see cref="EntityDeserializer.OldestSupportedVersion"/> that point out that v3
/// isn't even really loadable anymore.
/// </remarks>
[Test]
public async Task TestLoadV3()
{
var server = StartServer(new ServerIntegrationOptions {ExtraPrototypes = PrototypeV3});
await server.WaitIdleAsync();
var entMan = server.EntMan;
var mapSys = server.System<SharedMapSystem>();
var loader = server.System<MapLoaderSystem>();
var meta = server.System<MetaDataSystem>();
var tileMan = server.ResolveDependency<ITileDefinitionManager>();
var resourceManager = server.ResolveDependency<IResourceManagerInternal>();
SerializationTestHelper.LoadTileDefs(server.ProtoMan, tileMan);
var gridPath = new ResPath($"{nameof(MapDataV3Grid)}.yml");
resourceManager.MountString(gridPath.ToString(), MapDataV3Grid);
MapId mapId = default;
EntityUid mapUid = default;
Entity<TransformComponent, EntitySaveTestComponent> map;
Entity<TransformComponent, EntitySaveTestComponent> ent;
Entity<TransformComponent, EntitySaveTestComponent> grid;
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
await server.WaitPost(() => mapUid = mapSys.CreateMap(out mapId));
await server.WaitAssertion(() => Assert.That(loader.TryLoadGrid(mapId, gridPath, out _)));
Assert.That(entMan.Count<LoadedMapComponent>(), Is.EqualTo(0));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(2));
ent = Find(nameof(ent), entMan);
grid = Find(nameof(grid), entMan);
Assert.That(ent.Comp1.ParentUid, Is.EqualTo(grid.Owner));
Assert.That(grid.Comp1.ParentUid, Is.EqualTo(mapUid));
Assert.That(meta.EntityPaused(ent), Is.False);
Assert.That(meta.EntityPaused(grid), Is.False);
Assert.That(meta.EntityPaused(mapUid), Is.False);
Assert.That(entMan.GetComponent<MetaDataComponent>(ent).EntityLifeStage,
Is.EqualTo(EntityLifeStage.MapInitialized));
Assert.That(entMan.GetComponent<MetaDataComponent>(grid).EntityLifeStage,
Is.EqualTo(EntityLifeStage.MapInitialized));
Assert.That(entMan.GetComponent<MetaDataComponent>(mapUid).EntityLifeStage,
Is.EqualTo(EntityLifeStage.MapInitialized));
await server.WaitPost(() => entMan.DeleteEntity(mapUid));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
var mapPath = new ResPath($"{nameof(MapDataV3Map)}.yml");
resourceManager.MountString(mapPath.ToString(), MapDataV3Map);
await server.WaitAssertion(() => Assert.That(loader.TryLoadMap(mapPath, out _, out _)));
Assert.That(entMan.Count<LoadedMapComponent>(), Is.EqualTo(1));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(3));
ent = Find(nameof(ent), entMan);
grid = Find(nameof(grid), entMan);
map = Find(nameof(map), entMan);
Assert.That(ent.Comp1.ParentUid, Is.EqualTo(grid.Owner));
Assert.That(grid.Comp1.ParentUid, Is.EqualTo(map.Owner));
Assert.That(map.Comp1.ParentUid, Is.EqualTo(EntityUid.Invalid));
Assert.That(meta.EntityPaused(ent), Is.True);
Assert.That(meta.EntityPaused(grid), Is.True);
Assert.That(meta.EntityPaused(map), Is.True);
Assert.That(entMan.GetComponent<MetaDataComponent>(ent).EntityLifeStage,
Is.EqualTo(EntityLifeStage.Initialized));
Assert.That(entMan.GetComponent<MetaDataComponent>(grid).EntityLifeStage,
Is.EqualTo(EntityLifeStage.Initialized));
Assert.That(entMan.GetComponent<MetaDataComponent>(map).EntityLifeStage,
Is.EqualTo(EntityLifeStage.Initialized));
await server.WaitPost(() => entMan.DeleteEntity(map));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
// Repeat test, but with the initialize maps option enabled.
// Apparently mounted strings can only be read a single time.
// So have to re-mount them.
var mapPath2 = new ResPath($"{nameof(MapDataV3Map)}2.yml");
resourceManager.MountString(mapPath2.ToString(), MapDataV3Map);
var opts = DeserializationOptions.Default with {InitializeMaps = true};
await server.WaitAssertion(() => Assert.That(loader.TryLoadMap(mapPath2, out _, out _, opts)));
Assert.That(entMan.Count<LoadedMapComponent>(), Is.EqualTo(1));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(3));
ent = Find(nameof(ent), entMan);
grid = Find(nameof(grid), entMan);
map = Find(nameof(map), entMan);
Assert.That(ent.Comp1.ParentUid, Is.EqualTo(grid.Owner));
Assert.That(grid.Comp1.ParentUid, Is.EqualTo(map.Owner));
Assert.That(map.Comp1.ParentUid, Is.EqualTo(EntityUid.Invalid));
Assert.That(meta.EntityPaused(ent), Is.False);
Assert.That(meta.EntityPaused(grid), Is.False);
Assert.That(meta.EntityPaused(map), Is.False);
Assert.That(entMan.GetComponent<MetaDataComponent>(ent).EntityLifeStage,
Is.EqualTo(EntityLifeStage.MapInitialized));
Assert.That(entMan.GetComponent<MetaDataComponent>(grid).EntityLifeStage,
Is.EqualTo(EntityLifeStage.MapInitialized));
Assert.That(entMan.GetComponent<MetaDataComponent>(map).EntityLifeStage,
Is.EqualTo(EntityLifeStage.MapInitialized));
await server.WaitPost(() => entMan.DeleteEntity(map));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
Assert.That(entMan.Count<LoadedMapComponent>(), Is.EqualTo(0));
Assert.That(entMan.Count<MapComponent>(), Is.EqualTo(0));
}
private const string MapDataV3Grid = @"
meta:
format: 3
name: DemoStation
author: Space-Wizards
postmapinit: false
tilemap:
0: Space
1: 1
2: 2
3: 3
4: 4
5: 5
6: 6
7: 7
8: 8
9: 9
10: 10
11: 11
12: 12
13: 13
14: 14
15: 15
16: 16
17: 17
18: 18
19: 19
20: 20
21: 21
22: 22
23: 23
24: 24
25: 25
26: 26
27: 27
28: 28
29: 29
30: 30
31: 31
32: 32
33: 33
34: 34
35: 35
36: 36
37: 37
38: 38
39: 39
40: 40
41: 41
42: 42
43: 43
44: 44
45: 45
46: 46
47: 47
48: 48
49: 49
50: 50
51: 51
52: 52
53: 53
54: 54
55: 55
56: 56
57: 57
58: 58
59: 59
60: 60
61: 61
62: 62
63: 63
64: 64
65: 65
66: 66
67: 67
68: 68
69: 69
70: 70
71: 71
72: 72
73: 73
74: 74
75: 75
76: 76
77: 77
78: 78
79: 79
80: 80
81: 81
82: 82
83: 83
84: 84
85: 85
86: 86
87: 87
88: 88
entities:
- uid: 0
components:
- type: MetaData
- parent: null
type: Transform
- chunks:
-1,-1:
ind: -1,-1
tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsAAAALAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAAPgAAAA==
-1,0:
ind: -1,0
tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAACwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
0,0:
ind: 0,0
tiles: CwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
0,-1:
ind: 0,-1
tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
type: MapGrid
- type: Broadphase
- angularDamping: 0.05
linearDamping: 0.05
fixedRotation: False
bodyType: Dynamic
type: Physics
- fixtures: {}
type: Fixtures
- type: OccluderTree
- id: grid
type: EntitySaveTest
- uid: 1
type: V3TestProto
components:
- pos: 0.5,0.5
parent: 0
type: Transform
- id: ent
type: EntitySaveTest
...
";
private const string MapDataV3Map = @"
meta:
format: 3
name: DemoStation
author: Space-Wizards
postmapinit: false
tilemap:
0: Space
1: 1
2: 2
3: 3
4: 4
5: 5
6: 6
7: 7
8: 8
9: 9
10: 10
11: 11
12: 12
13: 13
14: 14
15: 15
16: 16
17: 17
18: 18
19: 19
20: 20
21: 21
22: 22
23: 23
24: 24
25: 25
26: 26
27: 27
28: 28
29: 29
30: 30
31: 31
32: 32
33: 33
34: 34
35: 35
36: 36
37: 37
38: 38
39: 39
40: 40
41: 41
42: 42
43: 43
44: 44
45: 45
46: 46
47: 47
48: 48
49: 49
50: 50
51: 51
52: 52
53: 53
54: 54
55: 55
56: 56
57: 57
58: 58
59: 59
60: 60
61: 61
62: 62
63: 63
64: 64
65: 65
66: 66
67: 67
68: 68
69: 69
70: 70
71: 71
72: 72
73: 73
74: 74
75: 75
76: 76
77: 77
78: 78
79: 79
80: 80
81: 81
82: 82
83: 83
84: 84
85: 85
86: 86
87: 87
88: 88
entities:
- uid: 123
components:
- type: MetaData
- type: Transform
- type: Map
- type: EntitySaveTest
id: map
- uid: 0
components:
- type: MetaData
- parent: 123
type: Transform
- chunks:
-1,-1:
ind: -1,-1
tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsAAAALAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAAPgAAAA==
-1,0:
ind: -1,0
tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAACwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
0,0:
ind: 0,0
tiles: CwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
0,-1:
ind: 0,-1
tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
type: MapGrid
- type: Broadphase
- angularDamping: 0.05
linearDamping: 0.05
fixedRotation: False
bodyType: Dynamic
type: Physics
- fixtures: {}
type: Fixtures
- type: OccluderTree
- id: grid
type: EntitySaveTest
- uid: 1
type: V3TestProto
components:
- pos: 0.5,0.5
parent: 0
type: Transform
- id: ent
type: EntitySaveTest
...
";
private static string GenerateTileDefs(int count)
{
var sb = new StringBuilder();
for (var i = 0; i < count; i++)
{
sb.Append($@"
- type: testTileDef
id: {i}"
);
}
return sb.ToString();
}
private static readonly string PrototypeV3 = $@"
- type: entity
id: V3TestProto
components:
- type: EntitySaveTest
list: [ 1, 2 ]
- type: testTileDef
id: Space
{GenerateTileDefs(88)}
";
}

View File

@@ -0,0 +1,267 @@
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Shared.ContentPack;
using Robust.Shared.EntitySerialization;
using Robust.Shared.EntitySerialization.Components;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Utility;
using static Robust.UnitTesting.Shared.EntitySerialization.EntitySaveTestComponent;
namespace Robust.UnitTesting.Shared.EntitySerialization;
[TestFixture]
internal sealed partial class BackwardsCompatibilityTest
{
/// <summary>
/// Check that v4 maps can be loaded. This simply tries to load a file and doesn't do a lot of extra validation.
/// </summary>
/// <remarks>
/// The file was pilfered from content integration tests ("floor3x3.yml") and modified slightly.
/// </remarks>
[Test]
public async Task TestLoadV4()
{
var server = StartServer(new ServerIntegrationOptions {ExtraPrototypes = PrototypeV4});
await server.WaitIdleAsync();
var entMan = server.EntMan;
var mapSys = server.System<SharedMapSystem>();
var loader = server.System<MapLoaderSystem>();
var meta = server.System<MetaDataSystem>();
var tileMan = server.ResolveDependency<ITileDefinitionManager>();
var resourceManager = server.ResolveDependency<IResourceManagerInternal>();
SerializationTestHelper.LoadTileDefs(server.ProtoMan, tileMan, "Space");
var gridPath = new ResPath($"{nameof(MapDataV4Grid)}.yml");
resourceManager.MountString(gridPath.ToString(), MapDataV4Grid);
MapId mapId = default;
EntityUid mapUid = default;
Entity<TransformComponent, EntitySaveTestComponent> map;
Entity<TransformComponent, EntitySaveTestComponent> ent;
Entity<TransformComponent, EntitySaveTestComponent> grid;
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
await server.WaitPost(() => mapUid = mapSys.CreateMap(out mapId));
await server.WaitAssertion(() => Assert.That(loader.TryLoadGrid(mapId, gridPath, out _)));
Assert.That(entMan.Count<LoadedMapComponent>(), Is.EqualTo(0));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(2));
ent = Find(nameof(ent), entMan);
grid = Find(nameof(grid), entMan);
Assert.That(ent.Comp1.ParentUid, Is.EqualTo(grid.Owner));
Assert.That(grid.Comp1.ParentUid, Is.EqualTo(mapUid));
Assert.That(meta.EntityPaused(ent), Is.False);
Assert.That(meta.EntityPaused(grid), Is.False);
Assert.That(meta.EntityPaused(mapUid), Is.False);
Assert.That(entMan.GetComponent<MetaDataComponent>(ent).EntityLifeStage,
Is.EqualTo(EntityLifeStage.MapInitialized));
Assert.That(entMan.GetComponent<MetaDataComponent>(grid).EntityLifeStage,
Is.EqualTo(EntityLifeStage.MapInitialized));
Assert.That(entMan.GetComponent<MetaDataComponent>(mapUid).EntityLifeStage,
Is.EqualTo(EntityLifeStage.MapInitialized));
await server.WaitPost(() => entMan.DeleteEntity(mapUid));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
var mapPath = new ResPath($"{nameof(MapDataV4Map)}.yml");
resourceManager.MountString(mapPath.ToString(), MapDataV4Map);
await server.WaitAssertion(() => Assert.That(loader.TryLoadMap(mapPath, out _, out _)));
Assert.That(entMan.Count<LoadedMapComponent>(), Is.EqualTo(1));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(3));
ent = Find(nameof(ent), entMan);
grid = Find(nameof(grid), entMan);
map = Find(nameof(map), entMan);
Assert.That(ent.Comp1.ParentUid, Is.EqualTo(grid.Owner));
Assert.That(grid.Comp1.ParentUid, Is.EqualTo(map.Owner));
Assert.That(map.Comp1.ParentUid, Is.EqualTo(EntityUid.Invalid));
Assert.That(meta.EntityPaused(ent), Is.True);
Assert.That(meta.EntityPaused(grid), Is.True);
Assert.That(meta.EntityPaused(map), Is.True);
Assert.That(entMan.GetComponent<MetaDataComponent>(ent).EntityLifeStage,
Is.EqualTo(EntityLifeStage.Initialized));
Assert.That(entMan.GetComponent<MetaDataComponent>(grid).EntityLifeStage,
Is.EqualTo(EntityLifeStage.Initialized));
Assert.That(entMan.GetComponent<MetaDataComponent>(map).EntityLifeStage,
Is.EqualTo(EntityLifeStage.Initialized));
await server.WaitPost(() => entMan.DeleteEntity(map));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
// Repeat test, but with the initialize maps option enabled.
// Apparently mounted strings can only be read a single time.
// So have to re-mount them.
var mapPath2 = new ResPath($"{nameof(MapDataV4Map)}2.yml");
resourceManager.MountString(mapPath2.ToString(), MapDataV4Map);
var opts = DeserializationOptions.Default with {InitializeMaps = true};
await server.WaitAssertion(() => Assert.That(loader.TryLoadMap(mapPath2, out _, out _, opts)));
Assert.That(entMan.Count<LoadedMapComponent>(), Is.EqualTo(1));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(3));
ent = Find(nameof(ent), entMan);
grid = Find(nameof(grid), entMan);
map = Find(nameof(map), entMan);
Assert.That(ent.Comp1.ParentUid, Is.EqualTo(grid.Owner));
Assert.That(grid.Comp1.ParentUid, Is.EqualTo(map.Owner));
Assert.That(map.Comp1.ParentUid, Is.EqualTo(EntityUid.Invalid));
Assert.That(meta.EntityPaused(ent), Is.False);
Assert.That(meta.EntityPaused(grid), Is.False);
Assert.That(meta.EntityPaused(map), Is.False);
Assert.That(entMan.GetComponent<MetaDataComponent>(ent).EntityLifeStage,
Is.EqualTo(EntityLifeStage.MapInitialized));
Assert.That(entMan.GetComponent<MetaDataComponent>(grid).EntityLifeStage,
Is.EqualTo(EntityLifeStage.MapInitialized));
Assert.That(entMan.GetComponent<MetaDataComponent>(map).EntityLifeStage,
Is.EqualTo(EntityLifeStage.MapInitialized));
await server.WaitPost(() => entMan.DeleteEntity(map));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
Assert.That(entMan.Count<LoadedMapComponent>(), Is.EqualTo(0));
Assert.That(entMan.Count<MapComponent>(), Is.EqualTo(0));
}
private const string MapDataV4Grid = @"
meta:
format: 4
name: DemoStation
author: Space-Wizards
postmapinit: false
tilemap:
0: Space
11: A
68: B
entities:
- proto: """"
entities:
- uid: 2
components:
- type: MetaData
- parent: invalid
type: Transform
- chunks:
-1,-1:
ind: -1,-1
tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsAAAALAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAARAAAAA==
-1,0:
ind: -1,0
tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAACwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
0,0:
ind: 0,0
tiles: CwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
0,-1:
ind: 0,-1
tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
type: MapGrid
- type: Broadphase
- angularDamping: 0.05
linearDamping: 0.05
fixedRotation: False
bodyType: Dynamic
type: Physics
- type: OccluderTree
- id: grid
type: EntitySaveTest
- proto: V4TestProto
entities:
- uid: 1
components:
- pos: 0.5,0.5
parent: 2
type: Transform
- id: ent
type: EntitySaveTest
...
";
private const string MapDataV4Map = @"
meta:
format: 4
name: DemoStation
author: Space-Wizards
postmapinit: false
tilemap:
0: Space
11: A
68: B
entities:
- proto: """"
entities:
- uid: 123
components:
- type: MetaData
- type: Transform
- type: Map
- type: EntitySaveTest
id: map
- uid: 2
components:
- type: MetaData
- parent: 123
type: Transform
- chunks:
-1,-1:
ind: -1,-1
tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsAAAALAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAARAAAAA==
-1,0:
ind: -1,0
tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAACwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
0,0:
ind: 0,0
tiles: CwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
0,-1:
ind: 0,-1
tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
type: MapGrid
- type: Broadphase
- angularDamping: 0.05
linearDamping: 0.05
fixedRotation: False
bodyType: Dynamic
type: Physics
- type: OccluderTree
- id: grid
type: EntitySaveTest
- proto: V4TestProto
entities:
- uid: 1
components:
- pos: 0.5,0.5
parent: 2
type: Transform
- id: ent
type: EntitySaveTest
";
private const string PrototypeV4 = @"
- type: entity
id: V4TestProto
components:
- type: EntitySaveTest
list: [ 1, 2 ]
- type: testTileDef
id: Space
- type: testTileDef
id: A
- type: testTileDef
id: B
";
}

View File

@@ -0,0 +1,266 @@
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Shared.ContentPack;
using Robust.Shared.EntitySerialization;
using Robust.Shared.EntitySerialization.Components;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Utility;
using static Robust.UnitTesting.Shared.EntitySerialization.EntitySaveTestComponent;
namespace Robust.UnitTesting.Shared.EntitySerialization;
[TestFixture]
internal sealed partial class BackwardsCompatibilityTest
{
/// <summary>
/// Check that v5 maps can be loaded. This simply tries to load a file and doesn't do a lot of extra validation.
/// </summary>
/// <remarks>
/// The file was pilfered from content integration tests ("floor3x3.yml") and modified slightly.
/// </remarks>
[Test]
public async Task TestLoadV5()
{
var server = StartServer(new ServerIntegrationOptions {ExtraPrototypes = PrototypeV5});
await server.WaitIdleAsync();
var entMan = server.EntMan;
var mapSys = server.System<SharedMapSystem>();
var loader = server.System<MapLoaderSystem>();
var meta = server.System<MetaDataSystem>();
var tileMan = server.ResolveDependency<ITileDefinitionManager>();
var resourceManager = server.ResolveDependency<IResourceManagerInternal>();
SerializationTestHelper.LoadTileDefs(server.ProtoMan, tileMan, "Space");
var gridPath = new ResPath($"{nameof(MapDataV5Grid)}.yml");
resourceManager.MountString(gridPath.ToString(), MapDataV5Grid);
MapId mapId = default;
EntityUid mapUid = default;
Entity<TransformComponent, EntitySaveTestComponent> map;
Entity<TransformComponent, EntitySaveTestComponent> ent;
Entity<TransformComponent, EntitySaveTestComponent> grid;
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
await server.WaitPost(() => mapUid = mapSys.CreateMap(out mapId));
await server.WaitAssertion(() => Assert.That(loader.TryLoadGrid(mapId, gridPath, out _)));
Assert.That(entMan.Count<LoadedMapComponent>(), Is.EqualTo(0));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(2));
ent = Find(nameof(ent), entMan);
grid = Find(nameof(grid), entMan);
Assert.That(ent.Comp1.ParentUid, Is.EqualTo(grid.Owner));
Assert.That(grid.Comp1.ParentUid, Is.EqualTo(mapUid));
Assert.That(meta.EntityPaused(ent), Is.False);
Assert.That(meta.EntityPaused(grid), Is.False);
Assert.That(meta.EntityPaused(mapUid), Is.False);
Assert.That(entMan.GetComponent<MetaDataComponent>(ent).EntityLifeStage,
Is.EqualTo(EntityLifeStage.MapInitialized));
Assert.That(entMan.GetComponent<MetaDataComponent>(grid).EntityLifeStage,
Is.EqualTo(EntityLifeStage.MapInitialized));
Assert.That(entMan.GetComponent<MetaDataComponent>(mapUid).EntityLifeStage,
Is.EqualTo(EntityLifeStage.MapInitialized));
await server.WaitPost(() => entMan.DeleteEntity(mapUid));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
var mapPath = new ResPath($"{nameof(MapDataV5Map)}.yml");
resourceManager.MountString(mapPath.ToString(), MapDataV5Map);
await server.WaitAssertion(() => Assert.That(loader.TryLoadMap(mapPath, out _, out _)));
Assert.That(entMan.Count<LoadedMapComponent>(), Is.EqualTo(1));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(3));
ent = Find(nameof(ent), entMan);
grid = Find(nameof(grid), entMan);
map = Find(nameof(map), entMan);
Assert.That(ent.Comp1.ParentUid, Is.EqualTo(grid.Owner));
Assert.That(grid.Comp1.ParentUid, Is.EqualTo(map.Owner));
Assert.That(map.Comp1.ParentUid, Is.EqualTo(EntityUid.Invalid));
Assert.That(meta.EntityPaused(ent), Is.True);
Assert.That(meta.EntityPaused(grid), Is.True);
Assert.That(meta.EntityPaused(map), Is.True);
Assert.That(entMan.GetComponent<MetaDataComponent>(ent).EntityLifeStage,
Is.EqualTo(EntityLifeStage.Initialized));
Assert.That(entMan.GetComponent<MetaDataComponent>(grid).EntityLifeStage,
Is.EqualTo(EntityLifeStage.Initialized));
Assert.That(entMan.GetComponent<MetaDataComponent>(map).EntityLifeStage,
Is.EqualTo(EntityLifeStage.Initialized));
await server.WaitPost(() => entMan.DeleteEntity(map));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
// Repeat test, but with the initialize maps option enabled.
// Apparently mounted strings can only be read a single time.
// So have to re-mount them.
var mapPath2 = new ResPath($"{nameof(MapDataV5Map)}2.yml");
resourceManager.MountString(mapPath2.ToString(), MapDataV5Map);
var opts = DeserializationOptions.Default with {InitializeMaps = true};
await server.WaitAssertion(() => Assert.That(loader.TryLoadMap(mapPath2, out _, out _, opts)));
Assert.That(entMan.Count<LoadedMapComponent>(), Is.EqualTo(1));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(3));
ent = Find(nameof(ent), entMan);
grid = Find(nameof(grid), entMan);
map = Find(nameof(map), entMan);
Assert.That(ent.Comp1.ParentUid, Is.EqualTo(grid.Owner));
Assert.That(grid.Comp1.ParentUid, Is.EqualTo(map.Owner));
Assert.That(map.Comp1.ParentUid, Is.EqualTo(EntityUid.Invalid));
Assert.That(meta.EntityPaused(ent), Is.False);
Assert.That(meta.EntityPaused(grid), Is.False);
Assert.That(meta.EntityPaused(map), Is.False);
Assert.That(entMan.GetComponent<MetaDataComponent>(ent).EntityLifeStage,
Is.EqualTo(EntityLifeStage.MapInitialized));
Assert.That(entMan.GetComponent<MetaDataComponent>(grid).EntityLifeStage,
Is.EqualTo(EntityLifeStage.MapInitialized));
Assert.That(entMan.GetComponent<MetaDataComponent>(map).EntityLifeStage,
Is.EqualTo(EntityLifeStage.MapInitialized));
await server.WaitPost(() => entMan.DeleteEntity(map));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
Assert.That(entMan.Count<LoadedMapComponent>(), Is.EqualTo(0));
Assert.That(entMan.Count<MapComponent>(), Is.EqualTo(0));
}
private const string MapDataV5Grid = @"
meta:
format: 5
postmapinit: false
tilemap:
0: Space
11: A
69: B
entities:
- proto: """"
entities:
- uid: 2
components:
- type: MetaData
- parent: invalid
type: Transform
- chunks:
-1,-1:
ind: -1,-1
tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsAAAALAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAARQAAAA==
-1,0:
ind: -1,0
tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAACwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
0,0:
ind: 0,0
tiles: CwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
0,-1:
ind: 0,-1
tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
type: MapGrid
- type: Broadphase
- angularDamping: 0.05
linearDamping: 0.05
fixedRotation: False
bodyType: Dynamic
type: Physics
- fixtures: {}
type: Fixtures
- type: OccluderTree
- id: grid
type: EntitySaveTest
- proto: V5TestProto
entities:
- uid: 1
components:
- pos: 0.5,0.5
parent: 2
type: Transform
- id: ent
type: EntitySaveTest
";
private const string MapDataV5Map = @"
meta:
format: 5
postmapinit: false
tilemap:
0: Space
11: A
69: B
entities:
- proto: """"
entities:
- uid: 123
components:
- type: MetaData
- type: Transform
- type: Map
- type: EntitySaveTest
id: map
- uid: 2
components:
- type: MetaData
- parent: 123
type: Transform
- chunks:
-1,-1:
ind: -1,-1
tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsAAAALAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAARQAAAA==
-1,0:
ind: -1,0
tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAACwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
0,0:
ind: 0,0
tiles: CwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
0,-1:
ind: 0,-1
tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
type: MapGrid
- type: Broadphase
- angularDamping: 0.05
linearDamping: 0.05
fixedRotation: False
bodyType: Dynamic
type: Physics
- fixtures: {}
type: Fixtures
- type: OccluderTree
- id: grid
type: EntitySaveTest
- proto: V5TestProto
entities:
- uid: 1
components:
- pos: 0.5,0.5
parent: 2
type: Transform
- id: ent
type: EntitySaveTest
";
private const string PrototypeV5 = @"
- type: entity
id: V5TestProto
components:
- type: EntitySaveTest
list: [ 1, 2 ]
- type: testTileDef
id: Space
- type: testTileDef
id: A
- type: testTileDef
id: B
";
}

View File

@@ -0,0 +1,273 @@
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Shared.ContentPack;
using Robust.Shared.EntitySerialization;
using Robust.Shared.EntitySerialization.Components;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Utility;
using static Robust.UnitTesting.Shared.EntitySerialization.EntitySaveTestComponent;
namespace Robust.UnitTesting.Shared.EntitySerialization;
[TestFixture]
internal sealed partial class BackwardsCompatibilityTest
{
/// <summary>
/// Check that v6 maps can be loaded. This simply tries to load a file and doesn't do a lot of extra validation.
/// </summary>
/// <remarks>
/// The file was pilfered from content integration tests ("floor3x3.yml") and modified slightly.
/// </remarks>
[Test]
public async Task TestLoadV6()
{
var server = StartServer(new ServerIntegrationOptions {ExtraPrototypes = PrototypeV6});
await server.WaitIdleAsync();
var entMan = server.EntMan;
var mapSys = server.System<SharedMapSystem>();
var loader = server.System<MapLoaderSystem>();
var meta = server.System<MetaDataSystem>();
var tileMan = server.ResolveDependency<ITileDefinitionManager>();
var resourceManager = server.ResolveDependency<IResourceManagerInternal>();
SerializationTestHelper.LoadTileDefs(server.ProtoMan, tileMan, "Space");
var gridPath = new ResPath($"{nameof(MapDataV6Grid)}.yml");
resourceManager.MountString(gridPath.ToString(), MapDataV6Grid);
MapId mapId = default;
EntityUid mapUid = default;
Entity<TransformComponent, EntitySaveTestComponent> map;
Entity<TransformComponent, EntitySaveTestComponent> ent;
Entity<TransformComponent, EntitySaveTestComponent> grid;
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
await server.WaitPost(() => mapUid = mapSys.CreateMap(out mapId));
await server.WaitAssertion(() => Assert.That(loader.TryLoadGrid(mapId, gridPath, out _)));
Assert.That(entMan.Count<LoadedMapComponent>(), Is.EqualTo(0));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(2));
ent = Find(nameof(ent), entMan);
grid = Find(nameof(grid), entMan);
Assert.That(ent.Comp1.ParentUid, Is.EqualTo(grid.Owner));
Assert.That(grid.Comp1.ParentUid, Is.EqualTo(mapUid));
Assert.That(meta.EntityPaused(ent), Is.False);
Assert.That(meta.EntityPaused(grid), Is.False);
Assert.That(meta.EntityPaused(mapUid), Is.False);
Assert.That(entMan.GetComponent<MetaDataComponent>(ent).EntityLifeStage,
Is.EqualTo(EntityLifeStage.MapInitialized));
Assert.That(entMan.GetComponent<MetaDataComponent>(grid).EntityLifeStage,
Is.EqualTo(EntityLifeStage.MapInitialized));
Assert.That(entMan.GetComponent<MetaDataComponent>(mapUid).EntityLifeStage,
Is.EqualTo(EntityLifeStage.MapInitialized));
await server.WaitPost(() => entMan.DeleteEntity(mapUid));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
var mapPath = new ResPath($"{nameof(MapDataV6Map)}.yml");
resourceManager.MountString(mapPath.ToString(), MapDataV6Map);
await server.WaitAssertion(() => Assert.That(loader.TryLoadMap(mapPath, out _, out _)));
Assert.That(entMan.Count<LoadedMapComponent>(), Is.EqualTo(1));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(3));
ent = Find(nameof(ent), entMan);
grid = Find(nameof(grid), entMan);
map = Find(nameof(map), entMan);
Assert.That(ent.Comp1.ParentUid, Is.EqualTo(grid.Owner));
Assert.That(grid.Comp1.ParentUid, Is.EqualTo(map.Owner));
Assert.That(map.Comp1.ParentUid, Is.EqualTo(EntityUid.Invalid));
Assert.That(meta.EntityPaused(ent), Is.True);
Assert.That(meta.EntityPaused(grid), Is.True);
Assert.That(meta.EntityPaused(map), Is.True);
Assert.That(entMan.GetComponent<MetaDataComponent>(ent).EntityLifeStage,
Is.EqualTo(EntityLifeStage.Initialized));
Assert.That(entMan.GetComponent<MetaDataComponent>(grid).EntityLifeStage,
Is.EqualTo(EntityLifeStage.Initialized));
Assert.That(entMan.GetComponent<MetaDataComponent>(map).EntityLifeStage,
Is.EqualTo(EntityLifeStage.Initialized));
await server.WaitPost(() => entMan.DeleteEntity(map));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
// Repeat test, but with the initialize maps option enabled.
// Apparently mounted strings can only be read a single time.
// So have to re-mount them.
var mapPath2 = new ResPath($"{nameof(MapDataV6Map)}2.yml");
resourceManager.MountString(mapPath2.ToString(), MapDataV6Map);
var opts = DeserializationOptions.Default with {InitializeMaps = true};
await server.WaitAssertion(() => Assert.That(loader.TryLoadMap(mapPath2, out _, out _, opts)));
Assert.That(entMan.Count<LoadedMapComponent>(), Is.EqualTo(1));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(3));
ent = Find(nameof(ent), entMan);
grid = Find(nameof(grid), entMan);
map = Find(nameof(map), entMan);
Assert.That(ent.Comp1.ParentUid, Is.EqualTo(grid.Owner));
Assert.That(grid.Comp1.ParentUid, Is.EqualTo(map.Owner));
Assert.That(map.Comp1.ParentUid, Is.EqualTo(EntityUid.Invalid));
Assert.That(meta.EntityPaused(ent), Is.False);
Assert.That(meta.EntityPaused(grid), Is.False);
Assert.That(meta.EntityPaused(map), Is.False);
Assert.That(entMan.GetComponent<MetaDataComponent>(ent).EntityLifeStage,
Is.EqualTo(EntityLifeStage.MapInitialized));
Assert.That(entMan.GetComponent<MetaDataComponent>(grid).EntityLifeStage,
Is.EqualTo(EntityLifeStage.MapInitialized));
Assert.That(entMan.GetComponent<MetaDataComponent>(map).EntityLifeStage,
Is.EqualTo(EntityLifeStage.MapInitialized));
await server.WaitPost(() => entMan.DeleteEntity(map));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
Assert.That(entMan.Count<LoadedMapComponent>(), Is.EqualTo(0));
Assert.That(entMan.Count<MapComponent>(), Is.EqualTo(0));
}
private const string MapDataV6Grid = @"
meta:
format: 6
postmapinit: false
tilemap:
0: Space
11: A
89: B
entities:
- proto: """"
entities:
- uid: 2
components:
- type: MetaData
- type: Transform
parent: invalid
- type: MapGrid
chunks:
-1,-1:
ind: -1,-1
tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAACwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAAWQAAAAAA
version: 6
-1,0:
ind: -1,0
tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAACwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
version: 6
0,0:
ind: 0,0
tiles: CwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
version: 6
0,-1:
ind: 0,-1
tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
version: 6
- type: Broadphase
- type: Physics
bodyStatus: InAir
angularDamping: 0.05
linearDamping: 0.05
fixedRotation: False
bodyType: Dynamic
- type: OccluderTree
- type: EntitySaveTest
id: grid
- proto: V6TestProto
entities:
- uid: 1
components:
- type: Transform
pos: 0.5,0.5
parent: 2
- type: EntitySaveTest
id: ent
";
private const string MapDataV6Map = @"
meta:
format: 6
postmapinit: false
tilemap:
0: Space
11: A
89: B
entities:
- proto: """"
entities:
- uid: 123
components:
- type: MetaData
- type: Transform
- type: Map
mapPaused: True
- type: EntitySaveTest
id: map
- uid: 2
components:
- type: MetaData
- type: Transform
parent: 123
- type: MapGrid
chunks:
-1,-1:
ind: -1,-1
tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAACwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAAWQAAAAAA
version: 6
-1,0:
ind: -1,0
tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAACwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
version: 6
0,0:
ind: 0,0
tiles: CwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
version: 6
0,-1:
ind: 0,-1
tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
version: 6
- type: Broadphase
- type: Physics
bodyStatus: InAir
angularDamping: 0.05
linearDamping: 0.05
fixedRotation: False
bodyType: Dynamic
- type: OccluderTree
- type: EntitySaveTest
id: grid
- proto: V6TestProto
entities:
- uid: 1
components:
- type: Transform
pos: 0.5,0.5
parent: 2
- type: EntitySaveTest
id: ent
";
private const string PrototypeV6 = @"
- type: entity
id: V6TestProto
components:
- type: EntitySaveTest
list: [ 1, 2 ]
- type: testTileDef
id: Space
- type: testTileDef
id: A
- type: testTileDef
id: B
";
}

View File

@@ -0,0 +1,352 @@
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Shared.ContentPack;
using Robust.Shared.EntitySerialization;
using Robust.Shared.EntitySerialization.Components;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Utility;
using static Robust.UnitTesting.Shared.EntitySerialization.EntitySaveTestComponent;
namespace Robust.UnitTesting.Shared.EntitySerialization;
/// <summary>
/// Test that older file formats can still be loaded.
/// </summary>
[TestFixture]
internal sealed partial class BackwardsCompatibilityTest : RobustIntegrationTest
{
/// <summary>
/// Check that v7 maps can be loaded. This just re-uses some map files that are generated by other tests, and then
/// checks that the post-load debug asserts still pass. specifically, it uses the second to last file from
/// <see cref="AutoIncludeSerializationTest"/> and the initial file from
/// <see cref="LifestageSerializationTest.TestMixedLifestageSerialization"/>.
/// </summary>
/// <remarks>
/// At the time of writing, v7 is the current version, but this is here for when the version increases in the future.
/// </remarks>
[Test]
public async Task TestLoadV7()
{
var server = StartServer(new ServerIntegrationOptions { ExtraPrototypes = PrototypeV7 });
await server.WaitIdleAsync();
var entMan = server.EntMan;
var mapSys = server.System<SharedMapSystem>();
var loader = server.System<MapLoaderSystem>();
var tileMan = server.ResolveDependency<ITileDefinitionManager>();
var resourceManager = server.ResolveDependency<IResourceManagerInternal>();
SerializationTestHelper.LoadTileDefs(server.ProtoMan, tileMan, "space");
void AssertCount(int expected) => Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(expected));
Assert.That(entMan.Count<LoadedMapComponent>(), Is.EqualTo(0));
var mapLoadOpts = MapLoadOptions.Default with
{
DeserializationOptions = DeserializationOptions.Default with {LogOrphanedGrids = false}
};
// Test the file from AutoIncludeSerializationTest
{
var path = new ResPath($"{nameof(MapDataV7)}.yml");
resourceManager.MountString(path.ToString(), MapDataV7);
Entity<TransformComponent, EntitySaveTestComponent> grid;
Entity<TransformComponent, EntitySaveTestComponent> onGrid;
Entity<TransformComponent, EntitySaveTestComponent> otherMap;
Entity<TransformComponent, EntitySaveTestComponent> otherEnt;
Entity<TransformComponent, EntitySaveTestComponent> nullSpace;
LoadResult? result = default;
await server.WaitAssertion(() => Assert.That(loader.TryLoadGeneric(path, out result, mapLoadOpts)));
Assert.That(result!.Version, Is.EqualTo(7));
Assert.That(result.Grids, Has.Count.EqualTo(1));
Assert.That(result.Orphans, Is.Empty); // Grid was orphaned, but was adopted after a new map was created
Assert.That(result.Maps, Has.Count.EqualTo(2));
Assert.That(result.NullspaceEntities, Has.Count.EqualTo(1));
Assert.That(entMan.Count<LoadedMapComponent>(), Is.EqualTo(1)); // auto-generated map isn't marked as "loaded"
AssertCount(5);
grid = Find(nameof(grid), entMan);
onGrid = Find(nameof(onGrid), entMan);
otherMap = Find(nameof(otherMap), entMan);
otherEnt = Find(nameof(otherEnt), entMan);
nullSpace = Find(nameof(nullSpace), entMan);
Assert.That(onGrid.Comp1.ParentUid, Is.EqualTo(grid.Owner));
Assert.That(otherEnt.Comp1.ParentUid, Is.EqualTo(otherMap.Owner));
Assert.That(otherMap.Comp1.ParentUid, Is.EqualTo(EntityUid.Invalid));
Assert.That(nullSpace.Comp1.ParentUid, Is.EqualTo(EntityUid.Invalid));
await server.WaitPost(() => entMan.DeleteEntity(otherMap));
await server.WaitPost(() => entMan.DeleteEntity(grid.Comp1.ParentUid));
await server.WaitPost(() => entMan.DeleteEntity(nullSpace));
AssertCount(0);
await server.WaitPost(() => loader.Delete(result));
}
// Test the file from LifestageSerializationTest.TestMixedLifestageSerialization
{
var pathLifestage = new ResPath($"{nameof(MapDataV7Lifestage)}.yml");
resourceManager.MountString(pathLifestage.ToString(), MapDataV7Lifestage);
Entity<TransformComponent, EntitySaveTestComponent> mapA; // preinit Map
Entity<TransformComponent, EntitySaveTestComponent> mapB; // postinit unpaused Map
Entity<TransformComponent, EntitySaveTestComponent> entA; // postinit entity on preinit map
Entity<TransformComponent, EntitySaveTestComponent> entB; // paused entity on postinit unpaused map
Entity<TransformComponent, EntitySaveTestComponent> entC; // preinit entity on postinit map
Entity<TransformComponent, EntitySaveTestComponent> nullA; // postinit nullspace entity
Entity<TransformComponent, EntitySaveTestComponent> nullB; // preinit nullspace entity
Entity<TransformComponent, EntitySaveTestComponent> nullC; // paused postinit nullspace entity
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
LoadResult? result = default;
await server.WaitAssertion(() => Assert.That(loader.TryLoadGeneric(pathLifestage, out result)));
Assert.That(result!.Version, Is.EqualTo(7));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(8));
mapA = Find(nameof(mapA), entMan);
mapB = Find(nameof(mapB), entMan);
entA = Find(nameof(entA), entMan);
entB = Find(nameof(entB), entMan);
entC = Find(nameof(entC), entMan);
nullA = Find(nameof(nullA), entMan);
nullB = Find(nameof(nullB), entMan);
nullC = Find(nameof(nullC), entMan);
AssertPaused(true, mapA, entB, nullC);
AssertPaused(false, mapB, entA, entC, nullA, nullB);
AssertPreInit(true, mapA, entC, nullB);
AssertPreInit(false, mapB, entA, entB, nullA, nullC);
void AssertPaused(bool expected, params EntityUid[] uids)
{
foreach (var uid in uids)
{
Assert.That(entMan.GetComponent<MetaDataComponent>(uid).EntityPaused, Is.EqualTo(expected));
}
}
void AssertPreInit(bool expected, params EntityUid[] uids)
{
foreach (var uid in uids)
{
Assert.That(entMan!.GetComponent<MetaDataComponent>(uid).EntityLifeStage,
expected
? Is.LessThan(EntityLifeStage.MapInitialized)
: Is.EqualTo(EntityLifeStage.MapInitialized));
}
}
await server.WaitPost(() => loader.Delete(result));
}
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
Assert.That(entMan.Count<LoadedMapComponent>(), Is.EqualTo(0));
Assert.That(entMan.Count<MapComponent>(), Is.EqualTo(0));
}
private const string MapDataV7 = @"
meta:
format: 7
category: Unknown
engineVersion: 238.0.0
forkId: """"
forkVersion: """"
time: 12/25/2024 00:40:09
entityCount: 5
maps:
- 3
grids:
- 1
orphans:
- 1
nullspace:
- 5
tilemap:
1: space
0: a
entities:
- proto: """"
entities:
- uid: 1
paused: false
components:
- type: MetaData
name: grid
- type: Transform
parent: invalid
- type: MapGrid
chunks:
0,0:
ind: 0,0
tiles: AAAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAA
version: 6
- type: Broadphase
- type: Physics
- type: Fixtures
fixtures: {}
- type: OccluderTree
- type: EntitySaveTest
list: []
id: grid
- uid: 2
mapInit: true
components:
- type: MetaData
- type: Transform
pos: 0.5,0.5
parent: 1
- type: EntitySaveTest
list: []
entity: 3
id: onGrid
- uid: 3
mapInit: true
components:
- type: MetaData
name: Map Entity
- type: Transform
- type: Map
mapInitialized: True
- type: GridTree
- type: Broadphase
- type: OccluderTree
- type: EntitySaveTest
list: []
id: otherMap
- uid: 4
mapInit: true
components:
- type: MetaData
- type: Transform
parent: 3
- type: EntitySaveTest
list: []
entity: 5
id: otherEnt
- uid: 5
mapInit: true
components:
- type: MetaData
- type: Transform
- type: EntitySaveTest
list: []
id: nullSpace
";
private const string MapDataV7Lifestage = @"
meta:
format: 7
category: Unknown
engineVersion: 238.0.0
forkId: """"
forkVersion: """"
time: 12/25/2024 00:50:59
entityCount: 8
maps:
- 1
- 3
grids: []
orphans: []
nullspace:
- 6
- 7
- 8
tilemap: {}
entities:
- proto: """"
entities:
- uid: 1
components:
- type: MetaData
name: Map Entity
- type: Transform
- type: Map
mapPaused: True
- type: GridTree
- type: Broadphase
- type: OccluderTree
- type: EntitySaveTest
list: []
id: mapA
- uid: 2
mapInit: true
components:
- type: MetaData
- type: Transform
parent: 1
- type: EntitySaveTest
list: []
id: entA
- uid: 3
mapInit: true
components:
- type: MetaData
name: Map Entity
- type: Transform
- type: Map
mapInitialized: True
- type: GridTree
- type: Broadphase
- type: OccluderTree
- type: EntitySaveTest
list: []
id: mapB
- uid: 4
mapInit: true
paused: true
components:
- type: MetaData
- type: Transform
parent: 3
- type: EntitySaveTest
list: []
id: entB
- uid: 5
paused: false
components:
- type: MetaData
- type: Transform
parent: 3
- type: EntitySaveTest
list: []
id: entC
- uid: 6
mapInit: true
components:
- type: MetaData
- type: Transform
- type: EntitySaveTest
list: []
id: nullA
- uid: 7
paused: false
components:
- type: MetaData
- type: Transform
- type: EntitySaveTest
list: []
id: nullB
- uid: 8
mapInit: true
paused: true
components:
- type: MetaData
- type: Transform
- type: EntitySaveTest
list: []
id: nullC
";
private const string PrototypeV7 = @"
- type: testTileDef
id: space
- type: testTileDef
id: a
";
}

View File

@@ -0,0 +1,137 @@
using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Shared.EntitySerialization;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
namespace Robust.UnitTesting.Shared.EntitySerialization;
[TestFixture]
internal sealed partial class CategorizationTest : RobustIntegrationTest
{
private const string TestTileDefId = "a";
private const string TestPrototypes = $@"
- type: testTileDef
id: space
- type: testTileDef
id: {TestTileDefId}
";
/// <summary>
/// Check that file categories are correctly assigned when saving & loading different combinations of entites.
/// </summary>
[Test]
[TestOf(typeof(FileCategory))]
public async Task TestCategorization()
{
var server = StartServer(new() { Pool = false, ExtraPrototypes = TestPrototypes }); // Pool=false due to TileDef registration
await server.WaitIdleAsync();
var entMan = server.EntMan;
var meta = server.System<MetaDataSystem>();
var mapSys = server.System<SharedMapSystem>();
var loader = server.System<MapLoaderSystem>();
var mapMan = server.ResolveDependency<IMapManager>();
var tileMan = server.ResolveDependency<ITileDefinitionManager>();
var path = new ResPath($"{nameof(TestCategorization)}.yml");
SerializationTestHelper.LoadTileDefs(server.ProtoMan, tileMan, "space");
var tDef = server.ProtoMan.Index<TileDef>(TestTileDefId);
EntityUid mapA = default;
EntityUid mapB = default;
EntityUid gridA = default; // grid on map A
EntityUid gridB = default; // grid on map B
EntityUid entA = default; // ent on grid A
EntityUid entB = default; // ent on grid B
EntityUid entC = default; // a separate entity on grid B
EntityUid child = default; // child of entB
EntityUid @null = default; // nullspace entity
await server.WaitPost(() =>
{
mapA = mapSys.CreateMap(out var mapIdA);
mapB = mapSys.CreateMap(out var mapIdB);
var gridEntA = mapMan.CreateGridEntity(mapIdA);
var gridEntB = mapMan.CreateGridEntity(mapIdB);
mapSys.SetTile(gridEntA, Vector2i.Zero, new Tile(tDef.TileId));
mapSys.SetTile(gridEntB, Vector2i.Zero, new Tile(tDef.TileId));
gridA = gridEntA.Owner;
gridB = gridEntB.Owner;
entA = entMan.SpawnEntity(null, new EntityCoordinates(gridA, 0.5f, 0.5f));
entB = entMan.SpawnEntity(null, new EntityCoordinates(gridB, 0.5f, 0.5f));
entC = entMan.SpawnEntity(null, new EntityCoordinates(gridB, 0.5f, 0.5f));
child = entMan.SpawnEntity(null, new EntityCoordinates(entB, 0.5f, 0.5f));
@null = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
});
FileCategory Save(params EntityUid[] ents)
{
FileCategory cat = FileCategory.Unknown;
Assert.That(loader.TrySaveGeneric(ents.ToHashSet(), path, out cat));
return cat;
}
async Task<LoadResult> Load(FileCategory expected, int count)
{
var opts = MapLoadOptions.Default with
{
ExpectedCategory = expected,
DeserializationOptions = DeserializationOptions.Default with { LogOrphanedGrids = false}
};
LoadResult? result = null;
await server.WaitAssertion(() => Assert.That(loader.TryLoadGeneric(path, out result, opts)));
Assert.That(result!.Category, Is.EqualTo(expected));
Assert.That(result.Entities, Has.Count.EqualTo(count));
return result;
}
async Task SaveAndLoad(FileCategory expected, int count, params EntityUid[] ents)
{
var cat = Save(ents);
Assert.That(cat, Is.EqualTo(expected));
var result = await Load(expected, count);
await server.WaitPost(() => loader.Delete(result));
}
// Saving a single entity works as expected, even if it also serializes their children
await SaveAndLoad(FileCategory.Entity, 1, entA);
await SaveAndLoad(FileCategory.Entity, 2, entB);
await SaveAndLoad(FileCategory.Entity, 1, child);
// Including nullspace entities doesn't change the category, though a file containing only null-space entities
// is "unkown". Maybe in future they will get their own category
await SaveAndLoad(FileCategory.Entity, 2, entA, @null);
await SaveAndLoad(FileCategory.Entity, 3, entB, @null);
await SaveAndLoad(FileCategory.Entity, 2, child, @null);
await SaveAndLoad(FileCategory.Unknown, 1, @null);
// More than one entity is unknown
await SaveAndLoad(FileCategory.Unknown, 3, entA, entB);
await SaveAndLoad(FileCategory.Unknown, 4, entA, entB, @null);
// Saving grids works as expected. All counts are 1 higher than expected due to a map being automatically created.
await SaveAndLoad(FileCategory.Grid, 3, gridA);
await SaveAndLoad(FileCategory.Grid, 5, gridB);
await SaveAndLoad(FileCategory.Grid, 4, gridA, @null);
await SaveAndLoad(FileCategory.Grid, 6, gridB, @null);
// And saving maps also works
await SaveAndLoad(FileCategory.Map, 3, mapA);
await SaveAndLoad(FileCategory.Map, 5, mapB);
await SaveAndLoad(FileCategory.Map, 4, mapA, @null);
await SaveAndLoad(FileCategory.Map, 6, mapB, @null);
// Combinations of grids, entities, and maps, are unknown
await SaveAndLoad(FileCategory.Unknown, 4, mapA, child);
await SaveAndLoad(FileCategory.Unknown, 4, gridA, child);
await SaveAndLoad(FileCategory.Unknown, 8, gridA, mapB);
await SaveAndLoad(FileCategory.Unknown, 5, mapA, child, @null);
await SaveAndLoad(FileCategory.Unknown, 5, gridA, child, @null);
await SaveAndLoad(FileCategory.Unknown, 9, gridA, mapB, @null);
}
}

View File

@@ -0,0 +1,374 @@
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Shared.EntitySerialization;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Utility;
using static Robust.UnitTesting.Shared.EntitySerialization.EntitySaveTestComponent;
namespace Robust.UnitTesting.Shared.EntitySerialization;
[TestFixture]
internal sealed partial class LifestageSerializationTest : RobustIntegrationTest
{
/// <summary>
/// Check that whether or not an entity has been paused or map-initialized is preserved across saves & loads.
/// </summary>
[Test]
public async Task TestLifestageSerialization()
{
var server = StartServer();
await server.WaitIdleAsync();
var entMan = server.EntMan;
var mapSys = server.System<SharedMapSystem>();
var loader = server.System<MapLoaderSystem>();
var preInitPath = new ResPath($"{nameof(TestLifestageSerialization)}_preInit.yml");
var postInitPath = new ResPath($"{nameof(TestLifestageSerialization)}_postInit.yml");
var pausedPostInitPath = new ResPath($"{nameof(TestLifestageSerialization)}_paused.yml");
// Create a pre-init map, and spawn multiple entities on it
Entity<TransformComponent, EntitySaveTestComponent> map = default;
Entity<TransformComponent, EntitySaveTestComponent> entA = default;
Entity<TransformComponent, EntitySaveTestComponent> entB = default;
Entity<TransformComponent, EntitySaveTestComponent> childA = default;
Entity<TransformComponent, EntitySaveTestComponent> childB = default;
await server.WaitPost(() =>
{
var mapUid = mapSys.CreateMap(out var mapId, runMapInit: false);
var entAUid = entMan.SpawnEntity(null, new MapCoordinates(0, 0, mapId));
var entBUid = entMan.SpawnEntity(null, new MapCoordinates(0, 0, mapId));
var childAUid = entMan.SpawnEntity(null, new EntityCoordinates(entAUid, 0, 0));
var childBUid = entMan.SpawnEntity(null, new EntityCoordinates(entBUid, 0, 0));
map = Get(mapUid, entMan);
entA = Get(entAUid, entMan);
entB = Get(entBUid, entMan);
childA = Get(childAUid, entMan);
childB = Get(childBUid, entMan);
map.Comp2.Id = nameof(map);
entA.Comp2.Id = nameof(entA);
entB.Comp2.Id = nameof(entB);
childA.Comp2.Id = nameof(childA);
childB.Comp2.Id = nameof(childB);
});
void AssertPaused(bool expected, params EntityUid[] uids)
{
foreach (var uid in uids)
{
Assert.That(entMan.GetComponent<MetaDataComponent>(uid).EntityPaused, Is.EqualTo(expected));
}
}
void AssertPreInit(bool expected, params EntityUid[] uids)
{
foreach (var uid in uids)
{
Assert.That(entMan!.GetComponent<MetaDataComponent>(uid).EntityLifeStage,
expected
? Is.LessThan(EntityLifeStage.MapInitialized)
: Is.EqualTo(EntityLifeStage.MapInitialized));
}
}
// All entities should initially be un-initialized and paused.
AssertPaused(true, map, entA, entB, childA, childB);
AssertPreInit(true, map, entA, entB, childA, childB);
Assert.That(loader.TrySaveMap(map, preInitPath));
async Task Delete()
{
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(5));
await server.WaitPost(() => entMan.DeleteEntity(map));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
}
async Task Load(ResPath f, DeserializationOptions? o)
{
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
await server.WaitAssertion(() => Assert.That(loader.TryLoadMap(f, out _, out _, o)));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(5));
}
void FindAll()
{
map = Find(nameof(map), entMan);
entA = Find(nameof(entA), entMan);
entB = Find(nameof(entB), entMan);
childA = Find(nameof(childA), entMan);
childB = Find(nameof(childB), entMan);
}
async Task Reload(ResPath f, DeserializationOptions? o = null)
{
await Delete();
await Load(f, o);
FindAll();
}
// Saving and loading the pre-init map should have no effect.
await Reload(preInitPath);
AssertPaused(true, map, entA, entB, childA, childB);
AssertPreInit(true, map, entA, entB, childA, childB);
// Saving and loading with the map-init option set to true should initialize & unpause all entities
var opts = DeserializationOptions.Default with {InitializeMaps = true};
await Reload(preInitPath, opts);
AssertPaused(false, map, entA, entB, childA, childB);
AssertPreInit(false, map, entA, entB, childA, childB);
Assert.That(loader.TrySaveMap(map, postInitPath));
// re-loading the post-init map should keep everything initialized, even without explicitly asking to initialize maps.
await Reload(postInitPath);
AssertPaused(false, map, entA, entB, childA, childB);
AssertPreInit(false, map, entA, entB, childA, childB);
// Load & initialize a pre-init map, but with the pause maps option enabled.
opts = DeserializationOptions.Default with {InitializeMaps = true, PauseMaps = true};
await Reload(preInitPath, opts);
AssertPaused(true, map, entA, entB, childA, childB);
AssertPreInit(false, map, entA, entB, childA, childB);
Assert.That(loader.TrySaveMap(map, pausedPostInitPath));
// The pause map option also works when loading un-paused post-init maps
opts = DeserializationOptions.Default with {PauseMaps = true};
await Reload(postInitPath, opts);
AssertPaused(true, map, entA, entB, childA, childB);
AssertPreInit(false, map, entA, entB, childA, childB);
// loading & initializing a post-init map should cause no issues.
opts = DeserializationOptions.Default with {InitializeMaps = true};
await Reload(postInitPath, opts);
AssertPaused(false, map, entA, entB, childA, childB);
AssertPreInit(false, map, entA, entB, childA, childB);
// Loading a paused post init map does NOT automatically un-pause entities
await Reload(pausedPostInitPath);
AssertPaused(true, map, entA, entB, childA, childB);
AssertPreInit(false, map, entA, entB, childA, childB);
// The above holds even if we are explicitly initialising maps.
opts = DeserializationOptions.Default with {InitializeMaps = true};
await Reload(pausedPostInitPath, opts);
AssertPaused(true, map, entA, entB, childA, childB);
AssertPreInit(false, map, entA, entB, childA, childB);
// And re-paused an already paused map should have no impact.
opts = DeserializationOptions.Default with {InitializeMaps = true, PauseMaps = true};
await Reload(pausedPostInitPath, opts);
AssertPaused(true, map, entA, entB, childA, childB);
AssertPreInit(false, map, entA, entB, childA, childB);
}
/// <summary>
/// Variant of <see cref="TestLifestageSerialization"/> that has multiple maps and combinations. E.g., a single
/// paused entity on an un-paused map.
/// </summary>
[Test]
public async Task TestMixedLifestageSerialization()
{
var server = StartServer();
await server.WaitIdleAsync();
var entMan = server.EntMan;
var meta = server.System<MetaDataSystem>();
var mapSys = server.System<SharedMapSystem>();
var loader = server.System<MapLoaderSystem>();
var path = new ResPath($"{nameof(TestMixedLifestageSerialization)}.yml");
var altPath = new ResPath($"{nameof(TestMixedLifestageSerialization)}_alt.yml");
Entity<TransformComponent, EntitySaveTestComponent> mapA = default; // preinit Map
Entity<TransformComponent, EntitySaveTestComponent> mapB = default; // postinit unpaused Map
Entity<TransformComponent, EntitySaveTestComponent> entA = default; // postinit entity on preinit map
Entity<TransformComponent, EntitySaveTestComponent> entB = default; // paused entity on postinit unpaused map
Entity<TransformComponent, EntitySaveTestComponent> entC = default; // preinit entity on postinit map
Entity<TransformComponent, EntitySaveTestComponent> nullA = default; // postinit nullspace entity
Entity<TransformComponent, EntitySaveTestComponent> nullB = default; // preinit nullspace entity
Entity<TransformComponent, EntitySaveTestComponent> nullC = default; // paused postinit nullspace entity
await server.WaitPost(() =>
{
var mapAUid = mapSys.CreateMap(out var mapIdA, runMapInit: false);
var mapBUid = mapSys.CreateMap(out var mapIdB, runMapInit: true);
var entAUid = entMan.SpawnEntity(null, new MapCoordinates(0, 0, mapIdA));
entMan.RunMapInit(entAUid, entMan.GetComponent<MetaDataComponent>(entAUid));
meta.SetEntityPaused(entAUid, false);
var entBUid = entMan.SpawnEntity(null, new MapCoordinates(0, 0, mapIdB));
meta.SetEntityPaused(entBUid, true);
var entCUid = entMan.CreateEntityUninitialized(null, new MapCoordinates(0, 0, mapIdB));
entMan.InitializeAndStartEntity(entCUid, doMapInit: false);
var nullAUid = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
var nullBUid = entMan.CreateEntityUninitialized(null, MapCoordinates.Nullspace);
entMan.InitializeAndStartEntity(nullBUid, doMapInit: false);
var nullCUid = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
meta.SetEntityPaused(nullCUid, true);
mapA = Get(mapAUid, entMan);
mapB = Get(mapBUid, entMan);
entA = Get(entAUid, entMan);
entB = Get(entBUid, entMan);
entC = Get(entCUid, entMan);
nullA = Get(nullAUid, entMan);
nullB = Get(nullBUid, entMan);
nullC = Get(nullCUid, entMan);
mapA.Comp2.Id = nameof(mapA);
mapB.Comp2.Id = nameof(mapB);
entA.Comp2.Id = nameof(entA);
entB.Comp2.Id = nameof(entB);
entC.Comp2.Id = nameof(entC);
nullA.Comp2.Id = nameof(nullA);
nullB.Comp2.Id = nameof(nullB);
nullC.Comp2.Id = nameof(nullC);
});
string? Name(EntityUid uid)
{
return entMan.GetComponentOrNull<EntitySaveTestComponent>(uid)?.Id;
}
void AssertPaused(bool expected, params EntityUid[] uids)
{
foreach (var uid in uids)
{
Assert.That(entMan.GetComponent<MetaDataComponent>(uid).EntityPaused, Is.EqualTo(expected), Name(uid));
}
}
void AssertPreInit(bool expected, params EntityUid[] uids)
{
foreach (var uid in uids)
{
Assert.That(entMan!.GetComponent<MetaDataComponent>(uid).EntityLifeStage,
expected
? Is.LessThan(EntityLifeStage.MapInitialized)
: Is.EqualTo(EntityLifeStage.MapInitialized));
}
}
void Save(ResPath f)
{
Assert.That(loader.TrySaveGeneric([mapA, mapB, nullA, nullB, nullC], f, out _));
}
async Task Delete()
{
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(8));
await server.WaitPost(() => entMan.DeleteEntity(mapA));
await server.WaitPost(() => entMan.DeleteEntity(mapB));
await server.WaitPost(() => entMan.DeleteEntity(nullA));
await server.WaitPost(() => entMan.DeleteEntity(nullB));
await server.WaitPost(() => entMan.DeleteEntity(nullC));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
}
async Task Load(ResPath f, DeserializationOptions? o)
{
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
var oo = MapLoadOptions.Default with
{
DeserializationOptions = o ?? DeserializationOptions.Default
};
await server.WaitAssertion(() => Assert.That(loader.TryLoadGeneric(f, out _, oo)));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(8));
}
void FindAll()
{
mapA = Find(nameof(mapA), entMan);
mapB = Find(nameof(mapB), entMan);
entA = Find(nameof(entA), entMan);
entB = Find(nameof(entB), entMan);
entC = Find(nameof(entC), entMan);
nullA = Find(nameof(nullA), entMan);
nullB = Find(nameof(nullB), entMan);
nullC = Find(nameof(nullC), entMan);
}
async Task Reload(ResPath f, DeserializationOptions? o = null)
{
await Delete();
await Load(f, o);
FindAll();
}
// All entities should initially be in their respective expected states.
// entC (pre-mapinit entity on a post-mapinit map) is a bit fucky, and I don't know if that should even be allowed.
// Note that its just pre-init, not paused, as pre-mapinit entities get paused due to the maps state, not as a general result of being pre-mapinit.
// If this ever changes, these assers need fixing.
AssertPaused(true, mapA, entB, nullC);
AssertPaused(false, mapB, entA, entC, nullA, nullB);
AssertPreInit(true, mapA, entC, nullB);
AssertPreInit(false, mapB, entA, entB, nullA, nullC);
// Saving and re-loading entities should leave their metadata unchanged.
Save(path);
await Reload(path);
AssertPaused(true, mapA, entB, nullC);
AssertPaused(false, mapB, entA, entC, nullA, nullB);
AssertPreInit(true, mapA, entC, nullB);
AssertPreInit(false, mapB, entA, entB, nullA, nullC);
// reload maps with the mapinit option. This should only affect mapA, as entA is the only one on the map and it
// is already initialized,
var opts = DeserializationOptions.Default with {InitializeMaps = true};
await Reload(path, opts);
AssertPaused(true, entB, nullC);
AssertPaused(false, mapA, mapB, entA, entC, nullA, nullB);
AssertPreInit(true, entC, nullB);
AssertPreInit(false, mapA, mapB, entA, entB, nullA, nullC);
// Reloading the new configuration changes nothing
Save(altPath);
await Reload(altPath, opts);
AssertPaused(true, entB, nullC);
AssertPaused(false, mapA, mapB, entA, entC, nullA, nullB);
AssertPreInit(true, entC, nullB);
AssertPreInit(false, mapA, mapB, entA, entB, nullA, nullC);
// Pause all maps. This will not actually pause entityA, as mapA is already paused (due to being pre-init), so
// it will not iterate through its children. Maybe this will change in future, but I don't think we should even
// be trying to actively support having post-init entities on a pre-init map. This is subject to maybe change
// one day, though if it does the option should be changed to PauseEntities to clarify that it will pause ALL
// entities, not just maps.
opts = DeserializationOptions.Default with {PauseMaps = true};
await Reload(path, opts);
AssertPaused(true, mapA, mapB, entC, entB, nullC);
AssertPaused(false, entA, nullA, nullB);
AssertPreInit(true, mapA, entC, nullB);
AssertPreInit(false, mapB, entA, entB, nullA, nullC);
// Reloading the new configuration changes nothing
Save(altPath);
await Reload(altPath, opts);
AssertPaused(true, mapA, mapB, entC, entB, nullC);
AssertPaused(false, entA, nullA, nullB);
AssertPreInit(true, mapA, entC, nullB);
AssertPreInit(false, mapB, entA, entB, nullA, nullC);
// Initialise and pause all maps. Similar to the previous test with entA, this will not affect entC even
// though it is pre-init, because it is on a post-init map. Again, this is subject to maybe change one day.
// Though if it does, the option should be changed to MapInitializeEntities to clarify that it will mapinit ALL
// entities, not just maps.
opts = DeserializationOptions.Default with {InitializeMaps = true, PauseMaps = true};
await Reload(path, opts);
AssertPaused(true, mapA, mapB, entB, entC, nullC);
AssertPaused(false, entA, nullA, nullB);
AssertPreInit(true, entC, nullB);
AssertPreInit(false, mapA, mapB, entA, entB, nullA, nullC);
// Reloading the new configuration changes nothing
Save(altPath);
await Reload(altPath, opts);
AssertPaused(true, mapA, mapB, entB, entC, nullC);
AssertPaused(false, entA, nullA, nullB);
AssertPreInit(true, entC, nullB);
AssertPreInit(false, mapA, mapB, entA, entB, nullA, nullC);
}
}

View File

@@ -0,0 +1,239 @@
using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Shared.EntitySerialization;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using static Robust.UnitTesting.Shared.EntitySerialization.EntitySaveTestComponent;
namespace Robust.UnitTesting.Shared.EntitySerialization;
/// <summary>
/// Test that loading a pre-init map/grid onto a post-init map should initialize, while loading a post-init map/grid
/// onto a paused map should pause it.
/// </summary>
[TestFixture]
internal sealed partial class MapMergeTest : RobustIntegrationTest
{
private const string TestTileDefId = "a";
private const string TestPrototypes = $@"
- type: testTileDef
id: space
- type: testTileDef
id: {TestTileDefId}
";
[Test]
public async Task TestMapMerge()
{
var server = StartServer(new() { Pool = false, ExtraPrototypes = TestPrototypes }); // Pool=false due to TileDef registration
await server.WaitIdleAsync();
var entMan = server.EntMan;
var mapSys = server.System<SharedMapSystem>();
var loader = server.System<MapLoaderSystem>();
var mapMan = server.ResolveDependency<IMapManager>();
var tileMan = server.ResolveDependency<ITileDefinitionManager>();
var mapPath = new ResPath($"{nameof(TestMapMerge)}_map.yml");
var gridPath = new ResPath($"{nameof(TestMapMerge)}_grid.yml");
SerializationTestHelper.LoadTileDefs(server.ProtoMan, tileMan, "space");
var tDef = server.ProtoMan.Index<TileDef>(TestTileDefId);
MapId mapId = default;
Entity<TransformComponent, EntitySaveTestComponent> map = default;
Entity<TransformComponent, EntitySaveTestComponent> ent = default;
Entity<TransformComponent, EntitySaveTestComponent> grid = default;
await server.WaitPost(() =>
{
var mapUid = mapSys.CreateMap(out mapId, runMapInit: false);
var gridEnt = mapMan.CreateGridEntity(mapId);
mapSys.SetTile(gridEnt, Vector2i.Zero, new Tile(tDef.TileId));
var entUid = entMan.SpawnEntity(null, new MapCoordinates(10, 10, mapId));
map = Get(mapUid, entMan);
ent = Get(entUid, entMan);
grid = Get(gridEnt.Owner, entMan);
});
void AssertPaused(EntityUid uid, bool expected = true)
{
Assert.That(entMan.GetComponent<MetaDataComponent>(uid).EntityPaused, Is.EqualTo(expected));
}
void AssertPreInit(EntityUid uid, bool expected = true)
{
Assert.That(entMan!.GetComponent<MetaDataComponent>(uid).EntityLifeStage,
expected
? Is.LessThan(EntityLifeStage.MapInitialized)
: Is.EqualTo(EntityLifeStage.MapInitialized));
}
map.Comp2!.Id = nameof(map);
ent.Comp2!.Id = nameof(ent);
grid.Comp2!.Id = nameof(grid);
AssertPaused(map);
AssertPreInit(map);
AssertPaused(ent);
AssertPreInit(ent);
AssertPaused(grid);
AssertPreInit(grid);
// Save then delete everything
await server.WaitAssertion(() => Assert.That(loader.TrySaveMap(map, mapPath)));
await server.WaitAssertion(() => Assert.That(loader.TrySaveGrid(grid, gridPath)));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(3));
await server.WaitPost(() => mapSys.DeleteMap(mapId));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
// Load a grid onto a pre-init map.
await server.WaitPost(() => mapSys.CreateMap(out mapId, runMapInit: false));
Assert.That(mapSys.IsInitialized(mapId), Is.False);
Assert.That(mapSys.IsPaused(mapId), Is.True);
await server.WaitAssertion(() => Assert.That(loader.TryLoadGrid(mapId, gridPath, out _)));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(1));
grid = Find(nameof(grid), entMan);
AssertPaused(grid);
AssertPreInit(grid);
await server.WaitPost(() => mapSys.DeleteMap(mapId));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
// Merge a map onto a pre-init map.
await server.WaitPost(() => mapSys.CreateMap(out mapId, runMapInit: false));
Assert.That(mapSys.IsInitialized(mapId), Is.False);
Assert.That(mapSys.IsPaused(mapId), Is.True);
await server.WaitAssertion(() => Assert.That(loader.TryMergeMap(mapId, mapPath, out _)));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(2)); // The loaded map entity gets deleted after merging
ent = Find(nameof(ent), entMan);
grid = Find(nameof(grid), entMan);
AssertPaused(grid);
AssertPreInit(grid);
AssertPaused(ent);
AssertPreInit(ent);
await server.WaitPost(() => mapSys.DeleteMap(mapId));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
// Load a grid onto a post-init map.
await server.WaitPost(() => mapSys.CreateMap(out mapId, runMapInit: true));
Assert.That(mapSys.IsInitialized(mapId), Is.True);
Assert.That(mapSys.IsPaused(mapId), Is.False);
await server.WaitAssertion(() => Assert.That(loader.TryLoadGrid(mapId, gridPath, out _)));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(1));
grid = Find(nameof(grid), entMan);
AssertPaused(grid, false);
AssertPreInit(grid, false);
await server.WaitPost(() => mapSys.DeleteMap(mapId));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
// Merge a map onto a post-init map.
await server.WaitPost(() => mapSys.CreateMap(out mapId, runMapInit: true));
Assert.That(mapSys.IsInitialized(mapId), Is.True);
Assert.That(mapSys.IsPaused(mapId), Is.False);
await server.WaitAssertion(() => Assert.That(loader.TryMergeMap(mapId, mapPath, out _)));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(2));
ent = Find(nameof(ent), entMan);
grid = Find(nameof(grid), entMan);
AssertPaused(grid, false);
AssertPreInit(grid, false);
AssertPaused(ent, false);
AssertPreInit(ent, false);
await server.WaitPost(() => mapSys.DeleteMap(mapId));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
// Load a grid onto a paused post-init map.
await server.WaitPost(() => mapSys.CreateMap(out mapId, runMapInit: true));
await server.WaitPost(() => mapSys.SetPaused(mapId, true));
Assert.That(mapSys.IsInitialized(mapId), Is.True);
Assert.That(mapSys.IsPaused(mapId), Is.True);
await server.WaitAssertion(() => Assert.That(loader.TryLoadGrid(mapId, gridPath, out _)));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(1));
grid = Find(nameof(grid), entMan);
AssertPaused(grid);
AssertPreInit(grid, false);
await server.WaitPost(() => mapSys.DeleteMap(mapId));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
// Merge a map onto a paused post-init map.
await server.WaitPost(() => mapSys.CreateMap(out mapId, runMapInit: true));
await server.WaitPost(() => mapSys.SetPaused(mapId, true));
Assert.That(mapSys.IsInitialized(mapId), Is.True);
Assert.That(mapSys.IsPaused(mapId), Is.True);
await server.WaitAssertion(() => Assert.That(loader.TryMergeMap(mapId, mapPath, out _)));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(2));
ent = Find(nameof(ent), entMan);
grid = Find(nameof(grid), entMan);
AssertPaused(grid);
AssertPreInit(grid, false);
AssertPaused(ent);
AssertPreInit(ent, false);
await server.WaitPost(() => mapSys.DeleteMap(mapId));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
// Check that the map initialization deserialziation options have no effect.
// We are loading onto an existing map, deserialization shouldn't modify it directly.
// Load a grid onto a pre-init map, with InitializeMaps = true
await server.WaitPost(() => mapSys.CreateMap(out mapId, runMapInit: false));
Assert.That(mapSys.IsInitialized(mapId), Is.False);
Assert.That(mapSys.IsPaused(mapId), Is.True);
var opts = DeserializationOptions.Default with {InitializeMaps = true};
await server.WaitAssertion(() => Assert.That(loader.TryLoadGrid(mapId, gridPath, out _, opts)));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(1));
grid = Find(nameof(grid), entMan);
AssertPaused(grid);
AssertPreInit(grid);
await server.WaitPost(() => mapSys.DeleteMap(mapId));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
// Merge a map onto a pre-init map, with InitializeMaps = true
await server.WaitPost(() => mapSys.CreateMap(out mapId, runMapInit: false));
Assert.That(mapSys.IsInitialized(mapId), Is.False);
Assert.That(mapSys.IsPaused(mapId), Is.True);
opts = DeserializationOptions.Default with {InitializeMaps = true};
await server.WaitAssertion(() => Assert.That(loader.TryMergeMap(mapId, mapPath, out _, opts)));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(2)); // The loaded map entity gets deleted after merging
ent = Find(nameof(ent), entMan);
grid = Find(nameof(grid), entMan);
AssertPaused(grid);
AssertPreInit(grid);
AssertPaused(ent);
AssertPreInit(ent);
await server.WaitPost(() => mapSys.DeleteMap(mapId));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
// Load a grid onto a post-init map, with PauseMaps = true
await server.WaitPost(() => mapSys.CreateMap(out mapId, runMapInit: true));
Assert.That(mapSys.IsInitialized(mapId), Is.True);
Assert.That(mapSys.IsPaused(mapId), Is.False);
opts = DeserializationOptions.Default with {PauseMaps = true};
await server.WaitAssertion(() => Assert.That(loader.TryLoadGrid(mapId, gridPath, out _, opts)));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(1));
grid = Find(nameof(grid), entMan);
AssertPaused(grid, false);
AssertPreInit(grid, false);
await server.WaitPost(() => mapSys.DeleteMap(mapId));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
// Load a grid onto a post-init map, with PauseMaps = true
await server.WaitPost(() => mapSys.CreateMap(out mapId, runMapInit: true));
Assert.That(mapSys.IsInitialized(mapId), Is.True);
Assert.That(mapSys.IsPaused(mapId), Is.False);
opts = DeserializationOptions.Default with {PauseMaps = true};
await server.WaitAssertion(() => Assert.That(loader.TryMergeMap(mapId, mapPath, out _, opts)));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(2));
ent = Find(nameof(ent), entMan);
grid = Find(nameof(grid), entMan);
AssertPaused(grid, false);
AssertPreInit(grid, false);
AssertPaused(ent, false);
AssertPreInit(ent, false);
await server.WaitPost(() => mapSys.DeleteMap(mapId));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
}
}

View File

@@ -0,0 +1,242 @@
using System.Numerics;
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Shared.EntitySerialization;
using Robust.Shared.EntitySerialization.Components;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using static Robust.UnitTesting.Shared.EntitySerialization.EntitySaveTestComponent;
namespace Robust.UnitTesting.Shared.EntitySerialization;
[TestFixture]
internal sealed partial class OrphanSerializationTest : RobustIntegrationTest
{
private const string TestTileDefId = "a";
private const string TestPrototypes = $@"
- type: testTileDef
id: space
- type: testTileDef
id: {TestTileDefId}
";
/// <summary>
/// Check that we can save & load a file containing multiple orphaned (non-grid) entities.
/// </summary>
[Test]
public async Task TestMultipleOrphanSerialization()
{
var server = StartServer();
await server.WaitIdleAsync();
var entMan = server.EntMan;
var mapSys = server.System<SharedMapSystem>();
var loader = server.System<MapLoaderSystem>();
var xform = server.System<SharedTransformSystem>();
var pathA = new ResPath($"{nameof(TestMultipleOrphanSerialization)}_A.yml");
var pathB = new ResPath($"{nameof(TestMultipleOrphanSerialization)}_B.yml");
var pathCombined = new ResPath($"{nameof(TestMultipleOrphanSerialization)}_C.yml");
// Spawn multiple entities on a map
MapId mapId = default;
Entity<TransformComponent, EntitySaveTestComponent> entA = default;
Entity<TransformComponent, EntitySaveTestComponent> entB = default;
Entity<TransformComponent, EntitySaveTestComponent> child = default;
await server.WaitPost(() =>
{
mapSys.CreateMap(out mapId);
var entAUid = entMan.SpawnEntity(null, new MapCoordinates(0, 0, mapId));
var entBUid = entMan.SpawnEntity(null, new MapCoordinates(0, 0, mapId));
var childUid = entMan.SpawnEntity(null, new EntityCoordinates(entBUid, 0, 0));
entA = Get(entAUid, entMan);
entB = Get(entBUid, entMan);
child = Get(childUid, entMan);
entA.Comp2.Id = nameof(entA);
entB.Comp2.Id = nameof(entB);
child.Comp2.Id = nameof(child);
xform.SetLocalPosition(entB.Owner, new (100,100));
});
// Entities are not in null-space
Assert.That(entA.Comp1!.ParentUid, Is.Not.EqualTo(EntityUid.Invalid));
Assert.That(entB.Comp1!.ParentUid, Is.Not.EqualTo(EntityUid.Invalid));
Assert.That(child.Comp1!.ParentUid, Is.EqualTo(entB.Owner));
// Save the entities without their map
Assert.That(loader.TrySaveEntity(entA, pathA));
Assert.That(loader.TrySaveEntity(entB, pathB));
Assert.That(loader.TrySaveGeneric([entA.Owner, entB.Owner], pathCombined, out var cat));
Assert.That(cat, Is.EqualTo(FileCategory.Unknown));
// Delete all the entities.
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(3));
await server.WaitPost(() => mapSys.DeleteMap(mapId));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
// Load in the file containing only entA.
await server.WaitAssertion(() => Assert.That(loader.TryLoadEntity(pathA, out _)));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(1));
entA = Find(nameof(entA), entMan);
Assert.That(entA.Comp1!.ParentUid, Is.EqualTo(EntityUid.Invalid));
await server.WaitPost(() => entMan.DeleteEntity(entA));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
// Load in the file containing entB and its child
await server.WaitAssertion(() => Assert.That(loader.TryLoadEntity(pathB, out _)));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(2));
entB = Find(nameof(entB), entMan);
child = Find(nameof(child), entMan);
// Even though the entities are in null-space their local position is preserved.
// This is so that you can save multiple entities on a map, without saving the map, while still preserving
// relative positions for loading them onto some other map.
Assert.That(entB.Comp1.LocalPosition, Is.Approximately(new Vector2(100, 100)));
Assert.That(entB.Comp1!.ParentUid, Is.EqualTo(EntityUid.Invalid));
Assert.That(child.Comp1!.ParentUid, Is.EqualTo(entB.Owner));
await server.WaitPost(() => entMan.DeleteEntity(entB));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
// Load the file that contains both of them
LoadResult? result = null;
await server.WaitAssertion(() => Assert.That(loader.TryLoadGeneric(pathCombined, out result)));
Assert.That(result!.Category, Is.EqualTo(FileCategory.Unknown));
Assert.That(result.Orphans, Has.Count.EqualTo(2));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(3));
entA = Find(nameof(entA), entMan);
entB = Find(nameof(entB), entMan);
child = Find(nameof(child), entMan);
Assert.That(entA.Comp1!.ParentUid, Is.EqualTo(EntityUid.Invalid));
Assert.That(entB.Comp1!.ParentUid, Is.EqualTo(EntityUid.Invalid));
Assert.That(entB.Comp1.LocalPosition, Is.Approximately(new Vector2(100, 100)));
Assert.That(child.Comp1!.ParentUid, Is.EqualTo(entB.Owner));
await server.WaitPost(() => entMan.DeleteEntity(entA));
await server.WaitPost(() => entMan.DeleteEntity(entB));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
}
/// <summary>
/// Check that we can save & load a file containing multiple orphaned grid entities.
/// </summary>
[Test]
public async Task TestOrphanedGridSerialization()
{
var server = StartServer(new() { Pool = false, ExtraPrototypes = TestPrototypes }); // Pool=false due to TileDef registration
await server.WaitIdleAsync();
var entMan = server.EntMan;
var mapSys = server.System<SharedMapSystem>();
var loader = server.System<MapLoaderSystem>();
var xform = server.System<SharedTransformSystem>();
var mapMan = server.ResolveDependency<IMapManager>();
var tileMan = server.ResolveDependency<ITileDefinitionManager>();
var pathA = new ResPath($"{nameof(TestOrphanedGridSerialization)}_A.yml");
var pathB = new ResPath($"{nameof(TestOrphanedGridSerialization)}_B.yml");
var pathCombined = new ResPath($"{nameof(TestOrphanedGridSerialization)}_C.yml");
SerializationTestHelper.LoadTileDefs(server.ProtoMan, tileMan, "space");
var tDef = server.ProtoMan.Index<TileDef>(TestTileDefId);
// Spawn multiple entities on a map
MapId mapId = default;
Entity<TransformComponent, EntitySaveTestComponent> map = default;
Entity<TransformComponent, EntitySaveTestComponent> gridA = default;
Entity<TransformComponent, EntitySaveTestComponent> gridB = default;
Entity<TransformComponent, EntitySaveTestComponent> child = default;
await server.WaitPost(() =>
{
var mapUid = mapSys.CreateMap(out mapId);
map = Get(mapUid, entMan);
var gridAUid = mapMan.CreateGridEntity(mapId);
mapSys.SetTile(gridAUid, Vector2i.Zero, new Tile(tDef.TileId));
gridA = Get(gridAUid, entMan);
xform.SetLocalPosition(gridA.Owner, new(100, 100));
var gridBUid = mapMan.CreateGridEntity(mapId);
mapSys.SetTile(gridBUid, Vector2i.Zero, new Tile(tDef.TileId));
gridB = Get(gridBUid, entMan);
var childUid = entMan.SpawnEntity(null, new EntityCoordinates(gridBUid, 0.5f, 0.5f));
child = Get(childUid, entMan);
map.Comp2.Id = nameof(map);
gridA.Comp2.Id = nameof(gridA);
gridB.Comp2.Id = nameof(gridB);
child.Comp2.Id = nameof(child);
});
await server.WaitRunTicks(5);
// grids are not in null-space
Assert.That(gridA.Comp1!.ParentUid, Is.EqualTo(map.Owner));
Assert.That(gridB.Comp1!.ParentUid, Is.EqualTo(map.Owner));
Assert.That(child.Comp1!.ParentUid, Is.EqualTo(gridB.Owner));
Assert.That(map.Comp1!.ParentUid, Is.EqualTo(EntityUid.Invalid));
// Save the grids without their map
await server.WaitAssertion(() => Assert.That(loader.TrySaveGrid(gridA, pathA)));
await server.WaitAssertion(() => Assert.That(loader.TrySaveGrid(gridB, pathB)));
FileCategory cat = default;
await server.WaitAssertion(() => Assert.That(loader.TrySaveGeneric([gridA.Owner, gridB.Owner], pathCombined, out cat)));
Assert.That(cat, Is.EqualTo(FileCategory.Unknown));
// Delete all the entities.
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(4));
await server.WaitPost(() => mapSys.DeleteMap(mapId));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
// Load in the file containing only gridA.
EntityUid newMap = default;
await server.WaitPost(() => newMap = mapSys.CreateMap(out mapId));
await server.WaitAssertion(() => Assert.That(loader.TryLoadGrid(mapId, pathA, out _)));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(1));
gridA = Find(nameof(gridA), entMan);
Assert.That(gridA.Comp1.LocalPosition, Is.Approximately(new Vector2(100, 100)));
Assert.That(gridA.Comp1!.ParentUid, Is.EqualTo(newMap));
await server.WaitPost(() => mapSys.DeleteMap(mapId));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
// Load in the file containing gridB and its child
await server.WaitPost(() => newMap = mapSys.CreateMap(out mapId));
await server.WaitAssertion(() => Assert.That(loader.TryLoadGrid(mapId, pathB, out _)));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(2));
gridB = Find(nameof(gridB), entMan);
child = Find(nameof(child), entMan);
Assert.That(gridB.Comp1!.ParentUid, Is.EqualTo(newMap));
Assert.That(child.Comp1!.ParentUid, Is.EqualTo(gridB.Owner));
await server.WaitPost(() => mapSys.DeleteMap(mapId));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
// Load the file that contains both of them.
// This uses the generic loader, and should automatically create maps for both grids.
LoadResult? result = null;
var opts = MapLoadOptions.Default with
{
DeserializationOptions = DeserializationOptions.Default with {LogOrphanedGrids = false}
};
await server.WaitAssertion(() => Assert.That(loader.TryLoadGeneric(pathCombined, out result, opts)));
Assert.That(result!.Category, Is.EqualTo(FileCategory.Unknown));
Assert.That(result.Grids, Has.Count.EqualTo(2));
Assert.That(result.Maps, Has.Count.EqualTo(2));
Assert.That(entMan.Count<LoadedMapComponent>(), Is.EqualTo(0));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(3));
gridA = Find(nameof(gridA), entMan);
gridB = Find(nameof(gridB), entMan);
child = Find(nameof(child), entMan);
Assert.That(gridA.Comp1.LocalPosition, Is.Approximately(new Vector2(100, 100)));
Assert.That(gridA.Comp1!.ParentUid, Is.Not.EqualTo(EntityUid.Invalid));
Assert.That(gridB.Comp1!.ParentUid, Is.Not.EqualTo(EntityUid.Invalid));
Assert.That(child.Comp1!.ParentUid, Is.EqualTo(gridB.Owner));
await server.WaitPost(() =>
{
foreach (var ent in result.Maps)
{
entMan.DeleteEntity(ent.Owner);
}
});
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
}
}

View File

@@ -0,0 +1,130 @@
using NUnit.Framework;
using Robust.Shared.Analyzers;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Robust.UnitTesting.Shared.EntitySerialization;
[Serializable, NetSerializable]
[DataDefinition]
public sealed partial class RobustCloneableTestClass : IRobustCloneable<RobustCloneableTestClass>
{
[DataField]
public int IntValue;
public RobustCloneableTestClass Clone()
{
return new RobustCloneableTestClass
{
IntValue = IntValue
};
}
}
[Serializable, NetSerializable]
[DataDefinition]
public partial struct RobustCloneableTestStruct : IRobustCloneable<RobustCloneableTestStruct>
{
[DataField]
public int IntValue;
public RobustCloneableTestStruct Clone()
{
return new RobustCloneableTestStruct
{
IntValue = IntValue
};
}
}
[RegisterComponent]
[NetworkedComponent, AutoGenerateComponentState]
public sealed partial class RobustCloneableTestComponent : Component
{
[DataField, AutoNetworkedField]
public RobustCloneableTestClass TestClass = new();
[DataField, AutoNetworkedField]
public RobustCloneableTestStruct TestStruct = new();
[DataField, AutoNetworkedField]
public RobustCloneableTestStruct? NullableTestStruct;
}
internal sealed class RobustCloneableTest() : RobustIntegrationTest
{
[Test]
public async Task TestClone()
{
var server = StartServer();
var client = StartClient();
await Task.WhenAll(server.WaitIdleAsync(), client.WaitIdleAsync());
var sEntMan = server.EntMan;
var sPlayerMan = server.ResolveDependency<ISharedPlayerManager>();
var cEntMan = client.EntMan;
var cNetMan = client.ResolveDependency<IClientNetManager>();
MapId mapId = default;
await server.WaitPost(() =>
{
server.System<SharedMapSystem>().CreateMap(out mapId);
var coords = new MapCoordinates(0, 0, mapId);
var uid = sEntMan.SpawnEntity(null, coords);
var comp = sEntMan.EnsureComponent<RobustCloneableTestComponent>(uid);
comp.TestClass.IntValue = 50;
comp.TestStruct.IntValue = 60;
comp.NullableTestStruct = new() { IntValue = 70 };
});
// Connect client.
Assert.DoesNotThrow(() => client.SetConnectTarget(server));
await client.WaitPost(() => cNetMan.ClientConnect(null!, 0, null!));
async Task RunTicks()
{
for (int i = 0; i < 10; i++)
{
await server.WaitRunTicks(1);
await client.WaitRunTicks(1);
}
}
await RunTicks();
EntityUid player = default;
await server.WaitPost(() =>
{
var coords = new MapCoordinates(0, 0, mapId);
player = sEntMan.SpawnEntity(null, coords);
var session = sPlayerMan.Sessions.First();
server.PlayerMan.SetAttachedEntity(session, player);
sPlayerMan.JoinGame(session);
});
await RunTicks();
await server.WaitAssertion(() =>
{
Assert.That(cNetMan.IsConnected, Is.True);
var ents = cEntMan.AllEntities<RobustCloneableTestComponent>().ToList();
Assert.That(ents, Has.Count.EqualTo(1));
var testEnt = ents[0];
Assert.That(testEnt.Comp.TestClass.IntValue, Is.EqualTo(50));
Assert.That(testEnt.Comp.TestStruct.IntValue, Is.EqualTo(60));
Assert.That(testEnt.Comp.NullableTestStruct, Is.Not.Null);
Assert.That(testEnt.Comp.NullableTestStruct!.Value.IntValue, Is.EqualTo(70));
});
// Disconnect client
await client.WaitPost(() => cNetMan.ClientDisconnect(""));
await server.WaitRunTicks(5);
await client.WaitRunTicks(5);
}
}

View File

@@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
namespace Robust.UnitTesting.Shared.EntitySerialization;
public static class SerializationTestHelper
{
public static void LoadTileDefs(IPrototypeManager protoMan, ITileDefinitionManager tileMan, string? spaceId = "Space")
{
var prototypeList = new List<TileDef>();
foreach (var tileDef in protoMan.EnumeratePrototypes<TileDef>())
{
if (tileDef.ID == spaceId)
{
// Filter out the space tile def and register it first
tileMan.Register(tileDef);
continue;
}
prototypeList.Add(tileDef);
}
prototypeList.Sort((a, b) => string.Compare(a.ID, b.ID, StringComparison.Ordinal));
// Register the rest
foreach (var tileDef in prototypeList)
{
tileMan.Register(tileDef);
}
}
}

View File

@@ -0,0 +1,62 @@
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Utility;
namespace Robust.UnitTesting.Shared.EntitySerialization;
[RegisterComponent]
internal sealed partial class EntitySaveTestComponent : Component
{
/// <summary>
/// Give each entity a unique id to identify them across map saves & loads.
/// </summary>
[DataField] public string? Id;
[DataField] public EntityUid? Entity;
[DataField, AlwaysPushInheritance] public List<int> List = [];
/// <summary>
/// Find an entity with a <see cref="EntitySaveTestComponent"/> with the matching id.
/// </summary>
public static Entity<TransformComponent, EntitySaveTestComponent> Find(string id, IEntityManager entMan)
{
var ents = entMan.AllEntities<EntitySaveTestComponent>();
var matching = ents.Where(x => x.Comp.Id == id).ToArray();
Assert.That(matching, Has.Length.EqualTo(1));
return (matching[0].Owner, entMan.GetComponent<TransformComponent>(matching[0].Owner), matching[0].Comp);
}
public static Entity<TransformComponent, EntitySaveTestComponent> Get(EntityUid uid, IEntityManager entMan)
{
return new Entity<TransformComponent, EntitySaveTestComponent>(
uid,
entMan.GetComponent<TransformComponent>(uid),
entMan.EnsureComponent<EntitySaveTestComponent>(uid));
}
}
/// <summary>
/// Dummy tile definition for serializing grids.
/// </summary>
[Prototype("testTileDef")]
internal sealed partial class TileDef : ITileDefinition
{
public ushort TileId { get; set; }
public string Name => ID;
[IdDataField]
public string ID { get; private set; } = default!;
public ResPath? Sprite => null;
public Dictionary<Direction, ResPath> EdgeSprites => new();
public int EdgeSpritePriority => 0;
public float Friction => 0;
public byte Variants => 0;
public void AssignTileId(ushort id) => TileId = id;
}

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));
}
}
}

View File

@@ -0,0 +1,222 @@
using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Shared;
using Robust.Shared.Analyzers;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Robust.UnitTesting.Shared.GameState;
internal sealed partial class AutoNetworkingTests : RobustIntegrationTest
{
/// <summary>
/// Does basic testing for AutoNetworkedFieldAttribute and AutoGenerateComponentStateAttribute
/// to make sure the datafields are correctly networked to the client when dirtied.
/// TODO: Add test for field deltas.
/// </summary>
[Test]
public async Task AutoNetworkingTest()
{
var serverOpts = new ServerIntegrationOptions { Pool = false };
var clientOpts = new ClientIntegrationOptions { Pool = false };
using var server = StartServer(serverOpts);
using var client = StartClient(clientOpts);
await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync());
var netMan = client.ResolveDependency<IClientNetManager>();
Assert.DoesNotThrow(() => client.SetConnectTarget(server));
client.Post(() => netMan.ClientConnect(null!, 0, null!));
server.Post(() => server.CfgMan.SetCVar(CVars.NetPVS, true));
// Set up map.
EntityUid map = default;
server.Post(() =>
{
map = server.System<SharedMapSystem>().CreateMap();
});
await RunTicks();
// Spawn entities
var coords = new EntityCoordinates(map, default);
EntityUid player = default;
EntityUid cPlayer = default;
EntityUid serverEnt1 = default;
EntityUid serverEnt2 = default;
EntityUid serverEnt3 = default;
NetEntity serverNet1 = default;
NetEntity serverNet2 = default;
NetEntity serverNet3 = default;
await server.WaitPost(() =>
{
// Attach player.
player = server.EntMan.SpawnAttachedTo(null, coords);
var session = server.PlayerMan.Sessions.First();
server.PlayerMan.SetAttachedEntity(session, player);
server.PlayerMan.JoinGame(session);
// Spawn test entities.
serverEnt1 = server.EntMan.SpawnAttachedTo(null, coords);
serverEnt2 = server.EntMan.SpawnAttachedTo(null, coords);
serverEnt3 = server.EntMan.SpawnAttachedTo(null, coords);
serverNet1 = server.EntMan.GetNetEntity(serverEnt1);
serverNet2 = server.EntMan.GetNetEntity(serverEnt2);
serverNet3 = server.EntMan.GetNetEntity(serverEnt3);
// Setup components
server.EntMan.EnsureComponent<AutoNetworkingTestComponent>(serverEnt1);
server.EntMan.EnsureComponent<AutoNetworkingTestChildComponent>(serverEnt2);
server.EntMan.EnsureComponent<AutoNetworkingTestEmptyChildComponent>(serverEnt3);
});
await RunTicks();
// check client
await client.WaitPost(() =>
{
// Get the client-side entities
cPlayer = client.EntMan.GetEntity(server.EntMan.GetNetEntity(player));
var clientEnt1 = client.EntMan.GetEntity(serverNet1);
var clientEnt2 = client.EntMan.GetEntity(serverNet2);
var clientEnt3 = client.EntMan.GetEntity(serverNet3);
// Check player got properly attached
Assert.That(client.AttachedEntity, Is.EqualTo(cPlayer));
Assert.That(client.EntMan.EntityExists(cPlayer));
// Get the client-side components
Assert.That(client.EntMan.TryGetComponent(clientEnt1, out AutoNetworkingTestComponent? cmpClient1));
Assert.That(client.EntMan.TryGetComponent(clientEnt2, out AutoNetworkingTestChildComponent? cmpClient2));
Assert.That(client.EntMan.TryGetComponent(clientEnt3, out AutoNetworkingTestEmptyChildComponent? cmpClient3));
// All datafields should be the default value
Assert.That(cmpClient1?.IsNetworked, Is.EqualTo(1));
Assert.That(cmpClient1?.NotNetworked, Is.EqualTo(2));
Assert.That(cmpClient2?.ChildNetworked, Is.EqualTo(1));
Assert.That(cmpClient2?.Child, Is.EqualTo(2));
Assert.That(cmpClient2?.ParentNetworked, Is.EqualTo(3));
Assert.That(cmpClient2?.Parent, Is.EqualTo(4));
Assert.That(cmpClient3?.ParentNetworked, Is.EqualTo(3));
Assert.That(cmpClient3?.Parent, Is.EqualTo(4));
});
// make changes on the server
await server.WaitPost(() =>
{
// Get the server-side components
var cmpServer1 = server.EntMan.GetComponent<AutoNetworkingTestComponent>(serverEnt1);
var cmpServer2 = server.EntMan.GetComponent<AutoNetworkingTestChildComponent>(serverEnt2);
var cmpServer3 = server.EntMan.GetComponent<AutoNetworkingTestEmptyChildComponent>(serverEnt3);
// All datafields should be the default value
Assert.That(cmpServer1.IsNetworked, Is.EqualTo(1));
Assert.That(cmpServer1.NotNetworked, Is.EqualTo(2));
Assert.That(cmpServer2.ChildNetworked, Is.EqualTo(1));
Assert.That(cmpServer2.Child, Is.EqualTo(2));
Assert.That(cmpServer2.ParentNetworked, Is.EqualTo(3));
Assert.That(cmpServer2.Parent, Is.EqualTo(4));
Assert.That(cmpServer3.ParentNetworked, Is.EqualTo(3));
Assert.That(cmpServer3.Parent, Is.EqualTo(4));
// change the datafields and dirty them
cmpServer1.IsNetworked = 101;
cmpServer1.NotNetworked = 102;
cmpServer2.ChildNetworked = 101;
cmpServer2.Child = 102;
cmpServer2.ParentNetworked = 103;
cmpServer2.Parent = 104;
cmpServer3.ParentNetworked = 103;
cmpServer3.Parent = 104;
server.EntMan.Dirty(serverEnt1, cmpServer1);
server.EntMan.Dirty(serverEnt2, cmpServer2);
server.EntMan.Dirty(serverEnt3, cmpServer3);
});
await RunTicks();
// check the client again
await client.WaitPost(() =>
{
// Get the client-side entities
cPlayer = client.EntMan.GetEntity(server.EntMan.GetNetEntity(player));
var clientEnt1 = client.EntMan.GetEntity(serverNet1);
var clientEnt2 = client.EntMan.GetEntity(serverNet2);
var clientEnt3 = client.EntMan.GetEntity(serverNet3);
// Get the client-side components
Assert.That(client.EntMan.TryGetComponent(clientEnt1, out AutoNetworkingTestComponent? cmpClient1));
Assert.That(client.EntMan.TryGetComponent(clientEnt2, out AutoNetworkingTestChildComponent? cmpClient2));
Assert.That(client.EntMan.TryGetComponent(clientEnt3, out AutoNetworkingTestEmptyChildComponent? cmpClient3));
// All datafields should be the default value
Assert.That(cmpClient1?.IsNetworked, Is.EqualTo(101));
Assert.That(cmpClient1?.NotNetworked, Is.EqualTo(2)); // unchanged
Assert.That(cmpClient2?.ChildNetworked, Is.EqualTo(101));
Assert.That(cmpClient2?.Child, Is.EqualTo(2)); // unchanged
Assert.That(cmpClient2?.ParentNetworked, Is.EqualTo(103));
Assert.That(cmpClient2?.Parent, Is.EqualTo(4)); // unchanged
Assert.That(cmpClient3?.ParentNetworked, Is.EqualTo(103));
Assert.That(cmpClient3?.Parent, Is.EqualTo(4)); // unchanged
});
async Task RunTicks()
{
for (int i = 0; i < 10; i++)
{
await server!.WaitRunTicks(1);
await client!.WaitRunTicks(1);
}
}
await client.WaitPost(() => netMan.ClientDisconnect(""));
await server.WaitRunTicks(5);
await client.WaitRunTicks(5);
}
}
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class AutoNetworkingTestComponent : Component
{
[DataField, AutoNetworkedField]
public int IsNetworked = 1;
[DataField]
public int NotNetworked = 2;
}
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class AutoNetworkingTestChildComponent : AutoNetworkingTestParentComponent
{
[DataField, AutoNetworkedField]
public int ChildNetworked = 1;
[DataField]
public int Child = 2;
}
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class AutoNetworkingTestEmptyChildComponent : AutoNetworkingTestParentComponent
{
}
public abstract partial class AutoNetworkingTestParentComponent : Component
{
[DataField, AutoNetworkedField]
public int ParentNetworked = 3;
[DataField]
public int Parent = 4;
}

View File

@@ -0,0 +1,296 @@
using System.Linq;
using System.Numerics;
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Shared;
using Robust.Shared.Analyzers;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Reflection;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Robust.UnitTesting.Shared.GameState;
internal sealed partial class ComponentStateTests : RobustIntegrationTest
{
/// <summary>
/// This tests performs a basic check to ensure that there is no issue with entity states referencing other
/// entities that the client is not yet aware of. It does this by spawning two entities that reference each other,
/// and then ensuring that they get sent to the client one at a time.
/// </summary>
[Test]
public async Task UnknownEntityTest()
{
// Setup auto-comp-states. I hate this. Someone please fix reflection in RobustIntegrationTest
var serverOpts = new ServerIntegrationOptions { Pool = false };
var clientOpts = new ClientIntegrationOptions { Pool = false };
var server = StartServer(serverOpts);
var client = StartClient(clientOpts);
await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync());
var netMan = client.ResolveDependency<IClientNetManager>();
var xforms = server.System<SharedTransformSystem>();
Assert.DoesNotThrow(() => client.SetConnectTarget(server));
client.Post(() => netMan.ClientConnect(null!, 0, null!));
server.Post(() => server.CfgMan.SetCVar(CVars.NetPVS, true));
// Set up map.
EntityUid map = default;
server.Post(() =>
{
map = server.System<SharedMapSystem>().CreateMap();
});
await RunTicks();
// Spawn entities
var coordsA = new EntityCoordinates(map, default);
var coordsB = new EntityCoordinates(map, new Vector2(100, 100));
EntityUid player = default;
EntityUid cPlayer = default;
EntityUid serverEntA = default;
EntityUid serverEntB = default;
NetEntity serverNetA = default;
NetEntity serverNetB = default;
await server.WaitPost(() =>
{
// Attach player.
player = server.EntMan.Spawn();
var session = server.PlayerMan.Sessions.First();
server.PlayerMan.SetAttachedEntity(session, player);
server.PlayerMan.JoinGame(session);
// Spawn test entities.
serverEntA = server.EntMan.SpawnAttachedTo(null, coordsA);
serverEntB = server.EntMan.SpawnAttachedTo(null, coordsB);
serverNetA = server.EntMan.GetNetEntity(serverEntA);
serverNetB = server.EntMan.GetNetEntity(serverEntB);
// Setup components
var cmp = server.EntMan.EnsureComponent<UnknownEntityTestComponent>(serverEntA);
cmp.Other = serverEntB;
server.EntMan.Dirty(serverEntA, cmp);
cmp = server.EntMan.EnsureComponent<UnknownEntityTestComponent>(serverEntB);
cmp.Other = serverEntA;
server.EntMan.Dirty(serverEntB, cmp);
});
await RunTicks();
// Check player got properly attached and only knows about the expected entities
await client.WaitPost(() =>
{
cPlayer = client.EntMan.GetEntity(server.EntMan.GetNetEntity(player));
Assert.That(client.AttachedEntity, Is.EqualTo(cPlayer));
Assert.That(client.EntMan.EntityExists(cPlayer));
Assert.That(client.EntMan.EntityExists(client.EntMan.GetEntity(serverNetA)), Is.False);
Assert.That(client.EntMan.EntityExists(client.EntMan.GetEntity(serverNetB)), Is.False);
});
// Move the player into PVS range of one of the entities.
await server.WaitPost(() => xforms.SetCoordinates(player, coordsB));
await RunTicks();
await client.WaitPost(() =>
{
var clientEntA = client.EntMan.GetEntity(serverNetA);
var clientEntB = client.EntMan.GetEntity(serverNetB);
Assert.That(client.EntMan.EntityExists(clientEntB), Is.True);
Assert.That(client.EntMan.EntityExists(client.EntMan.GetEntity(serverNetA)), Is.False);
Assert.That(client.EntMan.TryGetComponent(clientEntB, out UnknownEntityTestComponent? cmp));
Assert.That(cmp?.Other, Is.EqualTo(clientEntA));
});
// Move the player into PVS range of the other entity
await server.WaitPost(() => xforms.SetCoordinates(player, coordsA));
await RunTicks();
await client.WaitPost(() =>
{
var clientEntA = client.EntMan.GetEntity(serverNetA);
var clientEntB = client.EntMan.GetEntity(serverNetB);
Assert.That(client.EntMan.EntityExists(clientEntB), Is.True);
Assert.That(client.EntMan.EntityExists(clientEntA), Is.True);
Assert.That(client.EntMan.TryGetComponent(clientEntB, out UnknownEntityTestComponent? cmp));
Assert.That(cmp?.Other, Is.EqualTo(clientEntA));
Assert.That(client.EntMan.TryGetComponent(clientEntA, out cmp));
Assert.That(cmp?.Other, Is.EqualTo(clientEntB));
});
server.Post(() => server.CfgMan.SetCVar(CVars.NetPVS, false));
// wait for errors.
await RunTicks();
async Task RunTicks()
{
for (int i = 0; i < 10; i++)
{
await server!.WaitRunTicks(1);
await client!.WaitRunTicks(1);
}
}
await client.WaitPost(() => netMan.ClientDisconnect(""));
await server.WaitRunTicks(5);
await client.WaitRunTicks(5);
}
/// <summary>
/// This is a variant of <see cref="UnknownEntityTest"/> that deletes one of the entities before the other entity gets sent.
/// </summary>
[Test]
public async Task UnknownEntityDeleteTest()
{
// The first chunk of the test just follows UnknownEntityTest
var serverOpts = new ServerIntegrationOptions { Pool = false };
var clientOpts = new ClientIntegrationOptions { Pool = false };
var server = StartServer(serverOpts);
var client = StartClient(clientOpts);
await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync());
var netMan = client.ResolveDependency<IClientNetManager>();
var xforms = server.System<SharedTransformSystem>();
Assert.DoesNotThrow(() => client.SetConnectTarget(server));
client.Post(() => netMan.ClientConnect(null!, 0, null!));
server.Post(() => server.CfgMan.SetCVar(CVars.NetPVS, true));
// Set up map.
EntityUid map = default;
server.Post(() =>
{
map = server.System<SharedMapSystem>().CreateMap();
});
await RunTicks();
// Spawn entities
var coordsA = new EntityCoordinates(map, default);
var coordsB = new EntityCoordinates(map, new Vector2(100, 100));
EntityUid player = default;
EntityUid cPlayer = default;
EntityUid serverEntA = default;
EntityUid serverEntB = default;
NetEntity serverNetA = default;
NetEntity serverNetB = default;
await server.WaitPost(() =>
{
// Attach player.
player = server.EntMan.Spawn();
var session = server.PlayerMan.Sessions.First();
server.PlayerMan.SetAttachedEntity(session, player);
server.PlayerMan.JoinGame(session);
// Spawn test entities.
serverEntA = server.EntMan.SpawnAttachedTo(null, coordsA);
serverEntB = server.EntMan.SpawnAttachedTo(null, coordsB);
serverNetA = server.EntMan.GetNetEntity(serverEntA);
serverNetB = server.EntMan.GetNetEntity(serverEntB);
// Setup components
var cmp = server.EntMan.EnsureComponent<UnknownEntityTestComponent>(serverEntA);
cmp.Other = serverEntB;
server.EntMan.Dirty(serverEntA, cmp);
cmp = server.EntMan.EnsureComponent<UnknownEntityTestComponent>(serverEntB);
cmp.Other = serverEntA;
server.EntMan.Dirty(serverEntB, cmp);
});
await RunTicks();
// Check player got properly attached and only knows about the expected entities
await client.WaitPost(() =>
{
cPlayer = client.EntMan.GetEntity(server.EntMan.GetNetEntity(player));
Assert.That(client.AttachedEntity, Is.EqualTo(cPlayer));
Assert.That(client.EntMan.EntityExists(cPlayer));
Assert.That(client.EntMan.EntityExists(client.EntMan.GetEntity(serverNetA)), Is.False);
Assert.That(client.EntMan.EntityExists(client.EntMan.GetEntity(serverNetB)), Is.False);
});
// Move the player into PVS range of one of the entities.
await server.WaitPost(() => xforms.SetCoordinates(player, coordsB));
await RunTicks();
await client.WaitPost(() =>
{
var clientEntA = client.EntMan.GetEntity(serverNetA);
var clientEntB = client.EntMan.GetEntity(serverNetB);
Assert.That(client.EntMan.EntityExists(clientEntB), Is.True);
Assert.That(client.EntMan.EntityExists(clientEntA), Is.False);
Assert.That(client.EntMan.TryGetComponent(clientEntB, out UnknownEntityTestComponent? cmp));
Assert.That(cmp?.Other, Is.EqualTo(clientEntA));
});
// This is where the test difffers from UnknownEntityTest:
// We delete the entity that the player knows about before it receives the entity that it references.
await server.WaitPost(() =>
{
server.EntMan.DeleteEntity(serverEntB);
var comp = server.EntMan.GetComponent<UnknownEntityTestComponent>(serverEntA);
comp.Other = EntityUid.Invalid;
server.EntMan.Dirty(serverEntA, comp);
});
await RunTicks();
await client.WaitPost(() =>
{
var clientEntA = client.EntMan.GetEntity(serverNetA);
var clientEntB = client.EntMan.GetEntity(serverNetB);
Assert.That(clientEntA, Is.EqualTo(EntityUid.Invalid));
Assert.That(clientEntB, Is.EqualTo(EntityUid.Invalid));
});
// Move the player into PVS range of the other entity
await server.WaitPost(() => xforms.SetCoordinates(player, coordsA));
await RunTicks();
await client.WaitPost(() =>
{
var clientEntA = client.EntMan.GetEntity(serverNetA);
var clientEntB = client.EntMan.GetEntity(serverNetB);
Assert.That(clientEntB, Is.EqualTo(EntityUid.Invalid));
Assert.That(client.EntMan.EntityExists(clientEntA), Is.True);
Assert.That(client.EntMan.TryGetComponent(clientEntA, out UnknownEntityTestComponent? cmp));
Assert.That(cmp?.Other, Is.EqualTo(EntityUid.Invalid));
});
server.Post(() => server.CfgMan.SetCVar(CVars.NetPVS, false));
// wait for errors.
await RunTicks();
async Task RunTicks()
{
for (int i = 0; i < 10; i++)
{
await server!.WaitRunTicks(1);
await client!.WaitRunTicks(1);
}
}
await client.WaitPost(() => netMan.ClientDisconnect(""));
await server.WaitRunTicks(5);
await client.WaitRunTicks(5);
}
}
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class UnknownEntityTestComponent : Component
{
[DataField, AutoNetworkedField]
public EntityUid? Other;
}

View File

@@ -0,0 +1,225 @@
using System.Linq;
using System.Numerics;
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Network;
using Robust.Shared.Player;
namespace Robust.UnitTesting.Shared.GameState;
/// <summary>
/// This test checks that when entities get deleted, the client receives the game states and deletes the entities.
/// </summary>
/// <remarks>
/// Should help prevent the issue fixed in PR #4044 from reoccurring.
/// </remarks>
internal sealed class DeletionNetworkingTests : RobustIntegrationTest
{
[Test]
public async Task DeletionNetworkingTest()
{
var server = StartServer();
var client = StartClient();
await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync());
var mapMan = server.ResolveDependency<IMapManager>();
var sEntMan = server.ResolveDependency<IEntityManager>();
var cEntMan = client.ResolveDependency<IEntityManager>();
var netMan = client.ResolveDependency<IClientNetManager>();
var confMan = server.ResolveDependency<IConfigurationManager>();
var cPlayerMan = client.ResolveDependency<ISharedPlayerManager>();
var sPlayerMan = server.ResolveDependency<ISharedPlayerManager>();
var xformSys = sEntMan.EntitySysManager.GetEntitySystem<SharedTransformSystem>();
var mapSys = sEntMan.EntitySysManager.GetEntitySystem<SharedMapSystem>();
Assert.DoesNotThrow(() => client.SetConnectTarget(server));
client.Post(() => netMan.ClientConnect(null!, 0, null!));
server.Post(() => confMan.SetCVar(CVars.NetPVS, true));
async Task RunTicks()
{
for (int i = 0; i < 10; i++)
{
await server.WaitRunTicks(1);
await client.WaitRunTicks(1);
}
}
await RunTicks();
// Set up map & grids
EntityUid grid1 = default;
EntityUid grid2 = default;
NetEntity grid1Net = default;
NetEntity grid2Net = default;
await server.WaitPost(() =>
{
mapSys.CreateMap(out var mapId);
var gridComp = mapMan.CreateGridEntity(mapId);
mapSys.SetTile(gridComp, Vector2i.Zero, new Tile(1));
grid1 = gridComp.Owner;
xformSys.SetLocalPosition(grid1, new Vector2(-2,0));
grid1Net = sEntMan.GetNetEntity(grid1);
gridComp = mapMan.CreateGridEntity(mapId);
mapSys.SetTile(gridComp, Vector2i.Zero, new Tile(1));
grid2 = gridComp.Owner;
xformSys.SetLocalPosition(grid2, new Vector2(2,0));
grid2Net = sEntMan.GetNetEntity(grid2);
});
// Spawn player entity on grid 1
EntityUid player = default;
await server.WaitPost(() =>
{
var coords = new EntityCoordinates(grid1, new Vector2(0.5f, 0.5f));
player = sEntMan.SpawnEntity(null, coords);
var session = sPlayerMan.Sessions.First();
server.PlayerMan.SetAttachedEntity(session, player);
sPlayerMan.JoinGame(session);
});
await RunTicks();
// Check player got properly attached
await client.WaitPost(() =>
{
var ent = cEntMan.GetNetEntity(cPlayerMan.LocalEntity);
Assert.That(ent, Is.EqualTo(sEntMan.GetNetEntity(player)));
});
// Spawn two entities, each with one child.
EntityUid entA = default;
EntityUid entB = default;
EntityUid childA = default;
EntityUid childB = default;
NetEntity entANet = default;
NetEntity entBNet = default;
NetEntity childANet = default;
NetEntity childBNet = default!;
var coords = new EntityCoordinates(grid2, new Vector2(0.5f, 0.5f));
await server.WaitPost(() =>
{
entA = sEntMan.SpawnEntity(null, coords);
entB = sEntMan.SpawnEntity(null, coords);
childA = sEntMan.SpawnEntity(null, new EntityCoordinates(entA, default));
childB = sEntMan.SpawnEntity(null, new EntityCoordinates(entB, default));
entANet = sEntMan.GetNetEntity(entA);
entBNet = sEntMan.GetNetEntity(entB);
childANet = sEntMan.GetNetEntity(childA);
childBNet = sEntMan.GetNetEntity(childB);
});
await RunTicks();
// Get the client version of the entities.
var cEntA = cEntMan.GetEntity(entANet);
var cEntB = cEntMan.GetEntity(entBNet);
var cChildA = cEntMan.GetEntity(childANet);
var cChildB = cEntMan.GetEntity(childBNet);
var cGrid2 = cEntMan.GetEntity(grid2Net);
// Spawn client-side children and one client-side entity
EntityUid cEntC = default;
EntityUid cChildC = default;
EntityUid clientChildA = default;
EntityUid clientChildB = default;
NetEntity entCNet = NetEntity.Invalid;
await client.WaitPost(() =>
{
cEntC = cEntMan.SpawnEntity(null, cEntMan.GetCoordinates(sEntMan.GetNetCoordinates(coords)));
entCNet = cEntMan.GetNetEntity(cEntC);
cChildC = cEntMan.SpawnEntity(null, new EntityCoordinates(cEntC, default));
clientChildA = cEntMan.SpawnEntity(null, new EntityCoordinates(cEntA, default));
clientChildB = cEntMan.SpawnEntity(null, new EntityCoordinates(cEntB, default));
});
await RunTicks();
// Verify entities exist and have the correct parents.
NetEntity Parent(EntityUid uid) => cEntMan.GetNetEntity(cEntMan.GetComponent<TransformComponent>(uid).ParentUid);
await client.WaitPost(() =>
{
// Exist
Assert.That(cEntMan.EntityExists(cEntA));
Assert.That(cEntMan.EntityExists(cEntB));
Assert.That(cEntMan.EntityExists(cEntC));
Assert.That(cEntMan.EntityExists(cChildA));
Assert.That(cEntMan.EntityExists(cChildB));
Assert.That(cEntMan.EntityExists(cChildC));
Assert.That(cEntMan.EntityExists(clientChildA));
Assert.That(cEntMan.EntityExists(clientChildB));
// Client-side where appropriate
Assert.That(cEntMan.IsClientSide(cEntC));
Assert.That(cEntMan.IsClientSide(cChildC));
Assert.That(cEntMan.IsClientSide(clientChildA));
Assert.That(cEntMan.IsClientSide(clientChildB));
Assert.That(!cEntMan.IsClientSide(cEntA));
Assert.That(!cEntMan.IsClientSide(cEntB));
Assert.That(!cEntMan.IsClientSide(cChildA));
Assert.That(!cEntMan.IsClientSide(cChildB));
// Correct parents.
Assert.That(Parent(cEntA), Is.EqualTo(grid2Net));
Assert.That(Parent(cEntB), Is.EqualTo(grid2Net));
Assert.That(Parent(cEntC), Is.EqualTo(grid2Net));
Assert.That(Parent(cChildA), Is.EqualTo(entANet));
Assert.That(Parent(cChildB), Is.EqualTo(entBNet));
Assert.That(Parent(cChildC), Is.EqualTo(entCNet));
Assert.That(Parent(clientChildA), Is.EqualTo(entANet));
Assert.That(Parent(clientChildB), Is.EqualTo(entBNet));
});
// Delete client-side entity.
await client.WaitPost(() => cEntMan.DeleteEntity(cEntC));
await RunTicks();
await client.WaitPost(() =>
{
Assert.That(!cEntMan.EntityExists(cEntC));
Assert.That(!cEntMan.EntityExists(cChildC));
});
// Delete server-side entity.
await server.WaitPost(() => sEntMan.DeleteEntity(entB));
await RunTicks();
await client.WaitPost(() =>
{
Assert.That(!cEntMan.EntityExists(cEntB));
Assert.That(!cEntMan.EntityExists(cChildB));
// Was never explicitly deleted by the client.
Assert.That(cEntMan.EntityExists(clientChildB));
});
// Delete the grid (and thus also entity A and all the children)
await server.WaitPost(() => sEntMan.DeleteEntity(grid2));
await RunTicks();
await client.WaitPost(() =>
{
Assert.That(!cEntMan.EntityExists(cGrid2));
Assert.That(!cEntMan.EntityExists(cEntA));
Assert.That(!cEntMan.EntityExists(cChildA));
// Was never explicitly deleted by the client.
Assert.That(cEntMan.EntityExists(clientChildA));
});
await client.WaitPost(() => netMan.ClientDisconnect(""));
await server.WaitRunTicks(5);
await client.WaitRunTicks(5);
}
}

View File

@@ -0,0 +1,118 @@
using System;
using NUnit.Framework;
using Robust.Client.GameStates;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Network;
using Robust.Shared.Serialization;
using System.Linq;
using System.Threading.Tasks;
using Robust.Shared.Analyzers;
using static Robust.UnitTesting.Shared.GameState.ExampleAutogeneratedComponent;
namespace Robust.UnitTesting.Shared.GameState;
/// <summary>
/// This is a test of test engine <see cref="RobustIntegrationTest"/>. Not a test of game engine.
/// </summary>
internal sealed partial class NoSharedReferencesTest : RobustIntegrationTest
{
/// <summary>
/// The test performs a basic check to ensure that there is no issue with server's object references leaking to client.
/// It accomplishments this by testing two things: 1) That the reference object on both sides is not the same; and
/// 2) That the client-side copy of server's component state (used for prediction resetting) is not the same.
/// </summary>
[Test]
public async Task ReferencesAreNotShared()
{
var serverOpts = new ServerIntegrationOptions { Pool = false };
var clientOpts = new ClientIntegrationOptions { Pool = false };
var server = StartServer(serverOpts);
var client = StartClient(clientOpts);
await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync());
var netMan = client.ResolveDependency<IClientNetManager>();
var clientGameStateManager = client.ResolveDependency<IClientGameStateManager>();
Assert.DoesNotThrow(() => client.SetConnectTarget(server));
client.Post(() => netMan.ClientConnect(null!, 0, null!));
// Set up map.
server.Post(() =>
{
server.System<SharedMapSystem>().CreateMap();
});
await RunTicks();
EntityUid sPlayer = default;
EntityUid cPlayer = default;
ExampleObject serverObject = default!;
var sEntMan = server.EntMan;
// Set up test entity (player).
await server.WaitPost(() =>
{
sPlayer = sEntMan.Spawn();
serverObject = new ExampleObject(5);
var comp = new ExampleAutogeneratedComponent { ReferenceObject = serverObject };
sEntMan.AddComponent(sPlayer, comp);
var session = server.PlayerMan.Sessions.First();
server.PlayerMan.SetAttachedEntity(session, sPlayer);
server.PlayerMan.JoinGame(session);
});
// Let Client sync state with Server
await RunTicks();
// Assert that Client's object and client-side server state objects are different to Server's object
Assert.Multiple(() =>
{
// Player attached assertions
var cEntMan = client.EntMan;
cPlayer = cEntMan.GetEntity(server.EntMan.GetNetEntity(sPlayer));
Assert.That(client.AttachedEntity, Is.EqualTo(cPlayer));
Assert.That(cEntMan.EntityExists(cPlayer));
// Assert client and server have different objects of same values
Assert.That(cEntMan.TryGetComponent(cPlayer, out ExampleAutogeneratedComponent? comp));
var clientObject = comp?.ReferenceObject;
Assert.That(clientObject, Is.EqualTo(serverObject));
Assert.That(ReferenceEquals(clientObject, serverObject), Is.False);
// Assert that client-side dictionary of server component state also isn't contaminated by server references
var componentStates = clientGameStateManager.GetFullRep()[cEntMan.GetNetEntity(cPlayer)];
var clientLastTickStateObject = ((ExampleAutogeneratedComponent_AutoState)componentStates.First(x => x.Value is ExampleAutogeneratedComponent_AutoState).Value!).ReferenceObject;
Assert.That(clientLastTickStateObject, Is.Not.Null);
Assert.That(ReferenceEquals(clientLastTickStateObject, serverObject), Is.False);
});
// wait for errors.
await RunTicks();
async Task RunTicks()
{
for (int i = 0; i < 10; i++)
{
await server.WaitRunTicks(1);
await client.WaitRunTicks(1);
}
}
await client.WaitPost(() => netMan.ClientDisconnect(""));
await server.WaitRunTicks(5);
await client.WaitRunTicks(5);
}
}
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class ExampleAutogeneratedComponent : Component
{
[AutoNetworkedField]
public ExampleObject ReferenceObject;
[Serializable, NetSerializable]
public sealed record ExampleObject(int Value);
}

View File

@@ -0,0 +1,132 @@
using System.Linq;
using System.Numerics;
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Network;
namespace Robust.UnitTesting.Shared.GameState;
internal sealed partial class VisibilityTest : RobustIntegrationTest
{
/// <summary>
/// This tests checks that entity visibility masks are recursively applied to children.
/// </summary>
[Test]
public async Task UnknownEntityTest()
{
var server = StartServer();
await server.WaitIdleAsync();
var xforms = server.System<SharedTransformSystem>();
var vis = server.System<VisibilitySystem>();
const int RequiredMask = 1;
// All entities need to have this mask set ... which defeat the whole point of that bit?
// Spawn a stack of entities
int N = 6;
var ents = new EntityUid[N];
var metaComp = new MetaDataComponent[N];
var visComp = new VisibilityComponent[N];
await server.WaitPost(() =>
{
for (int i = 0; i < N; i++)
{
var ent = server.EntMan.Spawn();
ents[i] = ent;
metaComp[i] = server.EntMan.GetComponent<MetaDataComponent>(ent);
visComp[i] = server.EntMan.AddComponent<VisibilityComponent>(ent);
vis.AddLayer((ent, visComp[i]), (ushort)(1 << i));
if (i > 0)
xforms.SetParent(ent, ents[i - 1]);
}
});
// Each entity's visibility mask should include the parent's mask
var mask = RequiredMask;
for (int i = 0; i < N; i++)
{
mask |= 1 << i;
var meta = metaComp[i];
Assert.That(meta.VisibilityMask, Is.EqualTo(mask));
}
// Adding a layer to the root entity's mask will apply it to all children
var extraMask = 1 << (N + 1);
mask = RequiredMask | extraMask;
vis.AddLayer((ents[0], visComp[0]), (ushort)extraMask);
for (int i = 0; i < N; i++)
{
mask |= 1 << i;
var meta = metaComp[i];
Assert.That(meta.VisibilityMask, Is.EqualTo(mask));
}
// Removing the removes it from all children.
vis.RemoveLayer((ents[0], visComp[0]), (ushort)extraMask);
mask = RequiredMask;
for (int i = 0; i < N; i++)
{
mask |= 1 << i;
var meta = metaComp[i];
Assert.That(meta.VisibilityMask, Is.EqualTo(mask));
}
// Detaching an entity from the stack updates it, and it's children's mask
var split = N / 2;
await server.WaitPost(() => xforms.SetParent(ents[split], EntityUid.Invalid));
mask = RequiredMask;
for (int i = 0; i < split; i++)
{
mask |= 1 << i;
var meta = metaComp[i];
Assert.That(meta.VisibilityMask, Is.EqualTo(mask));
}
mask = RequiredMask;
for (int i = split; i < N; i++)
{
mask |= 1 << i;
var meta = metaComp[i];
Assert.That(meta.VisibilityMask, Is.EqualTo(mask));
}
// Re-attaching the entity also updates the masks.
await server.WaitPost(() => xforms.SetParent(ents[split], ents[split - 1]));
mask = RequiredMask;
for (int i = 0; i < N; i++)
{
mask |= 1 << i;
var meta = metaComp[i];
Assert.That(meta.VisibilityMask, Is.EqualTo(mask));
}
// Setting a mask on a child does not propagate upwards, only downwards
vis.AddLayer((ents[split], visComp[split]), (ushort)extraMask);
mask = RequiredMask;
for (int i = 0; i < split; i++)
{
mask |= 1 << i;
var meta = metaComp[i];
Assert.That(meta.VisibilityMask, Is.EqualTo(mask));
}
mask |= extraMask;
for (int i = split; i < N; i++)
{
mask |= 1 << i;
var meta = metaComp[i];
Assert.That(meta.VisibilityMask, Is.EqualTo(mask));
}
}
}

View File

@@ -0,0 +1 @@
global using Is = NUnit.Framework.Is;

View File

@@ -0,0 +1,234 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.Input;
using Robust.Shared.Input.Binding;
using Robust.Shared.Player;
namespace Robust.UnitTesting.Shared.Input.Binding
{
[TestFixture, TestOf(typeof(CommandBindRegistry))]
internal sealed class CommandBindRegistry_Test : OurRobustUnitTest
{
private sealed class TypeA { }
private sealed class TypeB { }
private sealed class TypeC { }
private sealed class TestInputCmdHandler : InputCmdHandler
{
// these vars only for tracking / debugging during testing
private readonly string name;
public readonly Type? ForType;
public TestInputCmdHandler(Type? forType = null, string name = "")
{
ForType = forType;
this.name = name;
}
public override string ToString()
{
return name;
}
public override bool HandleCmdMessage(IEntityManager entManager, ICommonSession? session, IFullInputCmdMessage message)
{
return false;
}
}
[TestCase(1,1)]
[TestCase(10,10)]
public void ResolvesHandlers_WhenNoDependencies(int handlersPerType, int numFunctions)
{
var registry = new CommandBindRegistry(new DummySawmill());
var allHandlers = new Dictionary<BoundKeyFunction,List<InputCmdHandler>>();
for (int i = 0; i < numFunctions; i++)
{
var bkf = new BoundKeyFunction(i.ToString());
var theseHandlers = new List<InputCmdHandler>();
allHandlers[bkf] = theseHandlers;
var aHandlers = new List<InputCmdHandler>();
var bHandlers = new List<InputCmdHandler>();
var cHandlers = new List<InputCmdHandler>();
for (int j = 0; j < handlersPerType; j++)
{
aHandlers.Add(new TestInputCmdHandler(typeof(TypeA)));
bHandlers.Add(new TestInputCmdHandler(typeof(TypeB)));
cHandlers.Add(new TestInputCmdHandler(typeof(TypeC)));
}
theseHandlers.AddRange(aHandlers);
theseHandlers.AddRange(bHandlers);
theseHandlers.AddRange(cHandlers);
CommandBinds.Builder
.Bind(bkf, aHandlers)
.Register<TypeA>(registry);
CommandBinds.Builder
.Bind(bkf, bHandlers)
.Register<TypeB>(registry);
CommandBinds.Builder
.Bind(bkf, cHandlers)
.Register<TypeC>(registry);
}
//order doesn't matter, just verify that all handlers are returned
foreach (var bkfToExpectedHandlers in allHandlers)
{
var bkf = bkfToExpectedHandlers.Key;
var expectedHandlers = bkfToExpectedHandlers.Value;
HashSet<InputCmdHandler> returnedHandlers = registry.GetHandlers(bkf).ToHashSet();
Assert.That(expectedHandlers, Is.EqualTo(returnedHandlers).AsCollection);
}
// type b stuff should no longer fire
CommandBinds.Unregister<TypeB>(registry);
foreach (var bkfToExpectedHandlers in allHandlers)
{
var bkf = bkfToExpectedHandlers.Key;
var expectedHandlers = bkfToExpectedHandlers.Value;
expectedHandlers.RemoveAll(handler => ((TestInputCmdHandler) handler).ForType == typeof(TypeB));
HashSet<InputCmdHandler> returnedHandlers = registry.GetHandlers(bkf).ToHashSet();
Assert.That(expectedHandlers, Is.EqualTo(returnedHandlers).AsCollection);
}
}
[TestCase(true, false)]
[TestCase(false, true)]
[TestCase(true, true)]
public void ResolvesHandlers_WithDependency(bool before, bool after)
{
var registry = new CommandBindRegistry(new DummySawmill());
var bkf = new BoundKeyFunction("test");
var aHandler1 = new TestInputCmdHandler( );
var aHandler2 = new TestInputCmdHandler();
var bHandler1 = new TestInputCmdHandler();
var bHandler2 = new TestInputCmdHandler();
var cHandler1 = new TestInputCmdHandler();
var cHandler2 = new TestInputCmdHandler();
// a handler 2 should run after both b and c handlers for all the below cases
if (before && after)
{
CommandBinds.Builder
.Bind(bkf, aHandler1)
.BindAfter(bkf, aHandler2, typeof(TypeB), typeof(TypeC))
.Register<TypeA>(registry);
CommandBinds.Builder
.BindBefore(bkf, bHandler1, typeof(TypeA))
.BindBefore(bkf, bHandler2, typeof(TypeA))
.Register<TypeB>(registry);
CommandBinds.Builder
.BindBefore(bkf, cHandler1, typeof(TypeA))
.BindBefore(bkf, cHandler2, typeof(TypeA))
.Register<TypeC>(registry);
}
else if (before)
{
CommandBinds.Builder
.Bind(bkf, aHandler1)
.Bind(bkf, aHandler2)
.Register<TypeA>(registry);
CommandBinds.Builder
.BindBefore(bkf, bHandler1, typeof(TypeA))
.BindBefore(bkf, bHandler2, typeof(TypeA))
.Register<TypeB>(registry);
CommandBinds.Builder
.BindBefore(bkf, cHandler1, typeof(TypeA))
.BindBefore(bkf, cHandler2, typeof(TypeA))
.Register<TypeC>(registry);
}
else if (after)
{
CommandBinds.Builder
.Bind(bkf, aHandler1)
.BindAfter(bkf, aHandler2, typeof(TypeB), typeof(TypeC))
.Register<TypeA>(registry);
CommandBinds.Builder
.Bind(bkf, bHandler1)
.Bind(bkf, bHandler2)
.Register<TypeB>(registry);
CommandBinds.Builder
.Bind(bkf, cHandler1)
.Bind(bkf, cHandler2)
.Register<TypeC>(registry);
}
var returnedHandlers = registry.GetHandlers(bkf);
// b1 , b2, c1, c2 should be fired before a2
bool foundB1 = false, foundB2 = false, foundC1 = false, foundC2 = false;
foreach (var returnedHandler in returnedHandlers)
{
if (returnedHandler == bHandler1)
{
foundB1 = true;
}
else if (returnedHandler == bHandler2)
{
foundB2 = true;
}
else if (returnedHandler == cHandler1)
{
foundC1= true;
}
else if (returnedHandler == cHandler2)
{
foundC2 = true;
}
else if (returnedHandler == aHandler2)
{
Assert.That(foundB1 && foundB2 && foundC1 && foundC2, "bind registry didn't respect" +
" handler dependency order");
}
}
var expectedHandlers =
new []{aHandler1, aHandler2, bHandler1, bHandler2, cHandler1, cHandler2};
var returnedHandlerSet = new HashSet<InputCmdHandler>(returnedHandlers);
foreach (var expectedHandler in expectedHandlers)
{
Assert.That(returnedHandlerSet.Contains(expectedHandler));
}
}
[Test]
public void ThrowsError_WhenCircularDependency()
{
var registry = new CommandBindRegistry(new DummySawmill());
var bkf = new BoundKeyFunction("test");
var aHandler1 = new TestInputCmdHandler();
var aHandler2 = new TestInputCmdHandler();
var bHandler1 = new TestInputCmdHandler();
var bHandler2 = new TestInputCmdHandler();
var cHandler1 = new TestInputCmdHandler();
var cHandler2 = new TestInputCmdHandler();
CommandBinds.Builder
.Bind(bkf, aHandler1)
.BindAfter(bkf, aHandler2, typeof(TypeB), typeof(TypeC))
.Register<TypeA>(registry);
CommandBinds.Builder
.Bind(bkf, bHandler1)
.Bind(bkf, bHandler2)
.Register<TypeB>(registry);
CommandBinds.Builder
.Bind(bkf, cHandler1)
.BindAfter(bkf, cHandler2, typeof(TypeA))
.Register<TypeC>(registry);
Assert.Throws<InvalidOperationException>(registry.RebuildGraph);
}
}
}

View File

@@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using NUnit.Framework;
using Robust.Shared.ContentPack;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Log;
namespace Robust.UnitTesting.Shared.Localization;
[TestFixture]
internal sealed class LoadLocalizationTest : OurRobustUnitTest
{
private const string DuplicateTerm = @"
term1 = 1
term1 = 2
";
protected override void OverrideIoC()
{
base.OverrideIoC();
IoCManager.Register<ILogManager, SpyLogManager>(overwrite: true);
}
[Test]
public void TestLoadLocalization()
{
var res = IoCManager.Resolve<IResourceManagerInternal>();
res.MountString("/Locale/en-US/a.ftl", DuplicateTerm);
var loc = IoCManager.Resolve<ILocalizationManager>();
loc.Initialize();
var spyLog = (SpyLogManager) IoCManager.Resolve<ILogManager>();
var culture = new CultureInfo("en-US", false);
var x = spyLog.CountError;
loc.LoadCulture(culture);
Assert.That(spyLog.CountError, NUnit.Framework.Is.GreaterThan(x));
}
}

View File

@@ -0,0 +1,258 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using NUnit.Framework;
using Robust.Shared.ContentPack;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Components.Localization;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Utility;
namespace Robust.UnitTesting.Shared.Localization
{
[TestFixture]
internal sealed class LocalizationTests : OurRobustUnitTest
{
protected override Type[] ExtraComponents => new[] {typeof(GrammarComponent)};
[OneTimeSetUp]
public void Setup()
{
IoCManager.Resolve<ISerializationManager>().Initialize();
var res = IoCManager.Resolve<IResourceManagerInternal>();
res.MountString("/Locale/en-US/a.ftl", FluentCode);
res.MountString("/EnginePrototypes/a.yml", YAMLCode);
var protoMan = IoCManager.Resolve<IPrototypeManager>();
protoMan.RegisterKind(typeof(EntityPrototype), typeof(EntityCategoryPrototype));
protoMan.LoadDirectory(new ResPath("/EnginePrototypes"));
protoMan.ResolveResults();
var loc = IoCManager.Resolve<ILocalizationManager>();
var culture = new CultureInfo("en-US", false);
loc.Initialize();
loc.LoadCulture(culture);
}
private const string YAMLCode = @"
# Values specified in prototype
- type: entity
id: PropsInPrototype
name: A
description: B
suffix: C
loc:
gender: male
proper: true
# Values specified in fluent
- type: entity
id: PropsInLoc
# Values specified in YAML but overriden by fluent.
- type: entity
id: PropsInLocOverriding
name: XA
description: XB
suffix: XC
loc:
gender: female
proper: false
# Values specified in fluent at PARENT and replaced by child YAML.
- type: entity
id: TestInheritOverridingParent
- type: entity
id: TestInheritOverridingChild
parent: TestInheritOverridingParent
name: A
description: B
suffix: C
loc:
gender: male
proper: true
# Attribures stored in grammar component
- type: entity
id: PropsInGrammar
name: A
description: B
suffix: C
loc:
gender: female
proper: false
components:
- type: Grammar
attributes:
gender: male
proper: true
- type: entity
id: GenderTestEntityNoComp
- type: entity
id: GenderTestEntityWithComp
components:
- type: Grammar
attributes:
gender: Female
";
private const string FluentCode = @"
enum-match = { $enum ->
[foo] A
*[bar] B
}
num-selector = { $num ->
[0] A
[1] B
*[2] C
}
ent-GenderTestEntityNoComp = Gender Test Entity
.gender = male
.otherAttrib = sausages
ent-GenderTestEntityWithComp = Gender Test Entity 2
ent-PropsInLoc = A
.desc = B
.suffix = C
.gender = male
.proper = true
ent-PropsInLocOverriding = A
.desc = B
.suffix = C
.gender = male
.proper = true
ent-TestInheritOverridingParent = XA
.desc = XB
.suffix = XC
.gender = female
.proper = false
test-message-gender = { GENDER($entity) ->
[male] male
[female] female
*[neuter] BAD
}
test-message-proper = { PROPER($entity) ->
[true] true
*[false] false
}
test-message-custom-attrib = { ATTRIB($entity, ""otherAttrib"") }
";
[Test]
public void TestEnumSelect()
{
var loc = IoCManager.Resolve<ILocalizationManager>();
Assert.That(loc.GetString("enum-match", ("enum", TestEnum.Foo)), Is.EqualTo("A"));
Assert.That(loc.GetString("enum-match", ("enum", TestEnum.Bar)), Is.EqualTo("B"));
Assert.That(loc.GetString("enum-match", ("enum", TestEnum.Baz)), Is.EqualTo("B"));
}
[Test]
public void TestCustomFunctions()
{
var entMan = IoCManager.Resolve<IEntityManager>();
var testEntNoComp = entMan.CreateEntityUninitialized("GenderTestEntityNoComp");
var testEntWithComp = entMan.CreateEntityUninitialized("GenderTestEntityWithComp");
var loc = IoCManager.Resolve<ILocalizationManager>();
var genderFromAttrib = loc.GetString("test-message-gender", ("entity", testEntNoComp));
var genderFromGrammar = loc.GetString("test-message-gender", ("entity", testEntWithComp));
var customAttrib = loc.GetString("test-message-custom-attrib", ("entity", testEntNoComp));
Assert.Multiple(() =>
{
Assert.That(genderFromAttrib, Is.EqualTo("male"));
Assert.That(genderFromGrammar, Is.EqualTo("female"));
Assert.That(customAttrib, Is.EqualTo("sausages"));
});
}
static IEnumerable<object[]> NumericTestSource()
{
const byte b1 = 0, b2 = 1, b3 = 5;
const sbyte sb1 = 0, sb2 = 1, sb3 = 5;
const short sh1 = 0, sh2 = 1, sh3 = 5;
const ushort ush1 = 0, ush2 = 1, ush3 = 5;
const int i1 = 0, i2 = 1, i3 = 5;
const uint ui1 = 0, ui2 = 1, ui3 = 5;
const long lng1 = 0, lng2 = 1, lng3 = 5;
const ulong ulng1 = 0, ulng2 = 1, ulng3 = 5;
const float f1 = 0, f2 = 1, f3 = 5;
const double d1 = 0, d2 = 1, d3 = 5;
yield return new object[] { b1, b2, b3 };
yield return new object[] { sb1, sb2, sb3 };
yield return new object[] { sh1, sh2, sh3 };
yield return new object[] { ush1, ush2, ush3 };
yield return new object[] { i1, i2, i3 };
yield return new object[] { ui1, ui2, ui3 };
yield return new object[] { lng1, lng2, lng3 };
yield return new object[] { ulng1, ulng2, ulng3 };
yield return new object[] { f1, f2, f3 };
yield return new object[] { d1, d2, d3 };
}
[Test]
[TestCaseSource(nameof(NumericTestSource))]
public void TestNumbers(object o1, object o2, object o3)
{
// Small test to check numbers are being properly converted
var loc = IoCManager.Resolve<ILocalizationManager>();
var func = new Func<object, string>(x1 => loc.GetString("num-selector", ("num", x1)));
Assert.Multiple(() =>
{
Assert.That(func(o1), Is.EqualTo("A"));
Assert.That(func(o2), Is.EqualTo("B"));
Assert.That(func(o3), Is.EqualTo("C"));
});
}
[Test]
[TestCase("PropsInPrototype")]
[TestCase("PropsInLoc")]
[TestCase("PropsInLocOverriding")]
[TestCase("PropsInGrammar")]
[TestCase("TestInheritOverridingChild")]
public void TestLocData(string prototype)
{
var loc = IoCManager.Resolve<ILocalizationManager>();
var entMan = IoCManager.Resolve<IEntityManager>();
var ent = entMan.CreateEntityUninitialized(prototype);
Assert.That(IoCManager.Resolve<IEntityManager>().GetComponent<MetaDataComponent>(ent).EntityName, Is.EqualTo("A"));
Assert.That(IoCManager.Resolve<IEntityManager>().GetComponent<MetaDataComponent>(ent).EntityDescription, Is.EqualTo("B"));
Assert.That(IoCManager.Resolve<IEntityManager>().GetComponent<MetaDataComponent>(ent).EntityPrototype!.EditorSuffix, Is.EqualTo("C"));
Assert.That(loc.GetString("test-message-gender", ("entity", ent)), Is.EqualTo("male"));
Assert.That(loc.GetString("test-message-proper", ("entity", ent)), Is.EqualTo("true"));
}
private enum TestEnum
{
Foo,
Bar,
Baz
}
}
}

View File

@@ -0,0 +1,301 @@
using System.Numerics;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager;
// ReSharper disable InconsistentNaming
namespace Robust.UnitTesting.Shared.Map
{
[TestFixture, Parallelizable, TestOf(typeof(EntityCoordinates))]
internal sealed class EntityCoordinates_Tests : OurRobustUnitTest
{
[OneTimeSetUp]
public void Setup()
{
IoCManager.Resolve<ISerializationManager>().Initialize();
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
prototypeManager.RegisterKind(typeof(EntityPrototype), typeof(EntityCategoryPrototype));
prototypeManager.LoadString(""); // Set _hasEverBeenReloaded to true;
prototypeManager.ResolveResults();
var factory = IoCManager.Resolve<IComponentFactory>();
factory.GenerateNetIds();
}
/// <summary>
/// Passing an invalid entity ID into the constructor makes it invalid.
/// </summary>
[Test]
public void IsValid_InvalidEntId_False()
{
var entityManager = IoCManager.Resolve<IEntityManager>();
// Same as EntityCoordinates.Invalid
var coords = new EntityCoordinates(EntityUid.Invalid, Vector2.Zero);
Assert.That(coords.IsValid(entityManager), Is.False);
}
/// <summary>
/// Deleting the parent entity should make the coordinates invalid.
/// </summary>
[Test]
public void IsValid_EntityDeleted_False()
{
var entityManager = IoCManager.Resolve<IEntityManager>();
var mapManager = IoCManager.Resolve<IMapManager>();
var mapEntity = entityManager.System<SharedMapSystem>().CreateMap(out var mapId);
var newEnt = entityManager.CreateEntityUninitialized(null, new MapCoordinates(Vector2.Zero, mapId));
var coords = IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(newEnt).Coordinates;
var result = coords.IsValid(entityManager);
Assert.That(result, Is.True);
IoCManager.Resolve<IEntityManager>().DeleteEntity(mapEntity);
result = coords.IsValid(entityManager);
Assert.That(result, Is.False);
}
/// <summary>
/// Passing a valid entity ID into the constructor with infinite numbers makes it invalid.
/// </summary>
[TestCase(float.NaN, float.NaN)]
[TestCase(0, float.NaN)]
[TestCase(float.NaN, 0)]
public void IsValid_NonFiniteVector_False(float x, float y)
{
var entityManager = IoCManager.Resolve<IEntityManager>();
entityManager.System<SharedMapSystem>().CreateMap(out var mapId);
var newEnt = entityManager.CreateEntityUninitialized(null, new MapCoordinates(new Vector2(x, y), mapId));
var coords = IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(newEnt).Coordinates;
Assert.That(coords.IsValid(entityManager), Is.False);
}
[Test]
public void EntityCoordinates_Map()
{
var mapEntity = IoCManager.Resolve<IEntityManager>().System<SharedMapSystem>().CreateMap();
Assert.That(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(mapEntity).ParentUid.IsValid(), Is.False);
Assert.That(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(mapEntity).Coordinates.EntityId, Is.EqualTo(mapEntity));
}
/// <summary>
/// Even if we change the local position of an entity without a parent,
/// EntityCoordinates should still return offset 0.
/// </summary>
[Test]
public void NoParent_OffsetZero()
{
var entMan = IoCManager.Resolve<IEntityManager>();
var xformSys = entMan.System<SharedTransformSystem>();
var uid = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
var xform = entMan.GetComponent<TransformComponent>(uid);
Assert.That(xform.Coordinates.Position, Is.EqualTo(Vector2.Zero));
xformSys.SetLocalPosition(uid, Vector2.One);
Assert.That(xform.Coordinates.Position, Is.EqualTo(Vector2.Zero));
}
[Test]
public void GetGridId_Map()
{
var entityManager = IoCManager.Resolve<IEntityManager>();
var xformSys = entityManager.System<SharedTransformSystem>();
var mapEnt = entityManager.System<SharedMapSystem>().CreateMap(out var mapId);
var newEnt = entityManager.CreateEntityUninitialized(null, new MapCoordinates(Vector2.Zero, mapId));
Assert.That(xformSys.GetGrid(entityManager.GetComponent<TransformComponent>(mapEnt).Coordinates), Is.Null);
Assert.That(xformSys.GetGrid(entityManager.GetComponent<TransformComponent>(newEnt).Coordinates), Is.Null);
Assert.That(entityManager.GetComponent<TransformComponent>(newEnt).Coordinates.EntityId, Is.EqualTo(mapEnt));
}
[Test]
public void GetGridId_Grid()
{
var entityManager = IoCManager.Resolve<IEntityManager>();
var mapManager = IoCManager.Resolve<IMapManager>();
var xformSys = entityManager.System<SharedTransformSystem>();
entityManager.System<SharedMapSystem>().CreateMap(out var mapId);
var grid = mapManager.CreateGridEntity(mapId);
var gridEnt = grid.Owner;
var newEnt = entityManager.CreateEntityUninitialized(null, new EntityCoordinates(gridEnt, Vector2.Zero));
// Grids aren't parented to other grids.
Assert.That(xformSys.GetGrid(entityManager.GetComponent<TransformComponent>(gridEnt).Coordinates), Is.Null);
Assert.That(xformSys.GetGrid(entityManager.GetComponent<TransformComponent>(newEnt).Coordinates), Is.EqualTo(grid.Owner));
Assert.That(entityManager.GetComponent<TransformComponent>(newEnt).Coordinates.EntityId, Is.EqualTo(gridEnt));
}
[Test]
public void GetMapId_Map()
{
var entityManager = IoCManager.Resolve<IEntityManager>();
var xformSys = entityManager.System<SharedTransformSystem>();
var mapEnt = entityManager.System<SharedMapSystem>().CreateMap(out var mapId);
var newEnt = entityManager.CreateEntityUninitialized(null, new MapCoordinates(Vector2.Zero, mapId));
Assert.That(xformSys.GetMapId(entityManager.GetComponent<TransformComponent>(mapEnt).Coordinates), Is.EqualTo(mapId));
Assert.That(xformSys.GetMapId(entityManager.GetComponent<TransformComponent>(newEnt).Coordinates), Is.EqualTo(mapId));
}
[Test]
public void GetMapId_Grid()
{
var entityManager = IoCManager.Resolve<IEntityManager>();
var mapManager = IoCManager.Resolve<IMapManager>();
var xformSys = entityManager.System<SharedTransformSystem>();
entityManager.System<SharedMapSystem>().CreateMap(out var mapId);
var grid = mapManager.CreateGridEntity(mapId);
var gridEnt = grid.Owner;
var newEnt = entityManager.CreateEntityUninitialized(null, new EntityCoordinates(gridEnt, Vector2.Zero));
Assert.That(xformSys.GetMapId(entityManager.GetComponent<TransformComponent>(gridEnt).Coordinates), Is.EqualTo(mapId));
Assert.That(xformSys.GetMapId(entityManager.GetComponent<TransformComponent>(newEnt).Coordinates), Is.EqualTo(mapId));
}
[Test]
public void GetParent()
{
var entityManager = IoCManager.Resolve<IEntityManager>();
var mapManager = IoCManager.Resolve<IMapManager>();
var xformSys = entityManager.System<SharedTransformSystem>();
var mapEnt = entityManager.System<SharedMapSystem>().CreateMap(out var mapId);
var grid = mapManager.CreateGridEntity(mapId);
var gridEnt = grid.Owner;
var newEnt = entityManager.CreateEntityUninitialized(null, new EntityCoordinates(grid, Vector2.Zero));
Assert.That(entityManager.GetComponent<TransformComponent>(mapEnt).Coordinates.EntityId, Is.EqualTo(mapEnt));
Assert.That(entityManager.GetComponent<TransformComponent>(gridEnt).Coordinates.EntityId, Is.EqualTo(mapEnt));
Assert.That(entityManager.GetComponent<TransformComponent>(newEnt).Coordinates.EntityId, Is.EqualTo(gridEnt));
// Reparenting the entity should return correct results.
xformSys.SetParent(newEnt, mapEnt);
Assert.That(entityManager.GetComponent<TransformComponent>(newEnt).Coordinates.EntityId, Is.EqualTo(mapEnt));
}
[Test]
public void TryGetParent()
{
var entityManager = IoCManager.Resolve<IEntityManager>();
var mapManager = IoCManager.Resolve<IMapManager>();
var xformSys = entityManager.System<SharedTransformSystem>();
var mapEnt = entityManager.System<SharedMapSystem>().CreateMap(out var mapId);
var grid = mapManager.CreateGridEntity(mapId);
var gridEnt = grid.Owner;
var newEnt = entityManager.CreateEntityUninitialized(null, new EntityCoordinates(grid, Vector2.Zero));
var mapCoords = entityManager.GetComponent<TransformComponent>(mapEnt).Coordinates;
Assert.That(mapCoords.IsValid(entityManager), Is.EqualTo(true));
Assert.That(mapCoords.EntityId, Is.EqualTo(mapEnt));
var gridCoords = entityManager.GetComponent<TransformComponent>(mapEnt).Coordinates;
Assert.That(gridCoords.IsValid(entityManager), Is.EqualTo(true));
Assert.That(gridCoords.EntityId, Is.EqualTo(mapEnt));
var newEntTransform = entityManager.GetComponent<TransformComponent>(newEnt);
var newEntCoords = newEntTransform.Coordinates;
Assert.That(newEntCoords.IsValid(entityManager), Is.EqualTo(true));
Assert.That(newEntCoords.EntityId, Is.EqualTo(gridEnt));
// Reparenting the entity should return correct results.
xformSys.SetParent(newEnt, mapEnt);
var newEntCoords2 = newEntTransform.Coordinates;
Assert.That(newEntCoords2.IsValid(entityManager), Is.EqualTo(true));
Assert.That(newEntCoords2.EntityId, Is.EqualTo(mapEnt));
// Deleting the parent should make TryGetParent return false.
entityManager.DeleteEntity(mapEnt);
// These shouldn't be valid anymore.
Assert.That(entityManager.Deleted(newEnt), Is.True);
Assert.That(entityManager.Deleted(gridEnt), Is.True);
Assert.That(entityManager.Deleted(newEntCoords.EntityId), Is.True);
Assert.That(entityManager.Deleted(gridCoords.EntityId), Is.True);
}
[Test]
[TestCase(0, 0, 0, 0)]
[TestCase(5, 0, 0, 0)]
[TestCase(5, 0, 0, 5)]
[TestCase(-5, 5, 5, -5)]
[TestCase(100, -500, -200, -300)]
public void ToMap_MoveGrid(float x1, float y1, float x2, float y2)
{
var gridPos = new Vector2(x1, y1);
var entPos = new Vector2(x2, y2);
var entityManager = IoCManager.Resolve<IEntityManager>();
var mapManager = IoCManager.Resolve<IMapManager>();
var xformSys = entityManager.System<SharedTransformSystem>();
entityManager.System<SharedMapSystem>().CreateMap(out var mapId);
var grid = mapManager.CreateGridEntity(mapId);
var gridEnt = grid.Owner;
var newEnt = entityManager.CreateEntityUninitialized(null, new EntityCoordinates(grid, entPos));
var newXform = entityManager.GetComponent<TransformComponent>(newEnt);
Assert.That(xformSys.ToMapCoordinates(newXform.Coordinates), Is.EqualTo(new MapCoordinates(entPos, mapId)));
Assert.That(xformSys.ToMapCoordinates(newXform.Coordinates).Position, Is.EqualTo(xformSys.ToWorldPosition(newXform.Coordinates)));
xformSys.SetLocalPosition(gridEnt, entityManager.GetComponent<TransformComponent>(gridEnt).LocalPosition + gridPos);
Assert.That(xformSys.ToMapCoordinates(newXform.Coordinates), Is.EqualTo(new MapCoordinates(entPos + gridPos, mapId)));
Assert.That(xformSys.ToMapCoordinates(newXform.Coordinates).Position, Is.EqualTo(xformSys.ToWorldPosition(newXform.Coordinates)));
}
[Test]
public void WithEntityId()
{
var entityManager = IoCManager.Resolve<IEntityManager>();
var mapManager = IoCManager.Resolve<IMapManager>();
var xformSys = entityManager.System<SharedTransformSystem>();
var mapEnt = entityManager.System<SharedMapSystem>().CreateMap(out var mapId);
var grid = mapManager.CreateGridEntity(mapId);
var gridEnt = grid.Owner;
var newEnt = entityManager.CreateEntityUninitialized(null, new EntityCoordinates(grid, Vector2.Zero));
var newEntXform = entityManager.GetComponent<TransformComponent>(newEnt);
Assert.That(xformSys.WithEntityId(newEntXform.Coordinates, mapEnt).Position, Is.EqualTo(Vector2.Zero));
xformSys.SetLocalPosition(newEnt, Vector2.One);
Assert.That(newEntXform.Coordinates.Position, Is.EqualTo(Vector2.One));
Assert.That(xformSys.WithEntityId(newEntXform.Coordinates, mapEnt).Position, Is.EqualTo(Vector2.One));
xformSys.SetLocalPosition(gridEnt, Vector2.One);
Assert.That(newEntXform.Coordinates.Position, Is.EqualTo(Vector2.One));
Assert.That(xformSys.WithEntityId(newEntXform.Coordinates, mapEnt).Position, Is.EqualTo(new Vector2(2, 2)));
var newEntTwo = entityManager.CreateEntityUninitialized(null, new EntityCoordinates(newEnt, Vector2.Zero));
var newEntTwoXform = entityManager.GetComponent<TransformComponent>(newEntTwo);
Assert.That(newEntTwoXform.Coordinates.Position, Is.EqualTo(Vector2.Zero));
Assert.That(xformSys.WithEntityId(newEntTwoXform.Coordinates, mapEnt).Position, Is.EqualTo(xformSys.WithEntityId(newEntXform.Coordinates, mapEnt).Position));
Assert.That(xformSys.WithEntityId(newEntTwoXform.Coordinates, gridEnt).Position, Is.EqualTo(newEntXform.Coordinates.Position));
xformSys.SetLocalPosition(newEntTwo, -Vector2.One);
Assert.That(newEntTwoXform.Coordinates.Position, Is.EqualTo(-Vector2.One));
Assert.That(xformSys.WithEntityId(newEntTwoXform.Coordinates, mapEnt).Position, Is.EqualTo(Vector2.One));
Assert.That(xformSys.WithEntityId(newEntTwoXform.Coordinates, gridEnt).Position, Is.EqualTo(Vector2.Zero));
}
}
}

View File

@@ -0,0 +1,74 @@
using NUnit.Framework;
using Robust.Shared.Map;
using Robust.Shared.Maths;
namespace Robust.UnitTesting.Shared.Map
{
[TestFixture, Parallelizable, TestOf(typeof(GridChunkPartition))]
internal sealed class GridChunkPartition_Tests : OurRobustUnitTest
{
// origin is top left
private static readonly int[] _testMiscTiles = {
1, 1, 0, 0,
0, 1, 1, 0,
0, 1, 1, 1,
1, 1, 0, 0,
};
[Test]
public void PartitionChunk_MiscTiles()
{
// Arrange
var chunk = ChunkFactory(4, _testMiscTiles);
// Act
GridChunkPartition.PartitionChunk(chunk, out var bounds, out _);
// box origin is top left
// algorithm goes down columns of array, starting on left side, then moves right, expanding rectangles to the right
/*
0 2 . .
. 2 3 .
. 2 3 4
1 2 . .
*/
Assert.That(bounds, Is.EqualTo(new Box2i(0,0,4,4)));
}
// origin is top left
private static readonly int[] _testJoinTiles = {
0, 1, 1, 0,
0, 1, 1, 0,
0, 1, 1, 0,
0, 0, 0, 0,
};
[Test]
public void PartitionChunk_JoinTiles()
{
// Arrange
var chunk = ChunkFactory(4, _testJoinTiles);
// Act
GridChunkPartition.PartitionChunk(chunk, out var bounds, out _);
Assert.That(bounds, Is.EqualTo(new Box2i(1, 0, 3, 3)));
}
private static MapChunk ChunkFactory(ushort size, int[] tiles)
{
var chunk = new MapChunk(0, 0, size);
for (var i = 0; i < tiles.Length; i++)
{
var x = i % size;
var y = i / size;
chunk.TrySetTile((ushort)x, (ushort)y, new Tile((ushort)tiles[i]), out _, out _);
}
return chunk;
}
}
}

View File

@@ -0,0 +1,108 @@
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Shared.GameObjects;
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;
namespace Robust.UnitTesting.Shared.Map
{
internal sealed class GridCollision_Test : RobustIntegrationTest
{
[Test]
public async Task TestGridsCollide()
{
var server = StartServer();
await server.WaitIdleAsync();
var mapManager = server.ResolveDependency<IMapManager>();
var entManager = server.ResolveDependency<IEntityManager>();
var physSystem = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<SharedPhysicsSystem>();
var mapSystem = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<SharedMapSystem>();
MapId mapId;
Entity<MapGridComponent>? gridId1 = null;
Entity<MapGridComponent>? gridId2 = null;
PhysicsComponent? physics1 = null;
PhysicsComponent? physics2 = null;
EntityUid? gridEnt1;
EntityUid? gridEnt2;
await server.WaitPost(() =>
{
entManager.System<SharedMapSystem>().CreateMap(out mapId);
gridId1 = mapManager.CreateGridEntity(mapId);
gridId2 = mapManager.CreateGridEntity(mapId);
gridEnt1 = gridId1.Value.Owner;
gridEnt2 = gridId2.Value.Owner;
physics1 = entManager.GetComponent<PhysicsComponent>(gridEnt1.Value);
physics2 = entManager.GetComponent<PhysicsComponent>(gridEnt2.Value);
// Can't collide static bodies and grids (at time of this writing) start as static
// (given most other games would probably prefer them as static) hence we need to make them dynamic.
physSystem.SetBodyType(gridEnt1.Value, BodyType.Dynamic, body: physics1);
physSystem.SetBodyType(gridEnt2.Value, BodyType.Dynamic, body: physics2);
});
await server.WaitRunTicks(1);
// No tiles set hence should be no collision
await server.WaitAssertion(() =>
{
var node = physics1?.Contacts.First;
while (node != null)
{
var contact = node.Value;
node = node.Next;
var bodyA = contact.BodyA;
var bodyB = contact.BodyB;
var other = physics1 == bodyA ? bodyB : bodyA;
Assert.That(other, Is.Not.EqualTo(physics2));
}
});
await server.WaitAssertion(() =>
{
mapSystem.SetTile(gridId1!.Value, new Vector2i(0, 0), new Tile(1));
mapSystem.SetTile(gridId2!.Value, new Vector2i(0, 0), new Tile(1));
});
await server.WaitRunTicks(1);
await server.WaitAssertion(() =>
{
var colliding = false;
var node = physics1?.Contacts.First;
while (node != null)
{
var contact = node.Value;
node = node.Next;
if (!contact.IsTouching)
continue;
var bodyA = contact.BodyA;
var bodyB = contact.BodyB;
var other = physics1 == bodyA ? bodyB : bodyA;
if (other == physics2)
{
colliding = true;
break;
}
}
Assert.That(colliding);
});
}
}
}

View File

@@ -0,0 +1,81 @@
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Shared;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
namespace Robust.UnitTesting.Shared.Map
{
[TestFixture]
internal sealed class GridContraction_Test : RobustIntegrationTest
{
[Test]
public async Task TestGridDeletes()
{
var server = StartServer();
await server.WaitIdleAsync();
var entManager = server.ResolveDependency<IEntityManager>();
var mapManager = server.ResolveDependency<IMapManager>();
var mapSystem = entManager.EntitySysManager.GetEntitySystem<SharedMapSystem>();
await server.WaitAssertion(() =>
{
entManager.System<SharedMapSystem>().CreateMap(out var mapId);
var grid = mapManager.CreateGridEntity(mapId);
var gridEntity = grid.Owner;
for (var i = 0; i < 10; i++)
{
mapSystem.SetTile(grid, new Vector2i(i, 0), new Tile(1));
}
for (var i = 10; i >= 0; i--)
{
mapSystem.SetTile(grid, new Vector2i(i, 0), Tile.Empty);
}
Assert.That(entManager.Deleted(gridEntity));
});
}
[Test]
public async Task TestGridNoDeletes()
{
var options = new ServerIntegrationOptions()
{
CVarOverrides =
{
{
CVars.GameDeleteEmptyGrids.Name, "false"
}
}
};
var server = StartServer(options);
await server.WaitIdleAsync();
var entManager = server.ResolveDependency<IEntityManager>();
var mapManager = server.ResolveDependency<IMapManager>();
var mapSystem = entManager.System<SharedMapSystem>();
await server.WaitAssertion(() =>
{
entManager.System<SharedMapSystem>().CreateMap(out var mapId);
var grid = mapManager.CreateGridEntity(mapId);
for (var i = 0; i < 10; i++)
{
mapSystem.SetTile(grid, new Vector2i(i, 0), new Tile(1));
}
for (var i = 10; i >= 0; i--)
{
mapSystem.SetTile(grid, new Vector2i(i, 0), Tile.Empty);
}
Assert.That(!((!entManager.EntityExists(grid) ? EntityLifeStage.Deleted : entManager.GetComponent<MetaDataComponent>(grid).EntityLifeStage) >= EntityLifeStage.Deleted));
});
}
}
}

View File

@@ -0,0 +1,101 @@
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
using Robust.UnitTesting.Server;
namespace Robust.UnitTesting.Shared.Map;
/// <summary>
/// Tests whether grid fixtures are being generated correctly.
/// </summary>
[Parallelizable(ParallelScope.All)]
[TestFixture]
internal sealed class GridFixtures_Tests : RobustIntegrationTest
{
/// <summary>
/// Tests that grid fixtures match what's expected.
/// </summary>
[Test]
public void TestGridFixtureDeletion()
{
var server = RobustServerSimulation.NewSimulation().InitializeInstance();
var map = server.CreateMap();
var entManager = server.Resolve<IEntityManager>();
var grid = server.Resolve<IMapManager>().CreateGridEntity(map.MapId);
var mapSystem = entManager.System<SharedMapSystem>();
var fixtures = entManager.GetComponent<FixturesComponent>(grid);
mapSystem.SetTiles(grid, new List<(Vector2i GridIndices, Tile Tile)>()
{
(Vector2i.Zero, new Tile(1)),
(Vector2i.Right, new Tile(1)),
(Vector2i.Right * 2, new Tile(1)),
(Vector2i.Up, new Tile(1)),
});
Assert.That(fixtures.FixtureCount, Is.EqualTo(2));
Assert.That(grid.Comp.LocalAABB.Equals(new Box2(0f, 0f, 3f, 2f)));
mapSystem.SetTile(grid, Vector2i.Up, Tile.Empty);
Assert.That(fixtures.FixtureCount, Is.EqualTo(1));
Assert.That(grid.Comp.LocalAABB.Equals(new Box2(0f, 0f, 3f, 1f)));
}
[Test]
public async Task TestGridFixtures()
{
var server = StartServer();
await server.WaitIdleAsync();
var entManager = server.ResolveDependency<IEntityManager>();
var mapManager = server.ResolveDependency<IMapManager>();
var physSystem = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<SharedPhysicsSystem>();
var mapSystem = entManager.EntitySysManager.GetEntitySystem<SharedMapSystem>();
await server.WaitAssertion(() =>
{
entManager.System<SharedMapSystem>().CreateMap(out var mapId);
var grid = mapManager.CreateGridEntity(mapId);
// Should be nothing if grid empty
Assert.That(entManager.TryGetComponent(grid, out PhysicsComponent? gridBody));
Assert.That(entManager.TryGetComponent(grid, out FixturesComponent? manager));
Assert.That(manager!.FixtureCount, Is.EqualTo(0));
Assert.That(gridBody!.BodyType, Is.EqualTo(BodyType.Static));
// 1 fixture if we only ever update the 1 chunk
mapSystem.SetTile(grid, Vector2i.Zero, new Tile(1));
Assert.That(manager.FixtureCount, Is.EqualTo(1));
// Also should only be a single tile.
var bounds = manager.Fixtures.First().Value.Shape.ComputeAABB(new Transform(Vector2.Zero, (float) Angle.Zero.Theta), 0);
// Poly probably has Box2D's radius added to it so won't be a unit square
Assert.That(MathHelper.CloseToPercent(Box2.Area(bounds), 1.0f, 0.1f));
// Now do 2 tiles (same chunk)
mapSystem.SetTile(grid, new Vector2i(0, 1), new Tile(1));
Assert.That(manager.FixtureCount, Is.EqualTo(1));
bounds = manager.Fixtures.First().Value.Shape.ComputeAABB(new Transform(Vector2.Zero, (float) Angle.Zero.Theta), 0);
// Even if we add a new tile old fixture should stay the same if they don't connect.
Assert.That(MathHelper.CloseToPercent(Box2.Area(bounds), 2.0f, 0.1f));
// If we add a new chunk should be 2 now
mapSystem.SetTile(grid, new Vector2i(0, -1), new Tile(1));
Assert.That(manager.FixtureCount, Is.EqualTo(2));
physSystem.SetLinearVelocity(grid, Vector2.One, manager: manager, body: gridBody);
Assert.That(gridBody.LinearVelocity.Length, Is.EqualTo(0f));
});
}
}

View File

@@ -0,0 +1,69 @@
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using Robust.Server.Physics;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.UnitTesting.Server;
namespace Robust.UnitTesting.Shared.Map;
[Parallelizable(ParallelScope.All | ParallelScope.Fixtures)]
[TestFixture]
internal sealed class GridMerge_Tests
{
private ISimulation GetSim()
{
var sim = RobustServerSimulation.NewSimulation().InitializeInstance();
var config = sim.Resolve<IConfigurationManager>();
config.SetCVar(CVars.GridSplitting, true);
return sim;
}
private static readonly TestCaseData[] MergeCases = new[]
{
new TestCaseData(new Vector2i(-1, 0), Angle.Zero, new Box2(-1f, 0f, 1f, 3f)),
new TestCaseData(new Vector2i(0, 3), Angle.Zero, new Box2(0f, 0f, 1f, 6f)),
new TestCaseData(new Vector2i(0, 1), Angle.FromDegrees(90), new Box2(-3f, 0f, 1f, 3f)),
new TestCaseData(new Vector2i(1, 3), Angle.FromDegrees(-90), new Box2(0f, 0f, 4f, 3f)),
};
/// <summary>
/// Checks 2 grids merge properly.
/// </summary>
[Test, TestCaseSource(nameof(MergeCases))]
public void Merge(Vector2i offset, Angle angle, Box2 bounds)
{
var sim = GetSim();
var mapManager = sim.Resolve<IMapManager>();
var entMan = sim.Resolve<IEntityManager>();
var mapSystem = entMan.System<SharedMapSystem>();
var gridFixtures = entMan.System<GridFixtureSystem>();
var mapId = sim.CreateMap().MapId;
var grid1 = mapManager.CreateGridEntity(mapId);
var grid2 = mapManager.CreateGridEntity(mapId);
var tiles = new List<(Vector2i, Tile)>();
for (var y = 0; y < 3; y++)
{
tiles.Add((new Vector2i(0, y), new Tile(1)));
}
mapSystem.SetTiles(grid1, tiles);
mapSystem.SetTiles(grid2, tiles);
Assert.That(mapManager.GetAllGrids(mapId).Count(), Is.EqualTo(2));
gridFixtures.Merge(grid1.Owner, grid2.Owner, offset, angle, grid1.Comp, grid2.Comp);
Assert.That(mapManager.GetAllGrids(mapId).Count(), Is.EqualTo(1));
Assert.That(grid1.Comp.LocalAABB, Is.EqualTo(bounds));
}
}

View File

@@ -0,0 +1,118 @@
using System;
using System.Linq;
using System.Numerics;
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
namespace Robust.UnitTesting.Shared.Map
{
[TestFixture]
internal sealed class GridRotation_Tests : RobustIntegrationTest
{
// Because integration tests are ten billion percent easier we'll just do all the rotation tests here.
// These are mainly looking out for situations where the grid is rotated 90 / 180 degrees and we
// need to rotate points about the grid's origin which is a /very/ common source of bugs.
[Test]
public async Task TestLocalWorldConversions()
{
var server = StartServer();
await server.WaitIdleAsync();
var entMan = server.ResolveDependency<IEntityManager>();
var mapMan = server.ResolveDependency<IMapManager>();
var mapSystem = entMan.System<SharedMapSystem>();
var transformSystem = entMan.System<SharedTransformSystem>();
await server.WaitAssertion(() =>
{
mapSystem.CreateMap(out var mapId);
var grid = mapMan.CreateGridEntity(mapId);
var gridEnt = grid.Owner;
var coordinates = new EntityCoordinates(gridEnt, new Vector2(10, 0));
// if no rotation and 0,0 position should just be the same coordinate.
Assert.That(transformSystem.GetWorldRotation(gridEnt), Is.EqualTo(Angle.Zero));
Assert.That(mapSystem.WorldToLocal(grid.Owner, grid.Comp, coordinates.Position), Is.EqualTo(coordinates.Position));
// Rotate 180 degrees should show -10, 0 for the position in map-terms and 10, 0 for the position in entity terms (i.e. no change).
transformSystem.SetWorldRotation(gridEnt, transformSystem.GetWorldRotation(gridEnt) + new Angle(MathF.PI));
Assert.That(transformSystem.GetWorldRotation(gridEnt), Is.EqualTo(new Angle(MathF.PI)));
// Check the map coordinate rotates correctly
Assert.That(mapSystem.WorldToLocal(gridEnt, grid.Comp, new Vector2(10, 0)).EqualsApprox(new Vector2(-10, 0), 0.01f));
Assert.That(mapSystem.LocalToWorld(gridEnt, grid.Comp, coordinates.Position).EqualsApprox(new Vector2(-10, 0), 0.01f));
// Now we'll do the same for 180 degrees.
transformSystem.SetWorldRotation(gridEnt, transformSystem.GetWorldRotation(gridEnt) + MathF.PI / 2f);
// If grid facing down then worldpos of 10, 0 gets rotated 90 degrees CCW and hence should be 0, 10
Assert.That(mapSystem.WorldToLocal(gridEnt, grid.Comp, new Vector2(10, 0)).EqualsApprox(new Vector2(0, 10), 0.01f));
// If grid facing down then local 10,0 pos should just return 0, -10 given it's aligned with the rotation.
Assert.That(mapSystem.LocalToWorld(gridEnt, grid.Comp, coordinates.Position).EqualsApprox(new Vector2(0, -10), 0.01f));
});
}
[Test]
public async Task TestChunkRotations()
{
// This is mainly checking for the purposes of rendering at this stage.
var server = StartServer();
await server.WaitIdleAsync();
var entMan = server.ResolveDependency<IEntityManager>();
var mapMan = server.ResolveDependency<IMapManager>();
var mapSystem = entMan.System<SharedMapSystem>();
await server.WaitAssertion(() =>
{
mapSystem.CreateMap(out var mapId);
var grid = mapMan.CreateGridEntity(mapId);
var gridEnt = grid.Owner;
/* Test for map chunk rotations */
var tile = new Tile(1);
for (var x = 0; x < 2; x++)
{
for (var y = 0; y < 10; y++)
{
mapSystem.SetTile(grid, new Vector2i(x, y), tile);
}
}
var chunks = mapSystem.GetMapChunks(gridEnt, grid.Comp).Select(c => c.Value).ToList();
Assert.That(chunks.Count, Is.EqualTo(1));
var chunk = chunks[0];
var aabb = mapSystem.CalcWorldAABB(gridEnt, grid, chunk);
var bounds = new Box2(new Vector2(0, 0), new Vector2(2, 10));
// With all cardinal directions these should align.
Assert.That(aabb, Is.EqualTo(bounds));
entMan.GetComponent<TransformComponent>(gridEnt).LocalRotation = new Angle(Math.PI);
aabb = mapSystem.CalcWorldAABB(gridEnt, grid, chunk);
bounds = new Box2(new Vector2(-2, -10), new Vector2(0, 0));
Assert.That(aabb.EqualsApprox(bounds), $"Expected bounds of {aabb} and got {bounds}");
entMan.GetComponent<TransformComponent>(gridEnt).LocalRotation = new Angle(-Math.PI / 2);
aabb = mapSystem.CalcWorldAABB(gridEnt, grid, chunk);
bounds = new Box2(new Vector2(0, -2), new Vector2(10, 0));
Assert.That(aabb.EqualsApprox(bounds), $"Expected bounds of {aabb} and got {bounds}");
entMan.GetComponent<TransformComponent>(gridEnt).LocalRotation = new Angle(-Math.PI / 4);
aabb = mapSystem.CalcWorldAABB(gridEnt, grid, chunk);
bounds = new Box2(new Vector2(0, -1.4142135f), new Vector2(8.485281f, 7.071068f));
Assert.That(aabb.EqualsApprox(bounds), $"Expected bounds of {aabb} and got {bounds}");
});
}
}
}

View File

@@ -0,0 +1,186 @@
using System.Linq;
using System.Numerics;
using NUnit.Framework;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.UnitTesting.Server;
namespace Robust.UnitTesting.Shared.Map;
[Parallelizable(ParallelScope.All | ParallelScope.Fixtures)]
[TestFixture]
internal sealed class GridSplit_Tests
{
private ISimulation GetSim()
{
var sim = RobustServerSimulation.NewSimulation().InitializeInstance();
var config = sim.Resolve<IConfigurationManager>();
config.SetCVar(CVars.GridSplitting, true);
return sim;
}
/// <summary>
/// Does the grid correctly not split when it's disabled.
/// </summary>
[Test]
public void NoSplit()
{
var sim = GetSim();
var mapManager = sim.Resolve<IMapManager>();
var mapSystem = sim.Resolve<IEntityManager>().System<SharedMapSystem>();
var mapId = sim.CreateMap().MapId;
var gridEnt = mapManager.CreateGridEntity(mapId);
var grid = gridEnt.Comp;
grid.CanSplit = false;
for (var x = 0; x < 5; x++)
{
mapSystem.SetTile(gridEnt, new Vector2i(x, 0), new Tile(1));
}
Assert.That(mapManager.GetAllGrids(mapId).Count(), Is.EqualTo(1));
mapSystem.SetTile(gridEnt, new Vector2i(1, 0), Tile.Empty);
Assert.That(mapManager.GetAllGrids(mapId).Count(), Is.EqualTo(1));
grid.CanSplit = true;
mapSystem.SetTile(gridEnt, new Vector2i(2, 0), Tile.Empty);
Assert.That(mapManager.GetAllGrids(mapId).Count(), Is.EqualTo(2));
mapSystem.DeleteMap(mapId);
}
[Test]
public void SimpleSplit()
{
var sim = GetSim();
var mapManager = sim.Resolve<IMapManager>();
var mapSystem = sim.Resolve<IEntityManager>().System<SharedMapSystem>();
var mapId = sim.CreateMap().MapId;
var gridEnt = mapManager.CreateGridEntity(mapId);
for (var x = 0; x < 3; x++)
{
mapSystem.SetTile(gridEnt, new Vector2i(x, 0), new Tile(1));
}
Assert.That(mapManager.GetAllGrids(mapId).Count(), Is.EqualTo(1));
mapSystem.SetTile(gridEnt, new Vector2i(1, 0), Tile.Empty);
Assert.That(mapManager.GetAllGrids(mapId).Count(), Is.EqualTo(2));
mapSystem.DeleteMap(mapId);
}
[Test]
public void DonutSplit()
{
var sim = GetSim();
var mapManager = sim.Resolve<IMapManager>();
var mapSystem = sim.Resolve<IEntityManager>().System<SharedMapSystem>();
var mapId = sim.CreateMap().MapId;
var gridEnt = mapManager.CreateGridEntity(mapId);
for (var x = 0; x < 3; x++)
{
for (var y = 0; y < 3; y++)
{
mapSystem.SetTile(gridEnt, new Vector2i(x, y), new Tile(1));
}
}
Assert.That(mapManager.GetAllGrids(mapId).Count(), Is.EqualTo(1));
mapSystem.SetTile(gridEnt, Vector2i.One, Tile.Empty);
Assert.That(mapManager.GetAllGrids(mapId).Count(), Is.EqualTo(1));
mapSystem.SetTile(gridEnt, new Vector2i(1, 2), Tile.Empty);
Assert.That(mapManager.GetAllGrids(mapId).Count(), Is.EqualTo(1));
mapSystem.SetTile(gridEnt, new Vector2i(1, 0), Tile.Empty);
Assert.That(mapManager.GetAllGrids(mapId).Count(), Is.EqualTo(2));
mapSystem.DeleteMap(mapId);
}
[Test]
public void TriSplit()
{
var sim = GetSim();
var mapManager = sim.Resolve<IMapManager>();
var mapSystem = sim.Resolve<IEntityManager>().System<SharedMapSystem>();
var mapId = sim.CreateMap().MapId;
var gridEnt = mapManager.CreateGridEntity(mapId);
for (var x = 0; x < 3; x++)
{
mapSystem.SetTile(gridEnt , new Vector2i(x, 0), new Tile(1));
}
mapSystem.SetTile(gridEnt, Vector2i.One, new Tile(1));
Assert.That(mapManager.GetAllGrids(mapId).Count(), Is.EqualTo(1));
mapSystem.SetTile(gridEnt, new Vector2i(1, 0), Tile.Empty);
Assert.That(mapManager.GetAllGrids(mapId).Count(), Is.EqualTo(3));
mapSystem.DeleteMap(mapId);
}
/// <summary>
/// Checks GridId and Parents update correctly for re-parented entities.
/// </summary>
[Test]
public void ReparentSplit()
{
var sim = GetSim();
var entManager = sim.Resolve<IEntityManager>();
var mapManager = sim.Resolve<IMapManager>();
var mapSystem = sim.Resolve<IEntityManager>().System<SharedMapSystem>();
var transformSystem = sim.Resolve<IEntityManager>().System<SharedTransformSystem>();
var mapId = sim.CreateMap().MapId;
var gridEnt = mapManager.CreateGridEntity(mapId);
var grid = gridEnt.Comp;
for (var x = 0; x < 4; x++)
{
mapSystem.SetTile(gridEnt, new Vector2i(x, 0), new Tile(1));
}
Assert.That(mapManager.GetAllGrids(mapId).Count(), Is.EqualTo(1));
var dummy = entManager.SpawnEntity(null, new EntityCoordinates(gridEnt, new Vector2(3.5f, 0.5f)));
var dummyXform = entManager.GetComponent<TransformComponent>(dummy);
var anchored = entManager.SpawnEntity(null, new EntityCoordinates(gridEnt, new Vector2(3.5f, 0.5f)));
var anchoredXform = entManager.GetComponent<TransformComponent>(anchored);
transformSystem.AnchorEntity((anchored, anchoredXform), gridEnt);
Assert.That(anchoredXform.Anchored);
mapSystem.SetTile(gridEnt, new Vector2i(2, 0), Tile.Empty);
Assert.That(mapManager.GetAllGrids(mapId).Count(), Is.EqualTo(2));
var newGrid = mapManager.GetAllGrids(mapId).First(x => x.Comp != grid);
var newGridXform = entManager.GetComponent<TransformComponent>(newGrid.Owner);
Assert.Multiple(() =>
{
// Assertions baby
Assert.That(anchoredXform.Anchored);
Assert.That(anchoredXform.ParentUid, Is.EqualTo(newGrid.Owner));
Assert.That(anchoredXform.GridUid, Is.EqualTo(newGrid.Owner));
Assert.That(newGridXform._children, Does.Contain(anchored));
Assert.That(dummyXform.ParentUid, Is.EqualTo(newGrid.Owner));
Assert.That(dummyXform.GridUid, Is.EqualTo(newGrid.Owner));
Assert.That(newGridXform._children, Does.Contain(dummy));
});
mapSystem.DeleteMap(mapId);
}
}

View File

@@ -0,0 +1,60 @@
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
using Robust.UnitTesting.Server;
namespace Robust.UnitTesting.Shared.Map;
[TestFixture]
internal sealed class MapGridMap_Tests
{
/// <summary>
/// Asserts FindGrids only returns the map once.
/// </summary>
[Test]
public void FindGrids()
{
var sim = RobustServerSimulation.NewSimulation().InitializeInstance();
var entManager = sim.Resolve<IEntityManager>();
var mapManager = sim.Resolve<IMapManager>();
var mapSystem = entManager.System<SharedMapSystem>();
var mapId = sim.CreateMap().MapId;
List<Entity<MapGridComponent>> grids = [];
mapManager.FindGridsIntersecting(mapId, Box2.UnitCentered, ref grids);
Assert.That(grids, Is.Empty);
entManager.AddComponent<MapGridComponent>(mapSystem.GetMapOrInvalid(mapId));
mapManager.FindGridsIntersecting(mapId, Box2.UnitCentered, ref grids);
Assert.That(grids, Has.Count.EqualTo(1));
}
/// <summary>
/// Asserts that adding <see cref="MapGridComponent"/> to an existing map with a grid on it doesn't explode.
/// </summary>
[Test]
public void AddGridCompToMap()
{
var sim = RobustServerSimulation.NewSimulation().InitializeInstance();
var entManager = sim.Resolve<IEntityManager>();
var mapManager = sim.Resolve<IMapManager>();
var mapSystem = entManager.System<SharedMapSystem>();
var mapId = sim.CreateMap().MapId;
mapManager.CreateGridEntity(mapId);
Assert.DoesNotThrow(() =>
{
entManager.AddComponent<MapGridComponent>(mapSystem.GetMapOrInvalid(mapId));
entManager.TickUpdate(0.016f, false);
});
mapSystem.DeleteMap(mapId);
}
}

View File

@@ -0,0 +1,218 @@
using System.Linq;
using System.Numerics;
using Moq;
using NUnit.Framework;
using Robust.Server.GameObjects;
using Robust.Server.Physics;
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.UnitTesting.Server;
namespace Robust.UnitTesting.Shared.Map
{
[TestFixture, TestOf(typeof(MapGridComponent))]
sealed class MapGrid_Tests
{
private static ISimulation SimulationFactory()
{
var sim = RobustServerSimulation
.NewSimulation()
.InitializeInstance();
return sim;
}
[Test]
public void GetTileRefCoords()
{
var sim = SimulationFactory();
var mapMan = sim.Resolve<IMapManager>();
var mapSystem = sim.Resolve<IEntityManager>().System<SharedMapSystem>();
var mapId = sim.CreateMap().MapId;
var gridOptions = new GridCreateOptions();
gridOptions.ChunkSize = 8;
var grid = mapMan.CreateGridEntity(mapId, gridOptions);
mapSystem.SetTile(grid, new Vector2i(-9, -1), new Tile(typeId: 1, flags: 1, variant: 1));
var result = mapSystem.GetTileRef(grid.Owner, grid.Comp, new Vector2i(-9, -1));
Assert.That(grid.Comp.ChunkCount, Is.EqualTo(1));
Assert.That(mapSystem.GetMapChunks(grid.Owner, grid.Comp).Keys.ToList()[0], Is.EqualTo(new Vector2i(-2, -1)));
Assert.That(result, Is.EqualTo(new TileRef(grid.Owner, new Vector2i(-9,-1), new Tile(typeId: 1, flags: 1, variant: 1))));
}
/// <summary>
/// Verifies that the world Bounds of the grid properly expand when tiles are placed.
/// </summary>
[Test]
public void BoundsExpansion()
{
var sim = SimulationFactory();
var mapMan = sim.Resolve<IMapManager>();
var mapSystem = sim.Resolve<IEntityManager>().System<SharedMapSystem>();
var transformSystem = sim.Resolve<IEntityManager>().System<SharedTransformSystem>();
var mapId = sim.CreateMap().MapId;
var gridOptions = new GridCreateOptions();
gridOptions.ChunkSize = 8;
var grid = mapMan.CreateGridEntity(mapId, gridOptions);
transformSystem.SetWorldPosition(grid, new Vector2(3, 5));
mapSystem.SetTile(grid, new Vector2i(-1, -2), new Tile(1));
mapSystem.SetTile(grid, new Vector2i(1, 2), new Tile(1));
var bounds = transformSystem.GetWorldMatrix(grid).TransformBox(grid.Comp.LocalAABB);
// this is world, so add the grid world pos
Assert.That(bounds.Bottom, Is.EqualTo(-2+5));
Assert.That(bounds.Left, Is.EqualTo(-1+3));
Assert.That(bounds.Top, Is.EqualTo(3+5));
Assert.That(bounds.Right, Is.EqualTo(2+3));
}
/// <summary>
/// Verifies that the world bounds of the grid properly contract when a tile is removed.
/// </summary>
[Test]
public void BoundsContract()
{
var sim = SimulationFactory();
var mapMan = sim.Resolve<IMapManager>();
var mapSystem = sim.Resolve<IEntityManager>().System<SharedMapSystem>();
var transformSystem = sim.Resolve<IEntityManager>().System<SharedTransformSystem>();
var mapId = sim.CreateMap().MapId;
var gridOptions = new GridCreateOptions();
gridOptions.ChunkSize = 8;
var grid = mapMan.CreateGridEntity(mapId, gridOptions);
transformSystem.SetWorldPosition(grid, new Vector2(3, 5));
mapSystem.SetTile(grid, new Vector2i(-1, -2), new Tile(1));
mapSystem.SetTile(grid, new Vector2i(1, 2), new Tile(1));
mapSystem.SetTile(grid, new Vector2i(1, 2), Tile.Empty);
var bounds = transformSystem.GetWorldMatrix(grid).TransformBox(grid.Comp.LocalAABB);
// this is world, so add the grid world pos
Assert.That(bounds.Bottom, Is.EqualTo(-2+5));
Assert.That(bounds.Left, Is.EqualTo(-1+3));
Assert.That(bounds.Top, Is.EqualTo(-1+5));
Assert.That(bounds.Right, Is.EqualTo(0+3));
}
[Test]
public void GridTileToChunkIndices()
{
var sim = SimulationFactory();
var mapMan = sim.Resolve<IMapManager>();
var mapSystem = sim.Resolve<IEntityManager>().System<SharedMapSystem>();
var mapId = sim.CreateMap().MapId;
var gridOptions = new GridCreateOptions();
gridOptions.ChunkSize = 8;
var grid = mapMan.CreateGridEntity(mapId, gridOptions);
var result = mapSystem.GridTileToChunkIndices(grid.Comp, new Vector2i(-9, -1));
Assert.That(result, Is.EqualTo(new Vector2i(-2, -1)));
}
/// <summary>
/// Verifies that the local position is centered on the tile, instead of bottom left.
/// </summary>
[Test]
public void ToLocalCentered()
{
var sim = SimulationFactory();
var mapMan = sim.Resolve<IMapManager>();
var mapSystem = sim.Resolve<IEntityManager>().System<SharedMapSystem>();
var mapId = sim.CreateMap().MapId;
var gridOptions = new GridCreateOptions();
gridOptions.ChunkSize = 8;
var grid = mapMan.CreateGridEntity(mapId, gridOptions);
var result = mapSystem.GridTileToLocal(grid.Owner, grid.Comp, new Vector2i(0, 0)).Position;
Assert.That(result.X, Is.EqualTo(0.5f));
Assert.That(result.Y, Is.EqualTo(0.5f));
}
[Test]
public void TryGetTileRefNoTile()
{
var sim = SimulationFactory();
var mapMan = sim.Resolve<IMapManager>();
var mapSystem = sim.Resolve<IEntityManager>().System<SharedMapSystem>();
var mapId = sim.CreateMap().MapId;
var gridOptions = new GridCreateOptions();
gridOptions.ChunkSize = 8;
var grid = mapMan.CreateGridEntity(mapId, gridOptions);
var foundTile = mapSystem.TryGetTileRef(grid.Owner, grid.Comp, new Vector2i(-9, -1), out var tileRef)
;
Assert.That(foundTile, Is.False);
Assert.That(tileRef, Is.EqualTo(new TileRef()));
Assert.That(grid.Comp.ChunkCount, Is.EqualTo(0));
}
[Test]
public void TryGetTileRefTileExists()
{
var sim = SimulationFactory();
var mapMan = sim.Resolve<IMapManager>();
var mapSystem = sim.Resolve<IEntityManager>().System<SharedMapSystem>();
var mapId = sim.CreateMap().MapId;
var gridOptions = new GridCreateOptions();
gridOptions.ChunkSize = 8;
var grid = mapMan.CreateGridEntity(mapId, gridOptions);
mapSystem.SetTile(grid, new Vector2i(-9, -1), new Tile(typeId: 1, flags: 1, variant: 1));
var foundTile = mapSystem.TryGetTileRef(grid.Owner, grid.Comp, new Vector2i(-9, -1), out var tileRef);
Assert.That(foundTile, Is.True);
Assert.That(grid.Comp.ChunkCount, Is.EqualTo(1));
Assert.That(mapSystem.GetMapChunks(grid.Owner, grid.Comp).Keys.ToList()[0], Is.EqualTo(new Vector2i(-2, -1)));
Assert.That(tileRef, Is.EqualTo(new TileRef(grid.Owner, new Vector2i(-9, -1), new Tile(typeId: 1, flags: 1, variant: 1))));
}
[Test]
public void PointCollidesWithGrid()
{
var sim = SimulationFactory();
var mapMan = sim.Resolve<IMapManager>();
var mapSystem = sim.Resolve<IEntityManager>().System<SharedMapSystem>();
var mapId = sim.CreateMap().MapId;
var gridOptions = new GridCreateOptions();
gridOptions.ChunkSize = 8;
var grid = mapMan.CreateGridEntity(mapId, gridOptions);
mapSystem.SetTile(grid, new Vector2i(19, 23), new Tile(1));
var result = mapSystem.CollidesWithGrid(grid.Owner, grid.Comp, new Vector2i(19, 23));
Assert.That(result, Is.True);
}
[Test]
public void PointNotCollideWithGrid()
{
var sim = SimulationFactory();
var mapMan = sim.Resolve<IMapManager>();
var mapSystem = sim.Resolve<IEntityManager>().System<SharedMapSystem>();
var mapId = sim.CreateMap().MapId;
var gridOptions = new GridCreateOptions();
gridOptions.ChunkSize = 8;
var grid = mapMan.CreateGridEntity(mapId, gridOptions);
mapSystem.SetTile(grid, new Vector2i(19, 23), new Tile(1));
var result = mapSystem.CollidesWithGrid(grid.Owner, grid.Comp, new Vector2i(19, 24));
Assert.That(result, Is.False);
}
}
}

View File

@@ -0,0 +1,83 @@
using System.Numerics;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.UnitTesting.Server;
namespace Robust.UnitTesting.Shared.Map
{
[TestFixture, TestOf(typeof(MapManager))]
internal sealed class MapManagerTests
{
private static ISimulation SimulationFactory()
{
var sim = RobustServerSimulation
.NewSimulation()
.InitializeInstance();
return sim;
}
/// <summary>
/// When the map manager is restarted, the maps are deleted.
/// </summary>
[Test]
public void Restart_ExistingMap_IsRemoved()
{
var sim = SimulationFactory();
var mapMan = sim.Resolve<IMapManager>();
var entMan = sim.Resolve<IEntityManager>();
var mapSys = entMan.System<SharedMapSystem>();
var mapID = sim.CreateMap().MapId;
mapMan.Restart();
Assert.That(mapSys.MapExists(mapID), Is.False);
}
/// <summary>
/// When the map manager is restarted, the grids are removed.
/// </summary>
[Test]
public void Restart_ExistingGrid_IsRemoved()
{
var sim = SimulationFactory();
var mapMan = sim.Resolve<IMapManager>();
var entMan = sim.Resolve<IEntityManager>();
var mapID = sim.CreateMap().MapId;
var grid = mapMan.CreateGridEntity(mapID);
mapMan.Restart();
Assert.That(entMan.HasComponent<MapGridComponent>(grid), Is.False);
}
/// <summary>
/// When entities are flushed check nullsapce is also culled.
/// </summary>
[Test]
public void Restart_NullspaceMap_IsEmptied()
{
var sim = SimulationFactory();
var entMan = sim.Resolve<IEntityManager>();
var oldEntity = entMan.CreateEntityUninitialized(null, MapCoordinates.Nullspace);
entMan.InitializeEntity(oldEntity);
entMan.FlushEntities();
Assert.That(entMan.Deleted(oldEntity), Is.True);
}
[Test]
public void Restart_MapEntity_IsRemoved()
{
var sim = SimulationFactory();
var entMan = sim.Resolve<IEntityManager>();
var mapMan = sim.Resolve<IMapManager>();
var entity = entMan.System<SharedMapSystem>().CreateMap();
mapMan.Restart();
Assert.That((!entMan.EntityExists(entity) ? EntityLifeStage.Deleted : entMan.GetComponent<MetaDataComponent>(entity).EntityLifeStage) >= EntityLifeStage.Deleted, Is.True);
}
}
}

View File

@@ -0,0 +1,233 @@
using System.Linq;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.UnitTesting.Server;
namespace Robust.UnitTesting.Shared.Map;
[TestFixture]
internal sealed class MapPauseTests
{
private static ISimulation SimulationFactory()
{
var sim = RobustServerSimulation
.NewSimulation()
.InitializeInstance();
return sim;
}
/// <summary>
/// When an entity is on a paused map, it does not get returned by an EntityQuery.
/// </summary>
[Test]
public void Paused_NotIncluded_NotInQuery()
{
var sim = SimulationFactory();
var entMan = sim.Resolve<IEntityManager>();
var mapMan = sim.Resolve<IMapManager>();
// arrange
var mapId = sim.CreateMap().Uid;
entMan.System<SharedMapSystem>().SetPaused(mapId, true);
entMan.SpawnEntity(null, new EntityCoordinates(mapId, default));
var query = entMan.EntityQuery<TransformComponent>(false).ToList();
// 0 ents, map and the spawned one are not returned
Assert.That(query.Count, Is.EqualTo(0));
}
/// <summary>
/// When an entity is on an unpaused map, it is returned by an EntityQuery.
/// </summary>
[Test]
public void UnPaused_NotIncluded_InQuery()
{
var sim = SimulationFactory();
var entMan = sim.Resolve<IEntityManager>();
var mapMan = sim.Resolve<IMapManager>();
// arrange
var mapId = sim.CreateMap().Uid;
entMan.System<SharedMapSystem>().SetPaused(mapId, false);
entMan.SpawnEntity(null, new EntityCoordinates(mapId, default));
var query = entMan.EntityQuery<TransformComponent>(false).ToList();
// 2 ents, map and the spawned one
Assert.That(query.Count, Is.EqualTo(2));
}
/// <summary>
/// When an entity is on a paused map, it is get returned by an EntityQuery when included.
/// </summary>
[Test]
public void Paused_Included_InQuery()
{
var sim = SimulationFactory();
var entMan = sim.Resolve<IEntityManager>();
var mapMan = sim.Resolve<IMapManager>();
// arrange
var mapId = sim.CreateMap().Uid;
entMan.System<SharedMapSystem>().SetPaused(mapId, true);
entMan.SpawnEntity(null, new EntityCoordinates(mapId, default));
var query = entMan.EntityQuery<TransformComponent>(true).ToList();
// 2 ents, map and the spawned one are returned because includePaused
Assert.That(query.Count, Is.EqualTo(2));
}
/// <summary>
/// A new child entity added to a paused map will be created paused.
/// </summary>
[Test]
public void Paused_AddEntity_IsPaused()
{
var sim = SimulationFactory();
var entMan = sim.Resolve<IEntityManager>();
var mapMan = sim.Resolve<IMapManager>();
// arrange
var mapId = sim.CreateMap().Uid;
entMan.System<SharedMapSystem>().SetPaused(mapId, true);
var newEnt = entMan.SpawnEntity(null, new EntityCoordinates(mapId, default));
var metaData = entMan.GetComponent<MetaDataComponent>(newEnt);
Assert.That(metaData.EntityPaused, Is.True);
}
/// <summary>
/// A new child entity added to an unpaused map will be created unpaused.
/// </summary>
[Test]
public void UnPaused_AddEntity_IsNotPaused()
{
var sim = SimulationFactory();
var entMan = sim.Resolve<IEntityManager>();
var mapMan = sim.Resolve<IMapManager>();
// arrange
var mapId = sim.CreateMap().Uid;
entMan.System<SharedMapSystem>().SetPaused(mapId, false);
var newEnt = entMan.SpawnEntity(null, new EntityCoordinates(mapId, default));
var metaData = entMan.GetComponent<MetaDataComponent>(newEnt);
Assert.That(metaData.EntityPaused, Is.False);
}
/// <summary>
/// When a new grid is added to a paused map, the grid becomes paused.
/// </summary>
[Test]
public void Paused_AddGrid_GridPaused()
{
var sim = SimulationFactory();
var entMan = sim.Resolve<IEntityManager>();
var mapMan = sim.Resolve<IMapManager>();
// arrange
var mapId = sim.CreateMap().MapId;
entMan.System<SharedMapSystem>().SetPaused(mapId, true);
// act
var newGrid = mapMan.CreateGridEntity(mapId);
// assert
var metaData = entMan.GetComponent<MetaDataComponent>(newGrid);
Assert.That(metaData.EntityPaused, Is.True);
}
/// <summary>
/// When a tree of entities are teleported from a paused map
/// to an unpaused map, all of the entities in the tree are unpaused.
/// </summary>
[Test]
public void Paused_TeleportBetweenMaps_Unpaused()
{
var sim = SimulationFactory();
var entMan = sim.Resolve<IEntityManager>();
// arrange
var map1 = sim.CreateMap().Uid;
entMan.System<SharedMapSystem>().SetPaused(map1, true);
var newEnt = entMan.SpawnEntity(null, new EntityCoordinates(map1, default));
var map2 = sim.CreateMap().Uid;
entMan.System<SharedMapSystem>().SetPaused(map2, false);
// Act
entMan.EntitySysManager.GetEntitySystem<SharedTransformSystem>().SetParent(newEnt, map2);
var metaData = entMan.GetComponent<MetaDataComponent>(newEnt);
Assert.That(metaData.EntityPaused, Is.False);
}
/// <summary>
/// When a tree of entities are teleported from an unpaused map
/// to a paused map, all of the entitites in the tree are paused.
/// </summary>
[Test]
public void Unpaused_TeleportBetweenMaps_IsPaused()
{
var sim = SimulationFactory();
var entMan = sim.Resolve<IEntityManager>();
// arrange
var map1 = sim.CreateMap().Uid;
entMan.System<SharedMapSystem>().SetPaused(map1, false);
var newEnt = entMan.SpawnEntity(null, new EntityCoordinates(map1, default));
var map2 = sim.CreateMap().Uid;
entMan.System<SharedMapSystem>().SetPaused(map2, true);
// Act
entMan.EntitySysManager.GetEntitySystem<SharedTransformSystem>().SetParent(newEnt, map2);
var metaData = entMan.GetComponent<MetaDataComponent>(newEnt);
Assert.That(metaData.EntityPaused, Is.True);
}
/// <summary>
/// When a paused map is unpaused, all of the entities on the map are unpaused.
/// </summary>
[Test]
public void Paused_UnpauseMap_UnpausedEntities()
{
var sim = SimulationFactory();
var entMan = sim.Resolve<IEntityManager>();
var mapMan = sim.Resolve<IMapManager>();
var mapId = sim.CreateMap().Uid;
entMan.System<SharedMapSystem>().SetPaused(mapId, true);
var newEnt = entMan.SpawnEntity(null, new EntityCoordinates(mapId, default));
entMan.System<SharedMapSystem>().SetPaused(mapId, false);
var metaData = entMan.GetComponent<MetaDataComponent>(newEnt);
Assert.That(metaData.EntityPaused, Is.False);
}
/// <summary>
/// When an unpaused map is paused, all of the entities on the map are paused.
/// </summary>
[Test]
public void Unpaused_PauseMap_PausedEntities()
{
var sim = SimulationFactory();
var entMan = sim.Resolve<IEntityManager>();
var mapMan = sim.Resolve<IMapManager>();
var mapId = sim.CreateMap().Uid;
entMan.System<SharedMapSystem>().SetPaused(mapId, false);
var newEnt = entMan.SpawnEntity(null, new EntityCoordinates(mapId, default));
entMan.System<SharedMapSystem>().SetPaused(mapId, true);
var metaData = entMan.GetComponent<MetaDataComponent>(newEnt);
Assert.That(metaData.EntityPaused, Is.True);
}
}

View File

@@ -0,0 +1,79 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
using Robust.UnitTesting.Server;
namespace Robust.UnitTesting.Shared.Map;
[TestFixture]
internal sealed class Query_Tests
{
private static readonly TestCaseData[] Box2Data = new[]
{
new TestCaseData(
Vector2.Zero,
0f,
Box2.UnitCentered.Translated(new Vector2(0f, 10f)),
true
),
new TestCaseData(
Vector2.Zero,
MathF.PI,
Box2.UnitCentered.Translated(new Vector2(0f, 10f)),
false
),
new TestCaseData(
Vector2.Zero,
MathF.PI,
Box2.UnitCentered.Translated(new Vector2(0f, -10f)),
true
),
new TestCaseData(
Vector2.Zero,
MathF.PI / 2f,
Box2.UnitCentered.Translated(new Vector2(-10f, 0f)),
true
),
new TestCaseData(
Vector2.Zero,
MathF.PI / 4f,
Box2.UnitCentered.Translated(new Vector2(-5f, 5f)),
true
),
};
[Test, TestCaseSource(nameof(Box2Data))]
public void TestBox2GridIntersection(Vector2 position, float radians, Box2 worldAABB, bool result)
{
var sim = RobustServerSimulation.NewSimulation().InitializeInstance();
var entManager = sim.Resolve<IEntityManager>();
var mapManager = sim.Resolve<IMapManager>();
var mapSystem = entManager.System<SharedMapSystem>();
var xformSystem = entManager.System<SharedTransformSystem>();
var map = mapSystem.CreateMap();
var grid = mapManager.CreateGridEntity(map);
for (var i = 0; i < 10; i++)
{
mapSystem.SetTile(grid, new Vector2i(0, i), new Tile(1));
}
xformSystem.SetWorldRotation(grid.Owner, radians);
var grids = new List<Entity<MapGridComponent>>();
mapManager.FindGridsIntersecting(map, worldAABB, ref grids);
Assert.That(grids.Count > 0, Is.EqualTo(result));
}
}

View File

@@ -0,0 +1,141 @@
using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Client.GameStates;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
using Robust.Shared.Network;
using Robust.Shared.Player;
namespace Robust.UnitTesting.Shared.Map;
internal sealed class GridDeleteSingleTileRemoveTestTest : RobustIntegrationTest
{
/// <summary>
/// Spawns a simple 1-tile grid with an entity on it, and then sets the tile to "space".
/// This should delete the grid without deleting the entity.
/// This also checks the networking to players, as previously this caused clients to crash.
/// </summary>
[Test]
public async Task TestRemoveSingleTile()
{
var server = StartServer();
var client = StartClient();
await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync());
var mapMan = server.ResolveDependency<IMapManager>();
var sEntMan = server.ResolveDependency<IEntityManager>();
var confMan = server.ResolveDependency<IConfigurationManager>();
var sPlayerMan = server.ResolveDependency<ISharedPlayerManager>();
var cEntMan = client.ResolveDependency<IEntityManager>();
var netMan = client.ResolveDependency<IClientNetManager>();
var cPlayerMan = client.ResolveDependency<ISharedPlayerManager>();
Assert.DoesNotThrow(() => client.SetConnectTarget(server));
client.Post(() => netMan.ClientConnect(null!, 0, null!));
server.Post(() => confMan.SetCVar(CVars.NetPVS, true));
for (int i = 0; i < 10; i++)
{
await server.WaitRunTicks(1);
await client.WaitRunTicks(1);
}
// Ensure client & server ticks are synced.
// Client runs 1 tick ahead
{
var sTick = (int)server.Timing.CurTick.Value;
var cTick = (int)client.Timing.CurTick.Value;
var delta = cTick - sTick;
if (delta > 1)
await server.WaitRunTicks(delta - 1);
else if (delta < 1)
await client.WaitRunTicks(1 - delta);
sTick = (int)server.Timing.CurTick.Value;
cTick = (int)client.Timing.CurTick.Value;
delta = cTick - sTick;
Assert.That(delta, Is.EqualTo(1));
}
// Set up map, grid, entity, and player
Entity<MapGridComponent> grid = default;
EntityUid sEntity = default;
EntityUid sMap = default;
EntityUid sPlayer = default;
var sys = sEntMan.System<SharedMapSystem>();
await server.WaitPost(() =>
{
sMap = sys.CreateMap(out var mapId);
var comp = mapMan.CreateGridEntity(mapId);
grid = (comp.Owner, comp);
sys.SetTile(grid, grid, new Vector2i(0, 0), new Tile(typeId: 1, flags: 1, variant: 1));
var coords = new EntityCoordinates(grid, 0.5f, 0.5f);
sPlayer = sEntMan.SpawnEntity(null, coords);
sEntity = sEntMan.SpawnEntity(null, coords);
// Attach player.
var session = sPlayerMan.Sessions.First();
server.PlayerMan.SetAttachedEntity(session, sPlayer);
sPlayerMan.JoinGame(session);
});
for (int i = 0; i < 10; i++)
{
await server.WaitRunTicks(1);
await client.WaitRunTicks(1);
}
var nEntity = sEntMan.GetNetEntity(sEntity);
var nPlayer = sEntMan.GetNetEntity(sPlayer);
var nGrid = sEntMan.GetNetEntity(grid);
var nMap = sEntMan.GetNetEntity(sMap);
// Check player got properly attached, and has received the other entity.
Assert.That(cEntMan.TryGetEntity(nEntity, out var cEntity));
Assert.That(cEntMan.TryGetEntity(nPlayer, out var cPlayerUid));
Assert.That(cEntMan.TryGetEntity(nGrid, out var cGrid));
Assert.That(cEntMan.TryGetEntity(nMap, out var cMap));
Assert.That(cPlayerMan.LocalEntity, Is.EqualTo(cPlayerUid));
var sQuery = sEntMan.GetEntityQuery<TransformComponent>();
Assert.That(sQuery.GetComponent(sEntity).ParentUid, Is.EqualTo(grid.Owner));
Assert.That(sQuery.GetComponent(grid.Owner).ParentUid, Is.EqualTo(sMap));
var cQuery = cEntMan.GetEntityQuery<TransformComponent>();
Assert.That(cQuery.GetComponent(cEntity!.Value).ParentUid, Is.EqualTo(cGrid));
Assert.That(cQuery.GetComponent(cGrid!.Value).ParentUid, Is.EqualTo(cMap));
// Remove the tile.
await server.WaitPost(() =>
{
sys.SetTile(grid, grid, new Vector2i(0, 0), Tile.Empty);
});
for (int i = 0; i < 10; i++)
{
await server.WaitRunTicks(1);
await client.WaitRunTicks(1);
}
// Grid should no longer exist.
Assert.That(!sEntMan.EntityExists(grid));
Assert.That(!cEntMan.EntityExists(cGrid));
// Entity should now be parented to the map
Assert.That(sQuery.GetComponent(sEntity).ParentUid, Is.EqualTo(sMap));
Assert.That(cQuery.GetComponent(cEntity.Value).ParentUid, Is.EqualTo(cMap));
await client.WaitPost(() => netMan.ClientDisconnect(""));
await server.WaitRunTicks(5);
await client.WaitRunTicks(5);
}
}

View File

@@ -0,0 +1,102 @@
using System.Linq;
using System.Numerics;
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Server.Player;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Network;
using Robust.Shared.Player;
using cIPlayerManager = Robust.Client.Player.IPlayerManager;
using sIPlayerManager = Robust.Server.Player.IPlayerManager;
namespace Robust.UnitTesting.Shared.Networking;
internal sealed class DisconnectTest : RobustIntegrationTest
{
/// <summary>
/// Check that client disconnection works as expected. This is effectively a test of
/// <see cref="RobustIntegrationTest.IntegrationNetManager"/>, not the main net manager.
/// </summary>
[Test]
[TestOf(typeof(IntegrationNetManager))]
public async Task TestConnectDisconnect()
{
var server = StartServer();
var client = StartClient();
await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync());
var cNetMan = client.ResolveDependency<IClientNetManager>();
var cPlayerMan = client.ResolveDependency<cIPlayerManager>();
var sPlayerMan = server.ResolveDependency<sIPlayerManager>();
ICommonSession session = default!;
AssertDisconnected();
// Connect client.
Assert.DoesNotThrow(() => client.SetConnectTarget(server));
await client.WaitPost(() => cNetMan.ClientConnect(null!, 0, null!));
await RunTicks();
AssertConnected();
// Disconnect the client
cNetMan.ClientDisconnect("test");
await RunTicks();
AssertDisconnected();
// Reconnect again
Assert.DoesNotThrow(() => client.SetConnectTarget(server));
await client.WaitPost(() => cNetMan.ClientConnect(null!, 0, null!));
await RunTicks();
AssertConnected();
// Disconnect again, but using the server-channel
session.Channel.Disconnect("test 2");
await RunTicks();
AssertDisconnected();
// Reconnect again
Assert.DoesNotThrow(() => client.SetConnectTarget(server));
await client.WaitPost(() => cNetMan.ClientConnect(null!, 0, null!));
await RunTicks();
AssertConnected();
void AssertConnected()
{
Assert.That(cNetMan.IsConnected, Is.True);
Assert.That(sPlayerMan.Sessions.Count(), Is.EqualTo(1));
session = sPlayerMan.Sessions.Single();
Assert.That(session.Status, Is.EqualTo(SessionStatus.Connected));
Assert.That(session.UserId, Is.EqualTo(cPlayerMan.LocalUser));
Assert.That(cPlayerMan.LocalSession, Is.Not.Null);
}
void AssertDisconnected()
{
Assert.That(cNetMan.IsConnected, Is.False);
Assert.That(sPlayerMan.Sessions.Count(), Is.EqualTo(0));
if (session != null)
Assert.That(session.Status, Is.EqualTo(SessionStatus.Disconnected));
}
async Task RunTicks()
{
for (int i = 0; i < 10; i++)
{
await server.WaitRunTicks(1);
await client.WaitRunTicks(1);
}
}
await client.WaitPost(() => cNetMan.ClientDisconnect(""));
await server.WaitRunTicks(5);
await client.WaitRunTicks(5);
}
}

View File

@@ -0,0 +1,11 @@
using System.Reflection;
namespace Robust.UnitTesting.Shared;
internal abstract class OurRobustUnitTest : RobustUnitTest
{
protected override Assembly[] GetContentAssemblies()
{
return [typeof(OurRobustUnitTest).Assembly];
}
}

View File

@@ -0,0 +1,169 @@
using System.Linq;
using System.Numerics;
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Network;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Player;
namespace Robust.UnitTesting.Shared.Physics;
internal sealed class BroadphaseNetworkingTest : RobustIntegrationTest
{
/// <summary>
/// Check that the transform/broadphase is properly networked when a player moves to a newly spawned map/grid.
/// </summary>
/// <remarks>
/// See PR #3919 or issue #3924
/// </remarks>
[Test]
public async Task TestBroadphaseNetworking()
{
var server = StartServer();
var client = StartClient();
await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync());
var mapMan = server.ResolveDependency<IMapManager>();
var sEntMan = server.ResolveDependency<IEntityManager>();
var cEntMan = client.ResolveDependency<IEntityManager>();
var netMan = client.ResolveDependency<IClientNetManager>();
var confMan = server.ResolveDependency<IConfigurationManager>();
var cPlayerMan = client.ResolveDependency<ISharedPlayerManager>();
var sPlayerMan = server.ResolveDependency<ISharedPlayerManager>();
var fixturesSystem = sEntMan.EntitySysManager.GetEntitySystem<FixtureSystem>();
var physicsSystem = sEntMan.EntitySysManager.GetEntitySystem<SharedPhysicsSystem>();
var mapSystem = sEntMan.EntitySysManager.GetEntitySystem<SharedMapSystem>();
Assert.DoesNotThrow(() => client.SetConnectTarget(server));
client.Post(() => netMan.ClientConnect(null!, 0, null!));
server.Post(() => confMan.SetCVar(CVars.NetPVS, true));
for (int i = 0; i < 10; i++)
{
await server.WaitRunTicks(1);
await client.WaitRunTicks(1);
}
// Set up maps 1 & grid 1
EntityUid grid1 = default;
EntityUid map1 = default;
await server.WaitPost(() =>
{
map1 = mapSystem.CreateMap(out var mapId);
var gridEnt = mapMan.CreateGridEntity(mapId);
mapSystem.SetTile(gridEnt, Vector2i.Zero, new Tile(1));
grid1 = gridEnt.Owner;
});
var map1Net = sEntMan.GetNetEntity(map1);
// Spawn player entity on grid 1
EntityUid player = default;
await server.WaitPost(() =>
{
var coords = new EntityCoordinates(grid1, new Vector2(0.5f, 0.5f));
player = sEntMan.SpawnEntity(null, coords);
// Enable physics
var physics = sEntMan.AddComponent<PhysicsComponent>(player);
var xform = sEntMan.GetComponent<TransformComponent>(player);
var shape = new PolygonShape();
shape.SetAsBox(0.5f, 0.5f);
var fixture = new Fixture(shape, 0, 0, true);
fixturesSystem.CreateFixture(player, "fix1", fixture, body: physics, xform: xform);
physicsSystem.SetCanCollide(player, true, body: physics);
physicsSystem.SetBodyType(player, BodyType.Dynamic);
Assert.That(physics.CanCollide);
// Attach player.
var session = sPlayerMan.Sessions.First();
server.PlayerMan.SetAttachedEntity(session, player);
sPlayerMan.JoinGame(session);
});
var playerNet = sEntMan.GetNetEntity(player);
for (int i = 0; i < 10; i++)
{
await server.WaitRunTicks(1);
await client.WaitRunTicks(1);
}
// Check player got properly attached
await client.WaitPost(() =>
{
var ent = cEntMan.GetNetEntity(cPlayerMan.LocalEntity);
Assert.That(ent, Is.EqualTo(playerNet));
});
var sPlayerXform = sEntMan.GetComponent<TransformComponent>(player);
var cPlayerXform = cEntMan.GetComponent<TransformComponent>(cEntMan.GetEntity(playerNet));
// Client initially has correct transform data.
var broadphase = new BroadphaseData(grid1, true, false);
var grid1Net = sEntMan.GetNetEntity(grid1);
Assert.That(cPlayerXform.GridUid, Is.EqualTo(cEntMan.GetEntity(grid1Net)));
Assert.That(sPlayerXform.GridUid, Is.EqualTo(grid1));
Assert.That(cPlayerXform.MapUid, Is.EqualTo(cEntMan.GetEntity(map1Net)));
Assert.That(sPlayerXform.MapUid, Is.EqualTo(map1));
Assert.That(cPlayerXform.Broadphase?.Uid, Is.EqualTo(cEntMan.GetEntity(sEntMan.GetNetEntity(broadphase.Uid))));
Assert.That(cPlayerXform.Broadphase?.Static, Is.EqualTo(broadphase.Static));
Assert.That(cPlayerXform.Broadphase?.CanCollide, Is.EqualTo(broadphase.CanCollide));
Assert.That(sPlayerXform.Broadphase, Is.EqualTo(broadphase));
// Set up maps 2 & grid 2 and move the player there (in the same tick).
EntityUid grid2 = default;
EntityUid map2 = default;
await server.WaitPost(() =>
{
// Create grid
map2 = mapSystem.CreateMap(out var mapId);
var gridEnt = mapMan.CreateGridEntity(mapId);
mapSystem.SetTile(gridEnt, Vector2i.Zero, new Tile(1));
grid2 = gridEnt.Owner;
// Move player
var coords = new EntityCoordinates(grid2, Vector2.Zero);
sEntMan.System<SharedTransformSystem>().SetCoordinates(player, coords);
});
var grid2Net = sEntMan.GetNetEntity(grid2);
var map2Net = sEntMan.GetNetEntity(map2);
for (int i = 0; i < 10; i++)
{
await server.WaitRunTicks(1);
await client.WaitRunTicks(1);
}
// Player & server xforms should match.
broadphase = new BroadphaseData(grid2, true, false);
Assert.That(cEntMan.GetNetEntity(cPlayerXform.GridUid), Is.EqualTo(grid2Net));
Assert.That(sPlayerXform.GridUid, Is.EqualTo(grid2));
Assert.That(cEntMan.GetNetEntity(cPlayerXform.MapUid), Is.EqualTo(map2Net));
Assert.That(sPlayerXform.MapUid, Is.EqualTo(map2));
Assert.That(cPlayerXform.Broadphase?.Uid, Is.EqualTo(cEntMan.GetEntity(sEntMan.GetNetEntity(broadphase.Uid))));
Assert.That(cPlayerXform.Broadphase?.Static, Is.EqualTo(broadphase.Static));
Assert.That(cPlayerXform.Broadphase?.CanCollide, Is.EqualTo(broadphase.CanCollide));
Assert.That(sPlayerXform.Broadphase, Is.EqualTo(broadphase));
await client.WaitPost(() => netMan.ClientDisconnect(""));
await server.WaitRunTicks(5);
await client.WaitRunTicks(5);
}
}

View File

@@ -0,0 +1,423 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Systems;
using Robust.UnitTesting.Server;
namespace Robust.UnitTesting.Shared.Physics;
[TestFixture]
internal sealed class Broadphase_Test
{
/// <summary>
/// Tests that spawned static ents properly collide with entities in range.
/// </summary>
[Test]
public void TestStaticSpawn()
{
var sim = RobustServerSimulation
.NewSimulation()
.InitializeInstance();
var entManager = sim.Resolve<IEntityManager>();
var fixtureSystem = entManager.System<FixtureSystem>();
var physicsSystem = entManager.System<SharedPhysicsSystem>();
var (mapEnt, mapId) = sim.CreateMap();
var dynamicEnt = entManager.SpawnAtPosition(null, new EntityCoordinates(mapEnt, Vector2.Zero));
var dynamicBody = entManager.AddComponent<PhysicsComponent>(dynamicEnt);
physicsSystem.SetBodyType(dynamicEnt, BodyType.Dynamic, body: dynamicBody);
fixtureSystem.TryCreateFixture(dynamicEnt, new PhysShapeCircle(1f), "fix1", collisionMask: 10);
physicsSystem.WakeBody(dynamicEnt, body: dynamicBody);
Assert.That(dynamicBody.Awake);
physicsSystem.SetAwake((dynamicEnt, dynamicBody), false);
Assert.That(!dynamicBody.Awake);
// Clear move buffer
entManager.System<SharedBroadphaseSystem>().FindNewContacts();
var staticEnt = entManager.SpawnAtPosition(null, new EntityCoordinates(mapEnt, Vector2.Zero));
var staticBody = entManager.AddComponent<PhysicsComponent>(staticEnt);
physicsSystem.SetBodyType(staticEnt, BodyType.Static, body: staticBody);
fixtureSystem.TryCreateFixture(staticEnt, new PhysShapeCircle(1f), "fix1", collisionLayer: 10);
physicsSystem.SetCanCollide(staticEnt, true);
Assert.That(!staticBody.Awake);
Assert.That(staticBody.ContactCount, Is.EqualTo(0));
entManager.System<SharedBroadphaseSystem>().FindNewContacts();
Assert.That(staticBody.ContactCount, Is.EqualTo(1));
physicsSystem.CollideContacts();
// Make sure it's actually marked as touching and not just "well it's in range right".
Assert.That(staticBody.Contacts.First!.Value.IsTouching, Is.EqualTo(true));
}
/// <summary>
/// If we reparent a sundries entity to another broadphase does it correctly update.
/// </summary>
[Test]
public void ReparentSundries()
{
var sim = RobustServerSimulation.NewSimulation().InitializeInstance();
var entManager = sim.Resolve<IEntityManager>();
var mapManager = sim.Resolve<IMapManager>();
var mapSys = entManager.System<SharedMapSystem>();
var xformSys = entManager.System<SharedTransformSystem>();
var (mapEnt, mapId) = sim.CreateMap();
var grid = mapManager.CreateGridEntity(mapId);
mapSys.SetTile(grid, Vector2i.Zero, new Tile(1));
Assert.That(entManager.HasComponent<BroadphaseComponent>(grid));
var broadphase = entManager.GetComponent<BroadphaseComponent>(grid);
var ent = entManager.SpawnEntity(null, new EntityCoordinates(grid, new Vector2(0.5f, 0.5f)));
var xform = entManager.GetComponent<TransformComponent>(ent);
Assert.That(broadphase.SundriesTree, Does.Contain(ent));
var broadphaseData = xform.Broadphase;
Assert.That(broadphaseData!.Value.Uid, Is.EqualTo(grid.Owner));
xformSys.SetCoordinates(ent, new EntityCoordinates(mapEnt, Vector2.One));
Assert.That(broadphase.SundriesTree, Does.Not.Contain(ent));
Assert.That(entManager.GetComponent<BroadphaseComponent>(mapEnt).SundriesTree, Does.Contain(ent));
broadphaseData = xform.Broadphase;
Assert.That(broadphaseData!.Value.Uid, Is.EqualTo(mapEnt));
}
/// <summary>
/// If we reparent a colliding physics entity to another broadphase does it correctly update.
/// </summary>
[Test]
public void ReparentBroadphase()
{
var sim = RobustServerSimulation.NewSimulation().InitializeInstance();
var entManager = sim.Resolve<IEntityManager>();
var mapManager = sim.Resolve<IMapManager>();
var fixturesSystem = entManager.EntitySysManager.GetEntitySystem<FixtureSystem>();
var physicsSystem = entManager.EntitySysManager.GetEntitySystem<SharedPhysicsSystem>();
var mapSys = entManager.System<SharedMapSystem>();
var xformSys = entManager.System<SharedTransformSystem>();
var (mapEnt, mapId) = sim.CreateMap();
var grid = mapManager.CreateGridEntity(mapId);
var gridUid = grid.Owner;
mapSys.SetTile(grid, Vector2i.Zero, new Tile(1));
Assert.That(entManager.HasComponent<BroadphaseComponent>(gridUid));
var broadphase = entManager.GetComponent<BroadphaseComponent>(gridUid);
var ent = entManager.SpawnEntity(null, new EntityCoordinates(gridUid, new Vector2(0.5f, 0.5f)));
var physics = entManager.AddComponent<PhysicsComponent>(ent);
var xform = entManager.GetComponent<TransformComponent>(ent);
// If we're not collidable we're still on the sundries tree.
Assert.That(broadphase.StaticSundriesTree, Does.Contain(ent));
Assert.That(xform.Broadphase!.Value.Uid, Is.EqualTo(gridUid));
var shape = new PolygonShape();
shape.SetAsBox(0.5f, 0.5f);
var fixture = new Fixture(shape, 0, 0, true);
fixturesSystem.CreateFixture(ent, "fix1", fixture, body: physics, xform: xform);
physicsSystem.SetCanCollide(ent, true, body: physics);
Assert.That(physics.CanCollide);
// Now that we're collidable should be correctly on the grid's tree.
Assert.That(fixture.ProxyCount, Is.EqualTo(1));
Assert.That(broadphase.StaticSundriesTree, Does.Not.Contain(ent));
Assert.That(broadphase.StaticTree.GetProxy(fixture.Proxies[0].ProxyId)!.Equals(fixture.Proxies[0]));
// Now check we go to the map's tree correctly.
xformSys.SetCoordinates(ent, new EntityCoordinates(mapEnt, Vector2.One));
Assert.That(entManager.GetComponent<BroadphaseComponent>(mapEnt).StaticTree.GetProxy(fixture.Proxies[0].ProxyId)!.Equals(fixture.Proxies[0]));
Assert.That(xform.Broadphase!.Value.Uid.Equals(mapEnt));
}
/// <summary>
/// If we change a grid's map does it still remain not on the general broadphase.
/// </summary>
/// <remarks>
/// Grids are stored on their own broadphase because moving them is costly.
/// </remarks>
[Test]
public void GridMapUpdate()
{
var sim = RobustServerSimulation.NewSimulation().InitializeInstance();
var entManager = sim.Resolve<IEntityManager>();
var mapManager = sim.Resolve<IMapManager>();
var mapSys = entManager.System<SharedMapSystem>();
var xformSys = entManager.System<SharedTransformSystem>();
var (map1, mapId1) = sim.CreateMap();
var (map2, _) = sim.CreateMap();
var grid = mapManager.CreateGridEntity(mapId1);
mapSys.SetTile(grid, Vector2i.Zero, new Tile(1));
var mapBroadphase1 = entManager.GetComponent<BroadphaseComponent>(map1);
var mapBroadphase2 = entManager.GetComponent<BroadphaseComponent>(map2);
entManager.TickUpdate(0.016f, false);
#pragma warning disable NUnit2046
Assert.That(mapBroadphase1.DynamicTree.Count, Is.EqualTo(0));
#pragma warning restore NUnit2046
xformSys.SetCoordinates(grid, new EntityCoordinates(map1, Vector2.One));
entManager.TickUpdate(0.016f, false);
#pragma warning disable NUnit2046
Assert.That(mapBroadphase2.DynamicTree.Count, Is.EqualTo(0));
#pragma warning restore NUnit2046
}
/// <summary>
/// If an entity's broadphase is changed are its children's broadphases recursively changed.
/// </summary>
[Test]
public void BroadphaseRecursiveUpdate()
{
var sim = RobustServerSimulation.NewSimulation().InitializeInstance();
var entManager = sim.Resolve<IEntityManager>();
var mapManager = sim.Resolve<IMapManager>();
var system = entManager.EntitySysManager;
var physicsSystem = system.GetEntitySystem<SharedPhysicsSystem>();
var lookup = system.GetEntitySystem<EntityLookupSystem>();
var mapSys = entManager.System<SharedMapSystem>();
var (map, mapId) = sim.CreateMap();
var grid = mapManager.CreateGridEntity(mapId);
mapSys.SetTile(grid, Vector2i.Zero, new Tile(1));
var gridBroadphase = entManager.GetComponent<BroadphaseComponent>(grid);
var mapBroadphase = entManager.GetComponent<BroadphaseComponent>(map);
Assert.That(entManager.EntityQuery<BroadphaseComponent>(true).Count(), Is.EqualTo(2));
var parent = entManager.SpawnEntity(null, new EntityCoordinates(grid, new Vector2(0.5f, 0.5f)));
var child1 = entManager.SpawnEntity(null, new EntityCoordinates(parent, Vector2.Zero));
var child1Xform = entManager.GetComponent<TransformComponent>(child1);
// Have a non-collidable child and check it doesn't get added too.
var child2 = entManager.SpawnEntity(null, new EntityCoordinates(child1, Vector2.Zero));
var child2Xform = entManager.GetComponent<TransformComponent>(child2);
var child2Body = entManager.AddComponent<PhysicsComponent>(child2);
physicsSystem.SetCanCollide(child2, false, body: child2Body);
Assert.That(!child2Body.CanCollide);
Assert.That(child1Xform.ParentUid, Is.EqualTo(parent));
Assert.That(child2Xform.ParentUid, Is.EqualTo(child1));
Assert.That(lookup.FindBroadphase(parent), Is.EqualTo(gridBroadphase));
Assert.That(lookup.FindBroadphase(child1), Is.EqualTo(gridBroadphase));
// They should get deparented to the map and updated to the map's broadphase instead.
mapSys.SetTile(grid, Vector2i.Zero, Tile.Empty);
Assert.That(lookup.FindBroadphase(parent), Is.EqualTo(mapBroadphase));
Assert.That(lookup.FindBroadphase(child1), Is.EqualTo(mapBroadphase));
Assert.That(lookup.FindBroadphase(child2), Is.EqualTo(mapBroadphase));
}
/// <summary>
/// Check that broadphases properly recursively update when entities move between maps and grids. The broadphase
/// updating handles grids separately from other entities, this is intended to be an exhaustive check that the
/// broadphase always gets updated. E.g., this previously failed when a grid moved from one map to another
/// </summary>
[Test]
public void EntMapChangeRecursiveUpdate()
{
var sim = RobustServerSimulation.NewSimulation().InitializeInstance();
var entManager = sim.Resolve<IEntityManager>();
var mapManager = sim.Resolve<IMapManager>();
var system = entManager.EntitySysManager;
var lookup = system.GetEntitySystem<EntityLookupSystem>();
var xforms = system.GetEntitySystem<SharedTransformSystem>();
var physSystem = system.GetEntitySystem<SharedPhysicsSystem>();
var fixtures = system.GetEntitySystem<FixtureSystem>();
var mapSys = entManager.System<SharedMapSystem>();
// setup maps
var (mapA, mapAId) = sim.CreateMap();
var (mapB, mapBId) = sim.CreateMap();
// setup grids
var gridAComp = mapManager.CreateGridEntity(mapAId);
var gridBComp = mapManager.CreateGridEntity(mapBId);
var gridCComp = mapManager.CreateGridEntity(mapAId);
var gridA = gridAComp.Owner;
var gridB = gridBComp.Owner;
var gridC = gridCComp.Owner;
xforms.SetLocalPosition(gridC, new Vector2(10, 10));
mapSys.SetTile(gridAComp, Vector2i.Zero, new Tile(1));
mapSys.SetTile(gridBComp, Vector2i.Zero, new Tile(1));
mapSys.SetTile(gridCComp, Vector2i.Zero, new Tile(1));
// set up test entities
var parent = entManager.SpawnEntity(null, new EntityCoordinates(mapA, new Vector2(200,200)));
var parentXform = entManager.GetComponent<TransformComponent>(parent);
var child = entManager.SpawnEntity(null, new EntityCoordinates(parent, Vector2.Zero));
var childXform = entManager.GetComponent<TransformComponent>(child);
var childBody = entManager.AddComponent<PhysicsComponent>(child);
var childFixtures = entManager.GetComponent<FixturesComponent>(child);
// enable collision for the child
var shape = new PolygonShape();
shape.SetAsBox(0.5f, 0.5f);
fixtures.CreateFixture(child, "fix1", new Fixture(shape, 0, 0, false), body: childBody, xform: childXform);
physSystem.SetCanCollide(child, true, body: childBody);
Assert.That(childBody.CanCollide);
// Initially on mapA
var AssertMap = (EntityUid map, EntityUid otherMap, Vector2 pos) =>
{
var broadphase = entManager.GetComponent<BroadphaseComponent>(map);
Assert.That(parentXform.ParentUid == map);
Assert.That(parentXform.MapUid == map);
Assert.That(childXform.MapUid == map);
Assert.That(lookup.FindBroadphase(parent), Is.EqualTo(broadphase));
Assert.That(lookup.FindBroadphase(child), Is.EqualTo(broadphase));
Assert.That(parentXform.Broadphase == new BroadphaseData(map, false, false));
Assert.That(childXform.Broadphase == new BroadphaseData(map, true, true));
};
AssertMap(mapA, mapB, new Vector2(200, 200));
// we are now going to test several broadphase updates where we relocate the parent entity such that it moves:
// - map to map with a map change
// - map to grid with a map change
// - grid to grid with a map change
// - grid to map with a map change
// - map to grid without a map change
// - grid to grid without a map change
// - grid to map without a map change
// Move to map B (map to map with a map change)
xforms.SetCoordinates(parent, new EntityCoordinates(mapB, new Vector2(100, 100)));
AssertMap(mapB, mapA, new Vector2(100, 100));
// Move to gridA on mapA (map to grid with a map change)
xforms.SetCoordinates(parent, new EntityCoordinates(gridA, default));
var AssertGrid = (EntityUid grid, EntityUid map, EntityUid otherMap, Vector2 pos) =>
{
var broadphase = entManager.GetComponent<BroadphaseComponent>(grid);
var gridXform = entManager.GetComponent<TransformComponent>(grid);
Assert.That(gridXform.ParentUid == map);
Assert.That(gridXform.MapUid == map);
Assert.That(parentXform.ParentUid == grid);
Assert.That(parentXform.MapUid == map);
Assert.That(childXform.MapUid == map);
Assert.That(lookup.FindBroadphase(parent), Is.EqualTo(broadphase));
Assert.That(lookup.FindBroadphase(child), Is.EqualTo(broadphase));
Assert.That(parentXform.Broadphase == new BroadphaseData(grid, false, false));
Assert.That(childXform.Broadphase == new BroadphaseData(grid, true, true));
};
AssertGrid(gridA, mapA, mapB, Vector2.Zero);
// Move to gridB on mapB (grid to grid with a map change)
xforms.SetCoordinates(parent, new EntityCoordinates(gridB, default));
AssertGrid(gridB, mapB, mapA, Vector2.Zero);
// move to mapA (grid to map with a map change)
xforms.SetCoordinates(parent, new EntityCoordinates(mapA, new Vector2(200, 200)));
AssertMap(mapA, mapB, new Vector2(200, 200));
// move to gridA on mapA (map to grid without a map change)
xforms.SetCoordinates(parent, new EntityCoordinates(gridA, default));
AssertGrid(gridA, mapA, mapB, Vector2.Zero);
// move to gridC on mapA (grid to grid without a map change)
xforms.SetCoordinates(parent, new EntityCoordinates(gridC, default));
AssertGrid(gridC, mapA, mapB, new Vector2(10, 10));
// move to gridC on mapA (grid to map without a map change)
xforms.SetCoordinates(parent, new EntityCoordinates(mapA, new Vector2(50, 50)));
AssertMap(mapA, mapB, new Vector2(50, 50));
// Finally, we check if the broadphase updates if the whole grid moves, instead of just the entity
// first, move it to a grid:
xforms.SetCoordinates(parent, new EntityCoordinates(gridC, default));
AssertGrid(gridC, mapA, mapB, new Vector2(10, 10));
// then move the grid to a new map:
xforms.SetCoordinates(gridC, new EntityCoordinates(mapB, new Vector2(200,200)));
// Asserting child pos NOT gridC pos.
AssertGrid(gridC, mapB, mapA, new Vector2(10, 10));
}
/// <summary>
/// If an entity's broadphase is changed to nullspace are its children updated.
/// </summary>
[Test]
public void BroadphaseRecursiveNullspaceUpdate()
{
var sim = RobustServerSimulation.NewSimulation().InitializeInstance();
var entManager = sim.Resolve<IEntityManager>();
var system = entManager.EntitySysManager;
var xformSystem = system.GetEntitySystem<SharedTransformSystem>();
var physSystem = system.GetEntitySystem<SharedPhysicsSystem>();
var lookup = system.GetEntitySystem<EntityLookupSystem>();
var fixtures = system.GetEntitySystem<FixtureSystem>();
var (mapUid, mapId) = sim.CreateMap();
var mapBroadphase = entManager.GetComponent<BroadphaseComponent>(mapUid);
Assert.That(entManager.EntityQuery<BroadphaseComponent>(true).Count(), Is.EqualTo(1));
var parent = entManager.SpawnEntity(null, new MapCoordinates(Vector2.Zero, mapId));
var parentXform = entManager.GetComponent<TransformComponent>(parent);
entManager.AddComponent<PhysicsComponent>(parent);
var child1 = entManager.SpawnEntity(null, new EntityCoordinates(parent, Vector2.Zero));
var child1Xform = entManager.GetComponent<TransformComponent>(child1);
var child1Body = entManager.AddComponent<PhysicsComponent>(child1);
var shape = new PolygonShape();
shape.SetAsBox(0.5f, 0.5f);
fixtures.CreateFixture(child1, "fix1", new Fixture(shape, 0, 0, false), body: child1Body, xform: child1Xform);
physSystem.SetCanCollide(child1, true, body: child1Body);
Assert.That(child1Body.CanCollide);
// Have a non-collidable child and check it doesn't get added too.
var child2 = entManager.SpawnEntity(null, new EntityCoordinates(child1, Vector2.Zero));
var child2Xform = entManager.GetComponent<TransformComponent>(child2);
var child2Body = entManager.AddComponent<PhysicsComponent>(child2);
physSystem.SetCanCollide(child2, false, body: child2Body);
Assert.That(!child2Body.CanCollide);
Assert.That(child1Xform.ParentUid, Is.EqualTo(parent));
Assert.That(child2Xform.ParentUid, Is.EqualTo(child1));
Assert.That(lookup.FindBroadphase(parent), Is.EqualTo(mapBroadphase));
Assert.That(lookup.FindBroadphase(child1), Is.EqualTo(mapBroadphase));
Assert.That(lookup.FindBroadphase(child2), Is.EqualTo(mapBroadphase));
// They should get deparented to the map and updated to the map's broadphase instead.
xformSystem.DetachEntity(parent, parentXform);
Assert.That(lookup.FindBroadphase(parent), Is.EqualTo(null));
Assert.That(lookup.FindBroadphase(child1), Is.EqualTo(null));
Assert.That(lookup.FindBroadphase(child2), Is.EqualTo(null));
// Can't assert CanCollide because they may still want to be valid when coming out of nullspace.
// Check it goes back to normal
xformSystem.SetParent(parent, parentXform, mapUid);
Assert.That(lookup.FindBroadphase(parent), Is.EqualTo(mapBroadphase));
Assert.That(lookup.FindBroadphase(child1), Is.EqualTo(mapBroadphase));
Assert.That(lookup.FindBroadphase(child2), Is.EqualTo(mapBroadphase));
}
}

View File

@@ -0,0 +1,519 @@
using System;
using System.Linq;
using System.Numerics;
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Client.Physics;
using Robust.Shared;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Events;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Serialization;
using Robust.Shared.Timing;
namespace Robust.UnitTesting.Shared.Physics;
/// <summary>
/// This test is meant to check that collision start & stop events are raised correctly by the client.
/// The expectation is that start & stop events are only raised if the client predicts that two entities will move into
/// contact. They do not get raised as a result of applying component states received from the server.
/// I.e., the assumption is that if a collision results in changes to data on a component, then that data will already
/// have been sent to clients in the component's state, so we don't want to "double count" collisions.
/// </summary>
internal sealed class CollisionPredictionTest : RobustIntegrationTest
{
private static readonly string Prototypes = @"
- type: entity
id: CollisionTest1
components:
- type: CollisionPredictionTest
- type: Physics
bodyType: Dynamic
sleepingAllowed: false
- type: entity
id: CollisionTest2
components:
- type: Physics
bodyType: Dynamic
sleepingAllowed: false
";
[Test]
[TestCase(true, true)]
[TestCase(true, false)]
[TestCase(false, true)]
[TestCase(false, false)]
public async Task TestCollisionPrediction(bool hard1, bool hard2)
{
var serverOpts = new ServerIntegrationOptions { Pool = false, ExtraPrototypes = Prototypes };
var clientOpts = new ClientIntegrationOptions { Pool = false, ExtraPrototypes = Prototypes };
var server = StartServer(serverOpts);
var client = StartClient(clientOpts);
await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync());
var netMan = client.ResolveDependency<IClientNetManager>();
Assert.DoesNotThrow(() => client.SetConnectTarget(server));
await server.WaitPost(() => server.CfgMan.SetCVar(CVars.NetPVS, false));
await client.WaitPost(() => netMan.ClientConnect(null!, 0, null!));
var sFix = server.System<FixtureSystem>();
var sPhys = server.System<SharedPhysicsSystem>();
var sSys = server.System<CollisionPredictionTestSystem>();
// Set up entities
EntityUid map = default;
EntityUid sEntity1 = default;
EntityUid sEntity2 = default;
MapCoordinates coords1 = default;
MapCoordinates coords2 = default;
await server.WaitPost(() =>
{
var radius = 0.25f;
map = server.System<SharedMapSystem>().CreateMap(out var mapId);
coords1 = new(default, mapId);
coords2 = new(Vector2.One, mapId);
sEntity1 = server.EntMan.Spawn("CollisionTest1", coords1);
sEntity2 = server.EntMan.Spawn("CollisionTest2", new MapCoordinates(coords2.Position + new Vector2(0, radius), mapId));
sFix.CreateFixture(sEntity1, "a", new Fixture(new PhysShapeCircle(radius), 1, 1, hard1));
sFix.CreateFixture(sEntity2, "a", new Fixture(new PhysShapeCircle(radius), 1, 1, hard2));
sPhys.SetCanCollide(sEntity1, true);
sPhys.SetCanCollide(sEntity2, true);
sPhys.SetAwake((sEntity1, server.EntMan.GetComponent<PhysicsComponent>(sEntity1)), true);
sPhys.SetAwake((sEntity2, server.EntMan.GetComponent<PhysicsComponent>(sEntity2)), true);
});
for (var i = 0; i < 10; i++)
{
await server.WaitRunTicks(1);
await client.WaitRunTicks(1);
}
await server.WaitPost(() => server.PlayerMan.JoinGame(server.PlayerMan.Sessions.First()));
for (var i = 0; i < 10; i++)
{
await server.WaitRunTicks(1);
await client.WaitRunTicks(1);
}
// Ensure client & server ticks are synced.
// Client runs 2 tick ahead
{
var targetDelta = 2;
var sTick = (int)server.Timing.CurTick.Value;
var cTick = (int)client.Timing.CurTick.Value;
var delta = cTick - sTick;
if (delta > targetDelta)
await server.WaitRunTicks(delta - targetDelta);
else if (delta < targetDelta)
await client.WaitRunTicks(targetDelta - delta);
sTick = (int)server.Timing.CurTick.Value;
cTick = (int)client.Timing.CurTick.Value;
delta = cTick - sTick;
Assert.That(delta, Is.EqualTo(targetDelta));
}
var cPhys = client.System<SharedPhysicsSystem>();
var cSys = client.System<CollisionPredictionTestSystem>();
void ResetSystem()
{
sSys.CollisionEnded = false;
sSys.CollisionStarted = false;
cSys.CollisionEnded = false;
cSys.CollisionStarted = false;
}
async Task Tick()
{
ResetSystem();
await server.WaitRunTicks(1);
await client.WaitRunTicks(1);
}
var nEntity1 = server.EntMan.GetNetEntity(sEntity1);
var nEntity2 = server.EntMan.GetNetEntity(sEntity2);
var cEntity1 = client.EntMan.GetEntity(nEntity1);
var cEntity2 = client.EntMan.GetEntity(nEntity2);
var sComp = server.EntMan.GetComponent<CollisionPredictionTestComponent>(sEntity1);
var cComp = client.EntMan.GetComponent<CollisionPredictionTestComponent>(cEntity1);
cPhys.UpdateIsPredicted(cEntity1);
// Initially, the objects are not colliding.
{
Assert.That(sComp.IsTouching, Is.False);
Assert.That(cComp.IsTouching, Is.False);
Assert.That(cComp.WasTouching, Is.False);
Assert.That(cComp.LastState, Is.False);
Assert.That(sPhys.GetContactingEntities(sEntity1), Is.Empty);
Assert.That(sPhys.GetContactingEntities(sEntity2), Is.Empty);
Assert.That(cPhys.GetContactingEntities(cEntity1), Is.Empty);
Assert.That(cPhys.GetContactingEntities(cEntity2), Is.Empty);
Assert.That(sSys.CollisionStarted, Is.False);
Assert.That(cSys.CollisionStarted, Is.False);
Assert.That(sSys.CollisionEnded, Is.False);
Assert.That(cSys.CollisionEnded, Is.False);
}
// We now simulate a predictive event that gets raised due to some client-side input, causing the entities to
// move and start colliding. Instead of setting up a proper input / keybind handler, The predictive event will
// just be raised in the system update method, which updates before the physics system does.
{
cSys.Ev = new CollisionTestMoveEvent(nEntity1, coords2);
await Tick();
Assert.That(sComp.IsTouching, Is.False);
Assert.That(cComp.IsTouching, Is.True);
Assert.That(cComp.WasTouching, Is.False);
Assert.That(cComp.LastState, Is.False);
Assert.That(sPhys.GetContactingEntities(sEntity1), Is.Empty);
Assert.That(sPhys.GetContactingEntities(sEntity2), Is.Empty);
Assert.That(cPhys.GetContactingEntities(cEntity1), Is.EquivalentTo(new []{ cEntity2 }));
Assert.That(cPhys.GetContactingEntities(cEntity2), Is.EquivalentTo(new []{ cEntity1 }));
Assert.That(sSys.CollisionStarted, Is.False);
Assert.That(cSys.CollisionStarted, Is.True);
Assert.That(sSys.CollisionEnded, Is.False);
Assert.That(cSys.CollisionEnded, Is.False);
}
// Run another tick. Client should reset states, and re-predict the event.
{
await Tick();
Assert.That(sComp.IsTouching, Is.False);
Assert.That(cComp.IsTouching, Is.True);
Assert.That(cComp.WasTouching, Is.True);
Assert.That(cComp.LastState, Is.False);
Assert.That(sPhys.GetContactingEntities(sEntity1), Is.Empty);
Assert.That(sPhys.GetContactingEntities(sEntity2), Is.Empty);
Assert.That(cPhys.GetContactingEntities(cEntity1), Is.EquivalentTo(new []{ cEntity2 }));
Assert.That(cPhys.GetContactingEntities(cEntity2), Is.EquivalentTo(new []{ cEntity1 }));
Assert.That(sSys.CollisionStarted, Is.False);
Assert.That(cSys.CollisionStarted, Is.True);
Assert.That(sSys.CollisionEnded, Is.False);
Assert.That(cSys.CollisionEnded, Is.False);
}
// Next tick the server should raise the event received from the client, which will raise a serve-side
// collide-start event.
{
await Tick();
Assert.That(sComp.IsTouching, Is.True);
Assert.That(cComp.IsTouching, Is.True);
Assert.That(cComp.StartTick, Is.EqualTo(sComp.StartTick));
Assert.That(cComp.WasTouching, Is.True);
Assert.That(cComp.LastState, Is.False);
Assert.That(sPhys.GetContactingEntities(sEntity1), Is.EquivalentTo(new []{ sEntity2 }));
Assert.That(sPhys.GetContactingEntities(sEntity2), Is.EquivalentTo(new []{ sEntity1 }));
Assert.That(cPhys.GetContactingEntities(cEntity1), Is.EquivalentTo(new []{ cEntity2 }));
Assert.That(cPhys.GetContactingEntities(cEntity2), Is.EquivalentTo(new []{ cEntity1 }));
Assert.That(sSys.CollisionStarted, Is.True);
Assert.That(cSys.CollisionStarted, Is.True);
Assert.That(sSys.CollisionEnded, Is.False);
Assert.That(cSys.CollisionEnded, Is.False);
}
// The client will have received the server-state, but will take some time for it to leave the state buffer.
// In the meantime, the client will keep predicting that the collision will "starts"
for (var i = 0; i < 2; i ++)
{
await Tick();
Assert.That(sComp.IsTouching, Is.True);
Assert.That(cComp.IsTouching, Is.True);
Assert.That(cComp.StartTick, Is.EqualTo(sComp.StartTick));
Assert.That(cComp.WasTouching, Is.True);
Assert.That(cComp.LastState, Is.False);
Assert.That(sPhys.GetContactingEntities(sEntity1), Is.EquivalentTo(new []{ sEntity2 }));
Assert.That(sPhys.GetContactingEntities(sEntity2), Is.EquivalentTo(new []{ sEntity1 }));
Assert.That(cPhys.GetContactingEntities(cEntity1), Is.EquivalentTo(new []{ cEntity2 }));
Assert.That(cPhys.GetContactingEntities(cEntity2), Is.EquivalentTo(new []{ cEntity1 }));
Assert.That(sSys.CollisionStarted, Is.False);
Assert.That(cSys.CollisionStarted, Is.True);
Assert.That(sSys.CollisionEnded, Is.False);
Assert.That(cSys.CollisionEnded, Is.False);
}
// Then in the next tick the client should apply the new server state, wherein the contacts were already touching.
// I.e., the contact start event never actually gets raised.
{
await Tick();
Assert.That(sComp.IsTouching, Is.True);
Assert.That(cComp.IsTouching, Is.True);
Assert.That(cComp.StartTick, Is.EqualTo(sComp.StartTick));
Assert.That(cComp.WasTouching, Is.False); // IsTouching gets resets to false before server state is applied
Assert.That(cComp.LastState, Is.True);
Assert.That(sPhys.GetContactingEntities(sEntity1), Is.EquivalentTo(new []{ sEntity2 }));
Assert.That(sPhys.GetContactingEntities(sEntity2), Is.EquivalentTo(new []{ sEntity1 }));
Assert.That(cPhys.GetContactingEntities(cEntity1), Is.EquivalentTo(new []{ cEntity2 }));
Assert.That(cPhys.GetContactingEntities(cEntity2), Is.EquivalentTo(new []{ cEntity1 }));
Assert.That(sSys.CollisionStarted, Is.False);
Assert.That(cSys.CollisionStarted, Is.False);
Assert.That(sSys.CollisionEnded, Is.False);
Assert.That(cSys.CollisionEnded, Is.False);
}
// for the next few ticks, nothing should change
for (var i = 0; i < 10; i ++)
{
await Tick();
Assert.That(sComp.IsTouching, Is.True);
Assert.That(cComp.IsTouching, Is.True);
Assert.That(cComp.StartTick, Is.EqualTo(sComp.StartTick));
Assert.That(cComp.WasTouching, Is.False);
Assert.That(cComp.LastState, Is.True);
Assert.That(sPhys.GetContactingEntities(sEntity1), Is.EquivalentTo(new []{ sEntity2 }));
Assert.That(sPhys.GetContactingEntities(sEntity2), Is.EquivalentTo(new []{ sEntity1 }));
Assert.That(cPhys.GetContactingEntities(cEntity1), Is.EquivalentTo(new []{ cEntity2 }));
Assert.That(cPhys.GetContactingEntities(cEntity2), Is.EquivalentTo(new []{ cEntity1 }));
Assert.That(sSys.CollisionStarted, Is.False);
Assert.That(cSys.CollisionStarted, Is.False);
Assert.That(sSys.CollisionEnded, Is.False);
Assert.That(cSys.CollisionEnded, Is.False);
}
// Next we move the entity away again, so the contact should stop
{
cSys.Ev = new CollisionTestMoveEvent(nEntity1, coords1);
await Tick();
Assert.That(sComp.IsTouching, Is.True);
Assert.That(cComp.IsTouching, Is.False);
Assert.That(cComp.StartTick, Is.EqualTo(sComp.StartTick));
Assert.That(cComp.WasTouching, Is.False);
Assert.That(cComp.LastState, Is.True);
Assert.That(sPhys.GetContactingEntities(sEntity1), Is.EquivalentTo(new []{ sEntity2 }));
Assert.That(sPhys.GetContactingEntities(sEntity2), Is.EquivalentTo(new []{ sEntity1 }));
Assert.That(cPhys.GetContactingEntities(cEntity1), Is.Empty);
Assert.That(cPhys.GetContactingEntities(cEntity2), Is.Empty);
Assert.That(sSys.CollisionStarted, Is.False);
Assert.That(cSys.CollisionStarted, Is.False);
Assert.That(sSys.CollisionEnded, Is.False);
Assert.That(cSys.CollisionEnded, Is.True);
}
// Next tick, the client should reset to a state where the entities were touching, and then re-predict the stop-collide events
{
await Tick();
Assert.That(sComp.IsTouching, Is.True);
Assert.That(cComp.IsTouching, Is.False);
Assert.That(cComp.StartTick, Is.EqualTo(sComp.StartTick));
Assert.That(cComp.WasTouching, Is.False);
Assert.That(cComp.LastState, Is.True);
Assert.That(sPhys.GetContactingEntities(sEntity1), Is.EquivalentTo(new []{ sEntity2 }));
Assert.That(sPhys.GetContactingEntities(sEntity2), Is.EquivalentTo(new []{ sEntity1 }));
Assert.That(cPhys.GetContactingEntities(cEntity1), Is.Empty);
Assert.That(cPhys.GetContactingEntities(cEntity2), Is.Empty);
Assert.That(sSys.CollisionStarted, Is.False);
Assert.That(cSys.CollisionStarted, Is.False);
Assert.That(sSys.CollisionEnded, Is.False);
Assert.That(cSys.CollisionEnded, Is.True);
}
// Next, the server should receive the networked event
{
await Tick();
Assert.That(sComp.IsTouching, Is.False);
Assert.That(cComp.IsTouching, Is.False);
Assert.That(cComp.StartTick, Is.EqualTo(sComp.StartTick));
Assert.That(cComp.StopTick, Is.EqualTo(sComp.StopTick));
Assert.That(cComp.WasTouching, Is.False);
Assert.That(cComp.LastState, Is.True);
Assert.That(sPhys.GetContactingEntities(sEntity1), Is.Empty);
Assert.That(sPhys.GetContactingEntities(sEntity2), Is.Empty);
Assert.That(cPhys.GetContactingEntities(cEntity1), Is.Empty);
Assert.That(cPhys.GetContactingEntities(cEntity2), Is.Empty);
Assert.That(sSys.CollisionStarted, Is.False);
Assert.That(cSys.CollisionStarted, Is.False);
Assert.That(sSys.CollisionEnded, Is.True);
Assert.That(cSys.CollisionEnded, Is.True);
}
// nothing changes while waiting for the client to apply the new server state
for (var i = 0; i < 2; i ++)
{
await Tick();
Assert.That(sComp.IsTouching, Is.False);
Assert.That(cComp.IsTouching, Is.False);
Assert.That(cComp.StartTick, Is.EqualTo(sComp.StartTick));
Assert.That(cComp.StopTick, Is.EqualTo(sComp.StopTick));
Assert.That(cComp.WasTouching, Is.False);
Assert.That(cComp.LastState, Is.True);
Assert.That(sPhys.GetContactingEntities(sEntity1), Is.Empty);
Assert.That(sPhys.GetContactingEntities(sEntity2), Is.Empty);
Assert.That(cPhys.GetContactingEntities(cEntity1), Is.Empty);
Assert.That(cPhys.GetContactingEntities(cEntity2), Is.Empty);
Assert.That(sSys.CollisionStarted, Is.False);
Assert.That(cSys.CollisionStarted, Is.False);
Assert.That(sSys.CollisionEnded, Is.False);
Assert.That(cSys.CollisionEnded, Is.True);
}
// And then the client should apply the new server state
{
await Tick();
Assert.That(sComp.IsTouching, Is.False);
Assert.That(cComp.IsTouching, Is.False);
Assert.That(cComp.StartTick, Is.EqualTo(sComp.StartTick));
Assert.That(cComp.StopTick, Is.EqualTo(sComp.StopTick));
Assert.That(cComp.WasTouching, Is.True);
Assert.That(cComp.LastState, Is.False);
Assert.That(sPhys.GetContactingEntities(sEntity1), Is.Empty);
Assert.That(sPhys.GetContactingEntities(sEntity2), Is.Empty);
Assert.That(cPhys.GetContactingEntities(cEntity1), Is.Empty);
Assert.That(cPhys.GetContactingEntities(cEntity2), Is.Empty);
Assert.That(sSys.CollisionStarted, Is.False);
Assert.That(cSys.CollisionStarted, Is.False);
Assert.That(sSys.CollisionEnded, Is.False);
Assert.That(cSys.CollisionEnded, Is.False);
}
// Nothing should change in the next few ticks
for (var i = 0; i < 10; i ++)
{
await Tick();
Assert.That(sComp.IsTouching, Is.False);
Assert.That(cComp.IsTouching, Is.False);
Assert.That(cComp.StartTick, Is.EqualTo(sComp.StartTick));
Assert.That(cComp.StopTick, Is.EqualTo(sComp.StopTick));
Assert.That(cComp.WasTouching, Is.True);
Assert.That(cComp.LastState, Is.False);
Assert.That(sPhys.GetContactingEntities(sEntity1), Is.Empty);
Assert.That(sPhys.GetContactingEntities(sEntity2), Is.Empty);
Assert.That(cPhys.GetContactingEntities(cEntity1), Is.Empty);
Assert.That(cPhys.GetContactingEntities(cEntity2), Is.Empty);
Assert.That(sSys.CollisionStarted, Is.False);
Assert.That(cSys.CollisionStarted, Is.False);
Assert.That(sSys.CollisionEnded, Is.False);
Assert.That(cSys.CollisionEnded, Is.False);
}
await client.WaitPost(() => netMan.ClientDisconnect(""));
await server.WaitRunTicks(5);
await client.WaitRunTicks(5);
}
}
[RegisterComponent, NetworkedComponent]
internal sealed partial class CollisionPredictionTestComponent : Component
{
public bool IsTouching;
public bool WasTouching;
public bool LastState;
public GameTick StartTick;
public GameTick StopTick;
[Serializable, NetSerializable]
internal sealed class State(bool isTouching) : ComponentState
{
public bool IsTouching = isTouching;
}
}
[Serializable, NetSerializable]
internal sealed class CollisionTestMoveEvent(NetEntity ent, MapCoordinates coords) : EntityEventArgs
{
public NetEntity Ent = ent;
public MapCoordinates Coords = coords;
}
internal sealed class CollisionPredictionTestSystem : EntitySystem
{
[Dependency] private readonly SharedTransformSystem _xform = default!;
[Dependency] private readonly IGameTiming _timing = default!;
public bool CollisionStarted;
public bool CollisionEnded;
public override void Initialize()
{
SubscribeLocalEvent<CollisionPredictionTestComponent, ComponentHandleState>(OnHandleState);
SubscribeLocalEvent<CollisionPredictionTestComponent, ComponentGetState>(OnGetState);
SubscribeLocalEvent<CollisionPredictionTestComponent, StartCollideEvent>(OnStartCollide);
SubscribeLocalEvent<CollisionPredictionTestComponent, EndCollideEvent>(OnEndCollide);
SubscribeLocalEvent<CollisionPredictionTestComponent, UpdateIsPredictedEvent>(OnIsPredicted);
SubscribeAllEvent<CollisionTestMoveEvent>(OnMove);
// Updates before physics to simulate input events.
// inputs are processed before systems update, but I CBF setting up a proper input / keybinding.
UpdatesBefore.Add(typeof(SharedPhysicsSystem));
}
public CollisionTestMoveEvent? Ev;
public override void Update(float frameTime)
{
if (Ev == null || !_timing.IsFirstTimePredicted)
return;
RaisePredictiveEvent(Ev);
Ev = null;
}
private void OnIsPredicted(Entity<CollisionPredictionTestComponent> ent, ref UpdateIsPredictedEvent args)
{
args.IsPredicted = true;
}
private void OnMove(CollisionTestMoveEvent ev)
{
_xform.SetMapCoordinates(GetEntity(ev.Ent), ev.Coords);
}
private void OnEndCollide(Entity<CollisionPredictionTestComponent> ent, ref EndCollideEvent args)
{
// TODO PHYSICS Collision Mispredicts
// Currently the client will raise collision start/stop events multiple times for each collision
// If this ever gets fixed, re-add the assert:
// Assert.That(ent.Comp.IsTouching, Is.True);
if (!ent.Comp.IsTouching)
return;
Assert.That(CollisionEnded, Is.False);
ent.Comp.StopTick = _timing.CurTick;
ent.Comp.IsTouching = false;
CollisionEnded = true;
Dirty(ent);
}
private void OnStartCollide(Entity<CollisionPredictionTestComponent> ent, ref StartCollideEvent args)
{
// TODO PHYSICS Collision Mispredicts
// Currently the client will raise collision start/stop events multiple times for each collision
// If this ever gets fixed, re-add the assert:
// Assert.That(ent.Comp.IsTouching, Is.False);
if (ent.Comp.IsTouching)
return;
Assert.That(CollisionStarted, Is.False);
ent.Comp.StartTick = _timing.CurTick;
ent.Comp.IsTouching = true;
CollisionStarted = true;
Dirty(ent);
}
private void OnGetState(Entity<CollisionPredictionTestComponent> ent, ref ComponentGetState args)
{
args.State = new CollisionPredictionTestComponent.State(ent.Comp.IsTouching);
}
private void OnHandleState(Entity<CollisionPredictionTestComponent> ent, ref ComponentHandleState args)
{
if (args.Current is not CollisionPredictionTestComponent.State state)
return;
ent.Comp.WasTouching = ent.Comp.IsTouching;
ent.Comp.LastState = state.IsTouching;
ent.Comp.IsTouching = state.IsTouching;
}
}

View File

@@ -0,0 +1,114 @@
using System.Numerics;
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
using Robust.Shared.Physics.Components;
namespace Robust.UnitTesting.Shared.Physics
{
[TestFixture, TestOf(typeof(CollisionWakeSystem))]
internal sealed class CollisionWake_Test : RobustIntegrationTest
{
private const string Prototype = @"
- type: entity
name: dummy
id: CollisionWakeTestItem
components:
- type: Transform
- type: Physics
bodyType: Dynamic
- type: Fixtures
fixtures:
fix1:
shape:
!type:PhysShapeCircle
radius: 0.35
- type: CollisionWake
";
/// <summary>
/// Test whether a CollisionWakeComponent correctly turns off collision on a grid and leaves it on off of a grid.
/// </summary>
[Test]
public async Task TestCollisionWakeGrid()
{
var options = new ServerIntegrationOptions {ExtraPrototypes = Prototype};
options.CVarOverrides["physics.timetosleep"] = "0.0";
var server = StartServer(options);
await server.WaitIdleAsync();
var entManager = server.ResolveDependency<IEntityManager>();
var mapManager = server.ResolveDependency<IMapManager>();
var mapSystem = entManager.System<SharedMapSystem>();
var transformSystem = entManager.System<SharedTransformSystem>();
Entity<MapGridComponent> grid = default!;
MapId mapId = default!;
PhysicsComponent entityOnePhysics = default!;
TransformComponent xform = default!;
PhysicsComponent entityTwoPhysics = default!;
EntityUid? entityOne = null;
EntityUid? entityTwo = null;
await server.WaitPost(() =>
{
mapSystem.CreateMap(out mapId);
grid = mapManager.CreateGridEntity(mapId);
mapSystem.SetTile(grid, Vector2i.Zero, new Tile(1));
entityOne = entManager.SpawnEntity("CollisionWakeTestItem", new MapCoordinates(Vector2.One * 2f, mapId));
entityOnePhysics = entManager.GetComponent<PhysicsComponent>(entityOne.Value);
xform = entManager.GetComponent<TransformComponent>(entityOne.Value);
mapSystem.TryGetMap(mapId, out var mapUid);
Assert.That(xform.ParentUid == mapUid);
entityTwo = entManager.SpawnEntity("CollisionWakeTestItem", new EntityCoordinates(grid, new Vector2(0.5f, 0.5f)));
entityTwoPhysics = entManager.GetComponent<PhysicsComponent>(entityTwo.Value);
Assert.That(entManager.GetComponent<TransformComponent>(entityTwo.Value).ParentUid == grid.Owner);
});
Assert.That(entityOne, Is.Not.Null);
Assert.That(entityTwo, Is.Not.Null);
// Item 1 Should still be collidable
await server.WaitRunTicks(1);
await server.WaitAssertion(() =>
{
Assert.That(entityOnePhysics.Awake, Is.EqualTo(false));
Assert.That(entityOnePhysics.CanCollide, Is.EqualTo(true));
transformSystem.SetLocalPosition(entityOne.Value, new Vector2(0.5f, 0.5f), xform);
transformSystem.SetParent(entityOne.Value, xform, grid);
// Entity 2 should immediately not be collidable on spawn
Assert.That(entityTwoPhysics.Awake, Is.EqualTo(false));
Assert.That(entityTwoPhysics.CanCollide, Is.EqualTo(false));
});
await server.WaitRunTicks(1);
await server.WaitAssertion(() =>
{
Assert.That(entityOnePhysics.Awake, Is.EqualTo(false));
Assert.That(entityOnePhysics.CanCollide, Is.EqualTo(false));
transformSystem.SetLocalPosition(entityOne.Value, Vector2.One * 2f);
transformSystem.SetParent(entityOne.Value, xform, mapSystem.GetMapOrInvalid(mapId));
});
// Juussttt in case we'll re-parent it to the map and check its collision is back on.
await server.WaitRunTicks(1);
await server.WaitAssertion(() =>
{
Assert.That(entityOnePhysics.Awake, Is.EqualTo(false));
Assert.That(entityOnePhysics.CanCollide, Is.EqualTo(true));
});
}
}
}

View File

@@ -0,0 +1,159 @@
// MIT License
// Copyright (c) 2020 Erin Catto
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Numerics;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Systems;
using Robust.UnitTesting.Server;
namespace Robust.UnitTesting.Shared.Physics;
[TestFixture]
internal sealed class Collision_Test
{
[Test]
public void TestHardCollidable()
{
var sim = RobustServerSimulation.NewSimulation().InitializeInstance();
var entManager = sim.Resolve<IEntityManager>();
var fixtures = entManager.System<FixtureSystem>();
var physics = entManager.System<SharedPhysicsSystem>();
var map = sim.CreateMap();
var bodyAUid = entManager.SpawnAttachedTo(null, new EntityCoordinates(map.Uid, Vector2.Zero));
var bodyBUid = entManager.SpawnAttachedTo(null, new EntityCoordinates(map.Uid, Vector2.Zero));
var bodyA = entManager.AddComponent<PhysicsComponent>(bodyAUid);
var bodyB = entManager.AddComponent<PhysicsComponent>(bodyBUid);
Assert.That(!physics.IsHardCollidable(bodyAUid, bodyBUid));
fixtures.CreateFixture(bodyAUid, "fix1", new Fixture(new PhysShapeCircle(0.5f), 1, 1, true));
fixtures.CreateFixture(bodyBUid, "fix1", new Fixture(new PhysShapeCircle(0.5f), 1, 1, true));
Assert.That(physics.IsHardCollidable(bodyAUid, bodyBUid));
}
[Test]
public void TestCollision()
{
var center = new Vector2(100.0f, -50.0f);
const float hx = 0.5f, hy = 1.5f;
const float angle1 = 0.25f;
// Data from issue #422. Not used because the data exceeds accuracy limits.
//const b2Vec2 center(-15000.0f, -15000.0f);
//const float hx = 0.72f, hy = 0.72f;
//const float angle1 = 0.0f;
PolygonShape polygon1 = new();
polygon1.SetAsBox(hx, hy, center, angle1);
const float absTol = 2.0f * float.Epsilon;
const float relTol = 2.0f * float.Epsilon;
Assert.That(Math.Abs(polygon1.Centroid.X - center.X), Is.LessThan(absTol + relTol * Math.Abs(center.X)));
Assert.That(Math.Abs(polygon1.Centroid.Y - center.Y), Is.LessThan(absTol + relTol * Math.Abs(center.Y)));
Span<Vector2> vertices = stackalloc Vector2[4];
vertices[0] = new Vector2(center.X - hx, center.Y - hy);
vertices[1] = new Vector2(center.X + hx, center.Y - hy);
vertices[2] = new Vector2(center.X - hx, center.Y + hy);
vertices[3] = new Vector2(center.X + hx, center.Y + hy);
PolygonShape polygon2 = new();
polygon2.Set(vertices, 4);
Assert.That(Math.Abs(polygon2.Centroid.X - center.X), Is.LessThan(absTol + relTol * Math.Abs(center.X)));
Assert.That(Math.Abs(polygon2.Centroid.Y - center.Y), Is.LessThan(absTol + relTol * Math.Abs(center.Y)));
const float mass = 4.0f * hx * hy;
var inertia = (mass / 3.0f) * (hx * hx + hy * hy) + mass * Vector2.Dot(center, center);
var massData1 = FixtureSystem.GetMassData(polygon1, 1f);
Assert.That(MathF.Abs(massData1.Center.X - center.X), Is.LessThan(absTol + relTol * Math.Abs(center.X)));
Assert.That(MathF.Abs(massData1.Center.Y - center.Y), Is.LessThan(absTol + relTol * Math.Abs(center.Y)));
// TODO: How the hell is this rounding enough that this test fails with the angle???
// Assert.That(MathF.Abs(massData1.Mass - mass), Is.LessThan(20.0f * (absTol + relTol * mass)));
// Assert.That(MathF.Abs(massData1.I - inertia), Is.LessThan(40.0f * (absTol + relTol * inertia)));
var massData2 = FixtureSystem.GetMassData(polygon2, 1f);
Assert.That(MathF.Abs(massData2.Center.X - center.X), Is.LessThan(absTol + relTol * Math.Abs(center.X)));
Assert.That(MathF.Abs(massData2.Center.Y - center.Y), Is.LessThan(absTol + relTol * Math.Abs(center.Y)));
Assert.That(MathF.Abs(massData2.Mass - mass), Is.LessThan(20.0f * (absTol + relTol * mass)));
Assert.That(MathF.Abs(massData2.I - inertia), Is.LessThan(40.0f * (absTol + relTol * inertia)));
}
/// <summary>
/// Asserts that cross-map contacts correctly destroy
/// </summary>
[Test]
public void CrossMapContacts()
{
var sim = RobustServerSimulation.NewSimulation().InitializeInstance();
var entManager = sim.Resolve<IEntityManager>();
var fixtures = entManager.System<FixtureSystem>();
var physics = entManager.System<SharedPhysicsSystem>();
var xformSystem = entManager.System<SharedTransformSystem>();
var mapSystem = entManager.System<SharedMapSystem>();
var mapId = sim.CreateMap().MapId;
var mapId2 = sim.CreateMap().MapId;
var ent1 = entManager.SpawnEntity(null, new MapCoordinates(Vector2.Zero, mapId));
var ent2 = entManager.SpawnEntity(null, new MapCoordinates(Vector2.Zero, mapId));
var body1 = entManager.AddComponent<PhysicsComponent>(ent1);
physics.SetBodyType(ent1, BodyType.Dynamic, body: body1);
var body2 = entManager.AddComponent<PhysicsComponent>(ent2);
physics.SetBodyType(ent2, BodyType.Dynamic, body: body2);
fixtures.CreateFixture(ent1, "fix1", new Fixture(new PhysShapeCircle(1f), 1, 0, true), body: body1);
fixtures.CreateFixture(ent2, "fix1", new Fixture(new PhysShapeCircle(1f), 0, 1, true), body: body2);
physics.WakeBody(ent1, body: body1);
physics.WakeBody(ent2, body: body2);
Assert.That(body1.Awake && body2.Awake);
Assert.That(body1.ContactCount == 0 && body2.ContactCount == 0);
physics.Update(0.01f);
Assert.That(body1.ContactCount == 1 && body2.ContactCount == 1);
// Reparent body2 and assert the contact is destroyed
xformSystem.SetParent(ent2, mapSystem.GetMapOrInvalid(mapId2));
physics.Update(0.01f);
Assert.That(body1.ContactCount == 0 && body2.ContactCount == 0);
}
}

View File

@@ -0,0 +1,71 @@
using System;
using System.Numerics;
using NUnit.Framework;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Physics.Systems;
namespace Robust.UnitTesting.Shared.Physics
{
[TestFixture]
[TestOf(typeof(FixtureSystem))]
internal sealed class FixtureShape_Test : OurRobustUnitTest
{
private FixtureSystem _shapeManager = default!;
[OneTimeSetUp]
public void Setup()
{
_shapeManager = new FixtureSystem();
}
[Test]
public void TestCirclePoint()
{
var circle = new PhysShapeCircle(0.5f);
var transform = new Transform(0f);
var posA = Vector2.One;
var posB = Vector2.Zero;
var posC = new Vector2(0.1f, 0.3f);
Assert.That(_shapeManager.TestPoint(circle, transform, posA), Is.EqualTo(false));
Assert.That(_shapeManager.TestPoint(circle, transform, posB), Is.EqualTo(true));
Assert.That(_shapeManager.TestPoint(circle, transform, posC), Is.EqualTo(true));
}
[Test]
public void TestEdgePoint()
{
// Edges never collide with a point because they're a damn line
var edge = new EdgeShape(Vector2.Zero, Vector2.One);
var transform = new Transform(0f);
var posA = Vector2.One;
var posB = Vector2.Zero;
var posC = new Vector2(0.1f, 0.3f);
Assert.That(_shapeManager.TestPoint(edge, transform, posA), Is.EqualTo(false));
Assert.That(_shapeManager.TestPoint(edge, transform, posB), Is.EqualTo(false));
Assert.That(_shapeManager.TestPoint(edge, transform, posC), Is.EqualTo(false));
}
[Test]
public void TestPolyPoint()
{
var poly = new PolygonShape();
poly.SetAsBox(0.5f, 0.5f);
var transform = new Transform(0f);
var posA = Vector2.One;
var posB = Vector2.Zero;
var posC = new Vector2(0.6f, 0.0f);
Assert.That(_shapeManager.TestPoint(poly, transform, posA), Is.EqualTo(false));
Assert.That(_shapeManager.TestPoint(poly, transform, posB), Is.EqualTo(true));
Assert.That(_shapeManager.TestPoint(poly, transform, posC), Is.EqualTo(false));
// Rotations
transform.Quaternion2D = new Quaternion2D(MathF.PI / 4);
Assert.That(_shapeManager.TestPoint(poly, transform, posC), Is.EqualTo(true));
}
}
}

View File

@@ -0,0 +1,41 @@
using System.Numerics;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Systems;
using Robust.UnitTesting.Server;
namespace Robust.UnitTesting.Shared.Physics;
[TestFixture]
internal sealed class Fixtures_Test
{
[Test]
public void SetDensity()
{
var sim = RobustServerSimulation.NewSimulation().InitializeInstance();
var entManager = sim.Resolve<IEntityManager>();
var sysManager = sim.Resolve<IEntitySystemManager>();
var fixturesSystem = sysManager.GetEntitySystem<FixtureSystem>();
var physicsSystem = sysManager.GetEntitySystem<SharedPhysicsSystem>();
var mapSystem = sysManager.GetEntitySystem<SharedMapSystem>();
var map = sim.CreateMap().MapId;
var ent = sim.SpawnEntity(null, new MapCoordinates(Vector2.Zero, map));
var body = entManager.AddComponent<PhysicsComponent>(ent);
physicsSystem.SetBodyType(ent, BodyType.Dynamic, body: body);
var fixture = new Fixture();
fixturesSystem.CreateFixture(ent, "fix1", fixture);
physicsSystem.SetDensity(ent, "fix1", fixture, 10f);
Assert.That(fixture.Density, Is.EqualTo(10f));
Assert.That(body.Mass, Is.EqualTo(10f));
mapSystem.DeleteMap(map);
}
}

View File

@@ -0,0 +1,63 @@
using System.Collections.Generic;
using System.Numerics;
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Shared.GameObjects;
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;
namespace Robust.UnitTesting.Shared.Physics;
/// <summary>
/// Tests moving and deleting a grid.
/// Mainly useful for grid dynamic tree.
/// </summary>
[TestFixture]
internal sealed class GridDeletion_Test : RobustIntegrationTest
{
[Test]
public async Task GridDeletionTest()
{
var server = StartServer();
await server.WaitIdleAsync();
var entManager = server.ResolveDependency<IEntityManager>();
var mapManager = server.ResolveDependency<IMapManager>();
var physSystem = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<SharedPhysicsSystem>();
PhysicsComponent physics = default!;
Entity<MapGridComponent> grid = default!;
MapId mapId = default!;
await server.WaitAssertion(() =>
{
entManager.System<SharedMapSystem>().CreateMap(out mapId);
grid = mapManager.CreateGridEntity(mapId);
physics = entManager.GetComponent<PhysicsComponent>(grid);
physSystem.SetBodyType(grid, BodyType.Dynamic, body: physics);
physSystem.SetLinearVelocity(grid, new Vector2(50f, 0f), body: physics);
Assert.That(physics.LinearVelocity.Length, NUnit.Framework.Is.GreaterThan(0f));
});
await server.WaitRunTicks(1);
await server.WaitAssertion(() =>
{
Assert.That(physics.LinearVelocity.Length, NUnit.Framework.Is.GreaterThan(0f));
entManager.DeleteEntity(grid);
List<Entity<MapGridComponent>> grids = [];
// So if gridtree is fucky then this SHOULD throw.
mapManager.FindGridsIntersecting(mapId,
new Box2(new Vector2(float.MinValue, float.MinValue),
new Vector2(float.MaxValue, float.MaxValue)), ref grids);
});
}
}

View File

@@ -0,0 +1,78 @@
using System.Numerics;
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Systems;
namespace Robust.UnitTesting.Shared.Physics;
[TestFixture, TestOf(typeof(SharedBroadphaseSystem))]
internal sealed class GridMovement_Test : RobustIntegrationTest
{
[Test]
public async Task TestFindGridContacts()
{
var server = StartServer();
await server.WaitIdleAsync();
// Checks that FindGridContacts succesfully overlaps a grid + map broadphase physics body
var systems = server.ResolveDependency<IEntitySystemManager>();
var fixtureSystem = systems.GetEntitySystem<FixtureSystem>();
var mapManager = server.ResolveDependency<IMapManager>();
var entManager = server.ResolveDependency<IEntityManager>();
var physSystem = systems.GetEntitySystem<SharedPhysicsSystem>();
var transformSystem = entManager.EntitySysManager.GetEntitySystem<SharedTransformSystem>();
var mapSystem = entManager.EntitySysManager.GetEntitySystem<SharedMapSystem>();
await server.WaitAssertion(() =>
{
entManager.System<SharedMapSystem>().CreateMap(out var mapId);
var grid = mapManager.CreateGridEntity(mapId);
// Setup 1 body on grid, 1 body off grid, and assert that it's all gucci.
mapSystem.SetTile(grid, Vector2i.Zero, new Tile(1));
var fixtures = entManager.GetComponent<FixturesComponent>(grid);
Assert.That(fixtures.FixtureCount, Is.EqualTo(1));
var onGrid = entManager.SpawnEntity(null, new EntityCoordinates(grid, 0.5f, 0.5f ));
var onGridBody = entManager.AddComponent<PhysicsComponent>(onGrid);
physSystem.SetBodyType(onGrid, BodyType.Dynamic, body: onGridBody);
var shapeA = new PolygonShape();
shapeA.SetAsBox(0.5f, 0.5f);
fixtureSystem.CreateFixture(onGrid, "fix1", new Fixture(shapeA, 1, 0, false), body: onGridBody);
Assert.That(fixtureSystem.GetFixtureCount(onGrid), Is.EqualTo(1));
Assert.That(entManager.GetComponent<TransformComponent>(onGrid).ParentUid, Is.EqualTo(grid.Owner));
physSystem.WakeBody(onGrid, body: onGridBody);
Assert.That(onGridBody.Awake);
var offGrid = entManager.SpawnEntity(null, new MapCoordinates(new Vector2(10f, 10f), mapId));
var offGridBody = entManager.AddComponent<PhysicsComponent>(offGrid);
physSystem.SetBodyType(offGrid, BodyType.Dynamic, body: offGridBody);
var shapeB = new PolygonShape();
shapeB.SetAsBox(0.5f, 0.5f);
fixtureSystem.CreateFixture(offGrid, "fix1", new Fixture(shapeB, 0, 1, false), body: offGridBody);
Assert.That(fixtureSystem.GetFixtureCount(offGrid), Is.EqualTo(1));
Assert.That(entManager.GetComponent<TransformComponent>(offGrid).ParentUid, Is.Not.EqualTo((grid.Owner)));
physSystem.WakeBody(offGrid, body: offGridBody);
Assert.That(offGridBody.Awake);
// Alright just a quick validation then we start the actual damn test.
physSystem.Update(0.001f);
Assert.That(onGridBody.ContactCount, Is.EqualTo(0));
// Alright now move the grid on top of the off grid body, run physics for a frame and see if they contact
transformSystem.SetLocalPosition(grid.Owner, new Vector2(10f, 10f));
physSystem.Update(0.001f);
Assert.That(onGridBody.ContactCount, Is.EqualTo(1));
});
}
}

View File

@@ -0,0 +1,211 @@
using System.Numerics;
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Systems;
using Robust.UnitTesting.Server;
namespace Robust.UnitTesting.Shared.Physics;
[TestFixture, TestOf(typeof(SharedPhysicsSystem))]
internal sealed class GridReparentVelocity_Test
{
private ISimulation _sim = default!;
private IEntitySystemManager _systems = default!;
private IEntityManager _entManager = default!;
private IMapManager _mapManager = default!;
private FixtureSystem _fixtureSystem = default!;
private SharedMapSystem _mapSystem = default!;
private SharedPhysicsSystem _physSystem = default!;
// Test objects.
private EntityUid _mapUid = default!;
private MapId _mapId = default!;
private EntityUid _gridUid = default!;
private EntityUid _objUid = default!;
[OneTimeSetUp]
public void FixtureSetup()
{
_sim = RobustServerSimulation.NewSimulation()
.InitializeInstance();
_systems = _sim.Resolve<IEntitySystemManager>();
_entManager = _sim.Resolve<IEntityManager>();
_mapManager = _sim.Resolve<IMapManager>();
_fixtureSystem = _systems.GetEntitySystem<FixtureSystem>();
_mapSystem = _systems.GetEntitySystem<SharedMapSystem>();
_physSystem = _systems.GetEntitySystem<SharedPhysicsSystem>();
}
[SetUp]
public void Setup()
{
_mapUid = _mapSystem.CreateMap(out _mapId);
// Spawn a 1x1 grid centered at (0.5, 0.5), ensure it's movable and its velocity has no damping.
var gridEnt = _mapManager.CreateGridEntity(_mapId);
var gridPhys = _entManager.GetComponent<PhysicsComponent>(gridEnt);
_physSystem.SetSleepingAllowed(gridEnt, gridPhys, false);
_physSystem.SetBodyType(gridEnt, BodyType.Dynamic, body: gridPhys);
_physSystem.SetLinearDamping(gridEnt, gridPhys, 0.0f);
_physSystem.SetAngularDamping(gridEnt, gridPhys, 0.0f);
_mapSystem.SetTile(gridEnt, Vector2i.Zero, new Tile(1));
_physSystem.WakeBody(gridEnt, body: gridPhys);
_gridUid = gridEnt.Owner;
}
// Spawn a bullet-like test object at the given position.
public EntityUid SetupTestObject(EntityCoordinates coords)
{
var obj = _entManager.SpawnEntity(null, coords);
var objPhys = _entManager.EnsureComponent<PhysicsComponent>(obj);
var objFix = _entManager.EnsureComponent<FixturesComponent>(obj);
// Set up physics (no velocity damping, dynamic body, physics enabled)
_entManager.GetComponent<PhysicsComponent>(obj);
_physSystem.SetSleepingAllowed(obj, objPhys, false);
_physSystem.SetBodyType(obj, BodyType.Dynamic, body: objPhys);
_physSystem.SetLinearDamping(obj, objPhys, 0.0f);
_physSystem.SetAngularDamping(obj, objPhys, 0.0f);
// Set up fixture.
var poly = new PolygonShape();
poly.SetAsBox(0.1f, 0.1f);
_fixtureSystem.CreateFixture(obj, "fix1", new Fixture(poly, 0, 0, false), manager: objFix, body: objPhys);
_physSystem.WakeBody(obj, body: objPhys);
return obj;
}
// Moves an object off of a moving grid, checks for conservation of linear velocity.
[Test]
public void TestLinearVelocityOnlyMoveOffGrid()
{
// Spawn our test object in the middle of the grid, ensure it has no damping.
_objUid = SetupTestObject(new EntityCoordinates(_gridUid, 0.5f, 0.5f));
Assert.Multiple(() =>
{
// Our object should start on the grid.
Assert.That(_entManager.GetComponent<TransformComponent>(_objUid).ParentUid, Is.EqualTo(_gridUid));
// Set the velocity of the grid and our object.
Assert.That(_physSystem.SetLinearVelocity(_objUid, new Vector2(3.5f, 4.75f)), Is.True);
Assert.That(_physSystem.SetLinearVelocity(_gridUid, new Vector2(1.0f, 2.0f)), Is.True);
// Wait a second to clear the grid
_physSystem.Update(1.0f);
// The object should be parented to the map and maintain its map velocity, the grid should be unchanged.
var objXform = _entManager.GetComponent<TransformComponent>(_objUid);
var gridXform = _entManager.GetComponent<TransformComponent>(_gridUid);
Assert.That(objXform.ParentUid, Is.EqualTo(_mapUid), $"Object is not on map - actual position: {objXform.ParentUid} {objXform.LocalPosition}, grid position: {gridXform.ParentUid} {gridXform.LocalPosition}");
Assert.That(_entManager.GetComponent<PhysicsComponent>(_objUid).LinearVelocity, Is.EqualTo(new Vector2(4.5f, 6.75f)));
Assert.That(_entManager.GetComponent<PhysicsComponent>(_gridUid).LinearVelocity, Is.EqualTo(new Vector2(1.0f, 2.0f)));
});
}
[Test]
// Moves an object onto a moving grid, checks for conservation of linear velocity.
public void TestLinearVelocityOnlyMoveOntoGrid()
{
// Spawn our test object 1 m off of the middle of the grid in both directions.
_objUid = SetupTestObject(new EntityCoordinates(_mapUid, 1.5f, 1.5f));
Assert.Multiple(() =>
{
// Assert that we start off the grid.
Assert.That(_entManager.GetComponent<TransformComponent>(_objUid).ParentUid, Is.EqualTo(_mapUid));
// Set the velocity of the grid and our object.
Assert.That(_physSystem.SetLinearVelocity(_objUid, new Vector2(-2.0f, -3.0f)), Is.True);
Assert.That(_physSystem.SetLinearVelocity(_gridUid, new Vector2(-1.0f, -2.0f)), Is.True);
// Wait a second to move onto the middle of the grid
_physSystem.Update(1.0f);
// The object should be parented to the grid and maintain its map velocity (slowing down), the grid should be unchanged.
var objXform = _entManager.GetComponent<TransformComponent>(_objUid);
var gridXform = _entManager.GetComponent<TransformComponent>(_gridUid);
Assert.That(objXform.ParentUid, Is.EqualTo(_gridUid), $"Object is not on grid - actual position: {objXform.ParentUid} {objXform.LocalPosition}, grid position: {gridXform.ParentUid} {gridXform.LocalPosition}");
Assert.That(_entManager.GetComponent<PhysicsComponent>(_objUid).LinearVelocity, Is.EqualTo(new Vector2(-1.0f, -1.0f)));
Assert.That(_entManager.GetComponent<PhysicsComponent>(_gridUid).LinearVelocity, Is.EqualTo(new Vector2(-1.0f, -2.0f)));
});
}
[Test]
// Moves a rotating object off of a rotating grid, checks for conservation of angular velocity.
public void TestLinearAndAngularVelocityMoveOffGrid()
{
// Spawn our test object in the middle of the grid.
_objUid = SetupTestObject(new EntityCoordinates(_gridUid, 0.5f, 0.5f));
Assert.Multiple(() =>
{
// Our object should start on the grid.
Assert.That(_entManager.GetComponent<TransformComponent>(_objUid).ParentUid, Is.EqualTo(_gridUid));
// Set the velocity of the grid and our object.
Assert.That(_physSystem.SetLinearVelocity(_objUid, new Vector2(3.5f, 4.75f)), Is.True);
Assert.That(_physSystem.SetAngularVelocity(_objUid, 1.0f), Is.True);
Assert.That(_physSystem.SetLinearVelocity(_gridUid, new Vector2(1.0f, 2.0f)), Is.True);
Assert.That(_physSystem.SetAngularVelocity(_gridUid, 2.0f), Is.True);
// Wait a second to clear the grid
_physSystem.Update(1.0f);
// The object should be parented to the map and maintain its map velocity, the grid should be unchanged.
var objXform = _entManager.GetComponent<TransformComponent>(_objUid);
var gridXform = _entManager.GetComponent<TransformComponent>(_gridUid);
Assert.That(objXform.ParentUid, Is.EqualTo(_mapUid), $"Object is not on map - actual position: {objXform.ParentUid} {objXform.LocalPosition}, grid position: {gridXform.ParentUid} {gridXform.LocalPosition}");
// Not checking object's linear velocity in this case, non-zero contribution from grid angular velocity.
Assert.That(_entManager.GetComponent<PhysicsComponent>(_objUid).AngularVelocity, Is.EqualTo(3.0f));
var gridPhys = _entManager.GetComponent<PhysicsComponent>(_gridUid);
Assert.That(gridPhys.LinearVelocity, Is.EqualTo(new Vector2(1.0f, 2.0f)));
Assert.That(gridPhys.AngularVelocity, Is.EqualTo(2.0f));
});
}
[Test]
// Moves a rotating object onto a rotating grid, checks for conservation of angular velocity.
public void TestLinearAndAngularVelocityMoveOntoGrid()
{
// Spawn our test object 1 m off of the middle of the grid in both directions.
_objUid = SetupTestObject(new EntityCoordinates(_mapUid, 1.5f, 1.5f));
Assert.Multiple(() =>
{
// Assert that we start off the grid.
Assert.That(_entManager.GetComponent<TransformComponent>(_objUid).ParentUid, Is.EqualTo(_mapUid));
// Set the velocity of the grid and our object.
Assert.That(_physSystem.SetLinearVelocity(_objUid, new Vector2(-2.0f, -3.0f)), Is.True);
Assert.That(_physSystem.SetAngularVelocity(_objUid, 1.0f), Is.True);
Assert.That(_physSystem.SetLinearVelocity(_gridUid, new Vector2(-1.0f, -2.0f)), Is.True);
Assert.That(_physSystem.SetAngularVelocity(_gridUid, 2.0f), Is.True);
// Wait a second to move onto the middle of the grid
_physSystem.Update(1.0f);
// The object should be parented to the grid and maintain its map velocity (slowing down), the grid should be unchanged.
var objXform = _entManager.GetComponent<TransformComponent>(_objUid);
var gridXform = _entManager.GetComponent<TransformComponent>(_gridUid);
Assert.That(objXform.ParentUid, Is.EqualTo(_gridUid), $"Object is not on grid - actual position: {objXform.ParentUid} {objXform.LocalPosition}, grid position: {gridXform.ParentUid} {gridXform.LocalPosition}");
// Not checking object's linear velocity in this case, non-zero contribution from grid angular velocity.
Assert.That(_entManager.GetComponent<PhysicsComponent>(_objUid).AngularVelocity, Is.EqualTo(-1.0f));
var gridPhys = _entManager.GetComponent<PhysicsComponent>(_gridUid);
Assert.That(gridPhys.LinearVelocity, Is.EqualTo(new Vector2(-1.0f, -2.0f)));
Assert.That(gridPhys.AngularVelocity, Is.EqualTo(2.0f));
});
}
}

View File

@@ -0,0 +1,78 @@
using System.Numerics;
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Dynamics.Joints;
using Robust.Shared.Physics.Systems;
namespace Robust.UnitTesting.Shared.Physics;
[TestFixture]
internal sealed class JointDeletion_Test : RobustIntegrationTest
{
[Test]
public async Task JointDeletionTest()
{
var server = StartServer();
await server.WaitIdleAsync();
var entManager = server.ResolveDependency<IEntityManager>();
var mapManager = server.ResolveDependency<IMapManager>();
var susManager = server.ResolveDependency<IEntitySystemManager>();
var jointSystem = susManager.GetEntitySystem<SharedJointSystem>();
var broadphase = susManager.GetEntitySystem<SharedBroadphaseSystem>();
var fixSystem = susManager.GetEntitySystem<FixtureSystem>();
var physicsSystem = susManager.GetEntitySystem<SharedPhysicsSystem>();
DistanceJoint joint = default!;
EntityUid ent1;
EntityUid ent2 = default!;
PhysicsComponent body1;
PhysicsComponent body2 = default!;
EntityUid mapEnt = default!;
MapId mapId = default!;
await server.WaitPost(() =>
{
mapEnt = entManager.System<SharedMapSystem>().CreateMap(out mapId);
ent1 = entManager.SpawnEntity(null, new MapCoordinates(Vector2.Zero, mapId));
ent2 = entManager.SpawnEntity(null, new MapCoordinates(Vector2.One, mapId));
body1 = entManager.AddComponent<PhysicsComponent>(ent1);
body2 = entManager.AddComponent<PhysicsComponent>(ent2);
var manager1 = entManager.EnsureComponent<FixturesComponent>(ent1);
var manager2 = entManager.EnsureComponent<FixturesComponent>(ent2);
entManager.AddComponent<CollisionWakeComponent>(ent2);
physicsSystem.SetBodyType(ent1, BodyType.Dynamic, manager: manager1, body: body1);
physicsSystem.SetBodyType(ent2, BodyType.Dynamic, manager: manager2, body: body2);
physicsSystem.SetCanCollide(ent1, true, manager: manager1, body: body1);
physicsSystem.SetCanCollide(ent2, true, manager: manager2, body: body2);
var shape = new PolygonShape();
shape.SetAsBox(0.5f, 0.5f);
fixSystem.CreateFixture(ent2, "fix1", new Fixture(shape, 0, 0, false), manager: manager2, body: body2);
joint = jointSystem.CreateDistanceJoint(ent1, ent2, id: "distance-joint");
joint.CollideConnected = false;
});
await server.WaitRunTicks(1);
await server.WaitAssertion(() =>
{
Assert.That(joint.Enabled);
physicsSystem.SetAwake((ent2, body2), false);
Assert.That(!body2.Awake);
entManager.DeleteEntity(ent2);
broadphase.FindNewContacts();
});
}
}

View File

@@ -0,0 +1,112 @@
using System.Numerics;
using NUnit.Framework;
using Robust.Server.Physics;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Systems;
using Robust.UnitTesting.Server;
namespace Robust.UnitTesting.Shared.Physics;
[TestFixture, TestOf(typeof(JointSystem))]
internal sealed class Joints_Test
{
[Test]
public void JointsRelayTest()
{
var factory = RobustServerSimulation.NewSimulation();
var sim = factory.InitializeInstance();
var entManager = sim.Resolve<IEntityManager>();
var jointSystem = entManager.System<SharedJointSystem>();
var mapSystem = entManager.System<SharedMapSystem>();
var mapId = sim.CreateMap().MapId;
var uidA = entManager.SpawnEntity(null, new MapCoordinates(0f, 0f, mapId));
var uidB = entManager.SpawnEntity(null, new MapCoordinates(0f, 0f, mapId));
var uidC = entManager.SpawnEntity(null, new MapCoordinates(0f, 0f, mapId));
entManager.AddComponent<PhysicsComponent>(uidA);
entManager.AddComponent<PhysicsComponent>(uidB);
entManager.AddComponent<PhysicsComponent>(uidC);
var containerSys = entManager.System<SharedContainerSystem>();
var container = containerSys.EnsureContainer<Container>(uidC, "weh");
jointSystem.CreateDistanceJoint(uidA, uidB);
jointSystem.Update(0.016f);
containerSys.Insert(uidA, container);
Assert.Multiple(() =>
{
Assert.That(container.Contains(uidA));
Assert.That(entManager.HasComponent<JointRelayTargetComponent>(uidC));
Assert.That(entManager.GetComponent<JointComponent>(uidA).Relay, Is.EqualTo(uidC));
containerSys.Remove(uidA, container);
Assert.That(entManager.GetComponent<JointRelayTargetComponent>(uidC).Relayed, Is.Empty);
Assert.That(entManager.GetComponent<JointComponent>(uidA).Relay, Is.EqualTo(null));
});
mapSystem.DeleteMap(mapId);
}
/// <summary>
/// Assert that if a joint exists between 2 bodies they can collide or not collide correctly.
/// </summary>
[Test]
public void JointsCollidableTest()
{
var factory = RobustServerSimulation.NewSimulation();
var server = factory.InitializeInstance();
var entManager = server.Resolve<IEntityManager>();
var fixtureSystem = entManager.EntitySysManager.GetEntitySystem<FixtureSystem>();
var jointSystem = entManager.EntitySysManager.GetEntitySystem<JointSystem>();
var broadphaseSystem = entManager.EntitySysManager.GetEntitySystem<SharedBroadphaseSystem>();
var physicsSystem = server.Resolve<IEntitySystemManager>().GetEntitySystem<SharedPhysicsSystem>();
var mapSystem = entManager.System<SharedMapSystem>();
var map = server.CreateMap();
var mapId = map.MapId;
var ent1 = entManager.SpawnEntity(null, new MapCoordinates(Vector2.Zero, mapId));
var ent2 = entManager.SpawnEntity(null, new MapCoordinates(Vector2.Zero, mapId));
var body1 = entManager.AddComponent<PhysicsComponent>(ent1);
var body2 = entManager.AddComponent<PhysicsComponent>(ent2);
var manager1 = entManager.EnsureComponent<FixturesComponent>(ent1);
var manager2 = entManager.EnsureComponent<FixturesComponent>(ent2);
physicsSystem.SetBodyType(ent1, BodyType.Dynamic, manager: manager1, body: body1);
physicsSystem.SetBodyType(ent2, BodyType.Dynamic, manager: manager2, body: body2);
fixtureSystem.CreateFixture(ent1, "fix1", new Fixture(new PhysShapeCircle(0.1f), 1, 1, false), manager: manager1, body: body1);
fixtureSystem.CreateFixture(ent2, "fix1", new Fixture(new PhysShapeCircle(0.1f), 1, 1, false), manager: manager2, body: body2);
var joint = jointSystem.CreateDistanceJoint(ent1, ent2);
Assert.That(joint.CollideConnected, Is.EqualTo(true));
// Joints are deferred because I hate them so need to make sure it exists
jointSystem.Update(0.016f);
Assert.That(entManager.HasComponent<JointComponent>(ent1), Is.EqualTo(true));
// We should have a contact in both situations.
broadphaseSystem.FindNewContacts();
Assert.That(body1.Contacts, Has.Count.EqualTo(1));
// Alright now try the other way
jointSystem.RemoveJoint(joint);
joint = jointSystem.CreateDistanceJoint(ent2, ent1);
Assert.That(joint.CollideConnected, Is.EqualTo(true));
jointSystem.Update(0.016f);
Assert.That(entManager.HasComponent<JointComponent>(ent1));
broadphaseSystem.FindNewContacts();
Assert.That(body1.Contacts, Has.Count.EqualTo(1));
mapSystem.DeleteMap(mapId);
}
}

View File

@@ -0,0 +1,111 @@
using System.Numerics;
using NUnit.Framework;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Utility;
namespace Robust.UnitTesting.Shared.Physics
{
[TestFixture]
[TestOf(typeof(IManifoldManager))]
internal sealed class ManifoldManager_Test : OurRobustUnitTest
{
private IManifoldManager _manifoldManager = default!;
private PhysShapeCircle _circleA = default!;
private PhysShapeCircle _circleB = default!;
private PolygonShape _polyA = default!;
private PolygonShape _polyB = default!;
[OneTimeSetUp]
public void Setup()
{
_manifoldManager = new CollisionManager();
_circleA = new PhysShapeCircle(0.5f);
_circleB = new PhysShapeCircle(0.5f);
_polyA = new PolygonShape();
_polyB = new PolygonShape();
_polyA.SetAsBox(0.5f, 0.5f);
_polyB.SetAsBox(0.5f, 0.5f);
}
[Test]
public void TestCircleCollision()
{
var transformA = new Transform(new Vector2(-1, -1), 0f);
var transformB = new Transform(Vector2.One, 0f);
// No overlap
Assert.That(_manifoldManager.TestOverlap(_circleA, 0, _circleB, 0, transformA, transformB), Is.EqualTo(false));
// Overlap directly
transformA = new Transform(transformB.Position, 0f);
Assert.That(_manifoldManager.TestOverlap(_circleA, 0, _circleB, 0, transformA, transformB), Is.EqualTo(true));
// Overlap on edge
transformA.Position = transformB.Position + new Vector2(0.5f, 0.0f);
Assert.That(_manifoldManager.TestOverlap(_circleA, 0, _circleB, 0, transformA, transformB), Is.EqualTo(true));
}
[Test]
public void TestPolyCollisions()
{
var transformA = new Transform(new Vector2(-1, -1), 0f);
var transformB = new Transform(Vector2.One, 0f);
// No overlap
Assert.That(_manifoldManager.TestOverlap(_polyA, 0, _polyB, 0, transformA, transformB), Is.EqualTo(false));
// Overlap directly
transformA = new Transform(transformB.Position, 0f);
Assert.That(_manifoldManager.TestOverlap(_polyA, 0, _polyB, 0, transformA, transformB), Is.EqualTo(true));
// Overlap on edge
transformA.Position = transformB.Position + new Vector2(0.5f, 0.0f);
Assert.That(_manifoldManager.TestOverlap(_polyA, 0, _polyB, 0, transformA, transformB), Is.EqualTo(true));
transformA.Quaternion2D = transformA.Quaternion2D.Set(45f);
Assert.That(_manifoldManager.TestOverlap(_polyA, 0, _polyB, 0, transformA, transformB), Is.EqualTo(true));
}
[Test]
public void TestPolyOnPolyManifolds()
{
var transformB = new Transform(Vector2.One, 0f);
var transformA = new Transform(transformB.Position + new Vector2(0.5f, 0.0f), 0f);
var manifold = new Manifold();
var expectedManifold = new Manifold
{
Type = ManifoldType.FaceA,
LocalNormal = new Vector2(-1, 0),
LocalPoint = new Vector2(-0.5f, 0),
PointCount = 2,
Points = new FixedArray2<ManifoldPoint>(
new ManifoldPoint
{
LocalPoint = new Vector2(0.5f, -0.5f),
Id = new ContactID {Key = 65795}
},
new ManifoldPoint
{
LocalPoint = new Vector2(0.5f, 0.5f),
Id = new ContactID {Key = 66051}
}
)
};
_manifoldManager.CollidePolygons(ref manifold, _polyA, transformA, _polyB, transformB);
for (var i = 0; i < manifold.PointCount; i++)
{
Assert.That(manifold.Points.AsSpan[i], Is.EqualTo(expectedManifold.Points.AsSpan[i]));
}
Assert.That(manifold, Is.EqualTo(expectedManifold));
}
}
}

View File

@@ -0,0 +1,177 @@
using System.Numerics;
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
namespace Robust.UnitTesting.Shared.Physics
{
internal sealed class MapVelocity_Test : RobustIntegrationTest
{
private const string DummyEntity = "Dummy";
private static readonly string Prototypes = $@"
- type: entity
name: {DummyEntity}
id: {DummyEntity}
components:
- type: Physics
bodyType: Dynamic
- type: Fixtures
fixtures:
fix1:
shape:
!type:PhysShapeCircle
radius: 0.35
";
[Test]
public async Task TestMapVelocities()
{
var server = StartServer(new ServerIntegrationOptions {ExtraPrototypes = Prototypes});
await server.WaitIdleAsync();
var entityManager = server.ResolveDependency<IEntityManager>();
var mapManager = server.ResolveDependency<IMapManager>();
var system = entityManager.EntitySysManager;
var physicsSys = system.GetEntitySystem<SharedPhysicsSystem>();
var xformSystem = system.GetEntitySystem<SharedTransformSystem>();
var traversal = entityManager.System<SharedGridTraversalSystem>();
traversal.Enabled = false;
await server.WaitAssertion(() =>
{
entityManager.System<SharedMapSystem>().CreateMap(out var mapId);
var grid = mapManager.CreateGridEntity(mapId);
var grid2 = mapManager.CreateGridEntity(mapId);
var gridUidA = grid.Owner;
Assert.That(entityManager.TryGetComponent<PhysicsComponent>(gridUidA, out var gridPhysics));
physicsSys.SetBodyType(gridUidA, BodyType.Dynamic, body: gridPhysics);
Vector2 offset = new(3, 4);
Vector2 expectedFinalVelocity = new Vector2(-4, 3) * 2 + Vector2.One;
var dummy = entityManager.SpawnEntity(DummyEntity, new EntityCoordinates(grid, offset));
Assert.That(entityManager.TryGetComponent(dummy, out PhysicsComponent? body));
Assert.That(entityManager.TryGetComponent(dummy, out TransformComponent? xform));
xformSystem.SetParent(dummy, xform!, gridUidA);
// Test Linear Velocities
physicsSys.SetLinearVelocity(gridUidA, Vector2.One, body: gridPhysics);
Assert.That(body!.LinearVelocity, Is.Approximately(Vector2.Zero, 1e-6));
Assert.That(body.AngularVelocity, Is.Approximately(0f, 1e-6));
var linearVelocity = physicsSys.GetMapLinearVelocity(dummy, body);
var angularVelocity = physicsSys.GetMapAngularVelocity(dummy, body);
var velocities = physicsSys.GetMapVelocities(dummy, body);
Assert.That(linearVelocity, Is.Approximately(Vector2.One, 1e-6));
Assert.That(angularVelocity, Is.Approximately(0f, 1e-6));
Assert.That(velocities.Item1, Is.Approximately(linearVelocity, 1e-6));
Assert.That(velocities.Item2, Is.Approximately(angularVelocity, 1e-6));
// Add angular velocity
physicsSys.SetAngularVelocity(gridUidA, 2, body: gridPhysics);
Assert.That(body.LinearVelocity, Is.EqualTo(Vector2.Zero));
Assert.That(body.AngularVelocity, Is.EqualTo(0f));
linearVelocity = physicsSys.GetMapLinearVelocity(dummy, body);
angularVelocity = physicsSys.GetMapAngularVelocity(dummy, body);
velocities = physicsSys.GetMapVelocities(dummy, body);
Assert.That(linearVelocity, Is.Approximately(expectedFinalVelocity, 1e-6));
Assert.That(angularVelocity, Is.Approximately(2f, 1e-6));
Assert.That(velocities.Item1, Is.Approximately(linearVelocity, 1e-6));
Assert.That(velocities.Item2, Is.Approximately(angularVelocity, 1e-6));
// Check that velocity does not change when changing parent
xformSystem.SetParent(dummy, xform!, grid2);
linearVelocity = physicsSys.GetMapLinearVelocity(dummy, body);
angularVelocity = physicsSys.GetMapAngularVelocity(dummy, body);
velocities = physicsSys.GetMapVelocities(dummy, body);
Assert.That(linearVelocity, Is.Approximately(expectedFinalVelocity, 1e-6));
Assert.That(angularVelocity, Is.Approximately(2f, 1e-6));
Assert.That(velocities.Item1, Is.Approximately(linearVelocity, 1e-6));
Assert.That(velocities.Item2, Is.Approximately(angularVelocity, 1e-6));
});
traversal.Enabled = true;
}
// Check that if something has more than one parent, the velocities are properly added
[Test]
public async Task TestNestedParentVelocities()
{
var server = StartServer(new ServerIntegrationOptions { ExtraPrototypes = Prototypes });
await server.WaitIdleAsync();
var entityManager = server.ResolveDependency<IEntityManager>();
var mapManager = server.ResolveDependency<IMapManager>();
var system = entityManager.EntitySysManager;
var physicsSys = system.GetEntitySystem<SharedPhysicsSystem>();
var xformSystem = system.GetEntitySystem<SharedTransformSystem>();
var traversal = entityManager.System<SharedGridTraversalSystem>();
traversal.Enabled = false;
await server.WaitAssertion(() =>
{
entityManager.System<SharedMapSystem>().CreateMap(out var mapId);
var grid = mapManager.CreateGridEntity(mapId);
var gridUid = grid.Owner;
Assert.That(entityManager.TryGetComponent<PhysicsComponent>(gridUid, out var gridPhysics));
physicsSys.SetBodyType(gridUid, BodyType.Dynamic, body: gridPhysics);
Vector2 offset1 = new(2, 0);
var dummy1 = entityManager.SpawnEntity(DummyEntity, new EntityCoordinates(gridUid, offset1));
Assert.That(entityManager.TryGetComponent(dummy1, out PhysicsComponent? body1));
Assert.That(entityManager.TryGetComponent(dummy1, out TransformComponent? xform1));
xformSystem.SetParent(dummy1, xform1!, gridUid);
// create another entity attached to the dummy1
Vector2 offset2 = new(-1, 0);
var dummy2 = entityManager.SpawnEntity(DummyEntity, new EntityCoordinates(dummy1, offset2));
Assert.That(entityManager.TryGetComponent(dummy2, out PhysicsComponent? body2));
Assert.That(entityManager.TryGetComponent(dummy2, out TransformComponent? xform2));
xformSystem.SetParent(dummy2, xform2!, dummy1);
Assert.That(xformSystem.GetWorldPosition(xform2!), Is.Approximately(new Vector2(1, 0), 1e-6));
physicsSys.SetLinearVelocity(gridUid, new Vector2(1, 0), body: gridPhysics);
physicsSys.SetAngularVelocity(gridUid, 1, body: gridPhysics);
// check that dummy2 properly gets the velocities from its grand-parent
var linearVelocity = physicsSys.GetMapLinearVelocity(dummy2, body2);
var angularVelocity = physicsSys.GetMapAngularVelocity(dummy2, body2);
var velocities = physicsSys.GetMapVelocities(dummy2, body2);
Assert.That(linearVelocity, Is.Approximately(Vector2.One, 1e-6));
Assert.That(angularVelocity, Is.Approximately(1f, 1e-6));
Assert.That(velocities.Item1, Is.Approximately(linearVelocity, 1e-6));
Assert.That(velocities.Item2, Is.Approximately(angularVelocity, 1e-6));
// check that if we make move in the opposite direction, but spin in the same direction, then dummy2 is
// (for this moment in time) stationary, but still rotating.
physicsSys.SetLinearVelocity(dummy1, -gridPhysics!.LinearVelocity, body: body1);
physicsSys.SetAngularVelocity(dummy1, gridPhysics.AngularVelocity, body: body1);
linearVelocity = physicsSys.GetMapLinearVelocity(dummy2, body2);
angularVelocity = physicsSys.GetMapAngularVelocity(dummy2, body2);
velocities = physicsSys.GetMapVelocities(dummy2, body2);
Assert.That(linearVelocity, Is.Approximately(Vector2.Zero, 1e-6));
Assert.That(angularVelocity, Is.Approximately(2f, 1e-6));
Assert.That(velocities.Item1, Is.Approximately(linearVelocity, 1e-6));
Assert.That(velocities.Item2, Is.Approximately(angularVelocity, 1e-6));
// but not if we update the local position:
xformSystem.SetWorldPosition((dummy2, xform2!), Vector2.Zero);
linearVelocity = physicsSys.GetMapLinearVelocity(dummy2, body2);
angularVelocity = physicsSys.GetMapAngularVelocity(dummy2, body2);
velocities = physicsSys.GetMapVelocities(dummy2, body2);
Assert.That(linearVelocity, Is.Approximately(new Vector2(0, -2), 1e-6));
Assert.That(angularVelocity, Is.Approximately(2f, 1e-6));
Assert.That(velocities.Item1, Is.Approximately(linearVelocity, 1e-6));
Assert.That(velocities.Item2, Is.Approximately(angularVelocity, 1e-6));
});
traversal.Enabled = true;
}
}
}

View File

@@ -0,0 +1,60 @@
using System.Collections.Generic;
using System.Numerics;
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Systems;
namespace Robust.UnitTesting.Shared.Physics
{
[TestFixture]
[TestOf(typeof(PhysicsComponent))]
internal sealed class PhysicsComponent_Test : RobustIntegrationTest
{
[Test]
public async Task TestPointLinearImpulse()
{
var server = StartServer();
await server.WaitIdleAsync();
var entManager = server.ResolveDependency<IEntityManager>();
var fixtureSystem = server.ResolveDependency<IEntitySystemManager>()
.GetEntitySystem<FixtureSystem>();
var physicsSystem = server.ResolveDependency<IEntitySystemManager>()
.GetEntitySystem<SharedPhysicsSystem>();
await server.WaitAssertion(() =>
{
entManager.System<SharedMapSystem>().CreateMap(out var mapId);
var boxEnt = entManager.SpawnEntity(null, new MapCoordinates(Vector2.Zero, mapId));
var box = entManager.AddComponent<PhysicsComponent>(boxEnt);
var poly = new PolygonShape();
poly.SetAsBox(0.5f, 0.5f);
fixtureSystem.CreateFixture(boxEnt, "fix1", new Fixture(poly, 0, 0, false), body: box);
physicsSystem.SetFixedRotation(boxEnt, false, body: box);
physicsSystem.SetBodyType(boxEnt, BodyType.Dynamic, body: box);
Assert.That(box.InvI, Is.GreaterThan(0f));
// Check regular impulse works
physicsSystem.ApplyLinearImpulse(boxEnt, new Vector2(0f, 1f), body: box);
Assert.That(box.LinearVelocity.Length, Is.GreaterThan(0f));
// Reset the box
physicsSystem.SetLinearVelocity(boxEnt, Vector2.Zero, body: box);
Assert.That(box.LinearVelocity.Length, Is.EqualTo(0f));
Assert.That(box.AngularVelocity, Is.EqualTo(0f));
// Check the angular impulse is applied from the point
physicsSystem.ApplyLinearImpulse(boxEnt, new Vector2(0f, 1f), new Vector2(0.5f, 0f), body: box);
Assert.That(box.LinearVelocity.Length, Is.GreaterThan(0f));
Assert.That(box.AngularVelocity, Is.Not.EqualTo(0f));
});
}
}
}

View File

@@ -0,0 +1,95 @@
using System;
using System.Numerics;
using NUnit.Framework;
using Robust.Shared.Physics;
namespace Robust.UnitTesting.Shared.Physics;
internal sealed class PhysicsHull_Test
{
private static readonly TestCaseData[] CollinearHulls = new TestCaseData[]
{
new TestCaseData(new Vector2[]
{
Vector2.Zero,
Vector2.One,
Vector2.UnitY,
}, 3),
// Same points
new TestCaseData(new Vector2[]
{
Vector2.Zero,
Vector2.One,
Vector2.One,
Vector2.UnitY,
}, 3),
new TestCaseData(new Vector2[]
{
Vector2.Zero,
Vector2.UnitX / 2f,
Vector2.UnitX,
Vector2.UnitY,
}, 3),
};
[Test, TestCaseSource(nameof(CollinearHulls))]
public void CollinearTest(Vector2[] vertices, int count)
{
var hull = InternalPhysicsHull.ComputeHull(vertices.AsSpan(), vertices.Length);
Assert.That(hull.Count, Is.EqualTo(count));
}
private static readonly TestCaseData[] ValidateHulls = new TestCaseData[]
{
new TestCaseData(Array.Empty<Vector2>(), false),
new TestCaseData(new Vector2[]
{
Vector2.Zero,
Vector2.One,
Vector2.UnitY,
}, true),
new TestCaseData(new Vector2[]
{
Vector2.Zero,
Vector2.UnitX,
Vector2.One,
Vector2.UnitY,
}, true),
// Same point
new TestCaseData(new Vector2[]
{
Vector2.Zero,
Vector2.One,
Vector2.One,
Vector2.UnitY,
}, false),
// Collinear point
new TestCaseData(new Vector2[]
{
Vector2.Zero,
Vector2.One / 2f,
Vector2.One,
}, false),
// Too many verts
new TestCaseData(new Vector2[]
{
Vector2.Zero,
Vector2.UnitX,
Vector2.One * 1f,
Vector2.One * 2f,
Vector2.One * 3f,
Vector2.One * 4f,
Vector2.One * 5f,
Vector2.One * 6f,
Vector2.One * 7f,
Vector2.One * 8f,
}, false),
};
[Test, TestCaseSource(nameof(ValidateHulls))]
public void ValidationTest(Vector2[] vertices, bool result)
{
var hull = new InternalPhysicsHull(vertices.AsSpan(), vertices.Length);
Assert.That(InternalPhysicsHull.ValidateHull(hull), Is.EqualTo(result));
}
}

View File

@@ -0,0 +1,145 @@
using System.Linq;
using System.Numerics;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Shapes;
using Robust.Shared.Physics.Systems;
using Robust.UnitTesting.Server;
namespace Robust.UnitTesting.Shared.Physics;
[TestFixture]
internal sealed class RayCast_Test
{
private static TestCaseData[] _rayCases =
{
// Ray goes through
new(new Vector2(0f, 0.5f), Vector2.UnitY * 2f, new Vector2(0f, 1f - PhysicsConstants.PolygonRadius)),
// Ray stops inside
new(new Vector2(0f, 0.5f), Vector2.UnitY, new Vector2(0f, 1f - PhysicsConstants.PolygonRadius)),
// Ray starts inside
new(new Vector2(0f, 1.5f), Vector2.UnitY, null),
// No hit
new(new Vector2(0f, 0.5f), -Vector2.UnitY, null),
};
private static TestCaseData[] _shapeCases =
{
// Circle
// - Initial overlap, no shapecast
new(new PhysShapeCircle(0.5f, Vector2.Zero), new Transform(Vector2.UnitY / 2f, Angle.Zero), Vector2.UnitY, null),
// - Cast
new(new PhysShapeCircle(0.5f, Vector2.Zero), new Transform(Vector2.Zero, Angle.Zero), Vector2.UnitY, new Vector2(0f, 1f - PhysicsConstants.PolygonRadius)),
// - Miss
new(new PhysShapeCircle(0.5f, Vector2.Zero), new Transform(Vector2.Zero, Angle.Zero), -Vector2.UnitY, null),
// Polygon
// - Initial overlap, no shapecast
new(new SlimPolygon(Box2.UnitCentered), new Transform(Vector2.UnitY / 2f, Angle.Zero), Vector2.UnitY, null),
// - Cast
new(new SlimPolygon(Box2.UnitCentered), new Transform(Vector2.Zero, Angle.Zero), Vector2.UnitY, new Vector2(0.5f, 1f - PhysicsConstants.PolygonRadius)),
// - Miss
new(new SlimPolygon(Box2.UnitCentered), new Transform(Vector2.Zero, Angle.Zero), -Vector2.UnitY, null),
};
[Test, TestCaseSource(nameof(_rayCases))]
public void RayCast(Vector2 origin, Vector2 direction, Vector2? point)
{
var sim = RobustServerSimulation.NewSimulation().RegisterEntitySystems(f =>
{
f.LoadExtraSystemType<RayCastSystem>();
}).InitializeInstance();
Setup(sim, out var mapId);
var raycast = sim.System<RayCastSystem>();
var hits = raycast.CastRayClosest(mapId,
origin,
direction,
new QueryFilter()
{
LayerBits = 1,
});
if (point == null)
{
Assert.That(!hits.Hit);
}
else
{
Assert.That(hits.Results.First().Point, Is.EqualTo(point.Value));
}
}
[Test, TestCaseSource(nameof(_shapeCases))]
public void ShapeCast(IPhysShape shape, Transform origin, Vector2 direction, Vector2? point)
{
var sim = RobustServerSimulation.NewSimulation().RegisterEntitySystems(f =>
{
f.LoadExtraSystemType<RayCastSystem>();
}).InitializeInstance();
Setup(sim, out var mapId);
var raycast = sim.System<RayCastSystem>();
var hits = raycast.CastShape(mapId,
shape,
origin,
direction,
new QueryFilter()
{
LayerBits = 1,
},
RayCastSystem.RayCastAllCallback);
if (point == null)
{
Assert.That(!hits.Hit);
}
else
{
Assert.That(hits.Results.First().Point, Is.EqualTo(point.Value));
}
}
private void Setup(ISimulation sim, out MapId mapId)
{
var entManager = sim.Resolve<IEntityManager>();
var mapSystem = entManager.System<SharedMapSystem>();
sim.System<SharedMapSystem>().CreateMap(out mapId);
var grid = sim.Resolve<IMapManager>().CreateGridEntity(mapId);
for (var i = 0; i < 3; i++)
{
mapSystem.SetTile(grid, new Vector2i(i, 0), new Tile(1));
}
// Spawn a wall in the middle tile.
var wall = entManager.SpawnEntity(null, new EntityCoordinates(grid.Owner, new Vector2(1.5f, 0.5f)));
var physics = entManager.AddComponent<PhysicsComponent>(wall);
var poly = new PolygonShape();
poly.SetAsBox(Box2.UnitCentered);
entManager.System<FixtureSystem>().CreateFixture(wall, "fix1", new Fixture(poly, 1, 1, true));
entManager.System<SharedPhysicsSystem>().SetCanCollide(wall, true, body: physics);
Assert.That(physics.CanCollide);
// Rotate it to be vertical
entManager.System<SharedTransformSystem>().SetLocalRotation(grid.Owner, Angle.FromDegrees(90));
entManager.System<SharedTransformSystem>().SetLocalPosition(grid.Owner, Vector2.UnitX / 2f);
}
}

View File

@@ -0,0 +1,272 @@
using System.Numerics;
using NUnit.Framework;
using Robust.Server.Containers;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.UnitTesting.Server;
namespace Robust.UnitTesting.Shared.Physics;
[TestFixture]
internal sealed class RecursiveUpdateTest
{
/// <summary>
/// Check that the broadphase updates if a an entity is a child of an entity that is in a container.
/// </summary>
[Test]
public void ContainerRecursiveUpdateTest()
{
var sim = RobustServerSimulation.NewSimulation().InitializeInstance();
var entManager = sim.Resolve<IEntityManager>();
var mapManager = sim.Resolve<IMapManager>();
var xforms = entManager.System<SharedTransformSystem>();
var mapSystem = entManager.System<SharedMapSystem>();
var containers = entManager.System<ContainerSystem>();
var mapId = sim.CreateMap().MapId;
var grid = mapManager.CreateGridEntity(mapId);
var guid = grid.Owner;
mapSystem.SetTile(grid, Vector2i.Zero, new Tile(1));
Assert.That(entManager.HasComponent<BroadphaseComponent>(guid));
var broadphase = entManager.GetComponent<BroadphaseComponent>(guid);
var coords = new EntityCoordinates(guid, new Vector2(0.5f, 0.5f));
var broadData = new BroadphaseData(guid, false, false);
var container = entManager.SpawnEntity(null, coords);
var containerXform = entManager.GetComponent<TransformComponent>(container);
Assert.That(broadphase.SundriesTree, Does.Contain(container));
Assert.That(containerXform.Broadphase, Is.EqualTo(broadData));
var contained = entManager.SpawnEntity(null, coords);
var childA = entManager.SpawnEntity(null, MapCoordinates.Nullspace);
var childB = entManager.SpawnEntity(null, MapCoordinates.Nullspace);
var containedXform = entManager.GetComponent<TransformComponent>(contained);
var childAXform = entManager.GetComponent<TransformComponent>(childA);
var childBXform = entManager.GetComponent<TransformComponent>(childB);
Assert.That(broadphase.SundriesTree, Does.Contain(contained));
Assert.That(containedXform.Broadphase, Is.EqualTo(broadData));
// Attach child A before inserting.
xforms.SetCoordinates(childA, childAXform, new EntityCoordinates(contained, Vector2.Zero));
Assert.That(broadphase.SundriesTree, Does.Contain(childA));
Assert.That(childAXform.Broadphase, Is.EqualTo(broadData));
// Insert into container.
var slot = containers.EnsureContainer<ContainerSlot>(container, "test");
containers.Insert(contained, slot);
// Attach child B after having inserted.
xforms.SetCoordinates(childB, childBXform, new EntityCoordinates(contained, Vector2.Zero));
Assert.That(broadphase.SundriesTree, Does.Contain(container));
Assert.That(broadphase.SundriesTree, Does.Not.Contain(contained));
Assert.That(broadphase.SundriesTree, Does.Not.Contain(childA));
Assert.That(broadphase.SundriesTree, Does.Not.Contain(childB));
Assert.That(containerXform.Broadphase, Is.EqualTo(broadData));
Assert.That(containedXform.Broadphase, Is.EqualTo(null));
Assert.That(childAXform.Broadphase, Is.EqualTo(null));
Assert.That(childBXform.Broadphase, Is.EqualTo(null));
Assert.That(containerXform.ParentUid, Is.EqualTo(guid));
Assert.That(containedXform.ParentUid, Is.EqualTo(container));
Assert.That(childAXform.ParentUid, Is.EqualTo(contained));
Assert.That(childBXform.ParentUid, Is.EqualTo(contained));
// Check that moving the container does not re-add the contained entities to the broadphase.
var newCoords = new EntityCoordinates(guid, new Vector2(0.25f, 0.25f));
xforms.SetCoordinates(container, newCoords);
Assert.That(broadphase.SundriesTree, Does.Contain(container));
Assert.That(broadphase.SundriesTree, Does.Not.Contain(contained));
Assert.That(broadphase.SundriesTree, Does.Not.Contain(childA));
Assert.That(broadphase.SundriesTree, Does.Not.Contain(childB));
Assert.That(containerXform.Broadphase, Is.EqualTo(broadData));
Assert.That(containedXform.Broadphase, Is.EqualTo(null));
Assert.That(childAXform.Broadphase, Is.EqualTo(null));
Assert.That(childBXform.Broadphase, Is.EqualTo(null));
Assert.That(containerXform.ParentUid, Is.EqualTo(guid));
Assert.That(containedXform.ParentUid, Is.EqualTo(container));
Assert.That(childAXform.ParentUid, Is.EqualTo(contained));
Assert.That(childBXform.ParentUid, Is.EqualTo(contained));
// Remove from container.
containers.Remove(contained, slot);
Assert.That(broadphase.SundriesTree, Does.Contain(container));
Assert.That(broadphase.SundriesTree, Does.Contain(contained));
Assert.That(broadphase.SundriesTree, Does.Contain(childA));
Assert.That(broadphase.SundriesTree, Does.Contain(childB));
Assert.That(containerXform.Broadphase, Is.EqualTo(broadData));
Assert.That(containedXform.Broadphase, Is.EqualTo(broadData));
Assert.That(childAXform.Broadphase, Is.EqualTo(broadData));
Assert.That(childBXform.Broadphase, Is.EqualTo(broadData));
Assert.That(containerXform.ParentUid, Is.EqualTo(guid));
Assert.That(containedXform.ParentUid, Is.EqualTo(guid));
Assert.That(childAXform.ParentUid, Is.EqualTo(contained));
Assert.That(childBXform.ParentUid, Is.EqualTo(contained));
// Insert back into container.
containers.Insert(contained, slot);
Assert.That(broadphase.SundriesTree, Does.Contain(container));
Assert.That(broadphase.SundriesTree, Does.Not.Contain(contained));
Assert.That(broadphase.SundriesTree, Does.Not.Contain(childA));
Assert.That(broadphase.SundriesTree, Does.Not.Contain(childB));
Assert.That(containerXform.Broadphase, Is.EqualTo(broadData));
Assert.That(containedXform.Broadphase, Is.EqualTo(null));
Assert.That(childAXform.Broadphase, Is.EqualTo(null));
Assert.That(childBXform.Broadphase, Is.EqualTo(null));
Assert.That(containerXform.ParentUid, Is.EqualTo(guid));
Assert.That(containedXform.ParentUid, Is.EqualTo(container));
Assert.That(childAXform.ParentUid, Is.EqualTo(contained));
Assert.That(childBXform.ParentUid, Is.EqualTo(contained));
// re-remove from container, but this time WITHOUT changing parent.
containers.Remove(contained, slot, reparent: false);
Assert.That(broadphase.SundriesTree, Does.Contain(container));
Assert.That(broadphase.SundriesTree, Does.Contain(contained));
Assert.That(broadphase.SundriesTree, Does.Contain(childA));
Assert.That(broadphase.SundriesTree, Does.Contain(childB));
Assert.That(containerXform.Broadphase, Is.EqualTo(broadData));
Assert.That(containedXform.Broadphase, Is.EqualTo(broadData));
Assert.That(childAXform.Broadphase, Is.EqualTo(broadData));
Assert.That(childBXform.Broadphase, Is.EqualTo(broadData));
Assert.That(containerXform.ParentUid, Is.EqualTo(guid));
Assert.That(containedXform.ParentUid, Is.EqualTo(container));
Assert.That(childAXform.ParentUid, Is.EqualTo(contained));
Assert.That(childBXform.ParentUid, Is.EqualTo(contained));
}
/// <summary>
/// Check that the broadphase positions update when moving an entity's parent.
/// </summary>
[Test]
public void RecursiveMoveTest()
{
var sim = RobustServerSimulation.NewSimulation().InitializeInstance();
var entManager = sim.Resolve<IEntityManager>();
var mapManager = sim.Resolve<IMapManager>();
var mapSystem = entManager.EntitySysManager.GetEntitySystem<SharedMapSystem>();
var transforms = entManager.EntitySysManager.GetEntitySystem<SharedTransformSystem>();
var lookup = entManager.EntitySysManager.GetEntitySystem<EntityLookupSystem>();
var mapId = sim.CreateMap().MapId;
var map = mapSystem.GetMapOrInvalid(mapId);
var mapBroadphase = entManager.GetComponent<BroadphaseComponent>(map);
var coords = new EntityCoordinates(map, new Vector2(0.5f, 0.5f));
var mapBroadData = new BroadphaseData(map, false, false);
// Set up parent & child
var parent = entManager.SpawnEntity(null, coords);
var child = entManager.SpawnEntity(null, new EntityCoordinates(parent, Vector2.Zero));
var parentXform = entManager.GetComponent<TransformComponent>(parent);
var childXform = entManager.GetComponent<TransformComponent>(child);
// Check correct broadphase
Assert.That(parentXform.ParentUid, Is.EqualTo(map));
Assert.That(childXform.ParentUid, Is.EqualTo(parent));
Assert.That(mapBroadphase.SundriesTree, Does.Contain(parent));
Assert.That(mapBroadphase.SundriesTree, Does.Contain(child));
Assert.That(parentXform.Broadphase, Is.EqualTo(mapBroadData));
Assert.That(childXform.Broadphase, Is.EqualTo(mapBroadData));
// Check that the entities can be found via a lookup.
var box = Box2.CenteredAround(coords.Position, Vector2.One);
var ents = lookup.GetEntitiesIntersecting(mapId, box);
Assert.That(ents, Does.Contain(parent));
Assert.That(ents, Does.Contain(child));
// Move the parent far away.
var farCoords = new EntityCoordinates(map, new Vector2(100f, 100f));
transforms.SetCoordinates(parent, farCoords);
// broadphases have not changed
Assert.That(parentXform.ParentUid, Is.EqualTo(map));
Assert.That(childXform.ParentUid, Is.EqualTo(parent));
Assert.That(mapBroadphase.SundriesTree, Does.Contain(parent));
Assert.That(mapBroadphase.SundriesTree, Does.Contain(child));
Assert.That(parentXform.Broadphase, Is.EqualTo(mapBroadData));
Assert.That(childXform.Broadphase, Is.EqualTo(mapBroadData));
// old lookup area no longer finds anything
ents = lookup.GetEntitiesIntersecting(mapId, box);
Assert.That(ents.Count, Is.EqualTo(0));
// updated lookup area still finds entities
var farBox = Box2.CenteredAround(farCoords.Position, Vector2.One);
ents = lookup.GetEntitiesIntersecting(mapId, farBox);
Assert.That(ents, Does.Contain(parent));
Assert.That(ents, Does.Contain(child));
// Try again, but this time with a parent change.
var grid = mapManager.CreateGridEntity(mapId);
var guid = grid.Owner;
mapSystem.SetTile(grid, Vector2i.Zero, new Tile(1));
var gridBroadphase = entManager.GetComponent<BroadphaseComponent>(guid);
var gridBroadData = new BroadphaseData(guid, false, false);
var gridCoords = new EntityCoordinates(map, new Vector2(-100f, -100f));
transforms.SetCoordinates(guid, gridCoords);
// Move parent to the grid
var gridLocal = new EntityCoordinates(guid, new Vector2(0.5f, 0.5f));
transforms.SetCoordinates(parent, gridLocal);
// broadphases have now changed to be on the grids broadphase
Assert.That(parentXform.ParentUid, Is.EqualTo(guid));
Assert.That(childXform.ParentUid, Is.EqualTo(parent));
Assert.That(gridBroadphase.SundriesTree, Does.Contain(parent));
Assert.That(gridBroadphase.SundriesTree, Does.Contain(child));
Assert.That(mapBroadphase.SundriesTree, Does.Not.Contain(parent));
Assert.That(mapBroadphase.SundriesTree, Does.Not.Contain(child));
Assert.That(parentXform.Broadphase, Is.EqualTo(gridBroadData));
Assert.That(childXform.Broadphase, Is.EqualTo(gridBroadData));
// old lookup areas no longer finds anything
ents = lookup.GetEntitiesIntersecting(mapId, box);
Assert.That(ents.Count, Is.EqualTo(0));
ents = lookup.GetEntitiesIntersecting(mapId, farBox);
Assert.That(ents.Count, Is.EqualTo(0));
// grid lookup works
var gridBox = Box2.CenteredAround(gridCoords.Position, Vector2.One);
ents = lookup.GetEntitiesIntersecting(mapId, gridBox);
Assert.That(ents, Does.Contain(parent));
Assert.That(ents, Does.Contain(child));
// Move grid far away
var newGridCoords = new EntityCoordinates(map, new Vector2(-100f, 100f));
transforms.SetCoordinates(guid, newGridCoords);
// Check lookups again
ents = lookup.GetEntitiesIntersecting(mapId, box);
Assert.That(ents.Count, Is.EqualTo(0));
ents = lookup.GetEntitiesIntersecting(mapId, farBox);
Assert.That(ents.Count, Is.EqualTo(0));
ents = lookup.GetEntitiesIntersecting(mapId, gridBox);
Assert.That(ents.Count, Is.EqualTo(0));
// grid lookup works
var newGridBox = Box2.CenteredAround(newGridCoords.Position, Vector2.One);
ents = lookup.GetEntitiesIntersecting(mapId, newGridBox);
Assert.That(ents, Does.Contain(parent));
Assert.That(ents, Does.Contain(child));
}
}

View File

@@ -0,0 +1,84 @@
using System;
using System.Numerics;
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision.Shapes;
namespace Robust.UnitTesting.Shared.Physics
{
[TestFixture]
internal sealed class ShapeAABB_Test : OurRobustUnitTest
{
private Transform _transform;
private Transform _rotatedTransform;
[OneTimeSetUp]
public void Setup()
{
_transform = new Transform(Vector2.One, 0f);
// We'll use 45 degrees as it's easier to spot bugs
_rotatedTransform = new Transform(Vector2.One, MathF.PI / 4f);
}
[Test]
public void TestCircleAABB()
{
var circle = new PhysShapeCircle(0.5f);
var aabb = circle.ComputeAABB(_transform, 0);
Assert.That(aabb.Width, Is.EqualTo(1f));
Assert.That(aabb, Is.EqualTo(new Box2(0.5f, 0.5f, 1.5f, 1.5f)));
}
[Test]
public void TestRotatedCircleAABB()
{
var circle = new PhysShapeCircle(0.5f);
var aabb = circle.ComputeAABB(_rotatedTransform, 0);
Assert.That(aabb.Width, Is.EqualTo(1f));
Assert.That(aabb, Is.EqualTo(new Box2(0.5f, 0.5f, 1.5f, 1.5f)));
}
[Test]
public void TestEdgeAABB()
{
var edge = new EdgeShape(Vector2.Zero, Vector2.One);
var aabb = edge.ComputeAABB(_transform, 0);
Assert.That(aabb.Width, Is.EqualTo(1.02f));
Assert.That(aabb, Is.EqualTo(new Box2(0.99f, 0.99f, 2.01f, 2.01f)));
}
[Test]
public void TestRotatedEdgeAABB()
{
var edge = new EdgeShape(Vector2.Zero, Vector2.One);
var aabb = edge.ComputeAABB(_rotatedTransform, 0);
Assert.That(MathHelper.CloseToPercent(aabb.Width, 0.02f));
Assert.That(aabb.EqualsApprox(new Box2(0.99f, 0.99f, 1.01f, 2.42f), 0.01f));
}
[Test]
public void TestPolyAABB()
{
var polygon = new PolygonShape();
// Radius is added to the AABB hence we'll just deduct it here for simplicity
polygon.SetAsBox(0.49f, 0.49f);
var aabb = polygon.ComputeAABB(_transform, 0);
Assert.That(aabb.Width, Is.EqualTo(1f));
Assert.That(aabb, Is.EqualTo(new Box2(0.5f, 0.5f, 1.5f, 1.5f)));
}
[Test]
public void TestRotatedPolyAABB()
{
var polygon = new PolygonShape();
// Radius is added to the AABB hence we'll just deduct it here for simplicity
polygon.SetAsBox(0.49f, 0.49f);
var aabb = polygon.ComputeAABB(_rotatedTransform, 0);
// I already had a rough idea of what the AABB should be, I just put these in so the test passes.
Assert.That(aabb.Width, Is.EqualTo(1.40592933f));
Assert.That(aabb, Is.EqualTo(new Box2(0.29703534f, 0.29703534f, 1.7029647f, 1.7029647f)));
}
}
}

View File

@@ -0,0 +1,33 @@
using System;
using System.Numerics;
using NUnit.Framework;
using Robust.Shared.Physics.Collision.Shapes;
namespace Robust.UnitTesting.Shared.Physics
{
[TestFixture]
[TestOf(typeof(IPhysShape))]
internal sealed class Shape_Test : OurRobustUnitTest
{
[Test]
public void TestPolyNormals()
{
var poly = new PolygonShape();
Span<Vector2> verts = stackalloc Vector2[4];
verts[0] = new Vector2(1f, -1f);
verts[1] = new Vector2(1f, 1f);
verts[2] = new Vector2(-1f, 1f);
verts[3] = new Vector2(-1f, -1f);
poly.Set(verts, 4);
Assert.That(poly.VertexCount == 4);
Assert.That(poly.Normals[0], Is.EqualTo(new Vector2(1, 0)), $"Vert is {poly.Vertices[0]}");
Assert.That(poly.Normals[1], Is.EqualTo(new Vector2(0, 1)), $"Vert is {poly.Vertices[1]}");
Assert.That(poly.Normals[2], Is.EqualTo(new Vector2(-1, 0)), $"Vert is {poly.Vertices[2]}");
Assert.That(poly.Normals[3], Is.EqualTo(new Vector2(0, -1)), $"Vert is {poly.Vertices[3]}");
}
}
}

View File

@@ -0,0 +1,252 @@
/*
MIT License
Copyright (c) 2019 Erin Catto
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
These tests are derived from box2d's testbed tests but done in a way as to be automated and useful for CI.
*/
using System.Collections.Generic;
using System.Numerics;
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Controllers;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Systems;
namespace Robust.UnitTesting.Shared.Physics;
[TestFixture]
internal sealed class PhysicsTestBedTest : RobustIntegrationTest
{
[Test]
public async Task TestBoxStack()
{
var server = StartServer();
await server.WaitIdleAsync();
var entityManager = server.ResolveDependency<IEntityManager>();
var entitySystemManager = server.ResolveDependency<IEntitySystemManager>();
var fixtureSystem = entitySystemManager.GetEntitySystem<FixtureSystem>();
var physSystem = entitySystemManager.GetEntitySystem<SharedPhysicsSystem>();
var transformSystem = entitySystemManager.GetEntitySystem<SharedTransformSystem>();
const int columnCount = 1;
const int rowCount = 15;
Entity<PhysicsComponent>[] bodies = new Entity<PhysicsComponent>[columnCount * rowCount];
Vector2 firstPos = Vector2.Zero;
await server.WaitPost(() =>
{
var mapUid = entityManager.System<SharedMapSystem>().CreateMap(out var mapId);
physSystem.SetGravity(new Vector2(0f, -9.8f));
var groundUid = entityManager.SpawnEntity(null, new MapCoordinates(0, 0, mapId));
var ground = entityManager.AddComponent<PhysicsComponent>(groundUid);
var groundManager = entityManager.EnsureComponent<FixturesComponent>(groundUid);
var horizontal = new EdgeShape(new Vector2(-40, 0), new Vector2(40, 0));
fixtureSystem.CreateFixture(groundUid, "fix1", new Fixture(horizontal, 1, 1, true), manager: groundManager, body: ground);
var vertical = new EdgeShape(new Vector2(10, 0), new Vector2(10, 10));
fixtureSystem.CreateFixture(groundUid, "fix2", new Fixture(vertical, 1, 1, true), manager: groundManager, body: ground);
physSystem.WakeBody(groundUid, manager: groundManager, body: ground);
var xs = new[]
{
0.0f, -10.0f, -5.0f, 5.0f, 10.0f
};
for (var j = 0; j < columnCount; j++)
{
for (var i = 0; i < rowCount; i++)
{
var x = 0.0f;
var boxUid = entityManager.SpawnEntity(null,
new MapCoordinates(new Vector2(xs[j] + x, 0.55f + 2.1f * i), mapId));
var box = entityManager.AddComponent<PhysicsComponent>(boxUid);
var manager = entityManager.EnsureComponent<FixturesComponent>(boxUid);
physSystem.SetBodyType(boxUid, BodyType.Dynamic, manager: manager, body: box);
var poly = new PolygonShape(0.001f);
poly.Set(new List<Vector2>()
{
new(0.5f, -0.5f),
new(0.5f, 0.5f),
new(-0.5f, 0.5f),
new(-0.5f, -0.5f),
});
fixtureSystem.CreateFixture(boxUid, "fix1", new Fixture(poly, 1, 1, true), manager: manager, body: box);
physSystem.WakeBody(boxUid, manager: manager, body: box);
bodies[j * rowCount + i] = (boxUid, box);
}
}
var bodyOne = bodies[0].Owner;
firstPos = transformSystem.GetWorldPosition(bodyOne);
});
await server.WaitRunTicks(1);
// Check that gravity workin
await server.WaitAssertion(() =>
{
var tempQualifier = bodies[0].Owner;
Assert.That(firstPos, Is.Not.EqualTo(transformSystem.GetWorldPosition(tempQualifier)));
});
// Assert
await server.WaitRunTicks(200);
// Assert settled, none below 0, etc.
await server.WaitAssertion(() =>
{
for (var j = 0; j < columnCount; j++)
{
for (var i = 0; i < bodies.Length; i++)
{
var body = bodies[j * columnCount + i];
var worldPos = transformSystem.GetWorldPosition(body);
// TODO: Multi-column support but I cbf right now
// Can't be more exact as some level of sinking is allowed.
Assert.That(worldPos.EqualsApprox(new Vector2(0.0f, i + 0.5f), 0.2f), $"Expected y-value of {i + 0.5f} but found {worldPos.Y}");
Assert.That(!body.Comp.Awake, $"Body {i} wasn't asleep");
}
}
});
}
[Test]
public async Task TestCircleStack()
{
var server = StartServer();
await server.WaitIdleAsync();
var entityManager = server.ResolveDependency<IEntityManager>();
var mapManager = server.ResolveDependency<IMapManager>();
var entitySystemManager = server.ResolveDependency<IEntitySystemManager>();
var fixtureSystem = entitySystemManager.GetEntitySystem<FixtureSystem>();
var physSystem = entitySystemManager.GetEntitySystem<SharedPhysicsSystem>();
var transformSystem = entitySystemManager.GetEntitySystem<SharedTransformSystem>();
MapId mapId;
var columnCount = 1;
var rowCount = 15;
Entity<PhysicsComponent>[] bodies = new Entity<PhysicsComponent>[columnCount * rowCount];
Vector2 firstPos = Vector2.Zero;
await server.WaitPost(() =>
{
var mapUid = entityManager.System<SharedMapSystem>().CreateMap(out mapId);
physSystem.SetGravity(new Vector2(0f, -9.8f));
var groundUid = entityManager.SpawnEntity(null, new MapCoordinates(0, 0, mapId));
var ground = entityManager.AddComponent<PhysicsComponent>(groundUid);
var groundManager = entityManager.EnsureComponent<FixturesComponent>(groundUid);
var horizontal = new EdgeShape(new Vector2(-40, 0), new Vector2(40, 0));
fixtureSystem.CreateFixture(groundUid, "fix1", new Fixture(horizontal, 1, 1, true), manager: groundManager, body: ground);
var vertical = new EdgeShape(new Vector2(10, 0), new Vector2(10, 10));
fixtureSystem.CreateFixture(groundUid, "fix2", new Fixture(vertical, 1, 1, true), manager: groundManager, body: ground);
physSystem.WakeBody(groundUid, manager: groundManager, body: ground);
var xs = new[]
{
0.0f, -10.0f, -5.0f, 5.0f, 10.0f
};
PhysShapeCircle shape;
for (var j = 0; j < columnCount; j++)
{
for (var i = 0; i < rowCount; i++)
{
var x = 0.0f;
var circleUid = entityManager.SpawnEntity(null,
new MapCoordinates(new Vector2(xs[j] + x, 0.55f + 1.1f * i), mapId));
var circle = entityManager.AddComponent<PhysicsComponent>(circleUid);
var manager = entityManager.EnsureComponent<FixturesComponent>(circleUid);
physSystem.SetLinearDamping(circleUid, circle, 0.05f);
physSystem.SetBodyType(circleUid, BodyType.Dynamic, manager: manager, body: circle);
shape = new PhysShapeCircle(0.5f);
fixtureSystem.CreateFixture(circleUid, "fix1", new Fixture(shape, 1, 1, true), manager: manager, body: circle);
physSystem.WakeBody(circleUid, manager: manager, body: circle);
bodies[j * rowCount + i] = (circleUid, circle);
}
}
EntityUid tempQualifier3 = bodies[0].Owner;
firstPos = transformSystem.GetWorldPosition(tempQualifier3);
});
await server.WaitRunTicks(1);
// Check that gravity workin
await server.WaitAssertion(() =>
{
EntityUid tempQualifier = bodies[0].Owner;
Assert.That(firstPos, Is.Not.EqualTo(transformSystem.GetWorldPosition(tempQualifier)));
});
// Assert
await server.WaitRunTicks(215);
// Assert settled, none below 0, etc.
await server.WaitAssertion(() =>
{
for (var j = 0; j < columnCount; j++)
{
for (var i = 0; i < bodies.Length; i++)
{
var body = bodies[j * columnCount + i];
var worldPos = transformSystem.GetWorldPosition(body);
var expectedY = 0.5f + i;
// TODO: Multi-column support but I cbf right now
// Can't be more exact as some level of sinking is allowed.
Assert.That(worldPos.EqualsApproxPercent(new Vector2(0.0f, expectedY), 0.1f), $"Expected y-value of {expectedY} but found {worldPos.Y}");
Assert.That(!body.Comp.Awake);
}
}
});
}
}

View File

@@ -0,0 +1,40 @@
using System;
using System.Numerics;
using System.Runtime.Intrinsics;
using NUnit.Framework;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
namespace Robust.UnitTesting.Shared.Physics;
[TestFixture]
[TestOf(typeof(Transform))]
internal sealed class TransformTest
{
private static (Vector2 V, Transform T, Vector2 Exp)[] _vecMulData =
[
(Vector2.Zero, new Transform(Vector2.Zero, 0), Exp: Vector2.Zero),
(Vector2.Zero, new Transform(Vector2.One, 0), Exp: Vector2.One),
(Vector2.Zero, new Transform(Vector2.One, Angle.FromDegrees(45)), Vector2.One),
(Vector2.One, new Transform(Vector2.Zero, Angle.FromDegrees(45)), new(0, MathF.Sqrt(2))),
(Vector2.One, new Transform(Vector2.One, Angle.FromDegrees(45)), new(1, 1+MathF.Sqrt(2))),
(new Vector2(2f, 1f), new Transform(Vector2.One, Angle.FromDegrees(45)), new(1.707107f, 3.12132f)),
];
[Test]
public void TestVectorMul([ValueSource(nameof(_vecMulData))] (Vector2 V, Transform T, Vector2 Exp) dat)
{
var result = Transform.Mul(dat.T, dat.V);
Assert.That(result, Is.Approximately(dat.Exp, 0.001f));
}
[Test]
public void TestVectorMulSimd([ValueSource(nameof(_vecMulData))] (Vector2 V, Transform T, Vector2 Exp) dat)
{
var x = Vector128.Create(dat.V.X);
var y = Vector128.Create(dat.V.Y);
Transform.MulSimd(dat.T, x, y, out var xOut, out var yOut);
Assert.That(xOut, Is.Approximately(Vector128.Create(dat.Exp[0]), 0.0001f));
Assert.That(yOut, Is.Approximately(Vector128.Create(dat.Exp[1]), 0.0001f));
}
}

View File

@@ -0,0 +1,122 @@
using System.Collections.Generic;
using System.Numerics;
using NUnit.Framework;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
namespace Robust.UnitTesting.Shared.Physics
{
[TestFixture, Parallelizable]
[TestOf(typeof(IVerticesSimplifier))]
internal sealed class VerticesSimplifier_Test : OurRobustUnitTest
{
/*
* Collinear tests
*/
[Test]
public void TestCollinearLine()
{
var simplifier = new CollinearSimplifier();
var line = new List<Vector2>
{
new(0.0f, 0f),
new(0.5f, 0f),
new(1.0f, 0f),
new(1.5f, 0f),
new(2.0f, 0f),
};
Assert.That(simplifier.Simplify(line, 0.01f).Count, Is.EqualTo(2));
}
[Test]
public void TestCollinearBox()
{
// Box should still simplify to a box.
var simplifier = new CollinearSimplifier();
var line = new List<Vector2>
{
new(0.0f, 0f),
new(0.0f, 1.0f),
new(1.0f, 1.0f),
new(1.0f, 0f),
};
Assert.That(simplifier.Simplify(line, 0.01f).Count, Is.EqualTo(4));
}
[Test]
public void TestCollinearSquiggle()
{
var simplifier = new CollinearSimplifier();
var line = new List<Vector2>
{
new(0.0f, 0f),
new(0.5f, 0.05f),
new(1.0f, 0f),
new(1.5f, -0.05f),
new(2.0f, 0f),
};
Assert.That(simplifier.Simplify(line, 0.1f).Count, Is.EqualTo(2));
}
/*
* Douglas Peucker tests
*/
[Test]
public void TestDouglasPeuckerLine()
{
var simplifier = new RamerDouglasPeuckerSimplifier();
var line = new List<Vector2>
{
new(0.0f, 0f),
new(0.5f, 0f),
new(1.0f, 0f),
new(1.5f, 0f),
new(2.0f, 0f),
};
Assert.That(simplifier.Simplify(line, 0.01f).Count, Is.EqualTo(2));
}
[Test]
public void TestDouglasPeuckerBox()
{
// Box should still simplify to a box.
var simplifier = new RamerDouglasPeuckerSimplifier();
var line = new List<Vector2>
{
new(0.0f, 0f),
new(0.0f, 1.0f),
new(1.0f, 1.0f),
new(1.0f, 0f),
};
Assert.That(simplifier.Simplify(line, 0.01f).Count, Is.EqualTo(4));
}
[Test]
public void TestDouglasPeuckerSquiggle()
{
var simplifier = new RamerDouglasPeuckerSimplifier();
var line = new List<Vector2>
{
new(0.0f, 0f),
new(0.5f, 0.05f),
new(1.0f, 0f),
new(1.5f, -0.05f),
new(2.0f, 0f),
};
Assert.That(simplifier.Simplify(line, 0.1f).Count, Is.EqualTo(2));
}
}
}

View File

@@ -0,0 +1,114 @@
#if TOOLS
using System;
using System.Collections.Generic;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Robust.UnitTesting.Shared.Prototypes
{
[TestFixture]
internal sealed class HotReloadTest : OurRobustUnitTest
{
private const string DummyId = "Dummy";
public const string HotReloadTestComponentOneId = "HotReloadTestOne";
public const string HotReloadTestComponentTwoId = "HotReloadTestTwo";
private static readonly string InitialPrototypes = $@"
- type: entity
id: {DummyId}
components:
- type: {HotReloadTestComponentOneId}
value: 5";
private static readonly string ReloadedPrototypes = $@"
- type: entity
id: {DummyId}
components:
- type: {HotReloadTestComponentOneId}
value: 10
- type: {HotReloadTestComponentTwoId}";
private PrototypeManager _prototypes = default!;
private IMapManager _maps = default!;
private IEntityManager _entities = default!;
protected override Type[]? ExtraComponents => new[] {typeof(HotReloadTestOneComponent), typeof(HotReloadTestTwoComponent)};
[OneTimeSetUp]
public void Setup()
{
IoCManager.Resolve<ISerializationManager>().Initialize();
_prototypes = (PrototypeManager) IoCManager.Resolve<IPrototypeManager>();
_prototypes.RegisterKind(typeof(EntityPrototype), typeof(EntityCategoryPrototype));
_prototypes.LoadString(InitialPrototypes);
_prototypes.ResolveResults();
_maps = IoCManager.Resolve<IMapManager>();
_entities = IoCManager.Resolve<IEntityManager>();
}
[Test]
public void TestHotReload()
{
IoCManager.Resolve<IEntityManager>().System<SharedMapSystem>().CreateMap(out var id);
var entity = _entities.SpawnEntity(DummyId, new MapCoordinates(default, id));
var entityComponent = IoCManager.Resolve<IEntityManager>().GetComponent<HotReloadTestOneComponent>(entity);
Assert.That(entityComponent.Value, Is.EqualTo(5));
Assert.That(IoCManager.Resolve<IEntityManager>().HasComponent<HotReloadTestTwoComponent>(entity), Is.False);
var reloaded = false;
_prototypes.PrototypesReloaded += _ => reloaded = true;
_prototypes.ReloadPrototypes(new Dictionary<Type, HashSet<string>>());
Assert.That(reloaded);
reloaded = false;
Assert.That(entityComponent.Value, Is.EqualTo(5));
Assert.That(IoCManager.Resolve<IEntityManager>().HasComponent<HotReloadTestTwoComponent>(entity), Is.False);
var changedPrototypes = new Dictionary<Type, HashSet<string>>();
_prototypes.LoadString(ReloadedPrototypes, true, changedPrototypes);
_prototypes.ReloadPrototypes(changedPrototypes);
Assert.That(reloaded);
reloaded = false;
// Existing component values are not modified in the current implementation
Assert.That(entityComponent.Value, Is.EqualTo(5));
// New components are added
Assert.That(IoCManager.Resolve<IEntityManager>().HasComponent<HotReloadTestTwoComponent>(entity));
changedPrototypes = new Dictionary<Type, HashSet<string>>();
_prototypes.LoadString(InitialPrototypes, true, changedPrototypes);
_prototypes.ReloadPrototypes(changedPrototypes);
Assert.That(reloaded);
reloaded = false;
// Existing component values are not modified in the current implementation
Assert.That(entityComponent.Value, Is.EqualTo(5));
// Old components are removed
Assert.That(IoCManager.Resolve<IEntityManager>().HasComponent<HotReloadTestTwoComponent>(entity), Is.False);
}
}
internal sealed partial class HotReloadTestOneComponent : Component
{
[DataField("value")]
public int Value { get; private set; }
}
internal sealed partial class HotReloadTestTwoComponent : Component
{
}
}
#endif

View File

@@ -0,0 +1,272 @@
using System;
using System.Linq;
using JetBrains.Annotations;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager;
namespace Robust.UnitTesting.Shared.Prototypes;
[UsedImplicitly]
[TestFixture]
internal sealed class PrototypeManagerCategoriesTest : OurRobustUnitTest
{
private IPrototypeManager _protoMan = default!;
protected override Type[] ExtraComponents => [typeof(AutoCategoryComponent)];//, typeof(AttributeAutoCategoryComponent)];
[OneTimeSetUp]
public void Setup()
{
IoCManager.Resolve<ISerializationManager>().Initialize();
_protoMan = IoCManager.Resolve<IPrototypeManager>();
_protoMan.RegisterKind(typeof(EntityPrototype), typeof(EntityCategoryPrototype));
_protoMan.LoadString(TestPrototypes);
_protoMan.ResolveResults();
}
[Test]
public void TestExplicitCategories()
{
var @default = _protoMan.Index<EntityPrototype>(DefaultEntity);
Assert.That(@default.Categories, Is.Empty);
Assert.That(@default.CategoriesInternal, Is.Null);
Assert.That(@default.HideSpawnMenu, Is.False);
var hide = _protoMan.Index<EntityPrototype>(HideEntity);
Assert.That(hide.Categories.Count, Is.EqualTo(1));
Assert.That(hide.CategoriesInternal?.Count, Is.EqualTo(1));
Assert.That(hide.HideSpawnMenu, Is.True);
}
[Test]
public void TestInheritance()
{
var child = _protoMan.Index<EntityPrototype>(InheritChildEntity);
Assert.That(child.Categories.Count, Is.EqualTo(1));
Assert.That(child.CategoriesInternal, Is.Null);
Assert.That(child.HideSpawnMenu, Is.True);
var noInheritParent = _protoMan.Index<EntityPrototype>(NoInheritParentEntity);
Assert.That(noInheritParent.Categories.Count, Is.EqualTo(1));
Assert.That(noInheritParent.CategoriesInternal?.Count, Is.EqualTo(1));
var noInheritChild = _protoMan.Index<EntityPrototype>(NoInheritChildEntity);
Assert.That(noInheritChild.Categories, Is.Empty);
Assert.That(noInheritChild.CategoriesInternal, Is.Null);
}
[Test]
public void TestAbstractInheritance()
{
Assert.That(_protoMan.HasIndex<EntityPrototype>(AbstractParentEntity), Is.False);
Assert.That(_protoMan.HasIndex<EntityPrototype>(AbstractGrandChildEntity), Is.False);
var concreteChild = _protoMan.Index<EntityPrototype>(ConcreteChildEntity);
Assert.That(concreteChild.Categories.Select(x => x.ID),
Is.EquivalentTo(new []{DefaultCategory}));
Assert.That(concreteChild.CategoriesInternal, Is.Null);
var composition = _protoMan.Index<EntityPrototype>(CompositionAbstractEntity);
Assert.That(composition.Categories.Select(x => x.ID),
Is.EquivalentTo(new []{DefaultCategory, HideCategory, AutoCategory}));
Assert.That(composition.CategoriesInternal, Is.Null);
}
[Test]
public void TestComposition()
{
var compA = _protoMan.Index<EntityPrototype>(CompositionAEntity);
Assert.That(compA.Categories.Select(x => x.ID),
Is.EquivalentTo(new []{DefaultCategory, HideCategory}));
Assert.That(compA.CategoriesInternal?.Count, Is.EqualTo(2));
var compB = _protoMan.Index<EntityPrototype>(CompositionBEntity);
Assert.That(compB.Categories.Select(x => x.ID),
Is.EquivalentTo(new []{DefaultCategory, NoInheritCategory, AutoCategory}));
Assert.That(compB.CategoriesInternal?.Count, Is.EqualTo(3));
var childA = _protoMan.Index<EntityPrototype>(CompositionChildAEntity);
Assert.That(childA.Categories.Select(x => x.ID),
Is.EquivalentTo(new []{DefaultCategory, HideCategory, AutoCategory}));
Assert.That(childA.CategoriesInternal, Is.Null);
var childB = _protoMan.Index<EntityPrototype>(CompositionChildBEntity);
Assert.That(childB.Categories.Select(x => x.ID),
Is.EquivalentTo(new []{DefaultCategory, HideCategory, AutoCategory, Default2Category}));
Assert.That(childB.CategoriesInternal?.Count, Is.EqualTo(1));
}
[Test]
public void TestAutoCategorization()
{
var auto = _protoMan.Index<EntityPrototype>(AutoEntity);
Assert.That(auto.Categories.Select(x => x.ID), Is.EquivalentTo(new []{AutoCategory}));
Assert.That(auto.CategoriesInternal, Is.Null);
//var autoAttrib = _protoMan.Index<EntityPrototype>("AutoAttribute");
//Assert.That(autoAttrib.Categories.Select(x => x.ID), Is.EquivalentTo(new []{"Auto"}));
//Assert.That(autoAttrib.CategoriesInternal, Is.Null);
var autoChild = _protoMan.Index<EntityPrototype>(AutoChildEntity);
Assert.That(autoChild.Categories.Select(x => x.ID),
Is.EquivalentTo(new []{AutoCategory, DefaultCategory}));
Assert.That(autoChild.CategoriesInternal?.Count, Is.EqualTo(1));
}
[Test]
public void TestCategoryGrouping()
{
var none = _protoMan.Categories[new(NoneCategory)].Select(x=> x.ID);
Assert.That(none, Is.Empty);
var @default = _protoMan.Categories[new(DefaultCategory)].Select(x=> x.ID);
Assert.That(@default, Is.EquivalentTo(new[] {ConcreteChildEntity, CompositionAbstractEntity, CompositionAEntity, CompositionBEntity, CompositionChildAEntity, CompositionChildBEntity, AutoChildEntity}));
var default2 = _protoMan.Categories[new(Default2Category)].Select(x=> x.ID);
Assert.That(default2, Is.EquivalentTo(new[] {CompositionChildBEntity}));
var hide = _protoMan.Categories[new(HideCategory)].Select(x=> x.ID);
Assert.That(hide, Is.EquivalentTo(new[] {HideEntity, CompositionAbstractEntity, CompositionAEntity, CompositionChildAEntity, CompositionChildBEntity, InheritChildEntity}));
var noInherit = _protoMan.Categories[new(NoInheritCategory)].Select(x=> x.ID);
Assert.That(noInherit, Is.EquivalentTo(new[] {NoInheritParentEntity, CompositionBEntity}));
var auto = _protoMan.Categories[new(AutoCategory)].Select(x=> x.ID);
Assert.That(auto, Is.EquivalentTo(new[] {CompositionAbstractEntity, CompositionBEntity, CompositionChildAEntity, CompositionChildBEntity, AutoEntity, AutoChildEntity}));//, "AutoAttribute"}));
}
const string NoneCategory = "None";
const string DefaultCategory = "Default";
const string Default2Category = "Default2";
const string HideCategory = "Hide";
const string NoInheritCategory = "NoInherit";
const string AutoCategory = "Auto";
const string DefaultEntity = "Default";
const string HideEntity = "Hide";
const string InheritChildEntity = "InheritChild";
const string NoInheritParentEntity = "NoInheritParent";
const string NoInheritChildEntity = "NoInheritChild";
const string CompositionAEntity = "CompositionA";
const string CompositionBEntity = "CompositionB";
const string CompositionChildAEntity = "CompositionChildA";
const string CompositionChildBEntity = "CompositionChildB";
const string AbstractParentEntity = "AbstractParent";
const string ConcreteChildEntity = "ConcreteChild";
const string AbstractGrandChildEntity = "AbstractGrandChild";
const string CompositionAbstractEntity = "CompositionAbstract";
const string AutoEntity = "Auto";
const string AutoParentEntity = "AutoParent";
const string AutoChildEntity = "AutoChild";
const string TestPrototypes = $@"
- type: entityCategory
id: {NoneCategory}
- type: entityCategory
id: {DefaultCategory}
- type: entityCategory
id: {Default2Category}
- type: entityCategory
id: {HideCategory}
hideSpawnMenu: true
- type: entityCategory
id: {NoInheritCategory}
inheritable: false
- type: entityCategory
id: {AutoCategory}
components: [ AutoCategory ]
- type: entity
id: {DefaultEntity}
- type: entity
id: {HideEntity}
categories: [ {HideCategory} ]
- type: entity
id: {InheritChildEntity}
parent: Hide
- type: entity
id: {NoInheritParentEntity}
categories: [ {NoInheritCategory} ]
- type: entity
id: {NoInheritChildEntity}
parent: NoInheritParent
- type: entity
id: {CompositionAEntity}
categories: [ {DefaultCategory}, {HideCategory} ]
- type: entity
id: {CompositionBEntity}
categories: [ {DefaultCategory}, {NoInheritCategory}, {AutoCategory} ]
- type: entity
id: {CompositionChildAEntity}
parent: [{CompositionAEntity}, {CompositionBEntity}]
- type: entity
id: {CompositionChildBEntity}
parent: [{CompositionAEntity}, {CompositionBEntity}]
categories: [ {Default2Category} ]
- type: entity
id: {AbstractParentEntity}
abstract: true
categories: [ {DefaultCategory} ]
- type: entity
id: {ConcreteChildEntity}
parent: {AbstractParentEntity}
- type: entity
abstract: true
id: {AbstractGrandChildEntity}
parent: {ConcreteChildEntity}
categories: [ {HideCategory} ]
- type: entity
id: {CompositionAbstractEntity}
parent: [ {AbstractGrandChildEntity}, {CompositionBEntity} ]
- type: entity
id: {AutoEntity}
components:
- type: AutoCategory
#- type: entity
# id: AutoAttribute
# components:
# - type: AttributeAutoCategory
- type: entity
id: {AutoParentEntity}
abstract: true
categories: [ {NoInheritCategory} ]
components:
- type: AutoCategory
- type: entity
id: {AutoChildEntity}
parent: {AutoParentEntity}
categories: [ {DefaultCategory} ]
";
}
internal sealed partial class AutoCategoryComponent : Component;
// TODO test-local IReflectionManager
// [EntityCategory("Auto")]
// internal sealed partial class AttributeAutoCategoryComponent : Component;

View File

@@ -0,0 +1,257 @@
using System;
using System.Numerics;
using JetBrains.Annotations;
using NUnit.Framework;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array;
namespace Robust.UnitTesting.Shared.Prototypes
{
[UsedImplicitly]
[TestFixture]
internal sealed class PrototypeManager_Test : OurRobustUnitTest
{
private const string FakeWrenchProtoId = "wrench";
private const string YamlTesterProtoId = "yamltester";
private const string MountTesterProtoId = "mounttester";
private const string PlacementInheritanceTesterProtoId = "PlaceInheritTester";
private const string LoadStringTestDummyId = "LoadStringTestDummy";
private IPrototypeManager manager = default!;
protected override Type[] ExtraComponents => new[] {typeof(TestBasicPrototypeComponent), typeof(PointLightComponent)};
[OneTimeSetUp]
public void Setup()
{
IoCManager.Resolve<ISerializationManager>().Initialize();
manager = IoCManager.Resolve<IPrototypeManager>();
manager.RegisterKind(typeof(EntityPrototype), typeof(EntityCategoryPrototype));
manager.LoadString(DOCUMENT);
manager.ResolveResults();
}
[Test]
public void TestBasicPrototype()
{
var prototype = manager.Index<EntityPrototype>(FakeWrenchProtoId);
Assert.That(prototype.Name, Is.EqualTo("Not a wrench. Tricked!"));
var mapping = prototype.Components["TestBasicPrototype"].Component as TestBasicPrototypeComponent;
Assert.That(mapping!.Foo, Is.EqualTo("bar!"));
}
[Test, Combinatorial]
public void TestLightPrototype([Values("wallLight", "wallLightChild")] string id)
{
var prototype = manager.Index<EntityPrototype>(id);
Assert.Multiple(() =>
{
Assert.That(prototype.Name, Is.EqualTo("Wall Light"));
Assert.That(prototype.ID, Is.EqualTo(id));
Assert.That(prototype.Components, Contains.Key("Transform"));
Assert.That(prototype.Components, Contains.Key("PointLight"));
});
var componentData = prototype.Components["PointLight"].Component as SharedPointLightComponent;
Assert.That(componentData!.NetSyncEnabled, Is.EqualTo(false));
}
[Test]
public void TestYamlHelpersPrototype()
{
var prototype = manager.Index<EntityPrototype>(YamlTesterProtoId);
Assert.That(prototype.Components, Contains.Key("TestBasicPrototype"));
var componentData = prototype.Components["TestBasicPrototype"].Component as TestBasicPrototypeComponent;
Assert.That(componentData, Is.Not.Null);
Assert.That(componentData!.Str, Is.EqualTo("hi!"));
Assert.That(componentData!.int_field, Is.EqualTo(10));
Assert.That(componentData!.float_field, Is.EqualTo(10f));
Assert.That(componentData!.float2_field, Is.EqualTo(10.5f));
Assert.That(componentData!.boolt, Is.EqualTo(true));
Assert.That(componentData!.boolf, Is.EqualTo(false));
Assert.That(componentData!.vec2, Is.EqualTo(new Vector2(1.5f, 1.5f)));
Assert.That(componentData!.vec2i, Is.EqualTo(new Vector2i(1, 1)));
Assert.That(componentData!.vec3, Is.EqualTo(new Vector3(1.5f, 1.5f, 1.5f)));
Assert.That(componentData!.vec4, Is.EqualTo(new Vector4(1.5f, 1.5f, 1.5f, 1.5f)));
Assert.That(componentData!.color, Is.EqualTo(new Color(0xAA, 0xBB, 0xCC, 0xFF)));
Assert.That(componentData!.enumf, Is.EqualTo(YamlTestEnum.Foo));
Assert.That(componentData!.enumb, Is.EqualTo(YamlTestEnum.Bar));
}
[Test]
public void TestPlacementProperties()
{
var prototype = manager.Index<EntityPrototype>(MountTesterProtoId);
Assert.That(prototype.MountingPoints, Is.EquivalentTo(new int[] { 1, 2, 3 }));
Assert.That(prototype.PlacementMode, Is.EqualTo("AlignWall"));
Assert.That(prototype.PlacementRange, Is.EqualTo(300));
Assert.That(prototype.PlacementOffset, Is.EqualTo(new Vector2i(30, 45)));
}
[Test]
public void TestPlacementInheritance()
{
var prototype = manager.Index<EntityPrototype>(PlacementInheritanceTesterProtoId);
Assert.That(prototype.PlacementMode, Is.EqualTo("SnapgridCenter"));
}
[Test]
public void TestLoadString()
{
manager.LoadString(LoadStringDocument);
manager.ResolveResults();
var prototype = manager.Index<EntityPrototype>(LoadStringTestDummyId);
Assert.That(prototype.Name, Is.EqualTo(LoadStringTestDummyId));
}
[Test]
public void TestCircleException()
{
string GenerateCircleTestPrototype(string id, string parent)
{
return $@"- type: circle
id: {id}
parent: {parent}";
}
manager.RegisterKind(typeof(CircleTestPrototype));
var directCircle = $@"{GenerateCircleTestPrototype("1", "2")}
{GenerateCircleTestPrototype("2", "1")}";
Assert.Throws<PrototypeLoadException>(() => manager.LoadString(directCircle));
manager.RemoveString(directCircle);
var indirectCircle = $@"{GenerateCircleTestPrototype("1", "2")}
{GenerateCircleTestPrototype("2", "3")}
{GenerateCircleTestPrototype("3", "1")}";
Assert.Throws<PrototypeLoadException>(() => manager.LoadString(indirectCircle));
}
[Prototype("circle")]
private sealed partial class CircleTestPrototype : IPrototype, IInheritingPrototype
{
[IdDataField()]
public string ID { get; private set; } = default!;
[ParentDataField(typeof(AbstractPrototypeIdArraySerializer<CircleTestPrototype>))]
public string[]? Parents { get; private set; }
[AbstractDataField]
public bool Abstract { get; }
}
public enum YamlTestEnum : byte
{
Foo,
Bar
}
const string DOCUMENT = $@"
- type: entity
id: {FakeWrenchProtoId}
name: Not a wrench. Tricked!
components:
- type: TestBasicPrototype
foo: bar!
- type: entity
id: wallLight
name: Wall Light
components:
- type: Transform
- type: Sprite
- type: PointLight
netsync: False
- type: entity
id: wallLightChild
parent: wallLight
- type: entity
id: {YamlTesterProtoId}
components:
- type: TestBasicPrototype
str: hi!
int: 10
float: 10
float2: 10.5
boolt: true
boolf: false
vec2: 1.5, 1.5
vec2i: 1, 1
vec3: 1.5, 1.5, 1.5
vec4: 1.5, 1.5, 1.5, 1.5
color: '#aabbcc'
enumf: Foo
enumb: Bar
- type: entity
id: {MountTesterProtoId}
placement:
mode: AlignWall
range: 300
offset: 30, 45
nodes:
- 1
- 2
- 3
- type: entity
id: {PlacementInheritanceTesterProtoId}
parent: {MountTesterProtoId}
placement:
mode: SnapgridCenter
";
private static readonly string LoadStringDocument = $@"
- type: entity
id: {LoadStringTestDummyId}
name: {LoadStringTestDummyId}";
}
internal sealed partial class TestBasicPrototypeComponent : Component
{
[DataField("foo")] public string Foo = null!;
[DataField("str")] public string Str = null!;
[DataField("int")] public int? int_field = null!;
[DataField("float")] public float? float_field = null!;
[DataField("float2")] public float? float2_field = null!;
[DataField("boolt")] public bool? @boolt = null!;
[DataField("boolf")] public bool? @boolf = null!;
[DataField("vec2")] public Vector2 vec2 = default;
[DataField("vec2i")] public Vector2i vec2i = default;
[DataField("vec3")] public Vector3 vec3 = default;
[DataField("vec4")] public Vector4 vec4 = default;
[DataField("color")] public Color color = default;
[DataField("enumf")] public PrototypeManager_Test.YamlTestEnum enumf = default;
[DataField("enumb")] public PrototypeManager_Test.YamlTestEnum enumb = default;
}
}

View File

@@ -0,0 +1,113 @@
using NUnit.Framework;
using Robust.Shared.IoC;
using Robust.Shared.Reflection;
using System.Collections.Generic;
using JetBrains.Annotations;
namespace Robust.UnitTesting.Shared.Reflection
{
internal sealed class ReflectionManagerTest : ReflectionManager
{
protected override IEnumerable<string> TypePrefixes => new[] { "", "Robust.UnitTesting.", "Robust.Server.", "Robust.Shared." };
}
[TestFixture]
internal sealed class ReflectionManager_Test : OurRobustUnitTest
{
protected override void OverrideIoC()
{
base.OverrideIoC();
IoCManager.Register<IReflectionManager, ReflectionManagerTest>(overwrite: true);
}
[Test]
public void ReflectionManager_TestGetAllChildren()
{
IReflectionManager reflectionManager = IoCManager.Resolve<IReflectionManager>();
// I have no idea how to better do this.
bool did1 = false;
bool did2 = false;
foreach (var type in reflectionManager.GetAllChildren<IReflectionManagerTest>())
{
if (!did1 && type == typeof(TestClass1))
{
did1 = true;
}
else if (!did2 && type == typeof(TestClass2))
{
did2 = true;
}
else if (type == typeof(TestClass3))
{
// Not possible since it has [Reflect(false)]
Assert.Fail("ReflectionManager returned the [Reflect(false)] class.");
}
else if (type == typeof(TestClass4))
{
Assert.Fail("ReflectionManager returned the abstract class");
}
else
{
Assert.Fail("ReflectionManager returned too many types.");
}
}
Assert.That(did1 && did2, Is.True, $"IoCManager did not return both expected types. First: {did1}, Second: {did2}");
}
public interface IReflectionManagerTest { }
// These two pass like normal.
internal sealed class TestClass1 : IReflectionManagerTest { }
internal sealed class TestClass2 : IReflectionManagerTest { }
// These two should both NOT be passed.
[Reflect(false)]
internal sealed class TestClass3 : IReflectionManagerTest { }
public abstract class TestClass4 : IReflectionManagerTest { }
[Test]
public void ReflectionManager_TestGetType()
{
IReflectionManager reflectionManager = IoCManager.Resolve<IReflectionManager>();
Assert.Multiple(() =>
{
Assert.That(reflectionManager.GetType("Shared.Reflection.TestGetType1"), Is.EqualTo(typeof(TestGetType1)));
Assert.That(reflectionManager.GetType("Shared.Reflection.TestGetType2"), Is.EqualTo(typeof(TestGetType2)));
Assert.That(reflectionManager.GetType("Shared.Reflection.ITestGetType3"), Is.EqualTo(typeof(ITestGetType3)));
});
}
[Test]
public void ReflectionManager_TestTryParseEnumReference()
{
IReflectionManager reflectionManager = IoCManager.Resolve<IReflectionManager>();
reflectionManager.TryParseEnumReference("enum.TestParseEnumReferenceType1.Value", out var out1);
reflectionManager.TryParseEnumReference("enum.TestParseEnumReferenceType2.InnerValue", out var out2);
reflectionManager.TryParseEnumReference("enum.TestParseEnumReferenceType3.OuterValue", out var out3);
reflectionManager.TryParseEnumReference("enum.TestParseEnumReferenceTypeClass+TestParseEnumReferenceType2.InnerValue", out var out4);
Assert.Multiple(() =>
{
Assert.That(out1, Is.EqualTo(TestParseEnumReferenceType1.Value));
Assert.That(out2, Is.EqualTo(TestParseEnumReferenceTypeClass.TestParseEnumReferenceType2.InnerValue));
Assert.That(out3, Is.EqualTo(TestParseEnumReferenceType3.OuterValue));
Assert.That(out4, Is.EqualTo(TestParseEnumReferenceTypeClass.TestParseEnumReferenceType2.InnerValue));
});
}
}
internal sealed class TestGetType1 { }
public abstract class TestGetType2 { }
public interface ITestGetType3 { }
public enum TestParseEnumReferenceType1 { Value }
[UsedImplicitly]
internal sealed class TestParseEnumReferenceTypeClass
{
public enum TestParseEnumReferenceType2 { InnerValue }
}
}
public enum TestParseEnumReferenceType3 { OuterValue }

View File

@@ -0,0 +1,63 @@
using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Shared.ContentPack;
using Robust.Shared.Utility;
namespace Robust.UnitTesting.Shared.Resources;
internal sealed class ContentFileReadTest : RobustIntegrationTest
{
[Test]
[TestOf(typeof(ResourceManager))]
public async Task TestFileReading()
{
var client = StartClient();
await client.WaitIdleAsync();
var resMan = client.ResolveDependency<IResourceManager>();
// This tests relies on /Textures/error.rsi existing.
var rsiFolder = new ResPath("/Textures/error.rsi");
var rsiFolderExplicit = new ResPath("/Textures/error.rsi/");
var jsonFile = rsiFolder / "meta.json";
var missingFile = rsiFolder / "404FileNotFound";
var missingFolder = rsiFolder / "404FolderNotFound/";
Assert.Multiple(() =>
{
Assert.That(resMan.ContentFileExists(jsonFile));
Assert.That(resMan.TryContentFileRead(jsonFile, out _));
Assert.That(resMan.ContentGetDirectoryEntries(rsiFolder).Any());
Assert.That(resMan.ContentGetDirectoryEntries(rsiFolderExplicit).Any());
});
Assert.Multiple(() =>
{
Assert.DoesNotThrow(() => resMan.ContentFileExists(rsiFolder));
Assert.DoesNotThrow(() => resMan.ContentFileExists(rsiFolderExplicit));
Assert.DoesNotThrow(() => resMan.ContentFileExists(missingFile));
Assert.DoesNotThrow(() => resMan.ContentFileExists(missingFolder));
Assert.DoesNotThrow(() => resMan.TryContentFileRead(rsiFolder, out _));
Assert.DoesNotThrow(() => resMan.TryContentFileRead(rsiFolderExplicit, out _));
Assert.DoesNotThrow(() => resMan.TryContentFileRead(missingFile, out _));
Assert.DoesNotThrow(() => resMan.TryContentFileRead(missingFolder, out _));
});
Assert.Multiple(() =>
{
Assert.That(resMan.ContentFileExists(rsiFolder), Is.False);
Assert.That(resMan.ContentFileExists(rsiFolderExplicit), Is.False);
Assert.That(resMan.ContentFileExists(missingFile), Is.False);
Assert.That(resMan.ContentFileExists(missingFolder), Is.False);
Assert.That(resMan.TryContentFileRead(rsiFolder, out _), Is.False);
Assert.That(resMan.TryContentFileRead(rsiFolderExplicit, out _), Is.False);
Assert.That(resMan.TryContentFileRead(missingFile, out _), Is.False);
Assert.That(resMan.TryContentFileRead(missingFolder , out _), Is.False);
});
}
}

View File

@@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\MSBuild\Robust.Engine.props"/>
<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<OutputPath>../bin/Shared.IntegrationTests</OutputPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="JetBrains.Annotations"/>
<PackageReference Include="Microsoft.NET.Test.Sdk"/>
<PackageReference Include="Moq" />
<PackageReference Include="NUnit"/>
<PackageReference Include="NUnit3TestAdapter"/>
<PackageReference Include="NUnit.Analyzers"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Robust.Server.Testing\Robust.Server.Testing.csproj" />
<ProjectReference Include="..\Robust.Server\Robust.Server.csproj" />
<ProjectReference Include="..\Robust.Shared.Maths.Tests\Robust.Shared.Maths.Tests.csproj" />
<ProjectReference Include="..\Robust.Shared.Maths\Robust.Shared.Maths.csproj"/>
<ProjectReference Include="..\Robust.Shared.Maths.Testing\Robust.Shared.Maths.Testing.csproj"/>
<ProjectReference Include="..\Robust.Shared\Robust.Shared.csproj"/>
<ProjectReference Include="..\Robust.UnitTesting\Robust.UnitTesting.csproj" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="ContentPack\ZipTest.zip" />
</ItemGroup>
<Import Project="..\MSBuild\Robust.Properties.targets"/>
<Import Project="..\MSBuild\Robust.CompNetworkGenerator.targets" />
</Project>

View File

@@ -0,0 +1,50 @@
using System.Collections.Generic;
using NUnit.Framework;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.Markdown.Mapping;
namespace Robust.UnitTesting.Shared.Serialization;
[TestFixture]
internal sealed partial class CompositionTest : OurSerializationTest
{
[DataDefinition]
private sealed partial class CompositionTestClass
{
[DataField("f1")] public int ChildValue;
[DataField("f2")] public int Parent1Value;
[DataField("f3")] public int Parent2Value;
[DataField("f4"), NeverPushInheritance]
public int NeverPushValueParent1;
[DataField("f5"), NeverPushInheritance]
public int NeverPushValueParent2;
}
[Test]
public void TestPushComposition()
{
var child = new MappingDataNode { { "f1", "1" } };
var parent1 = new MappingDataNode
{
{ "f1", "2" },
{ "f2", "1" },
{ "f4", "1" }
};
var parent2 = new MappingDataNode
{
{ "f1", "3" },
{ "f2", "2" },
{ "f3", "1" },
{ "f5", "1" }
};
var finalMapping = Serialization.PushComposition<CompositionTestClass, MappingDataNode>(new[] { parent1, parent2 }, child);
var val = Serialization.Read<CompositionTestClass>(finalMapping, notNullableOverride: true);
Assert.That(val.ChildValue, Is.EqualTo(1));
Assert.That(val.Parent1Value, Is.EqualTo(1));
Assert.That(val.Parent2Value, Is.EqualTo(1));
Assert.That(val.NeverPushValueParent1, Is.EqualTo(0));
Assert.That(val.NeverPushValueParent2, Is.EqualTo(0));
}
}

View File

@@ -0,0 +1,328 @@
using System.Numerics;
using NUnit.Framework;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.Markdown.Mapping;
using Robust.Shared.Serialization.Markdown.Value;
namespace Robust.UnitTesting.Shared.Serialization;
public sealed partial class DataRecordTest : OurSerializationTest
{
[DataRecord]
public record TwoIntRecord(int aTest, int AnotherTest);
[DataRecord]
public record OneByteOneDefaultIntRecord(byte A, int B = 5);
[DataRecord]
public record OneLongRecord(long A);
[DataRecord]
public record OneLongDefaultRecord(long A = 5);
[DataRecord]
public record OneULongRecord(ulong A);
[PrototypeRecord("emptyTestPrototypeRecord")]
public record PrototypeRecord([field: IdDataField] string ID) : IPrototype;
[DataRecord]
public record IntStructHolder(IntStruct Struct);
[DataDefinition]
public partial struct IntStruct
{
[DataField("value")] public int Value;
public IntStruct(int value)
{
Value = value;
}
}
[DataRecord]
public record TwoIntStructHolder(IntStruct Struct1, IntStruct Struct2);
[DataRecord]
public record struct DataRecordStruct(IntStruct Struct, string String, int Integer);
[DataRecord]
public record struct DataRecordWithProperties
{
public Vector2 Position;
public int Foo { get; }
public int Bar { get; set; }
public float X => Position.X;
}
[DataRecord]
public readonly record struct ReadonlyDataRecord
{
public readonly Vector2 Position;
public int Foo { get; }
public float X => Position.X;
}
[Test]
public void TwoIntRecordTest()
{
var mapping = new MappingDataNode
{
{"aTest", "1"},
{"anotherTest", "2"}
};
var val = Serialization.Read<TwoIntRecord>(mapping, notNullableOverride: true);
Assert.Multiple(() =>
{
Assert.That(val.aTest, Is.EqualTo(1));
Assert.That(val.AnotherTest, Is.EqualTo(2));
});
var newMapping = Serialization.WriteValueAs<MappingDataNode>(val);
Assert.Multiple(() =>
{
Assert.That(newMapping, Has.Count.EqualTo(2));
Assert.That(newMapping.TryGet<ValueDataNode>("aTest", out var aTestNode));
Assert.That(aTestNode!.Value, Is.EqualTo("1"));
Assert.That(newMapping.TryGet<ValueDataNode>("anotherTest", out var anotherTestNode));
Assert.That(anotherTestNode!.Value, Is.EqualTo("2"));
});
}
[Test]
public void OneByteOneDefaultIntRecordTest()
{
var mapping = new MappingDataNode {{"a", "1"}};
var val = Serialization.Read<OneByteOneDefaultIntRecord>(mapping, notNullableOverride: true);
Assert.Multiple(() =>
{
Assert.That(val.A, Is.EqualTo(1));
Assert.That(val.B, Is.EqualTo(5));
});
}
[Test]
public void OneLongRecordTest()
{
var mapping = new MappingDataNode {{"a", "1"}};
var val = Serialization.Read<OneLongRecord>(mapping, notNullableOverride: true);
Assert.That(val.A, Is.EqualTo(1));
}
[Test]
public void OneLongMinValueRecordTest()
{
var mapping = new MappingDataNode {{"a", long.MinValue.ToString()}};
var val = Serialization.Read<OneLongRecord>(mapping, notNullableOverride: true);
Assert.That(val.A, Is.EqualTo(long.MinValue));
}
[Test]
public void OneLongMaxValueRecordTest()
{
var mapping = new MappingDataNode {{"a", long.MaxValue.ToString()}};
var val = Serialization.Read<OneLongRecord>(mapping, notNullableOverride: true);
Assert.That(val.A, Is.EqualTo(long.MaxValue));
}
[Test]
public void OneLongDefaultRecordTest()
{
var mapping = new MappingDataNode();
var val = Serialization.Read<OneLongDefaultRecord>(mapping, notNullableOverride: true);
Assert.That(val.A, Is.EqualTo(5));
}
[Test]
public void OneULongRecordMaxValueTest()
{
var mapping = new MappingDataNode {{"a", ulong.MaxValue.ToString()}};
var val = Serialization.Read<OneULongRecord>(mapping, notNullableOverride: true);
Assert.That(val.A, Is.EqualTo(ulong.MaxValue));
}
[Test]
public void PrototypeTest()
{
var mapping = new MappingDataNode {{"id", "ABC"}};
var val = Serialization.Read<PrototypeRecord>(mapping, notNullableOverride: true);
Assert.That(val.ID, Is.EqualTo("ABC"));
}
[Test]
public void RegisterPrototypeTest()
{
var prototypes = IoCManager.Resolve<IPrototypeManager>();
prototypes.Initialize();
Assert.That(prototypes.HasKind("emptyTestPrototypeRecord"), Is.True);
}
[Test]
public void IntStructHolderTest()
{
var mapping = new MappingDataNode
{
{
"struct", new MappingDataNode
{
{"value", "42"}
}
}
};
var val = Serialization.Read<IntStructHolder>(mapping, notNullableOverride: true);
Assert.That(val.Struct.Value, Is.EqualTo(42));
}
[Test]
public void TwoIntStructHolderTest()
{
var mapping = new MappingDataNode
{
{
"struct1", new MappingDataNode
{
{"value", "5"}
}
},
{
"struct2", new MappingDataNode
{
{"value", "10"}
}
}
};
var val = Serialization.Read<TwoIntStructHolder>(mapping, notNullableOverride: true);
Assert.Multiple(() =>
{
Assert.That(val.Struct1.Value, Is.EqualTo(5));
Assert.That(val.Struct2.Value, Is.EqualTo(10));
});
}
[Test]
public void DataRecordStructTest()
{
var mapping = new MappingDataNode
{
{
"struct", new MappingDataNode
{
{"value", "1"}
}
},
{
"string", new ValueDataNode("A")
},
{
"integer", new ValueDataNode("2")
}
};
var val = Serialization.Read<DataRecordStruct>(mapping);
Assert.Multiple(() =>
{
Assert.That(val.Struct.Value, Is.EqualTo(1));
Assert.That(val.String, Is.EqualTo("A"));
Assert.That(val.Integer, Is.EqualTo(2));
});
var newMapping = Serialization.WriteValueAs<MappingDataNode>(val);
Assert.Multiple(() =>
{
Assert.That(newMapping, Has.Count.EqualTo(3));
Assert.That(newMapping.TryGet<MappingDataNode>("struct", out var structNode));
Assert.That(structNode, Has.Count.EqualTo(1));
Assert.That(structNode!.TryGet<ValueDataNode>("value", out var structValueNode));
Assert.That(structValueNode!.Value, Is.EqualTo("1"));
Assert.That(newMapping.TryGet<ValueDataNode>("string", out var stringNode));
Assert.That(stringNode!.Value, Is.EqualTo("A"));
Assert.That(newMapping.TryGet<ValueDataNode>("integer", out var integerNode));
Assert.That(integerNode!.Value, Is.EqualTo("2"));
});
}
[Test]
public void DataRecordWithPropertiesTest()
{
var mapping = new MappingDataNode
{
["foo"] = new ValueDataNode("1"),
["bar"] = new ValueDataNode("2"),
["position"] = new ValueDataNode("3, .4"),
};
var val = Serialization.Read<DataRecordWithProperties>(mapping);
Assert.Multiple(() =>
{
Assert.That(val.Foo, Is.EqualTo(1));
Assert.That(val.Bar, Is.EqualTo(2));
Assert.That(val.Position, Is.EqualTo(new Vector2(3, .4f)));
});
var newMapping = Serialization.WriteValueAs<MappingDataNode>(val);
Assert.Multiple(() =>
{
Assert.That(newMapping, Has.Count.EqualTo(3));
Assert.That(newMapping.TryGet<ValueDataNode>("foo", out var node));
Assert.That(node!.Value, Is.EqualTo("1"));
Assert.That(newMapping.TryGet<ValueDataNode>("bar", out node));
Assert.That(node!.Value, Is.EqualTo("2"));
Assert.That(newMapping.TryGet<ValueDataNode>("position", out node));
Assert.That(node!.Value, Is.EqualTo("3,0.4"));
});
}
[Test]
public void ReadonlyDataRecordTest()
{
var mapping = new MappingDataNode
{
["foo"] = new ValueDataNode("1"),
["position"] = new ValueDataNode("3, .4"),
};
var val = Serialization.Read<ReadonlyDataRecord>(mapping);
Assert.Multiple(() =>
{
Assert.That(val.Foo, Is.EqualTo(1));
Assert.That(val.Position, Is.EqualTo(new Vector2(3, .4f)));
});
var newMapping = Serialization.WriteValueAs<MappingDataNode>(val);
Assert.Multiple(() =>
{
Assert.That(newMapping, Has.Count.EqualTo(2));
Assert.That(newMapping.TryGet<ValueDataNode>("foo", out var node));
Assert.That(node!.Value, Is.EqualTo("1"));
Assert.That(newMapping.TryGet<ValueDataNode>("position", out node));
Assert.That(node!.Value, Is.EqualTo("3,0.4"));
});
}
}

View File

@@ -0,0 +1,27 @@
using NUnit.Framework;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.Markdown.Mapping;
namespace Robust.UnitTesting.Shared.Serialization;
internal sealed partial class DataStructTest : OurSerializationTest
{
[DataDefinition]
public partial struct DefaultIntDataStruct
{
public int A = 5;
public DefaultIntDataStruct()
{
}
}
[Test]
public void DefaultIntDataStructTest()
{
var mapping = new MappingDataNode();
var val = Serialization.Read<DefaultIntDataStruct>(mapping);
Assert.That(val.A, Is.EqualTo(5));
}
}

View File

@@ -0,0 +1,59 @@
using NUnit.Framework;
using Robust.Shared.IoC;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.Markdown.Mapping;
using Robust.Shared.Serialization.Markdown.Value;
namespace Robust.UnitTesting.Shared.Serialization;
[TestFixture]
internal sealed partial class IncludeTest : OurRobustUnitTest
{
[DataDefinition]
private sealed partial class ReadWriteTestDataDefinition
{
[DataField("f1")] public int F1;
[IncludeDataField] public ReadWriteTestNestedDataDefinition Nested = default!;
}
[DataDefinition]
private sealed partial class ReadWriteTestNestedDataDefinition
{
[DataField("f1")] public int F1;
[DataField("f2")] public bool F2;
}
[Test]
public void TestReadWrite()
{
var serv3Mgr = IoCManager.Resolve<ISerializationManager>();
serv3Mgr.Initialize();
var mapping = new MappingDataNode();
mapping.Add("f1", "1");
mapping.Add("f2", "true");
var val = serv3Mgr.Read<ReadWriteTestDataDefinition>(mapping, notNullableOverride: true);
Assert.That(val.F1, Is.EqualTo(1));
Assert.That(val.Nested.F1, Is.EqualTo(1));
Assert.That(val.Nested.F2, Is.EqualTo(true));
var newMapping = serv3Mgr.WriteValueAs<MappingDataNode>(val);
Assert.That(newMapping.Count, Is.EqualTo(2));
Assert.That(newMapping.TryGet<ValueDataNode>("f1", out var f1Node));
Assert.That(f1Node!.Value, Is.EqualTo("1"));
Assert.That(newMapping.TryGet<ValueDataNode>("f2", out var f2Node));
Assert.That(f2Node!.Value.ToLower(), Is.EqualTo("true"));
}
[Test]
public void TestPushComposition()
{
//todo paul
}
}

View File

@@ -0,0 +1,106 @@
using System;
using NUnit.Framework;
using Robust.Shared.Analyzers;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.Manager.Definition;
namespace Robust.UnitTesting.Shared.Serialization
{
[TestFixture]
[TestOf(typeof(DataDefinition))]
internal sealed partial class InheritanceSerializationTest : OurRobustUnitTest
{
private const string BaseEntityId = "BaseEntity";
private const string InheritorEntityId = "InheritorEntityId";
private const string FinalEntityId = "FinalEntityId";
private const string BaseComponentFieldValue = "BaseFieldValue";
private const string InheritorComponentFieldValue = "InheritorFieldValue";
private const string FinalComponentFieldValue = "FinalFieldValue";
private static readonly string Prototypes = $@"
- type: entity
id: {BaseEntityId}
components:
- type: TestBase
baseField: {BaseComponentFieldValue}
- type: entity
id: {InheritorEntityId}
components:
- type: TestInheritor
baseField: {BaseComponentFieldValue}
inheritorField: {InheritorComponentFieldValue}
- type: entity
id: {FinalEntityId}
components:
- type: TestFinal
baseField: {BaseComponentFieldValue}
inheritorField: {InheritorComponentFieldValue}
finalField: {FinalComponentFieldValue}";
protected override Type[]? ExtraComponents => new[] {typeof(TestBaseComponent), typeof(TestInheritorComponent), typeof(TestFinalComponent)};
[Test]
public void Test()
{
var serializationManager = IoCManager.Resolve<ISerializationManager>();
serializationManager.Initialize();
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
prototypeManager.RegisterKind(typeof(EntityPrototype), typeof(EntityCategoryPrototype));
prototypeManager.LoadString(Prototypes);
prototypeManager.ResolveResults();
var entityManager = IoCManager.Resolve<IEntityManager>();
entityManager.System<SharedMapSystem>().CreateMap(out var mapId);
var coordinates = new MapCoordinates(0, 0, mapId);
var baseEntity = entityManager.SpawnEntity(BaseEntityId, coordinates);
Assert.That(IoCManager.Resolve<IEntityManager>().TryGetComponent(baseEntity, out TestBaseComponent? baseComponent));
Assert.That(baseComponent!.BaseField, Is.EqualTo(BaseComponentFieldValue));
var inheritorEntity = entityManager.SpawnEntity(InheritorEntityId, coordinates);
Assert.That(IoCManager.Resolve<IEntityManager>().TryGetComponent(inheritorEntity, out TestInheritorComponent? inheritorComponent));
Assert.That(inheritorComponent!.BaseField, Is.EqualTo(BaseComponentFieldValue));
Assert.That(inheritorComponent!.InheritorField, Is.EqualTo(InheritorComponentFieldValue));
var finalEntity = entityManager.SpawnEntity(FinalEntityId, coordinates);
Assert.That(IoCManager.Resolve<IEntityManager>().TryGetComponent(finalEntity, out TestFinalComponent? finalComponent));
Assert.That(finalComponent!.BaseField, Is.EqualTo(BaseComponentFieldValue));
Assert.That(finalComponent!.InheritorField, Is.EqualTo(InheritorComponentFieldValue));
Assert.That(finalComponent!.FinalField, Is.EqualTo(FinalComponentFieldValue));
}
}
[Virtual]
public partial class TestBaseComponent : Component
{
[DataField("baseField")] public string? BaseField;
}
[Virtual]
public partial class TestInheritorComponent : TestBaseComponent
{
[DataField("inheritorField")] public string? InheritorField;
}
internal sealed partial class TestFinalComponent : TestInheritorComponent
{
[DataField("finalField")] public string? FinalField;
}
}

View File

@@ -0,0 +1,33 @@
using System;
using NUnit.Framework;
using Robust.Shared.IoC;
using Robust.Shared.Reflection;
using Robust.Shared.Serialization;
namespace Robust.UnitTesting.Shared.Serialization
{
[TestFixture]
internal sealed class NetSerializableAttribute_Test : OurRobustUnitTest
{
private IReflectionManager _reflection = default!;
[OneTimeSetUp]
public void TestFixtureSetup()
{
_reflection = IoCManager.Resolve<IReflectionManager>();
}
[Test]
public void AllNetSerializableObjectsHaveSerializableAttribute()
{
var types = _reflection.FindTypesWithAttribute<NetSerializableAttribute>();
foreach (var type in types)
{
Assert.That(Attribute.IsDefined(type, typeof(NetSerializableAttribute), true),
$"{type.FullName} has {nameof(NetSerializableAttribute)}, but not the required {nameof(SerializableAttribute)}.");
}
}
}
}

View File

@@ -0,0 +1,249 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Numerics;
using NetSerializer;
using NUnit.Framework;
using Robust.Shared.Maths;
using Robust.Shared.Serialization;
namespace Robust.UnitTesting.Shared.Serialization
{
// Tests NetSerializer itself because we have specific modifications.
// e.g. (at the time of writing) list serialization being more compact.
[Parallelizable(ParallelScope.All)]
internal sealed class NetSerializer_Test
{
public static readonly List<int>?[] ListValues =
{
null,
new List<int>(),
new List<int> {1, 2, 3},
};
[Test]
public void TestList([ValueSource(nameof(ListValues))] List<int>? list)
{
var serializer = new Serializer(new[] {typeof(List<int>)});
var stream = new MemoryStream();
serializer.SerializeDirect(stream, list);
stream.Position = 0;
serializer.DeserializeDirect<List<int>?>(stream, out var deserialized);
if (list == null)
{
Assert.That(deserialized, Is.Null);
}
else
{
Assert.That(deserialized, Is.EquivalentTo(list));
}
}
public static readonly Dictionary<string, int>?[] DictionaryValues =
{
null,
new Dictionary<string, int>(),
new Dictionary<string, int> {{"A", 1}},
new Dictionary<string, int> {{"A", 1}, {"B", 2}, {"C", 3}},
};
[Test]
public void TestDictionary([ValueSource(nameof(DictionaryValues))] Dictionary<string, int>? list)
{
var serializer = new Serializer(new[] {typeof(Dictionary<string, int>)});
var stream = new MemoryStream();
serializer.SerializeDirect(stream, list);
stream.Position = 0;
serializer.DeserializeDirect<Dictionary<string, int>?>(stream, out var deserialized);
if (list == null)
{
Assert.That(deserialized, Is.Null);
}
else
{
Assert.That(deserialized, Is.EquivalentTo(list));
}
}
public static readonly HashSet<int>?[] HashSetValues =
{
null,
new HashSet<int>(),
new HashSet<int> {1, 2, 3},
};
[Test]
public void TestHashSet([ValueSource(nameof(HashSetValues))] HashSet<int>? set)
{
var serializer = new Serializer(new[] {typeof(HashSet<int>)});
var stream = new MemoryStream();
serializer.SerializeDirect(stream, set);
stream.Position = 0;
serializer.DeserializeDirect<HashSet<int>?>(stream, out var deserialized);
if (set == null)
{
Assert.That(deserialized, Is.Null);
}
else
{
Assert.That(deserialized, Is.EquivalentTo(set));
}
}
public static readonly Vector2[] Vector2Values =
{
Vector2.Zero,
Vector2.One,
Vector2Helpers.NaN,
Vector2Helpers.Infinity,
new(-1f, -1f),
new(10f, -10f),
new(-10f, 10f),
};
[Test]
public void TestVector2([ValueSource(nameof(Vector2Values))] Vector2 vec)
{
var serializer = new Serializer(new[] {typeof(Vector2)}, new Settings()
{
CustomTypeSerializers = new ITypeSerializer[]
{
new NetMathSerializer(),
}
});
var stream = new MemoryStream();
serializer.SerializeDirect(stream, vec);
stream.Position = 0;
serializer.DeserializeDirect(stream, out Vector2 read);
Assert.That(read, NUnit.Framework.Is.EqualTo(vec));
}
[Test]
[TestCase(0f)]
[TestCase(-0f)]
[TestCase(1f)]
[TestCase(-1f)]
[TestCase(float.NaN)]
[TestCase(float.MaxValue)]
[TestCase(float.MinValue)]
[TestCase(float.NegativeInfinity)]
[TestCase(float.PositiveInfinity)]
[TestCase(MathF.PI)]
public void TestFloats(float f)
{
var stream = new MemoryStream();
Primitives.WritePrimitive(stream, f);
stream.Position = 0;
Primitives.ReadPrimitive(stream, out float read);
Assert.That(read, NUnit.Framework.Is.EqualTo(f));
}
[Test]
[TestCase(0d)]
[TestCase(-0d)]
[TestCase(1d)]
[TestCase(-1d)]
[TestCase(double.NaN)]
[TestCase(double.MaxValue)]
[TestCase(double.MinValue)]
[TestCase(double.NegativeInfinity)]
[TestCase(double.PositiveInfinity)]
[TestCase(Math.PI)]
public void TestDoubles(double d)
{
var stream = new MemoryStream();
Primitives.WritePrimitive(stream, d);
stream.Position = 0;
Primitives.ReadPrimitive(stream, out double read);
Assert.That(read, NUnit.Framework.Is.EqualTo(d));
}
[Test]
[TestCase(null)]
[TestCase("")]
[TestCase("ABC")]
// ReSharper disable StringLiteralTypo
[TestCase("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce arcu mi, vehicula at nunc sit amet, lobortis interdum eros. Ut nec tincidunt odio. Etiam at odio id mauris condimentum elementum eu sit amet sem. Donec libero ex, viverra non metus ut, imperdiet varius lectus. Vivamus ultrices orci sed urna cursus, vel cursus velit lacinia. Mauris pellentesque tristique metus, et iaculis est tincidunt at. Integer maximus elit quis mollis sodales. Sed luctus quam a tempus vulputate.")]
[TestCase("H̟̟̱̺͉͡o̭̲̱̲͜nk̰̤̙͍͕̘")]
[TestCase("🤔 U+1F914")]
[TestCase("壮健")]
// These emojis are very wide so get split down the middle in both the writing and reading code.
// So that makes sure those code paths are tested.
[TestCase("a🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔" +
"🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔" +
"🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔" +
"🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔" +
"🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔")]
public void TestString(string? str)
{
var stream = new MemoryStream();
Primitives.WritePrimitive(stream, str);
stream.Position = 0;
Primitives.ReadPrimitive(stream, out string? deserialized);
Assert.That(deserialized, Is.EqualTo(str));
}
[Test]
public void TestStringEndOfStream()
{
var stream = new MemoryStream();
Primitives.WritePrimitive(stream, (uint)2000);
Primitives.WritePrimitive(stream, (uint)1000);
stream.Position = 0;
Assert.That(() => Primitives.ReadPrimitive(stream, out string _), Throws.TypeOf<EndOfStreamException>());
}
[Test]
public void TestStringDestTooShort()
{
var stream = new MemoryStream();
Primitives.WritePrimitive(stream, (uint)2000);
Primitives.WritePrimitive(stream, (uint)5);
Primitives.WritePrimitive(stream, new byte[2000]);
stream.Position = 0;
Assert.That(() => Primitives.ReadPrimitive(stream, out string _), Throws.TypeOf<InvalidDataException>());
}
[Test]
public void TestImmutableDictionary()
{
var x = new Dictionary<string, int>
{
{ "A", 1 },
{ "B", 2 }
}.ToImmutableDictionary();
var serializer = new Serializer([typeof(ImmutableDictionary<string, int>)], new Settings());
var stream = new MemoryStream();
serializer.SerializeDirect(stream, x);
stream.Position = 0;
serializer.DeserializeDirect(stream, out ImmutableDictionary<string, int> read);
Assert.That(read, NUnit.Framework.Is.EquivalentTo(x));
}
[Test]
public void TestImmutableHashSet()
{
var x = new HashSet<string> {"A", "B"}.ToImmutableHashSet();
var serializer = new Serializer([typeof(ImmutableHashSet<string>)], new Settings());
var stream = new MemoryStream();
serializer.SerializeDirect(stream, x);
stream.Position = 0;
serializer.DeserializeDirect(stream, out ImmutableHashSet<string> read);
Assert.That(read, NUnit.Framework.Is.EquivalentTo(x));
}
}
}

Some files were not shown because too many files have changed in this diff Show More