// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Game.Beatmaps.Formats; using osu.Game.Rulesets.Objects; namespace osu.Game.Screens.Edit { /// /// Tracks changes to the . /// public class EditorChangeHandler : IEditorChangeHandler { public readonly Bindable CanUndo = new Bindable(); public readonly Bindable CanRedo = new Bindable(); private readonly LegacyEditorBeatmapPatcher patcher; private readonly List savedStates = new List(); private int currentState = -1; /// /// A SHA-2 hash representing the current visible editor state. /// public string CurrentStateHash { get { using (var stream = new MemoryStream(savedStates[currentState])) return stream.ComputeSHA2Hash(); } } private readonly EditorBeatmap editorBeatmap; private int bulkChangesStarted; private bool isRestoring; public const int MAX_SAVED_STATES = 50; /// /// Creates a new . /// /// The to track the s of. public EditorChangeHandler(EditorBeatmap editorBeatmap) { this.editorBeatmap = editorBeatmap; editorBeatmap.HitObjectAdded += hitObjectAdded; editorBeatmap.HitObjectRemoved += hitObjectRemoved; editorBeatmap.HitObjectUpdated += hitObjectUpdated; patcher = new LegacyEditorBeatmapPatcher(editorBeatmap); // Initial state. SaveState(); } private void hitObjectAdded(HitObject obj) => SaveState(); private void hitObjectRemoved(HitObject obj) => SaveState(); private void hitObjectUpdated(HitObject obj) => SaveState(); public void BeginChange() => bulkChangesStarted++; public void EndChange() { if (bulkChangesStarted == 0) throw new InvalidOperationException($"Cannot call {nameof(EndChange)} without a previous call to {nameof(BeginChange)}."); if (--bulkChangesStarted == 0) SaveState(); } public void SaveState() { if (bulkChangesStarted > 0) return; if (isRestoring) return; using (var stream = new MemoryStream()) { using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) new LegacyBeatmapEncoder(editorBeatmap, editorBeatmap.BeatmapSkin).Encode(sw); var newState = stream.ToArray(); // if the previous state is binary equal we don't need to push a new one, unless this is the initial state. if (savedStates.Count > 0 && newState.SequenceEqual(savedStates.Last())) return; if (currentState < savedStates.Count - 1) savedStates.RemoveRange(currentState + 1, savedStates.Count - currentState - 1); if (savedStates.Count > MAX_SAVED_STATES) savedStates.RemoveAt(0); savedStates.Add(newState); currentState = savedStates.Count - 1; updateBindables(); } } /// /// Restores an older or newer state. /// /// The direction to restore in. If less than 0, an older state will be used. If greater than 0, a newer state will be used. public void RestoreState(int direction) { if (bulkChangesStarted > 0) return; if (savedStates.Count == 0) return; int newState = Math.Clamp(currentState + direction, 0, savedStates.Count - 1); if (currentState == newState) return; isRestoring = true; patcher.Patch(savedStates[currentState], savedStates[newState]); currentState = newState; isRestoring = false; updateBindables(); } private void updateBindables() { CanUndo.Value = savedStates.Count > 0 && currentState > 0; CanRedo.Value = currentState < savedStates.Count - 1; } } }