Files
ichni_Creator_Studio/Assets/Scripts/CommandSystem/GameElementStructuralCommands.cs
2026-07-29 20:14:03 +08:00

558 lines
19 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using Ichni.RhythmGame;
using Ichni.RhythmGame.Beatmap;
using UnityEngine;
namespace Ichni.Editor.Commands
{
public sealed class ElementPasteResult
{
public List<GameElement> RootElements { get; } = new List<GameElement>();
public List<GameElement> AllElements { get; } = new List<GameElement>();
public Dictionary<Guid, GameElement> PastedElementBySourceGuid { get; } = new Dictionary<Guid, GameElement>();
}
public sealed class ElementClipboardSnapshot
{
private readonly List<BaseElement_BM> elements;
private readonly List<Guid> rootGuids;
private ElementClipboardSnapshot(List<BaseElement_BM> elements, List<Guid> rootGuids)
{
this.elements = elements;
this.rootGuids = rootGuids;
}
public IReadOnlyList<Guid> RootGuids => rootGuids;
public int RootCount => rootGuids.Count;
public static ElementClipboardSnapshot CaptureSingle(GameElement root)
{
if (root == null) return null;
List<GameElement> roots = new List<GameElement> { root };
return CaptureRoots(roots);
}
public static bool TryCaptureSiblingRoots(
IEnumerable<GameElement> selectedElements,
out ElementClipboardSnapshot snapshot,
out List<GameElement> orderedRoots,
out string errorMessage)
{
snapshot = null;
errorMessage = null;
orderedRoots = GetDistinctElements(selectedElements);
if (orderedRoots.Count == 0)
{
errorMessage = "No element selected to copy.";
return false;
}
GameElement parent = orderedRoots[0].parentElement;
if (orderedRoots.Any(element => element.parentElement != parent))
{
errorMessage = "Batch copy requires sibling elements.";
return false;
}
orderedRoots = OrderSiblings(orderedRoots, parent);
snapshot = CaptureRoots(orderedRoots);
return snapshot != null;
}
public static List<GameElement> GetDistinctElements(IEnumerable<GameElement> elements)
{
if (elements == null) return new List<GameElement>();
HashSet<GameElement> seen = new HashSet<GameElement>();
List<GameElement> result = new List<GameElement>();
foreach (GameElement element in elements)
{
if (element == null || !seen.Add(element)) continue;
result.Add(element);
}
return result;
}
public static List<GameElement> GetTopLevelElements(IEnumerable<GameElement> elements)
{
List<GameElement> distinctElements = GetDistinctElements(elements);
HashSet<GameElement> selectedSet = new HashSet<GameElement>(distinctElements);
List<GameElement> roots = distinctElements
.Where(element => !HasSelectedAncestor(element, selectedSet))
.ToList();
return roots
.OrderBy(element => element.parentElement == null
? 0
: element.parentElement.childElementList.IndexOf(element))
.ToList();
}
public ElementPasteResult PasteTo(GameElement parent)
{
ElementPasteResult result = new ElementPasteResult();
if (parent == null || elements.Count == 0) return result;
List<BaseElement_BM> workingElements = CloneElements(elements);
foreach (Guid rootGuid in rootGuids)
{
GameElement_BM rootBM = workingElements
.OfType<GameElement_BM>()
.FirstOrDefault(elementBM => elementBM.elementGuid == rootGuid);
GameElement pastedRoot = PasteGameElement(rootBM, parent, workingElements, result);
if (pastedRoot != null)
{
result.RootElements.Add(pastedRoot);
}
}
ResolvePastedReferences(result.AllElements, result.PastedElementBySourceGuid);
foreach (GameElement pasted in result.AllElements)
{
if (pasted == null) continue;
pasted.AfterInitialize();
pasted.Refresh();
}
return result;
}
private static ElementClipboardSnapshot CaptureRoots(IReadOnlyList<GameElement> roots)
{
if (roots == null || roots.Count == 0) return null;
List<BaseElement_BM> capturedElements = new List<BaseElement_BM>();
foreach (GameElement root in roots)
{
foreach (GameElement element in root.GetAllGameElementsFromThis())
{
if (element == null) continue;
element.SaveBM();
if (element.matchedBM != null)
{
capturedElements.Add(element.matchedBM);
}
element.submoduleList.RemoveAll(submodule => submodule == null);
foreach (SubmoduleBase submodule in element.submoduleList)
{
submodule.SaveBM();
if (submodule.matchedBM != null)
{
capturedElements.Add(submodule.matchedBM);
}
}
}
}
if (capturedElements.Count == 0) return null;
List<BaseElement_BM> snapshotElements = CloneElements(capturedElements);
List<Guid> snapshotRootGuids = roots.Select(root => root.elementGuid).ToList();
return new ElementClipboardSnapshot(snapshotElements, snapshotRootGuids);
}
private static List<BaseElement_BM> CloneElements(IReadOnlyList<BaseElement_BM> source)
{
if (source == null || source.Count == 0) return new List<BaseElement_BM>();
byte[] bytes = ES3.Serialize(source.ToList(), global::Ichni.ProjectManager.SaveSettings);
return ES3.Deserialize<List<BaseElement_BM>>(bytes, global::Ichni.ProjectManager.SaveSettings);
}
private static GameElement PasteGameElement(
GameElement_BM sourceBM,
GameElement parent,
List<BaseElement_BM> workingElements,
ElementPasteResult result)
{
if (sourceBM == null || parent == null) return null;
GameElement pastedElement = sourceBM.DuplicateBM(parent);
if (pastedElement == null)
{
LogWindow.Log("Paste failed: " + sourceBM.elementName + " could not be duplicated.", Color.red);
return null;
}
result.AllElements.Add(pastedElement);
result.PastedElementBySourceGuid[sourceBM.elementGuid] = pastedElement;
DuplicateAttachedSubmodules(sourceBM, pastedElement, workingElements);
IEnumerable<GameElement_BM> childBMs = workingElements
.OfType<GameElement_BM>()
.Where(gameElementBM => gameElementBM.attachedElementGuid == sourceBM.elementGuid);
foreach (GameElement_BM childBM in childBMs)
{
PasteGameElement(childBM, pastedElement, workingElements, result);
}
return pastedElement;
}
private static void DuplicateAttachedSubmodules(
GameElement_BM sourceBM,
GameElement pastedElement,
List<BaseElement_BM> workingElements)
{
IEnumerable<Submodule_BM> submoduleBMs = workingElements
.OfType<Submodule_BM>()
.Where(submoduleBM => submoduleBM.attachedElementGuid == sourceBM.elementGuid);
foreach (Submodule_BM submoduleBM in submoduleBMs)
{
SubmoduleBase existingSubmodule = pastedElement.submoduleList
.FirstOrDefault(submodule => submodule != null && submodule.GetType() == submoduleBM.GetType());
existingSubmodule?.Delete();
try
{
submoduleBM.DuplicateBM(pastedElement);
}
catch (Exception ex)
{
Debug.LogWarning("Submodule paste error: " + submoduleBM.GetType() + "\n" + ex);
}
}
}
private static void ResolvePastedReferences(
List<GameElement> pastedElements,
IReadOnlyDictionary<Guid, GameElement> pastedElementBySourceGuid)
{
if (pastedElementBySourceGuid == null || pastedElementBySourceGuid.Count == 0) return;
foreach (GameElement pastedElement in pastedElements)
{
if (pastedElement is IPastedReferenceResolver referenceResolver)
{
referenceResolver.ResolvePastedReferences(pastedElementBySourceGuid);
}
}
}
private static List<GameElement> OrderSiblings(List<GameElement> roots, GameElement parent)
{
if (parent == null) return roots;
return roots
.OrderBy(element => parent.childElementList.IndexOf(element))
.ToList();
}
private static bool HasSelectedAncestor(GameElement element, HashSet<GameElement> selectedSet)
{
for (GameElement parent = element.parentElement; parent != null; parent = parent.parentElement)
{
if (selectedSet.Contains(parent)) return true;
}
return false;
}
}
internal sealed class ElementTreeSnapshot
{
private readonly List<BaseElement_BM> elements = new List<BaseElement_BM>();
private readonly Guid rootGuid;
private readonly Guid parentGuid;
private readonly int parentChildIndex;
private ElementTreeSnapshot(GameElement root)
{
rootGuid = root.elementGuid;
parentGuid = root.parentElement?.elementGuid ?? Guid.Empty;
parentChildIndex = root.parentElement != null
? root.parentElement.childElementList.IndexOf(root)
: -1;
RegisterExistingAncestorChain(root.parentElement);
foreach (GameElement element in root.GetAllGameElementsFromThis())
{
if (element == null) continue;
element.SaveBM();
if (element.matchedBM != null)
elements.Add(element.matchedBM);
element.submoduleList.RemoveAll(submodule => submodule == null);
foreach (SubmoduleBase submodule in element.submoduleList)
{
submodule.SaveBM();
if (submodule.matchedBM != null)
elements.Add(submodule.matchedBM);
}
}
}
public static ElementTreeSnapshot Capture(GameElement root)
{
return root == null ? null : new ElementTreeSnapshot(root);
}
public GameElement Restore()
{
if (elements.Count == 0) return null;
GameElement parent = parentGuid == Guid.Empty ? null : FindExistingElement(parentGuid);
RegisterExistingAncestorChain(parent);
RepairRootAttachment();
foreach (GameElement_BM gameElementBM in elements.OfType<GameElement_BM>())
{
GameElement_BM.identifier[gameElementBM.elementGuid] = gameElementBM;
}
foreach (BaseElement_BM elementBM in elements)
{
elementBM.ExecuteBM();
}
EditorManager.instance.beatmapContainer.ExecuteLowPriorityActions();
GameElement root = GameElement_BM.GetElement(rootGuid);
if (root == null) return null;
RestoreParentOrder(root, parent);
foreach (GameElement element in root.GetAllGameElementsFromThis())
{
if (element == null) continue;
element.AfterInitialize();
element.Refresh();
}
EditorManager.instance.uiManager.hierarchy.FindTab(root);
return root;
}
private GameElement FindExistingElement(Guid elementGuid)
{
GameElement element = GameElement_BM.GetElement(elementGuid);
if (element != null) return element;
return EditorManager.instance.beatmapContainer.gameElementList
.FirstOrDefault(gameElement => gameElement != null && gameElement.elementGuid == elementGuid);
}
private void RepairRootAttachment()
{
if (parentGuid == Guid.Empty) return;
GameElement_BM rootBM = elements
.OfType<GameElement_BM>()
.FirstOrDefault(gameElementBM => gameElementBM.elementGuid == rootGuid);
if (rootBM != null)
{
rootBM.attachedElementGuid = parentGuid;
}
}
private void RegisterExistingAncestorChain(GameElement parent)
{
if (parent == null) return;
List<GameElement> ancestors = new List<GameElement>();
for (GameElement current = parent; current != null; current = current.parentElement)
{
ancestors.Add(current);
}
ancestors.Reverse();
foreach (GameElement ancestor in ancestors)
{
ancestor.SaveBM();
if (ancestor.matchedBM is not GameElement_BM ancestorBM) continue;
GameElement_BM.identifier[ancestor.elementGuid] = ancestorBM;
ancestorBM.matchedElement = ancestor;
}
}
private void RestoreParentOrder(GameElement root, GameElement parent)
{
if (parent == null || parentChildIndex < 0) return;
parent.childElementList.Remove(root);
int insertIndex = Mathf.Clamp(parentChildIndex, 0, parent.childElementList.Count);
parent.childElementList.Insert(insertIndex, root);
root.transform.SetSiblingIndex(insertIndex);
parent.Refresh();
}
}
public sealed class DeleteElementCommand : ICommand
{
private readonly ElementTreeSnapshot snapshot;
private GameElement target;
public DeleteElementCommand(GameElement target)
{
this.target = target;
snapshot = ElementTreeSnapshot.Capture(target);
}
public void Execute()
{
DeleteCurrentTarget();
}
public void Undo()
{
target = snapshot?.Restore();
}
public void Redo()
{
DeleteCurrentTarget();
}
public bool TryMerge(ICommand other) => false;
private void DeleteCurrentTarget()
{
if (target == null) return;
EditorManager.instance.operationManager.CopyPasteDeleteModule.DeleteElementRaw(target);
target = null;
}
}
public sealed class PasteElementCommand : ICommand
{
private readonly CopyPasteDeleteModule module;
private readonly ElementClipboardSnapshot snapshot;
private readonly GameElement parent;
private List<ElementTreeSnapshot> restoreSnapshots;
private List<GameElement> pastedRoots = new List<GameElement>();
public PasteElementCommand(CopyPasteDeleteModule module, ElementClipboardSnapshot snapshot, GameElement parent)
{
this.module = module;
this.snapshot = snapshot;
this.parent = parent;
}
public void Execute()
{
pastedRoots = module.PasteElementsRaw(snapshot, parent);
restoreSnapshots ??= pastedRoots
.Select(ElementTreeSnapshot.Capture)
.Where(elementSnapshot => elementSnapshot != null)
.ToList();
}
public void Undo()
{
for (int i = pastedRoots.Count - 1; i >= 0; i--)
{
module.DeleteElementRaw(pastedRoots[i], false);
}
pastedRoots.Clear();
}
public void Redo()
{
pastedRoots = restoreSnapshots == null
? new List<GameElement>()
: restoreSnapshots
.Select(elementSnapshot => elementSnapshot.Restore())
.Where(element => element != null)
.ToList();
}
public bool TryMerge(ICommand other) => false;
}
public sealed class CreateElementCommand : ICommand
{
private readonly Func<GameElement> createAction;
private ElementTreeSnapshot snapshot;
public GameElement CreatedElement { get; private set; }
public CreateElementCommand(Func<GameElement> createAction)
{
this.createAction = createAction;
}
public void Execute()
{
CreatedElement = createAction?.Invoke();
snapshot ??= ElementTreeSnapshot.Capture(CreatedElement);
}
public void Undo()
{
if (CreatedElement == null) return;
EditorManager.instance.operationManager.CopyPasteDeleteModule.DeleteElementRaw(CreatedElement, false);
CreatedElement = null;
}
public void Redo()
{
CreatedElement = snapshot?.Restore();
}
public bool TryMerge(ICommand other) => false;
}
public sealed class CreateElementsCommand : ICommand
{
private readonly Func<List<GameElement>> createAction;
private List<ElementTreeSnapshot> snapshots;
private List<GameElement> createdElements = new List<GameElement>();
public IReadOnlyList<GameElement> CreatedElements => createdElements;
public CreateElementsCommand(Func<List<GameElement>> createAction)
{
this.createAction = createAction;
}
public void Execute()
{
List<GameElement> rawCreatedElements = createAction?.Invoke() ?? new List<GameElement>();
createdElements = ElementClipboardSnapshot.GetTopLevelElements(rawCreatedElements);
snapshots ??= createdElements
.Select(ElementTreeSnapshot.Capture)
.Where(snapshot => snapshot != null)
.ToList();
}
public void Undo()
{
for (int i = createdElements.Count - 1; i >= 0; i--)
{
EditorManager.instance.operationManager.CopyPasteDeleteModule.DeleteElementRaw(createdElements[i], false);
}
createdElements.Clear();
}
public void Redo()
{
createdElements = snapshots == null
? new List<GameElement>()
: snapshots
.Select(snapshot => snapshot.Restore())
.Where(element => element != null)
.ToList();
}
public bool TryMerge(ICommand other) => false;
}
}