diff --git a/src/Editor/Ghost.Editor.Core/SceneGraph/ComponentNode.cs b/src/Editor/Ghost.Editor.Core/SceneGraph/ComponentNode.cs index 23338bc..a617485 100644 --- a/src/Editor/Ghost.Editor.Core/SceneGraph/ComponentNode.cs +++ b/src/Editor/Ghost.Editor.Core/SceneGraph/ComponentNode.cs @@ -68,7 +68,7 @@ public unsafe class ComponentNode throw new ArgumentException("Property type does not match value type"); } - _undoService.RecordEntityComponent(this, $"Edit property {property.DisplayName} on {Descriptor.DisplayName}"); + _undoService.RecordObject(EntityNode, $"Edit property {property.DisplayName} on {Descriptor.DisplayName}"); _worldService.Defer(() => { if (Descriptor.IsShared) @@ -99,7 +99,7 @@ public unsafe class ComponentNode throw new ArgumentException("Value type does not match component type"); } - _undoService.RecordEntityComponent(this, $"Edit component {Descriptor.DisplayName}"); + _undoService.RecordObject(EntityNode, $"Edit component {Descriptor.DisplayName}"); _worldService.Defer(() => { if (Descriptor.IsShared) diff --git a/src/Editor/Ghost.Editor.Core/SceneGraph/EntityNode.cs b/src/Editor/Ghost.Editor.Core/SceneGraph/EntityNode.cs index e910df5..0be057f 100644 --- a/src/Editor/Ghost.Editor.Core/SceneGraph/EntityNode.cs +++ b/src/Editor/Ghost.Editor.Core/SceneGraph/EntityNode.cs @@ -24,6 +24,199 @@ public sealed partial class EntityNode : SceneGraphNode public override SceneNode? GetOwningSceneNode() => SceneNode; + public unsafe override void SerializeState(BinaryWriter writer) + { + base.SerializeState(writer); + + var isAlive = World.EntityManager.Exists(Entity); + writer.Write(isAlive); + + if (!isAlive) + { + return; + } + + var locRes = World.EntityManager.GetEntityLocation(Entity); + if (!locRes.IsSuccess) + { + writer.Write(0); + return; + } + + var archetypeId = locRes.Value.archetypeID; + ref var archetype = ref World.ComponentManager.GetArchetypeReference(archetypeId); + ref var chunk = ref archetype.GetChunkReference(locRes.Value.chunkIndex); + + EditorApplication.TryGetService(out var syncService); + + writer.Write(archetype._layouts.Count); + + for (var i = 0; i < archetype._layouts.Count; i++) + { + var layout = archetype._layouts[i]; + var typeId = new Ghost.Core.Identifier(layout.componentID); + + writer.Write(typeId.Value); + writer.Write(layout.size); + + var pSrc = chunk.GetUnsafePtr() + layout.offset + (layout.size * locRes.Value.rowIndex); + + // Copy to temp buffer + var buffer = new byte[layout.size]; + fixed (byte* pDst = buffer) + { + Buffer.MemoryCopy(pSrc, pDst, layout.size, layout.size); + } + + // Reference Translation + var entityOffsets = Services.EntityFieldTracker.GetEntityOffsets(typeId.Value); + foreach (var offset in entityOffsets) + { + Entity oldEntity; + fixed (byte* pBuf = buffer) + { + oldEntity = *(Entity*)(pBuf + offset); + } + + Guid targetGuid = Guid.Empty; + if (syncService != null && syncService.TryGetNode(oldEntity, out var targetNode)) + { + targetGuid = targetNode.InstanceID; + } + + writer.Write(true); + writer.Write(offset); + writer.Write(targetGuid.ToByteArray()); + } + writer.Write(false); // End of patch records + + // Write patched bytes + writer.Write(buffer); + } + + // Shared Data + if (chunk._groupIndex >= 0 && chunk._groupIndex < archetype._chunkGroups.Count) + { + var group = archetype._chunkGroups[chunk._groupIndex]; + writer.Write(true); + writer.Write(group.sharedDataHash); + writer.Write(group.sharedData.Length); + writer.Write(group.sharedData.AsSpan().ToArray()); + } + else + { + writer.Write(false); + } + } + + public unsafe override void DeserializeState(BinaryReader reader) + { + base.DeserializeState(reader); + + var isAlive = reader.ReadBoolean(); + var currentlyAlive = World.EntityManager.Exists(Entity); + + if (isAlive && !currentlyAlive) + { + // Resurrect + var newEntity = World.EntityManager.CreateEntity(); + + // Update the Entity property via reflection + var entityField = typeof(EntityNode).GetField("k__BackingField", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + entityField?.SetValue(this, newEntity); + } + else if (!isAlive && currentlyAlive) + { + // Destroy + World.EntityManager.DestroyEntity(Entity); + return; + } + else if (!isAlive && !currentlyAlive) + { + return; + } + + var componentCount = reader.ReadInt32(); + var componentsToRestore = new List>(); + var componentDataMap = new Dictionary(); + + EditorApplication.TryGetService(out var syncService); + + for (var i = 0; i < componentCount; i++) + { + var typeIdVal = reader.ReadInt32(); + var size = reader.ReadInt32(); + var typeId = new Ghost.Core.Identifier(typeIdVal); + componentsToRestore.Add(typeId); + + var patchRecords = new List<(int offset, Guid guid)>(); + while (reader.ReadBoolean()) + { + var offset = reader.ReadInt32(); + var guidBytes = reader.ReadBytes(16); + patchRecords.Add((offset, new Guid(guidBytes))); + } + + var buffer = reader.ReadBytes(size); + + // Apply patch records + foreach (var record in patchRecords) + { + Entity newEntity = Entity.Invalid; + if (record.guid != Guid.Empty) + { + if (GhostObject.Find(record.guid) is EntityNode targetNode) + { + newEntity = targetNode.Entity; + } + } + + fixed (byte* pBuf = buffer) + { + *(Entity*)(pBuf + record.offset) = newEntity; + } + } + + componentDataMap[typeIdVal] = buffer; + } + + var hasSharedData = reader.ReadBoolean(); + int sharedDataHash = 0; + byte[] sharedData = Array.Empty(); + + if (hasSharedData) + { + sharedDataHash = reader.ReadInt32(); + var sharedSize = reader.ReadInt32(); + sharedData = reader.ReadBytes(sharedSize); + } + + // Migrate entity to match snapshot archetype + var view = new Ghost.Entities.ComponentSetView(componentsToRestore.ToArray(), sharedData); + World.EntityManager.MigrateEntity(Entity, view); + + // Restore unmanaged data + var locRes = World.EntityManager.GetEntityLocation(Entity); + if (locRes.IsSuccess) + { + ref var archetype = ref World.ComponentManager.GetArchetypeReference(locRes.Value.archetypeID); + ref var chunk = ref archetype.GetChunkReference(locRes.Value.chunkIndex); + + for (var i = 0; i < archetype._layouts.Count; i++) + { + var layout = archetype._layouts[i]; + if (componentDataMap.TryGetValue(layout.componentID, out var buffer)) + { + var pDst = chunk.GetUnsafePtr() + layout.offset + (layout.size * locRes.Value.rowIndex); + fixed (byte* pSrc = buffer) + { + Buffer.MemoryCopy(pSrc, pDst, layout.size, layout.size); + } + } + } + } + } + public void BuildComponents() { Components.Clear(); diff --git a/src/Editor/Ghost.Editor.Core/Services/EntityFieldTracker.cs b/src/Editor/Ghost.Editor.Core/Services/EntityFieldTracker.cs new file mode 100644 index 0000000..24ee561 --- /dev/null +++ b/src/Editor/Ghost.Editor.Core/Services/EntityFieldTracker.cs @@ -0,0 +1,58 @@ +using Ghost.Entities; +using System.Reflection; +using System.Runtime.InteropServices; + +namespace Ghost.Editor.Core.Services; + +/// +/// Caches the memory offsets of all fields of type `Entity` within unmanaged ECS components. +/// Used by the Undo system to patch references during serialization/deserialization. +/// +internal static class EntityFieldTracker +{ + private static readonly Dictionary s_entityOffsets = new(); + private static readonly Lock s_lock = new(); + + public static int[] GetEntityOffsets(int componentId) + { + lock (s_lock) + { + if (s_entityOffsets.TryGetValue(componentId, out var offsets)) + { + return offsets; + } + + if (!ComponentRegistry.s_runtimeIDToType.TryGetValue(componentId, out var type)) + { + s_entityOffsets[componentId] = Array.Empty(); + return Array.Empty(); + } + + var offsetList = new List(); + FindEntityFieldsRecursive(type, 0, offsetList); + + offsets = offsetList.ToArray(); + s_entityOffsets[componentId] = offsets; + return offsets; + } + } + + private static void FindEntityFieldsRecursive(Type type, int currentOffset, List offsetList) + { + var fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + foreach (var field in fields) + { + var fieldOffset = currentOffset + (int)Marshal.OffsetOf(type, field.Name); + + if (field.FieldType == typeof(Entity)) + { + offsetList.Add(fieldOffset); + } + else if (field.FieldType.IsValueType && !field.FieldType.IsPrimitive && !field.FieldType.IsEnum) + { + // Recursively check nested structs + FindEntityFieldsRecursive(field.FieldType, fieldOffset, offsetList); + } + } + } +} diff --git a/src/Editor/Ghost.Editor.Core/Services/UndoService.cs b/src/Editor/Ghost.Editor.Core/Services/UndoService.cs index d9d4dad..d055029 100644 --- a/src/Editor/Ghost.Editor.Core/Services/UndoService.cs +++ b/src/Editor/Ghost.Editor.Core/Services/UndoService.cs @@ -17,9 +17,7 @@ public interface IUndoService event Action? UndoRedoPerformed; void RecordObject(GhostObject obj, string actionName); - void RecordEntityComponent(ComponentNode node, string actionName); - void RecordEntityStructure(EntityNode node, string actionName); - void RecordEntityLifecycle(EntityNode node, LifecycleEvent type); + void RegisterCreatedObjectUndo(GhostObject obj, string actionName); void BeginTransaction(string name); void EndTransaction(); @@ -80,285 +78,6 @@ public class ObjectStateOperation : UndoOperation } } -public class EntityComponentOperation : UndoOperation -{ - public Guid InstanceID { get; set; } - public Entity Entity { get; set; } - public int ComponentId { get; set; } - public byte[] ComponentData { get; set; } = Array.Empty(); - - public unsafe override UndoOperation CreateReciprocal(IEditorWorldService worldService) - { - var node = GhostObject.Find(InstanceID) as EntityNode; - var targetEntity = node?.Entity ?? Entity; - - var reciprocal = new EntityComponentOperation { GroupId = GroupId, ActionName = ActionName, Entity = targetEntity, InstanceID = InstanceID, ComponentId = ComponentId }; - var pComp = worldService.EditorWorld.EntityManager.GetComponent(targetEntity, new Identifier(ComponentId)); - if (pComp != null) - { - var size = ComponentRegistry.GetComponentInfo(new Identifier(ComponentId)).size; - var data = new byte[size]; - fixed (byte* pDst = data) - { - Buffer.MemoryCopy(pComp, pDst, size, size); - } - reciprocal.ComponentData = data; - } - - return reciprocal; - } - - public override void Revert(IEditorWorldService worldService) - { - var cId = ComponentId; - var data = ComponentData; - var instId = InstanceID; - var fallbackEntity = Entity; - - worldService.Defer(() => - { - var node = GhostObject.Find(instId) as EntityNode; - var targetEntity = node?.Entity ?? fallbackEntity; - - unsafe - { - var pComp = worldService.EditorWorld.EntityManager.GetComponent(targetEntity, new Identifier(cId)); - if (pComp != null) - { - fixed (byte* pSrc = data) - { - Buffer.MemoryCopy(pSrc, pComp, data.Length, data.Length); - } - } - } - }); - } - - public override bool CanMerge(UndoOperation other) - { - if (other is EntityComponentOperation op) - { - if (op.Entity == Entity && op.ComponentId == ComponentId) - { - // Explicit transaction merge - if (op.GroupId != 0 && op.GroupId == GroupId) - { - return true; - } - - // Time-based merge fallback for non-transactional continuous edits (e.g. 500ms) - if (op.GroupId == 0) - { - return Math.Abs((op.Timestamp - Timestamp).TotalMilliseconds) < 500; - } - } - } - return false; - } -} - -public class EntityStructureOperation : UndoOperation -{ - public Guid InstanceID { get; set; } - public Entity Entity { get; set; } - public int ArchetypeID { get; set; } - public byte[] ComponentData { get; set; } = Array.Empty(); - public byte[] SharedData { get; set; } = Array.Empty(); - public int SharedDataHash { get; set; } - - public unsafe static EntityStructureOperation Capture(IEditorWorldService worldService, EntityNode node) - { - var entity = node.Entity; - var op = new EntityStructureOperation { Entity = entity, InstanceID = node.InstanceID }; - var locRes = worldService.EditorWorld.EntityManager.GetEntityLocation(entity); - if (locRes.IsSuccess) - { - op.ArchetypeID = locRes.Value.archetypeID; - ref var archetype = ref worldService.EditorWorld.ComponentManager.GetArchetypeReference(op.ArchetypeID); - ref var chunk = ref archetype.GetChunkReference(locRes.Value.chunkIndex); - - // Compute size of all unmanaged components - var totalSize = 0; - for (var i = 0; i < archetype._layouts.Count; i++) - { - totalSize += archetype._layouts[i].size; - } - - var data = new byte[totalSize]; - fixed (byte* pDst = data) - { - var offset = 0; - for (var i = 0; i < archetype._layouts.Count; i++) - { - var layout = archetype._layouts[i]; - var pSrc = chunk.GetUnsafePtr() + layout.offset + (layout.size * locRes.Value.rowIndex); - Buffer.MemoryCopy(pSrc, pDst + offset, layout.size, layout.size); - offset += layout.size; - } - } - op.ComponentData = data; - - if (chunk._groupIndex >= 0 && chunk._groupIndex < archetype._chunkGroups.Count) - { - var group = archetype._chunkGroups[chunk._groupIndex]; - op.SharedData = group.sharedData.AsSpan().ToArray(); - op.SharedDataHash = group.sharedDataHash; - } - } - return op; - } - - public override UndoOperation CreateReciprocal(IEditorWorldService worldService) - { - if (GhostObject.Find(InstanceID) is not EntityNode node) - { - return this; - } - - var reciprocal = Capture(worldService, node); - reciprocal.GroupId = GroupId; - reciprocal.ActionName = ActionName; - return reciprocal; - } - - public unsafe override void Revert(IEditorWorldService worldService) - { - var instId = InstanceID; - var fallbackEntity = Entity; - var archId = ArchetypeID; - var compData = ComponentData; - var sharedData = SharedData; - var sharedHash = SharedDataHash; - - worldService.Defer(() => - { - var node = GhostObject.Find(instId) as EntityNode; - var targetEntity = node?.Entity ?? fallbackEntity; - - var world = worldService.EditorWorld; - var locRes = world.EntityManager.GetEntityLocation(targetEntity); - if (!locRes.IsSuccess) - { - return; // Entity destroyed? Should use Lifecycle undo for that. - } - - if (locRes.Value.archetypeID != archId) - { - ref var targetArchetype = ref world.ComponentManager.GetArchetypeReference(archId); - - // Build ComponentSetView from the target archetype - var it = targetArchetype._signature.GetIterator(); - var components = new List>(); - while (it.Next(out var compId)) - { - components.Add(new Identifier(compId)); - } - - var set = new ComponentSetView(components.ToArray(), sharedData ?? Array.Empty()); - world.EntityManager.MigrateEntity(targetEntity, set); - } - - - // Overwrite unmanaged memory - locRes = world.EntityManager.GetEntityLocation(targetEntity); - if (locRes.IsSuccess) - { - ref var archetype = ref world.ComponentManager.GetArchetypeReference(locRes.Value.archetypeID); - ref var chunk = ref archetype.GetChunkReference(locRes.Value.chunkIndex); - - fixed (byte* pSrcBase = compData) - { - var offset = 0; - for (var i = 0; i < archetype._layouts.Count; i++) - { - var layout = archetype._layouts[i]; - var pDst = chunk.GetUnsafePtr() + layout.offset + (layout.size * locRes.Value.rowIndex); - Buffer.MemoryCopy(pSrcBase + offset, pDst, layout.size, layout.size); - offset += layout.size; - } - } - } - }); - } - - public override bool CanMerge(UndoOperation other) - { - if (other is EntityStructureOperation op) - { - return op.Entity == Entity && op.GroupId == GroupId; - } - return false; - } -} - -public class EntityLifecycleOperation : UndoOperation -{ - public Entity Entity { get; set; } - public Guid InstanceID { get; set; } - public LifecycleEvent EventType { get; set; } - - // State for destruction - public int ArchetypeID { get; set; } - public byte[] ComponentData { get; set; } = Array.Empty(); - public byte[] SharedData { get; set; } = Array.Empty(); - public int SharedDataHash { get; set; } - - public override UndoOperation CreateReciprocal(IEditorWorldService worldService) - { - var reciprocal = new EntityLifecycleOperation - { - GroupId = GroupId, - ActionName = ActionName, - Entity = Entity, - InstanceID = InstanceID, - EventType = EventType == LifecycleEvent.Created ? LifecycleEvent.Destroyed : LifecycleEvent.Created, - ArchetypeID = ArchetypeID, - ComponentData = ComponentData, - SharedData = SharedData, - SharedDataHash = SharedDataHash - }; - return reciprocal; - } - - public override void Revert(IEditorWorldService worldService) - { - worldService.Defer(() => - { - if (EventType == LifecycleEvent.Created) - { - // Revert a Creation = Destroy - var node = GhostObject.Find(InstanceID) as EntityNode; - var targetEntity = node?.Entity ?? Entity; - worldService.EditorWorld.EntityManager.DestroyEntity(targetEntity); - // The InstanceID GhostObject will be naturally unlinked, handles become null - } - else - { - // Revert a Destruction = Recreate - var newEntity = worldService.EditorWorld.EntityManager.CreateEntity(); - - // TODO: Apply the ArchetypeID, ComponentData, SharedData to the newEntity. - // We'd add the components using the archetype signature, then memcopy the bytes. - - // Fix the Node reference - if (GhostObject.Find(InstanceID) is not EntityNode node) - { - node = new EntityNode(worldService.EditorWorld, newEntity, "Resurrected", null); - // Force the InstanceID using backing field - var backingField = typeof(EntityNode).GetField("k__BackingField", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) - ?? typeof(SceneGraphNode).GetField("k__BackingField", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - backingField?.SetValue(node, InstanceID); - } - else - { - // Update the entity property of the existing node (using reflection since it's init/readonly) - var entityField = typeof(EntityNode).GetField("k__BackingField", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - entityField?.SetValue(node, newEntity); - } - } - }); - } -} internal class UndoService : IUndoService { @@ -428,58 +147,30 @@ internal class UndoService : IUndoService PushOperation(op); } - // TODO: We may want to have unified api RecordObject that can handle everything. - public unsafe void RecordEntityComponent(ComponentNode node, string actionName) + + + public void RegisterCreatedObjectUndo(GhostObject obj, string actionName) { - var op = new EntityComponentOperation + var op = new ObjectStateOperation() { ActionName = actionName, - Entity = node.EntityNode.Entity, - ComponentId = node.Descriptor.ComponentId, - InstanceID = node.EntityNode.InstanceID + InstanceID = obj.InstanceID }; - - var pComp = node.GetComponentPointer(); - var size = node.Descriptor.Size; - var data = new byte[size]; - - fixed (byte* pDst = data) + // The object is created, so before its creation, it did NOT exist. + // We write a manual state payload indicating it does not exist. + // During Undo, DeserializeState will read this and destroy the object. + using var ms = new MemoryStream(); + using var writer = new BinaryWriter(ms); + if (obj is SceneGraph.SceneGraphNode sgn) { - Buffer.MemoryCopy(pComp, pDst, size, size); + writer.Write(sgn.Name); } - - op.ComponentData = data; - - PushOperation(op); - } - - public void RecordEntityStructure(EntityNode node, string actionName) - { - var op = EntityStructureOperation.Capture(_worldService, node); - op.ActionName = actionName; - PushOperation(op); - } - - public void RecordEntityLifecycle(EntityNode node, LifecycleEvent type) - { - var op = new EntityLifecycleOperation + else { - ActionName = type == LifecycleEvent.Created ? "Create Entity" : "Destroy Entity", - Entity = node.Entity, - InstanceID = node.InstanceID, - EventType = type - }; - - if (type == LifecycleEvent.Destroyed) - { - // Capture state before destruction - var structure = EntityStructureOperation.Capture(_worldService, node); - op.ArchetypeID = structure.ArchetypeID; - op.ComponentData = structure.ComponentData; - op.SharedData = structure.SharedData; - op.SharedDataHash = structure.SharedDataHash; + writer.Write(""); } - + writer.Write(false); // IsAlive = false + op.State = ms.ToArray(); PushOperation(op); } diff --git a/src/Editor/Ghost.Editor/Views/Controls/Hierarchy.xaml b/src/Editor/Ghost.Editor/Views/Controls/Hierarchy.xaml index 8005d78..d83f07c 100644 --- a/src/Editor/Ghost.Editor/Views/Controls/Hierarchy.xaml +++ b/src/Editor/Ghost.Editor/Views/Controls/Hierarchy.xaml @@ -23,6 +23,7 @@ + diff --git a/src/Editor/Ghost.Editor/Views/Controls/Hierarchy.xaml.cs b/src/Editor/Ghost.Editor/Views/Controls/Hierarchy.xaml.cs index 9b7cbb3..c46d926 100644 --- a/src/Editor/Ghost.Editor/Views/Controls/Hierarchy.xaml.cs +++ b/src/Editor/Ghost.Editor/Views/Controls/Hierarchy.xaml.cs @@ -165,6 +165,12 @@ public sealed partial class Hierarchy : UserControl } } + private async void OnSaveSceneClick(object sender, RoutedEventArgs e) + { + var assetRegistry = App.GetService(); + await assetRegistry.SaveDirtyAssetsAsync(); + } + private void OnCreateChildClick(object sender, RoutedEventArgs e) { if (sender is MenuFlyoutItem menuItem && menuItem.DataContext is EntityNode entityNode) diff --git a/src/Runtime/Ghost.Entities/Component.cs b/src/Runtime/Ghost.Entities/Component.cs index e0ff24a..b294894 100644 --- a/src/Runtime/Ghost.Entities/Component.cs +++ b/src/Runtime/Ghost.Entities/Component.cs @@ -58,7 +58,7 @@ internal static class ComponentRegistry // NOTE: Can we remove the lock? Ideally all the component registeration will happend during module init, way before the first get. private static readonly Lock s_registerLock = new(); -#if GHOST_EDITOR +#if DEBUG || GHOST_EDITOR internal static readonly Dictionary s_runtimeIDToType = new(); #endif @@ -91,7 +91,7 @@ internal static class ComponentRegistry s_typeHandleToID[typeHandle] = newID; s_nameToRuntimeID[stableName] = newID; -#if GHOST_EDITOR +#if DEBUG || GHOST_EDITOR s_runtimeIDToType[newID.Value] = typeof(T); #endif diff --git a/src/Runtime/Ghost.Entities/ManagedComponent.cs b/src/Runtime/Ghost.Entities/ManagedComponent.cs index bb7a44b..1ed194c 100644 --- a/src/Runtime/Ghost.Entities/ManagedComponent.cs +++ b/src/Runtime/Ghost.Entities/ManagedComponent.cs @@ -38,7 +38,7 @@ internal static class ManagedComponentRegistry private static readonly List s_registeredComponents = new(); private static readonly Dictionary s_typeHandleToID = new(); private static readonly Dictionary s_nameToRuntimeID = new(); -#if GHOST_EDITOR +#if DEBUG || GHOST_EDITOR internal static readonly Dictionary s_runtimeIDToType = new(); #endif @@ -66,7 +66,7 @@ internal static class ManagedComponentRegistry s_typeHandleToID[typeHandle] = newID; s_nameToRuntimeID[stableName] = newID; -#if GHOST_EDITOR +#if DEBUG || GHOST_EDITOR s_runtimeIDToType[newID.Value] = typeof(T); #endif diff --git a/src/Test/Ghost.UnitTest/UndoServiceEcsTests.cs b/src/Test/Ghost.UnitTest/UndoServiceEcsTests.cs index 3248a6e..f006b6d 100644 --- a/src/Test/Ghost.UnitTest/UndoServiceEcsTests.cs +++ b/src/Test/Ghost.UnitTest/UndoServiceEcsTests.cs @@ -40,7 +40,7 @@ public class UndoServiceEcsTests var node = new EntityNode(world, e, "TestEntity", null); _undoService.BeginTransaction("Add CompB"); - _undoService.RecordEntityStructure(node, "Before Add CompB"); + _undoService.RecordObject(node, "Before Add CompB"); // Modify structure world.EntityManager.AddComponent(e); @@ -82,7 +82,7 @@ public class UndoServiceEcsTests var node = new EntityNode(world, e, "TestEntity", null); _undoService.BeginTransaction("Create Entity"); - _undoService.RecordEntityLifecycle(node, LifecycleEvent.Created); + _undoService.RegisterCreatedObjectUndo(node, "Create Entity"); _undoService.EndTransaction(); // Undo Creation (Expect destruction) @@ -100,7 +100,7 @@ public class UndoServiceEcsTests // Step 2: Destroy Entity _undoService.BeginTransaction("Destroy Entity"); - _undoService.RecordEntityLifecycle(node, LifecycleEvent.Destroyed); + _undoService.RecordObject(node, "Destroy Entity"); world.EntityManager.DestroyEntity(resurrectedEntity); _undoService.EndTransaction();