feat(undo) unified undo redo api.
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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<Services.SceneGraphSyncService>(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<IComponent>(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("<Entity>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<Ghost.Core.Identifier<IComponent>>();
|
||||
var componentDataMap = new Dictionary<int, byte[]>();
|
||||
|
||||
EditorApplication.TryGetService<Services.SceneGraphSyncService>(out var syncService);
|
||||
|
||||
for (var i = 0; i < componentCount; i++)
|
||||
{
|
||||
var typeIdVal = reader.ReadInt32();
|
||||
var size = reader.ReadInt32();
|
||||
var typeId = new Ghost.Core.Identifier<IComponent>(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<byte>();
|
||||
|
||||
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();
|
||||
|
||||
58
src/Editor/Ghost.Editor.Core/Services/EntityFieldTracker.cs
Normal file
58
src/Editor/Ghost.Editor.Core/Services/EntityFieldTracker.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using Ghost.Entities;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ghost.Editor.Core.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
internal static class EntityFieldTracker
|
||||
{
|
||||
private static readonly Dictionary<int, int[]> 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<int>();
|
||||
return Array.Empty<int>();
|
||||
}
|
||||
|
||||
var offsetList = new List<int>();
|
||||
FindEntityFieldsRecursive(type, 0, offsetList);
|
||||
|
||||
offsets = offsetList.ToArray();
|
||||
s_entityOffsets[componentId] = offsets;
|
||||
return offsets;
|
||||
}
|
||||
}
|
||||
|
||||
private static void FindEntityFieldsRecursive(Type type, int currentOffset, List<int> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<byte>();
|
||||
|
||||
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<IComponent>(ComponentId));
|
||||
if (pComp != null)
|
||||
{
|
||||
var size = ComponentRegistry.GetComponentInfo(new Identifier<IComponent>(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<IComponent>(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<byte>();
|
||||
public byte[] SharedData { get; set; } = Array.Empty<byte>();
|
||||
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<Identifier<IComponent>>();
|
||||
while (it.Next(out var compId))
|
||||
{
|
||||
components.Add(new Identifier<IComponent>(compId));
|
||||
}
|
||||
|
||||
var set = new ComponentSetView(components.ToArray(), sharedData ?? Array.Empty<byte>());
|
||||
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<byte>();
|
||||
public byte[] SharedData { get; set; } = Array.Empty<byte>();
|
||||
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("<InstanceID>k__BackingField", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
|
||||
?? typeof(SceneGraphNode).GetField("<InstanceID>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("<Entity>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);
|
||||
else
|
||||
{
|
||||
writer.Write("");
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
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(false); // IsAlive = false
|
||||
op.State = ms.ToArray();
|
||||
PushOperation(op);
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
<TreeViewItem.ContextFlyout>
|
||||
<MenuFlyout>
|
||||
<MenuFlyoutItem Click="OnCreateEntityClick" Text="Create Entity" />
|
||||
<MenuFlyoutItem Click="OnSaveSceneClick" Text="Save Scene" />
|
||||
</MenuFlyout>
|
||||
</TreeViewItem.ContextFlyout>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
|
||||
@@ -165,6 +165,12 @@ public sealed partial class Hierarchy : UserControl
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnSaveSceneClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var assetRegistry = App.GetService<IAssetRegistry>();
|
||||
await assetRegistry.SaveDirtyAssetsAsync();
|
||||
}
|
||||
|
||||
private void OnCreateChildClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is MenuFlyoutItem menuItem && menuItem.DataContext is EntityNode entityNode)
|
||||
|
||||
@@ -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<int, Type> 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
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ internal static class ManagedComponentRegistry
|
||||
private static readonly List<ManagedComponentInfo> s_registeredComponents = new();
|
||||
private static readonly Dictionary<IntPtr, int> s_typeHandleToID = new();
|
||||
private static readonly Dictionary<string, int> s_nameToRuntimeID = new();
|
||||
#if GHOST_EDITOR
|
||||
#if DEBUG || GHOST_EDITOR
|
||||
internal static readonly Dictionary<int, Type> 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
|
||||
|
||||
|
||||
@@ -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<CompB>(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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user