558 lines
19 KiB
C#
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;
|
|
}
|
|
}
|