2020-04-09 12:22:07 +00: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 06:30:18 +00:00
using System.Linq ;
2020-04-09 12:22:07 +00:00
using System.Text ;
2020-04-15 06:50:43 +00:00
using osu.Framework.Bindables ;
2020-09-09 10:40:41 +00:00
using osu.Framework.Extensions ;
2020-04-09 12:22:07 +00: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>
2020-10-08 07:57:04 +00:00
public class EditorChangeHandler : TransactionalCommitComponent , IEditorChangeHandler
2020-04-09 12:22:07 +00:00
{
2020-04-15 06:50:43 +00:00
public readonly Bindable < bool > CanUndo = new Bindable < bool > ( ) ;
public readonly Bindable < bool > CanRedo = new Bindable < bool > ( ) ;
2020-04-13 08:18:50 +00:00
2020-10-02 06:44:32 +00:00
public event Action OnStateChange ;
2020-04-15 06:50:43 +00:00
private readonly LegacyEditorBeatmapPatcher patcher ;
2020-04-13 08:18:50 +00:00
private readonly List < byte [ ] > savedStates = new List < byte [ ] > ( ) ;
2020-04-09 12:22:07 +00:00
private int currentState = - 1 ;
2020-09-09 10:40:41 +00: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 12:22:07 +00:00
private readonly EditorBeatmap editorBeatmap ;
private bool isRestoring ;
2020-04-13 11:34:18 +00:00
public const int MAX_SAVED_STATES = 50 ;
2020-04-09 12:22:07 +00: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-30 19:12:45 +00:00
public EditorChangeHandler ( EditorBeatmap editorBeatmap )
2020-04-09 12:22:07 +00:00
{
this . editorBeatmap = editorBeatmap ;
2020-10-08 08:22:35 +00:00
editorBeatmap . TransactionBegan + = BeginChange ;
editorBeatmap . TransactionEnded + = EndChange ;
editorBeatmap . SaveStateTriggered + = SaveState ;
2020-04-09 12:22:07 +00:00
2020-04-13 08:20:01 +00:00
patcher = new LegacyEditorBeatmapPatcher ( editorBeatmap ) ;
2020-04-09 12:22:07 +00:00
// Initial state.
SaveState ( ) ;
}
2020-10-08 07:57:04 +00:00
protected override void UpdateState ( )
2020-04-09 12:22:07 +00:00
{
if ( isRestoring )
return ;
2020-04-13 08:18:50 +00:00
using ( var stream = new MemoryStream ( ) )
{
using ( var sw = new StreamWriter ( stream , Encoding . UTF8 , 1024 , true ) )
2020-08-30 19:12:45 +00:00
new LegacyBeatmapEncoder ( editorBeatmap , editorBeatmap . BeatmapSkin ) . Encode ( sw ) ;
2020-04-13 08:18:50 +00:00
2020-09-28 06:30:18 +00: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.
2020-11-27 07:57:11 +00:00
if ( savedStates . Count > 0 & & newState . SequenceEqual ( savedStates [ currentState ] ) ) return ;
2020-04-13 08:18:50 +00:00
2020-09-28 06:30:18 +00:00
if ( currentState < savedStates . Count - 1 )
savedStates . RemoveRange ( currentState + 1 , savedStates . Count - currentState - 1 ) ;
2020-04-15 06:50:43 +00:00
2020-09-28 06:30:18 +00:00
if ( savedStates . Count > MAX_SAVED_STATES )
savedStates . RemoveAt ( 0 ) ;
savedStates . Add ( newState ) ;
currentState = savedStates . Count - 1 ;
2020-10-02 06:44:32 +00:00
OnStateChange ? . Invoke ( ) ;
2020-09-28 06:30:18 +00:00
updateBindables ( ) ;
}
2020-04-09 12:22:07 +00: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 )
{
2020-10-08 07:57:04 +00:00
if ( TransactionActive )
2020-04-09 12:22:07 +00:00
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 08:20:01 +00:00
patcher . Patch ( savedStates [ currentState ] , savedStates [ newState ] ) ;
2020-04-09 12:22:07 +00:00
currentState = newState ;
isRestoring = false ;
2020-04-15 06:50:43 +00:00
2020-10-02 06:44:32 +00:00
OnStateChange ? . Invoke ( ) ;
2020-04-15 06:50:43 +00:00
updateBindables ( ) ;
}
private void updateBindables ( )
{
CanUndo . Value = savedStates . Count > 0 & & currentState > 0 ;
CanRedo . Value = currentState < savedStates . Count - 1 ;
2020-04-09 12:22:07 +00:00
}
}
}