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 { /// /// Check that whether or not an entity has been paused or map-initialized is preserved across saves & loads. /// [Test] public async Task TestLifestageSerialization() { var server = StartServer(); await server.WaitIdleAsync(); var entMan = server.EntMan; var mapSys = server.System(); var loader = server.System(); 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 map = default; Entity entA = default; Entity entB = default; Entity childA = default; Entity 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(uid).EntityPaused, Is.EqualTo(expected)); } } void AssertPreInit(bool expected, params EntityUid[] uids) { foreach (var uid in uids) { Assert.That(entMan!.GetComponent(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(), Is.EqualTo(5)); await server.WaitPost(() => entMan.DeleteEntity(map)); Assert.That(entMan.Count(), Is.EqualTo(0)); } async Task Load(ResPath f, DeserializationOptions? o) { Assert.That(entMan.Count(), Is.EqualTo(0)); await server.WaitAssertion(() => Assert.That(loader.TryLoadMap(f, out _, out _, o))); Assert.That(entMan.Count(), 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); } /// /// Variant of that has multiple maps and combinations. E.g., a single /// paused entity on an un-paused map. /// [Test] public async Task TestMixedLifestageSerialization() { var server = StartServer(); await server.WaitIdleAsync(); var entMan = server.EntMan; var meta = server.System(); var mapSys = server.System(); var loader = server.System(); var path = new ResPath($"{nameof(TestMixedLifestageSerialization)}.yml"); var altPath = new ResPath($"{nameof(TestMixedLifestageSerialization)}_alt.yml"); Entity mapA = default; // preinit Map Entity mapB = default; // postinit unpaused Map Entity entA = default; // postinit entity on preinit map Entity entB = default; // paused entity on postinit unpaused map Entity entC = default; // preinit entity on postinit map Entity nullA = default; // postinit nullspace entity Entity nullB = default; // preinit nullspace entity Entity 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(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(uid)?.Id; } void AssertPaused(bool expected, params EntityUid[] uids) { foreach (var uid in uids) { Assert.That(entMan.GetComponent(uid).EntityPaused, Is.EqualTo(expected), Name(uid)); } } void AssertPreInit(bool expected, params EntityUid[] uids) { foreach (var uid in uids) { Assert.That(entMan!.GetComponent(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(), 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(), Is.EqualTo(0)); } async Task Load(ResPath f, DeserializationOptions? o) { Assert.That(entMan.Count(), 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(), 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); } }