using System; using System.Collections.Generic; using System.Linq; using Ichni.RhythmGame; using Ichni.RhythmGame.Beatmap; using UnityEngine; namespace Ichni.Editor.Commands { internal sealed class ElementTreeSnapshot { private readonly List elements = new List(); 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.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() .FirstOrDefault(gameElementBM => gameElementBM.elementGuid == rootGuid); if (rootBM != null) { rootBM.attachedElementGuid = parentGuid; } } private void RegisterExistingAncestorChain(GameElement parent) { if (parent == null) return; List ancestors = new List(); 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 GameElement source; private readonly GameElement parent; private ElementTreeSnapshot snapshot; private GameElement pastedRoot; public PasteElementCommand(CopyPasteDeleteModule module, GameElement source, GameElement parent) { this.module = module; this.source = source; this.parent = parent; } public void Execute() { pastedRoot = module.PasteElementRaw(source, parent); snapshot ??= ElementTreeSnapshot.Capture(pastedRoot); } public void Undo() { if (pastedRoot == null) return; module.DeleteElementRaw(pastedRoot, false); pastedRoot = null; } public void Redo() { pastedRoot = snapshot?.Restore(); } public bool TryMerge(ICommand other) => false; } public sealed class CreateElementCommand : ICommand { private readonly Func createAction; private ElementTreeSnapshot snapshot; public GameElement CreatedElement { get; private set; } public CreateElementCommand(Func 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; } }