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");
|
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(() =>
|
_worldService.Defer(() =>
|
||||||
{
|
{
|
||||||
if (Descriptor.IsShared)
|
if (Descriptor.IsShared)
|
||||||
@@ -99,7 +99,7 @@ public unsafe class ComponentNode
|
|||||||
throw new ArgumentException("Value type does not match component type");
|
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(() =>
|
_worldService.Defer(() =>
|
||||||
{
|
{
|
||||||
if (Descriptor.IsShared)
|
if (Descriptor.IsShared)
|
||||||
|
|||||||
@@ -24,6 +24,199 @@ public sealed partial class EntityNode : SceneGraphNode
|
|||||||
|
|
||||||
public override SceneNode? GetOwningSceneNode() => SceneNode;
|
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()
|
public void BuildComponents()
|
||||||
{
|
{
|
||||||
Components.Clear();
|
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;
|
event Action? UndoRedoPerformed;
|
||||||
|
|
||||||
void RecordObject(GhostObject obj, string actionName);
|
void RecordObject(GhostObject obj, string actionName);
|
||||||
void RecordEntityComponent(ComponentNode node, string actionName);
|
void RegisterCreatedObjectUndo(GhostObject obj, string actionName);
|
||||||
void RecordEntityStructure(EntityNode node, string actionName);
|
|
||||||
void RecordEntityLifecycle(EntityNode node, LifecycleEvent type);
|
|
||||||
|
|
||||||
void BeginTransaction(string name);
|
void BeginTransaction(string name);
|
||||||
void EndTransaction();
|
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
|
internal class UndoService : IUndoService
|
||||||
{
|
{
|
||||||
@@ -428,58 +147,30 @@ internal class UndoService : IUndoService
|
|||||||
PushOperation(op);
|
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,
|
ActionName = actionName,
|
||||||
Entity = node.EntityNode.Entity,
|
InstanceID = obj.InstanceID
|
||||||
ComponentId = node.Descriptor.ComponentId,
|
|
||||||
InstanceID = node.EntityNode.InstanceID
|
|
||||||
};
|
};
|
||||||
|
// The object is created, so before its creation, it did NOT exist.
|
||||||
var pComp = node.GetComponentPointer();
|
// We write a manual state payload indicating it does not exist.
|
||||||
var size = node.Descriptor.Size;
|
// During Undo, DeserializeState will read this and destroy the object.
|
||||||
var data = new byte[size];
|
using var ms = new MemoryStream();
|
||||||
|
using var writer = new BinaryWriter(ms);
|
||||||
fixed (byte* pDst = data)
|
if (obj is SceneGraph.SceneGraphNode sgn)
|
||||||
{
|
{
|
||||||
Buffer.MemoryCopy(pComp, pDst, size, size);
|
writer.Write(sgn.Name);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
op.ComponentData = data;
|
{
|
||||||
|
writer.Write("");
|
||||||
PushOperation(op);
|
|
||||||
}
|
}
|
||||||
|
writer.Write(false); // IsAlive = false
|
||||||
public void RecordEntityStructure(EntityNode node, string actionName)
|
op.State = ms.ToArray();
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
PushOperation(op);
|
PushOperation(op);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
<TreeViewItem.ContextFlyout>
|
<TreeViewItem.ContextFlyout>
|
||||||
<MenuFlyout>
|
<MenuFlyout>
|
||||||
<MenuFlyoutItem Click="OnCreateEntityClick" Text="Create Entity" />
|
<MenuFlyoutItem Click="OnCreateEntityClick" Text="Create Entity" />
|
||||||
|
<MenuFlyoutItem Click="OnSaveSceneClick" Text="Save Scene" />
|
||||||
</MenuFlyout>
|
</MenuFlyout>
|
||||||
</TreeViewItem.ContextFlyout>
|
</TreeViewItem.ContextFlyout>
|
||||||
<StackPanel Orientation="Horizontal">
|
<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)
|
private void OnCreateChildClick(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (sender is MenuFlyoutItem menuItem && menuItem.DataContext is EntityNode entityNode)
|
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.
|
// 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();
|
private static readonly Lock s_registerLock = new();
|
||||||
|
|
||||||
#if GHOST_EDITOR
|
#if DEBUG || GHOST_EDITOR
|
||||||
internal static readonly Dictionary<int, Type> s_runtimeIDToType = new();
|
internal static readonly Dictionary<int, Type> s_runtimeIDToType = new();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ internal static class ComponentRegistry
|
|||||||
|
|
||||||
s_typeHandleToID[typeHandle] = newID;
|
s_typeHandleToID[typeHandle] = newID;
|
||||||
s_nameToRuntimeID[stableName] = newID;
|
s_nameToRuntimeID[stableName] = newID;
|
||||||
#if GHOST_EDITOR
|
#if DEBUG || GHOST_EDITOR
|
||||||
s_runtimeIDToType[newID.Value] = typeof(T);
|
s_runtimeIDToType[newID.Value] = typeof(T);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ internal static class ManagedComponentRegistry
|
|||||||
private static readonly List<ManagedComponentInfo> s_registeredComponents = new();
|
private static readonly List<ManagedComponentInfo> s_registeredComponents = new();
|
||||||
private static readonly Dictionary<IntPtr, int> s_typeHandleToID = new();
|
private static readonly Dictionary<IntPtr, int> s_typeHandleToID = new();
|
||||||
private static readonly Dictionary<string, int> s_nameToRuntimeID = 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();
|
internal static readonly Dictionary<int, Type> s_runtimeIDToType = new();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ internal static class ManagedComponentRegistry
|
|||||||
|
|
||||||
s_typeHandleToID[typeHandle] = newID;
|
s_typeHandleToID[typeHandle] = newID;
|
||||||
s_nameToRuntimeID[stableName] = newID;
|
s_nameToRuntimeID[stableName] = newID;
|
||||||
#if GHOST_EDITOR
|
#if DEBUG || GHOST_EDITOR
|
||||||
s_runtimeIDToType[newID.Value] = typeof(T);
|
s_runtimeIDToType[newID.Value] = typeof(T);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ public class UndoServiceEcsTests
|
|||||||
var node = new EntityNode(world, e, "TestEntity", null);
|
var node = new EntityNode(world, e, "TestEntity", null);
|
||||||
|
|
||||||
_undoService.BeginTransaction("Add CompB");
|
_undoService.BeginTransaction("Add CompB");
|
||||||
_undoService.RecordEntityStructure(node, "Before Add CompB");
|
_undoService.RecordObject(node, "Before Add CompB");
|
||||||
|
|
||||||
// Modify structure
|
// Modify structure
|
||||||
world.EntityManager.AddComponent<CompB>(e);
|
world.EntityManager.AddComponent<CompB>(e);
|
||||||
@@ -82,7 +82,7 @@ public class UndoServiceEcsTests
|
|||||||
var node = new EntityNode(world, e, "TestEntity", null);
|
var node = new EntityNode(world, e, "TestEntity", null);
|
||||||
|
|
||||||
_undoService.BeginTransaction("Create Entity");
|
_undoService.BeginTransaction("Create Entity");
|
||||||
_undoService.RecordEntityLifecycle(node, LifecycleEvent.Created);
|
_undoService.RegisterCreatedObjectUndo(node, "Create Entity");
|
||||||
_undoService.EndTransaction();
|
_undoService.EndTransaction();
|
||||||
|
|
||||||
// Undo Creation (Expect destruction)
|
// Undo Creation (Expect destruction)
|
||||||
@@ -100,7 +100,7 @@ public class UndoServiceEcsTests
|
|||||||
|
|
||||||
// Step 2: Destroy Entity
|
// Step 2: Destroy Entity
|
||||||
_undoService.BeginTransaction("Destroy Entity");
|
_undoService.BeginTransaction("Destroy Entity");
|
||||||
_undoService.RecordEntityLifecycle(node, LifecycleEvent.Destroyed);
|
_undoService.RecordObject(node, "Destroy Entity");
|
||||||
world.EntityManager.DestroyEntity(resurrectedEntity);
|
world.EntityManager.DestroyEntity(resurrectedEntity);
|
||||||
_undoService.EndTransaction();
|
_undoService.EndTransaction();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user