2020-04-09 20:22:07 +08:00
// 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.
using System ;
using System.Collections.Generic ;
using System.IO ;
2020-09-28 14:30:18 +08:00
using System.Linq ;
2020-04-09 20:22:07 +08:00
using System.Text ;
2020-04-15 14:50:43 +08:00
using osu.Framework.Bindables ;
2020-09-09 18:40:41 +08:00
using osu.Framework.Extensions ;
2020-04-09 20:22:07 +08:00
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 class EditorChangeHandler : IEditorChangeHandler
{
2020-04-15 14:50:43 +08:00
public readonly Bindable < bool > CanUndo = new Bindable < bool > ( ) ;
public readonly Bindable < bool > CanRedo = new Bindable < bool > ( ) ;
2020-04-13 16:18:50 +08:00
2020-10-02 14:44:32 +08:00
public event Action OnStateChange ;
2020-04-15 14:50:43 +08:00
private readonly LegacyEditorBeatmapPatcher patcher ;
2020-04-13 16:18:50 +08:00
private readonly List < byte [ ] > savedStates = new List < byte [ ] > ( ) ;
2020-04-09 20:22:07 +08:00
private int currentState = - 1 ;
2020-09-09 18:40:41 +08:00
/// <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 ( ) ;
}
}
2020-04-09 20:22:07 +08:00
private readonly EditorBeatmap editorBeatmap ;
private int bulkChangesStarted ;
private bool isRestoring ;
2020-04-13 19:34:18 +08:00
public const int MAX_SAVED_STATES = 50 ;
2020-04-09 20:22:07 +08:00
/// <summary>
/// Creates a new <see cref="EditorChangeHandler"/>.
/// </summary>
/// <param name="editorBeatmap">The <see cref="EditorBeatmap"/> to track the <see cref="HitObject"/>s of.</param>
2020-08-31 03:12:45 +08:00
public EditorChangeHandler ( EditorBeatmap editorBeatmap )
2020-04-09 20:22:07 +08:00
{
this . editorBeatmap = editorBeatmap ;
editorBeatmap . HitObjectAdded + = hitObjectAdded ;
editorBeatmap . HitObjectRemoved + = hitObjectRemoved ;
editorBeatmap . HitObjectUpdated + = hitObjectUpdated ;
2020-04-13 16:20:01 +08:00
patcher = new LegacyEditorBeatmapPatcher ( editorBeatmap ) ;
2020-04-09 20:22:07 +08:00
// 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 ;
2020-04-13 16:18:50 +08:00
using ( var stream = new MemoryStream ( ) )
{
using ( var sw = new StreamWriter ( stream , Encoding . UTF8 , 1024 , true ) )
2020-08-31 03:12:45 +08:00
new LegacyBeatmapEncoder ( editorBeatmap , editorBeatmap . BeatmapSkin ) . Encode ( sw ) ;
2020-04-13 16:18:50 +08:00
2020-09-28 14:30:18 +08:00
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 ;
2020-04-13 16:18:50 +08:00
2020-09-28 14:30:18 +08:00
if ( currentState < savedStates . Count - 1 )
savedStates . RemoveRange ( currentState + 1 , savedStates . Count - currentState - 1 ) ;
2020-04-15 14:50:43 +08:00
2020-09-28 14:30:18 +08:00
if ( savedStates . Count > MAX_SAVED_STATES )
savedStates . RemoveAt ( 0 ) ;
savedStates . Add ( newState ) ;
currentState = savedStates . Count - 1 ;
2020-10-02 14:44:32 +08:00
OnStateChange ? . Invoke ( ) ;
2020-09-28 14:30:18 +08:00
updateBindables ( ) ;
}
2020-04-09 20:22:07 +08:00
}
/// <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 ( bulkChangesStarted > 0 )
return ;
if ( savedStates . Count = = 0 )
return ;
int newState = Math . Clamp ( currentState + direction , 0 , savedStates . Count - 1 ) ;
if ( currentState = = newState )
return ;
isRestoring = true ;
2020-04-13 16:20:01 +08:00
patcher . Patch ( savedStates [ currentState ] , savedStates [ newState ] ) ;
2020-04-09 20:22:07 +08:00
currentState = newState ;
isRestoring = false ;
2020-04-15 14:50:43 +08:00
2020-10-02 14:44:32 +08:00
OnStateChange ? . Invoke ( ) ;
2020-04-15 14:50:43 +08:00
updateBindables ( ) ;
}
private void updateBindables ( )
{
CanUndo . Value = savedStates . Count > 0 & & currentState > 0 ;
CanRedo . Value = currentState < savedStates . Count - 1 ;
2020-04-09 20:22:07 +08:00
}
}
}