using Ghost.Editor.Core.SceneGraph; using Ghost.Editor.Core.Services; using Ghost.Entities; namespace Ghost.UnitTest; [TestClass] public class UndoServiceEcsTests { private struct CompA : IComponentData { public int value; } private struct CompB : IComponentData { public int value; } private EditorWorldService _worldService = null!; private UndoService _undoService = null!; [TestInitialize] public void Setup() { _worldService = new EditorWorldService(); _undoService = new UndoService(_worldService); } [TestCleanup] public void Cleanup() { _worldService.Dispose(); } [TestMethod] public void TestRecordEntityStructure() { var world = _worldService.EditorWorld; var e = world.EntityManager.CreateEntity(); world.EntityManager.AddComponent(e); // Initial state: Entity has CompA ref var compA = ref world.EntityManager.GetComponent(e); compA.value = 10; var node = new EntityNode(world, e, "TestEntity", null); _undoService.BeginTransaction("Add CompB"); _undoService.RecordObject(node, "Before Add CompB"); // Modify structure world.EntityManager.AddComponent(e); ref var compB = ref world.EntityManager.GetComponent(e); compB.value = 20; // Re-fetch CompA because AddComponent moves the entity to a new chunk, // invalidating the previous ref! ref var compA_new = ref world.EntityManager.GetComponent(e); compA_new.value = 15; // also modify compA _undoService.EndTransaction(); // Perform Undo _undoService.PerformUndo(); Assert.IsTrue(world.EntityManager.HasComponent(e), "Should have CompA"); Assert.IsFalse(world.EntityManager.HasComponent(e), "Should NOT have CompB"); Assert.AreEqual(10, world.EntityManager.GetComponent(e).value, "CompA value should be reverted to 10"); // Perform Redo _undoService.PerformRedo(); Assert.IsTrue(world.EntityManager.HasComponent(e), "Should have CompA"); Assert.IsTrue(world.EntityManager.HasComponent(e), "Should have CompB"); Assert.AreEqual(15, world.EntityManager.GetComponent(e).value, "CompA value should be restored to 15"); Assert.AreEqual(20, world.EntityManager.GetComponent(e).value, "CompB value should be restored to 20"); } [TestMethod] public void TestRecordEntityLifecycle_CreateAndDestroy() { var world = _worldService.EditorWorld; // Step 1: Create Entity var e = world.EntityManager.CreateEntity(); world.EntityManager.AddComponent(e); world.EntityManager.GetComponent(e).value = 42; var node = new EntityNode(world, e, "TestEntity", null); _undoService.BeginTransaction("Create Entity"); _undoService.RegisterCreatedObjectUndo(node, "Create Entity"); _undoService.EndTransaction(); // Undo Creation (Expect destruction) _undoService.PerformUndo(); Assert.IsFalse(world.EntityManager.Exists(e), "Entity should be destroyed by Undo of Creation"); // Redo Creation (Expect resurrection) _undoService.PerformRedo(); // Note: The entity ID might be different, but the EntityNode should be updated var resurrectedEntity = node.Entity; Assert.IsTrue(world.EntityManager.Exists(resurrectedEntity), "Entity should be resurrected by Redo of Creation"); // In our current implementation, restoring components for created entities isn't fully robust yet, // but we verify the entity is alive. // Step 2: Destroy Entity _undoService.BeginTransaction("Destroy Entity"); _undoService.RecordObject(node, "Destroy Entity"); world.EntityManager.DestroyEntity(resurrectedEntity); _undoService.EndTransaction(); Assert.IsFalse(world.EntityManager.Exists(resurrectedEntity), "Entity destroyed manually"); // Undo Destruction (Expect resurrection) _undoService.PerformUndo(); var undoneDestroyEntity = node.Entity; Assert.IsTrue(world.EntityManager.Exists(undoneDestroyEntity), "Entity should be resurrected by Undo of Destruction"); // Redo Destruction (Expect destruction) _undoService.PerformRedo(); Assert.IsFalse(world.EntityManager.Exists(undoneDestroyEntity), "Entity should be destroyed by Redo of Destruction"); } }