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}
";
///
/// Check that we can save & load a file containing multiple orphaned (non-grid) entities.
///
[Test]
public async Task TestMultipleOrphanSerialization()
{
var server = StartServer();
await server.WaitIdleAsync();
var entMan = server.EntMan;
var mapSys = server.System();
var loader = server.System();
var xform = server.System();
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 entA = default;
Entity entB = default;
Entity 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(), Is.EqualTo(3));
await server.WaitPost(() => mapSys.DeleteMap(mapId));
Assert.That(entMan.Count(), Is.EqualTo(0));
// Load in the file containing only entA.
await server.WaitAssertion(() => Assert.That(loader.TryLoadEntity(pathA, out _)));
Assert.That(entMan.Count(), 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(), 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(), 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(), 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(), 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(), Is.EqualTo(0));
}
///
/// Check that we can save & load a file containing multiple orphaned grid entities.
///
[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();
var loader = server.System();
var xform = server.System();
var mapMan = server.ResolveDependency();
var tileMan = server.ResolveDependency();
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(TestTileDefId);
// Spawn multiple entities on a map
MapId mapId = default;
Entity map = default;
Entity gridA = default;
Entity gridB = default;
Entity 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(), Is.EqualTo(4));
await server.WaitPost(() => mapSys.DeleteMap(mapId));
Assert.That(entMan.Count(), 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(), 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(), 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(), 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(), 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(), Is.EqualTo(0));
Assert.That(entMan.Count(), 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(), Is.EqualTo(0));
}
}