osu/osu.Game/Screens/Edit/EditorChangeHandler.cs
2022-11-27 00:00:27 +09:00

132 lines
4.2 KiB
C#

// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
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
{
/// <summary>
/// Tracks changes to the <see cref="Editor"/>.
/// </summary>
public partial class EditorChangeHandler : TransactionalCommitComponent, IEditorChangeHandler
{
public readonly Bindable<bool> CanUndo = new Bindable<bool>();
public readonly Bindable<bool> CanRedo = new Bindable<bool>();
public event Action OnStateChange;
private readonly LegacyEditorBeatmapPatcher patcher;
private readonly List<byte[]> savedStates = new List<byte[]>();
private int currentState = -1;
/// <summary>
/// A SHA-2 hash representing the current visible editor state.
/// </summary>
public string CurrentStateHash
{
get
{
using (var stream = new MemoryStream(savedStates[currentState]))
return stream.ComputeSHA2Hash();
}
}
private readonly EditorBeatmap editorBeatmap;
private bool isRestoring;
public const int MAX_SAVED_STATES = 50;
/// <summary>
/// Creates a new <see cref="EditorChangeHandler"/>.
/// </summary>
/// <param name="editorBeatmap">The <see cref="EditorBeatmap"/> to track the <see cref="HitObject"/>s of.</param>
public EditorChangeHandler(EditorBeatmap editorBeatmap)
{
this.editorBeatmap = editorBeatmap;
editorBeatmap.TransactionBegan += BeginChange;
editorBeatmap.TransactionEnded += EndChange;
editorBeatmap.SaveStateTriggered += SaveState;
patcher = new LegacyEditorBeatmapPatcher(editorBeatmap);
// Initial state.
SaveState();
}
protected override void UpdateState()
{
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);
byte[] 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[currentState])) 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;
OnStateChange?.Invoke();
updateBindables();
}
}
/// <summary>
/// Restores an older or newer state.
/// </summary>
/// <param name="direction">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.</param>
public void RestoreState(int direction)
{
if (TransactionActive)
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;
OnStateChange?.Invoke();
updateBindables();
}
private void updateBindables()
{
CanUndo.Value = savedStates.Count > 0 && currentState > 0;
CanRedo.Value = currentState < savedStates.Count - 1;
}
}
}