743 lines
17 KiB
C#
743 lines
17 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
|
|
namespace BizHawk.Client.Common
|
|
{
|
|
public class TasMovieChangeLog
|
|
{
|
|
public TasMovieChangeLog(TasMovie movie)
|
|
{
|
|
_movie = movie;
|
|
}
|
|
|
|
private readonly List<List<IMovieAction>> _history = new List<List<IMovieAction>>();
|
|
private readonly TasMovie _movie;
|
|
|
|
private int _maxSteps = 100;
|
|
private int _totalSteps;
|
|
private bool _recordingBatch;
|
|
|
|
public List<string> Names { get; } = new List<string>();
|
|
public int UndoIndex { get; private set; } = -1;
|
|
|
|
public int MaxSteps
|
|
{
|
|
get
|
|
{
|
|
return _maxSteps;
|
|
}
|
|
|
|
set
|
|
{
|
|
_maxSteps = value;
|
|
if (_history.Count > value)
|
|
{
|
|
if (_history.Count <= value)
|
|
{
|
|
ClearLog();
|
|
}
|
|
else
|
|
{
|
|
ClearLog(_history.Count - value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether the movie is in recording mode
|
|
/// This is not intended to turn off the ChangeLog, but to disable the normal recording process.
|
|
/// Use this to manually control the ChangeLog. (Useful for when you are making lots of
|
|
/// </summary>
|
|
public bool IsRecording { get; set; } = true;
|
|
|
|
public void ClearLog(int upTo = -1)
|
|
{
|
|
if (upTo == -1)
|
|
{
|
|
upTo = _history.Count;
|
|
}
|
|
|
|
_history.RemoveRange(0, upTo);
|
|
Names.RemoveRange(0, upTo);
|
|
UndoIndex -= upTo;
|
|
if (UndoIndex < -1)
|
|
{
|
|
UndoIndex = -1;
|
|
}
|
|
|
|
if (_history.Count == 0)
|
|
{
|
|
_recordingBatch = false;
|
|
}
|
|
}
|
|
|
|
private void TruncateLog(int from)
|
|
{
|
|
_history.RemoveRange(from, _history.Count - from);
|
|
Names.RemoveRange(from, Names.Count - from);
|
|
|
|
if (UndoIndex < _history.Count - 1)
|
|
{
|
|
UndoIndex = _history.Count - 1;
|
|
}
|
|
|
|
if (_recordingBatch)
|
|
{
|
|
_recordingBatch = false;
|
|
BeginNewBatch();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// All changes made between calling Begin and End will be one Undo.
|
|
/// If already recording in a batch, calls EndBatch.
|
|
/// </summary>
|
|
/// <param name="name">The name of the batch</param>
|
|
/// <param name="keepOldBatch">If set and a batch is in progress, a new batch will not be created.</param>
|
|
/// <returns>Returns true if a new batch was started; otherwise false.</returns>
|
|
public bool BeginNewBatch(string name = "", bool keepOldBatch = false)
|
|
{
|
|
if (!IsRecording)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool ret = true;
|
|
if (_recordingBatch)
|
|
{
|
|
if (keepOldBatch)
|
|
{
|
|
ret = false;
|
|
}
|
|
else
|
|
{
|
|
EndBatch();
|
|
}
|
|
}
|
|
|
|
if (ret)
|
|
{
|
|
ret = AddMovieAction(name);
|
|
}
|
|
|
|
_recordingBatch = true;
|
|
|
|
return ret;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ends the current undo batch. Future changes will be one undo each.
|
|
/// If not already recording a batch, does nothing.
|
|
/// </summary>
|
|
public void EndBatch()
|
|
{
|
|
if (!IsRecording || !_recordingBatch)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_recordingBatch = false;
|
|
List<IMovieAction> last = _history.Last();
|
|
if (last.Count == 0) // Remove batch if it's empty.
|
|
{
|
|
_history.RemoveAt(_history.Count - 1);
|
|
Names.RemoveAt(Names.Count - 1);
|
|
UndoIndex--;
|
|
}
|
|
else
|
|
{
|
|
last.Capacity = last.Count;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Undoes the most recent action batch, if any exist.
|
|
/// </summary>
|
|
/// <returns>Returns the frame which the movie needs to rewind to.</returns>
|
|
public int Undo()
|
|
{
|
|
if (UndoIndex == -1)
|
|
{
|
|
return _movie.InputLogLength;
|
|
}
|
|
|
|
List<IMovieAction> batch = _history[UndoIndex];
|
|
for (int i = batch.Count - 1; i >= 0; i--)
|
|
{
|
|
batch[i].Undo(_movie);
|
|
}
|
|
|
|
UndoIndex--;
|
|
|
|
_recordingBatch = false;
|
|
|
|
if (batch.All(a => a.GetType() == typeof(MovieActionMarker)))
|
|
{
|
|
return _movie.InputLogLength;
|
|
}
|
|
|
|
return PreviousUndoFrame;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Redoes the most recent undo, if any exist.
|
|
/// </summary>
|
|
/// <returns>Returns the frame which the movie needs to rewind to.</returns>
|
|
public int Redo()
|
|
{
|
|
if (UndoIndex == _history.Count - 1)
|
|
{
|
|
return _movie.InputLogLength;
|
|
}
|
|
|
|
UndoIndex++;
|
|
List<IMovieAction> batch = _history[UndoIndex];
|
|
foreach (IMovieAction b in batch)
|
|
{
|
|
b.Redo(_movie);
|
|
}
|
|
|
|
_recordingBatch = false;
|
|
|
|
if (batch.All(a => a.GetType() == typeof(MovieActionMarker)))
|
|
{
|
|
return _movie.InputLogLength;
|
|
}
|
|
|
|
return PreviousRedoFrame;
|
|
}
|
|
|
|
public bool CanUndo => UndoIndex > -1;
|
|
public bool CanRedo => UndoIndex < _history.Count - 1;
|
|
|
|
public string NextUndoStepName
|
|
{
|
|
get
|
|
{
|
|
if (Names.Count == 0 || UndoIndex < 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return Names[UndoIndex];
|
|
}
|
|
}
|
|
|
|
public int PreviousUndoFrame
|
|
{
|
|
get
|
|
{
|
|
if (UndoIndex == _history.Count - 1)
|
|
{
|
|
return _movie.InputLogLength;
|
|
}
|
|
|
|
if (_history[UndoIndex + 1].Count == 0)
|
|
{
|
|
return _movie.InputLogLength;
|
|
}
|
|
|
|
return _history[UndoIndex + 1].Min(a => a.FirstFrame);
|
|
}
|
|
}
|
|
|
|
public int PreviousRedoFrame
|
|
{
|
|
get
|
|
{
|
|
if (UndoIndex == -1)
|
|
{
|
|
return _movie.InputLogLength;
|
|
}
|
|
|
|
if (_history[UndoIndex].Count == 0)
|
|
{
|
|
return _movie.InputLogLength;
|
|
}
|
|
|
|
return _history[UndoIndex].Min(a => a.FirstFrame);
|
|
}
|
|
}
|
|
|
|
#region Change History
|
|
|
|
private bool AddMovieAction(string name)
|
|
{
|
|
if (UndoIndex + 1 != _history.Count)
|
|
{
|
|
TruncateLog(UndoIndex + 1);
|
|
}
|
|
|
|
if (name == "")
|
|
{
|
|
name = $"Undo step {_totalSteps}";
|
|
}
|
|
|
|
bool ret = false;
|
|
if (!_recordingBatch)
|
|
{
|
|
ret = true;
|
|
_history.Add(new List<IMovieAction>(1));
|
|
Names.Add(name);
|
|
_totalSteps += 1;
|
|
|
|
if (_history.Count <= MaxSteps)
|
|
{
|
|
UndoIndex += 1;
|
|
}
|
|
else
|
|
{
|
|
_history.RemoveAt(0);
|
|
Names.RemoveAt(0);
|
|
ret = false;
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
public void SetName(string name)
|
|
{
|
|
Names[Names.Count - 1] = name;
|
|
}
|
|
|
|
// TODO: These probably aren't the best way to handle undo/redo.
|
|
private int _lastGeneral;
|
|
|
|
public void AddGeneralUndo(int first, int last, string name = "", bool force = false)
|
|
{
|
|
if (IsRecording || force)
|
|
{
|
|
AddMovieAction(name);
|
|
_history.Last().Add(new MovieAction(first, last, _movie));
|
|
_lastGeneral = _history.Last().Count - 1;
|
|
}
|
|
}
|
|
|
|
public void SetGeneralRedo(bool force = false)
|
|
{
|
|
if (IsRecording || force)
|
|
{
|
|
(_history.Last()[_lastGeneral] as MovieAction).SetRedoLog(_movie);
|
|
}
|
|
}
|
|
|
|
public void AddBoolToggle(int frame, string button, bool oldState, string name = "", bool force = false)
|
|
{
|
|
if (IsRecording || force)
|
|
{
|
|
AddMovieAction(name);
|
|
_history.Last().Add(new MovieActionFrameEdit(frame, button, oldState, !oldState));
|
|
}
|
|
}
|
|
|
|
public void AddFloatChange(int frame, string button, float oldState, float newState, string name = "", bool force = false)
|
|
{
|
|
if (IsRecording || force)
|
|
{
|
|
AddMovieAction(name);
|
|
_history.Last().Add(new MovieActionFrameEdit(frame, button, oldState, newState));
|
|
}
|
|
}
|
|
|
|
public void AddMarkerChange(TasMovieMarker newMarker, int oldPosition = -1, string oldMessage = "", string name = "", bool force = false)
|
|
{
|
|
if (IsRecording || force)
|
|
{
|
|
if (oldPosition == -1)
|
|
{
|
|
name = $"Set Marker at frame {newMarker.Frame}";
|
|
}
|
|
else
|
|
{
|
|
name = $"Remove Marker at frame {oldPosition}";
|
|
}
|
|
|
|
AddMovieAction(name);
|
|
_history.Last().Add(new MovieActionMarker(newMarker, oldPosition, oldMessage));
|
|
}
|
|
}
|
|
|
|
public void AddInputBind(int frame, bool isDelete, string name = "", bool force = false)
|
|
{
|
|
if (IsRecording || force)
|
|
{
|
|
AddMovieAction(name);
|
|
_history.Last().Add(new MovieActionBindInput(_movie, frame, isDelete));
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
|
|
#region Classes
|
|
|
|
public interface IMovieAction
|
|
{
|
|
void Undo(TasMovie movie);
|
|
void Redo(TasMovie movie);
|
|
|
|
int FirstFrame { get; }
|
|
int LastFrame { get; }
|
|
}
|
|
|
|
public class MovieAction : IMovieAction
|
|
{
|
|
public int FirstFrame { get; }
|
|
public int LastFrame { get; }
|
|
|
|
private readonly int _undoLength;
|
|
private int _redoLength;
|
|
|
|
private int Length => LastFrame - FirstFrame + 1;
|
|
|
|
private readonly List<string> _oldLog;
|
|
private List<string> _newLog;
|
|
private readonly bool _bindMarkers;
|
|
|
|
public MovieAction(int firstFrame, int lastFrame, TasMovie movie)
|
|
{
|
|
FirstFrame = firstFrame;
|
|
LastFrame = lastFrame;
|
|
_oldLog = new List<string>(Length);
|
|
_undoLength = Math.Min(LastFrame + 1, movie.InputLogLength) - FirstFrame;
|
|
|
|
for (int i = 0; i < _undoLength; i++)
|
|
{
|
|
_oldLog.Add(movie.GetLogEntries()[FirstFrame + i]);
|
|
}
|
|
|
|
_bindMarkers = movie.BindMarkersToInput;
|
|
}
|
|
|
|
public void SetRedoLog(TasMovie movie)
|
|
{
|
|
_redoLength = Math.Min(LastFrame + 1, movie.InputLogLength) - FirstFrame;
|
|
_newLog = new List<string>();
|
|
for (int i = 0; i < _redoLength; i++)
|
|
{
|
|
_newLog.Add(movie.GetLogEntries()[FirstFrame + i]);
|
|
}
|
|
}
|
|
|
|
public void Undo(TasMovie movie)
|
|
{
|
|
bool wasRecording = movie.ChangeLog.IsRecording;
|
|
bool wasBinding = movie.BindMarkersToInput;
|
|
movie.ChangeLog.IsRecording = false;
|
|
movie.BindMarkersToInput = _bindMarkers;
|
|
|
|
if (_redoLength != Length)
|
|
{
|
|
movie.InsertEmptyFrame(FirstFrame, Length - _redoLength, true);
|
|
}
|
|
|
|
if (_undoLength != Length)
|
|
{
|
|
movie.RemoveFrames(FirstFrame, movie.InputLogLength - _undoLength, true);
|
|
}
|
|
|
|
for (int i = 0; i < _undoLength; i++)
|
|
{
|
|
movie.SetFrame(FirstFrame + i, _oldLog[i]);
|
|
}
|
|
|
|
movie.ChangeLog.IsRecording = wasRecording;
|
|
movie.BindMarkersToInput = _bindMarkers;
|
|
}
|
|
|
|
public void Redo(TasMovie movie)
|
|
{
|
|
bool wasRecording = movie.ChangeLog.IsRecording;
|
|
bool wasBinding = movie.BindMarkersToInput;
|
|
movie.ChangeLog.IsRecording = false;
|
|
movie.BindMarkersToInput = _bindMarkers;
|
|
|
|
if (_undoLength != Length)
|
|
{
|
|
movie.InsertEmptyFrame(FirstFrame, Length - _undoLength);
|
|
}
|
|
|
|
if (_redoLength != Length)
|
|
{
|
|
movie.RemoveFrames(FirstFrame, movie.InputLogLength - _redoLength);
|
|
}
|
|
|
|
for (int i = 0; i < _redoLength; i++)
|
|
{
|
|
movie.SetFrame(FirstFrame + i, _newLog[i]);
|
|
}
|
|
|
|
movie.ChangeLog.IsRecording = wasRecording;
|
|
movie.BindMarkersToInput = _bindMarkers;
|
|
}
|
|
}
|
|
|
|
public class MovieActionMarker : IMovieAction
|
|
{
|
|
public int FirstFrame { get; }
|
|
public int LastFrame { get; }
|
|
|
|
private readonly string _oldMessage;
|
|
private readonly string _newMessage;
|
|
|
|
public MovieActionMarker(TasMovieMarker marker, int oldPosition = -1, string oldMessage = "")
|
|
{
|
|
FirstFrame = oldPosition;
|
|
if (marker == null)
|
|
{
|
|
LastFrame = -1;
|
|
_oldMessage = oldMessage;
|
|
}
|
|
else
|
|
{
|
|
LastFrame = marker.Frame;
|
|
_oldMessage = oldMessage == "" ? marker.Message : oldMessage;
|
|
_newMessage = marker.Message;
|
|
}
|
|
}
|
|
|
|
public void Undo(TasMovie movie)
|
|
{
|
|
if (FirstFrame == -1) // Action: Place marker
|
|
{
|
|
movie.Markers.Remove(movie.Markers.Get(LastFrame), true);
|
|
}
|
|
else if (LastFrame == -1) // Action: Remove marker
|
|
{
|
|
movie.Markers.Add(FirstFrame, _oldMessage, true);
|
|
}
|
|
else // Action: Move/rename marker
|
|
{
|
|
movie.Markers.Move(LastFrame, FirstFrame, true);
|
|
movie.Markers.Get(LastFrame).Message = _oldMessage;
|
|
}
|
|
}
|
|
|
|
public void Redo(TasMovie movie)
|
|
{
|
|
if (FirstFrame == -1) // Action: Place marker
|
|
{
|
|
movie.Markers.Add(LastFrame, _oldMessage, true);
|
|
}
|
|
else if (LastFrame == -1) // Action: Remove marker
|
|
{
|
|
movie.Markers.Remove(movie.Markers.Get(FirstFrame), true);
|
|
}
|
|
else // Action: Move/rename marker
|
|
{
|
|
movie.Markers.Move(FirstFrame, LastFrame, true);
|
|
movie.Markers.Get(LastFrame).Message = _newMessage;
|
|
}
|
|
}
|
|
}
|
|
|
|
public class MovieActionFrameEdit : IMovieAction
|
|
{
|
|
public int FirstFrame { get; }
|
|
public int LastFrame => FirstFrame;
|
|
|
|
private readonly float _oldState;
|
|
private readonly float _newState;
|
|
|
|
private readonly string _buttonName;
|
|
private readonly bool _isFloat;
|
|
|
|
public MovieActionFrameEdit(int frame, string button, bool oldS, bool newS)
|
|
{
|
|
_oldState = oldS ? 1 : 0;
|
|
_newState = newS ? 1 : 0;
|
|
FirstFrame = frame;
|
|
_buttonName = button;
|
|
}
|
|
|
|
public MovieActionFrameEdit(int frame, string button, float oldS, float newS)
|
|
{
|
|
_oldState = oldS;
|
|
_newState = newS;
|
|
FirstFrame = frame;
|
|
_buttonName = button;
|
|
_isFloat = true;
|
|
}
|
|
|
|
public void Undo(TasMovie movie)
|
|
{
|
|
bool wasRecording = movie.ChangeLog.IsRecording;
|
|
movie.ChangeLog.IsRecording = false;
|
|
|
|
if (_isFloat)
|
|
{
|
|
movie.SetFloatState(FirstFrame, _buttonName, _oldState);
|
|
}
|
|
else
|
|
{
|
|
movie.SetBoolState(FirstFrame, _buttonName, _oldState == 1);
|
|
}
|
|
|
|
movie.ChangeLog.IsRecording = wasRecording;
|
|
}
|
|
|
|
public void Redo(TasMovie movie)
|
|
{
|
|
bool wasRecording = movie.ChangeLog.IsRecording;
|
|
movie.ChangeLog.IsRecording = false;
|
|
|
|
if (_isFloat)
|
|
{
|
|
movie.SetFloatState(FirstFrame, _buttonName, _newState);
|
|
}
|
|
else
|
|
{
|
|
movie.SetBoolState(FirstFrame, _buttonName, _newState == 1);
|
|
}
|
|
|
|
movie.ChangeLog.IsRecording = wasRecording;
|
|
}
|
|
}
|
|
|
|
public class MovieActionPaint : IMovieAction
|
|
{
|
|
public int FirstFrame { get; }
|
|
public int LastFrame { get; }
|
|
private readonly List<float> _oldState;
|
|
private readonly float _newState;
|
|
private readonly string _buttonName;
|
|
private readonly bool _isFloat = false;
|
|
|
|
public MovieActionPaint(int startFrame, int endFrame, string button, bool newS, TasMovie movie)
|
|
{
|
|
_newState = newS ? 1 : 0;
|
|
FirstFrame = startFrame;
|
|
LastFrame = endFrame;
|
|
_buttonName = button;
|
|
_oldState = new List<float>(endFrame - startFrame + 1);
|
|
|
|
for (int i = 0; i < endFrame - startFrame + 1; i++)
|
|
{
|
|
_oldState.Add(movie.BoolIsPressed(startFrame + i, button) ? 1 : 0);
|
|
}
|
|
}
|
|
|
|
public MovieActionPaint(int startFrame, int endFrame, string button, float newS, TasMovie movie)
|
|
{
|
|
_newState = newS;
|
|
FirstFrame = startFrame;
|
|
LastFrame = endFrame;
|
|
_buttonName = button;
|
|
_isFloat = true;
|
|
_oldState = new List<float>(endFrame - startFrame + 1);
|
|
|
|
for (int i = 0; i < endFrame - startFrame + 1; i++)
|
|
{
|
|
_oldState.Add(movie.BoolIsPressed(startFrame + i, button) ? 1 : 0);
|
|
}
|
|
}
|
|
|
|
public void Undo(TasMovie movie)
|
|
{
|
|
bool wasRecording = movie.ChangeLog.IsRecording;
|
|
movie.ChangeLog.IsRecording = false;
|
|
|
|
if (_isFloat)
|
|
{
|
|
for (int i = 0; i < _oldState.Count; i++)
|
|
{
|
|
movie.SetFloatState(FirstFrame + i, _buttonName, _oldState[i]);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (int i = 0; i < _oldState.Count; i++)
|
|
{
|
|
movie.SetBoolState(FirstFrame + i, _buttonName, _oldState[i] == 1);
|
|
}
|
|
}
|
|
|
|
movie.ChangeLog.IsRecording = wasRecording;
|
|
}
|
|
|
|
public void Redo(TasMovie movie)
|
|
{
|
|
bool wasRecording = movie.ChangeLog.IsRecording;
|
|
movie.ChangeLog.IsRecording = false;
|
|
|
|
if (_isFloat)
|
|
{
|
|
movie.SetFloatStates(FirstFrame, LastFrame - FirstFrame + 1, _buttonName, _newState);
|
|
}
|
|
else
|
|
{
|
|
movie.SetBoolStates(FirstFrame, LastFrame - FirstFrame + 1, _buttonName, _newState == 1);
|
|
}
|
|
|
|
movie.ChangeLog.IsRecording = wasRecording;
|
|
}
|
|
}
|
|
|
|
public class MovieActionBindInput : IMovieAction
|
|
{
|
|
public int FirstFrame { get; }
|
|
public int LastFrame { get; }
|
|
|
|
private readonly string _log;
|
|
private readonly bool _delete;
|
|
|
|
private readonly bool _bindMarkers;
|
|
|
|
public MovieActionBindInput(TasMovie movie, int frame, bool isDelete)
|
|
{
|
|
FirstFrame = LastFrame = frame;
|
|
_log = movie.GetInputLogEntry(frame);
|
|
_delete = isDelete;
|
|
_bindMarkers = movie.BindMarkersToInput;
|
|
}
|
|
|
|
public void Undo(TasMovie movie)
|
|
{
|
|
bool wasRecording = movie.ChangeLog.IsRecording;
|
|
bool wasBinding = movie.BindMarkersToInput;
|
|
movie.ChangeLog.IsRecording = false;
|
|
movie.BindMarkersToInput = _bindMarkers;
|
|
|
|
if (_delete) // Insert
|
|
{
|
|
movie.InsertInput(FirstFrame, _log);
|
|
movie.InsertLagHistory(FirstFrame + 1, true);
|
|
}
|
|
else // Delete
|
|
{
|
|
movie.RemoveFrame(FirstFrame);
|
|
movie.RemoveLagHistory(FirstFrame + 1);
|
|
}
|
|
|
|
movie.ChangeLog.IsRecording = wasRecording;
|
|
movie.BindMarkersToInput = _bindMarkers;
|
|
}
|
|
|
|
public void Redo(TasMovie movie)
|
|
{
|
|
bool wasRecording = movie.ChangeLog.IsRecording;
|
|
bool wasBinding = movie.BindMarkersToInput;
|
|
movie.ChangeLog.IsRecording = false;
|
|
movie.BindMarkersToInput = _bindMarkers;
|
|
|
|
if (_delete)
|
|
{
|
|
movie.RemoveFrame(FirstFrame);
|
|
movie.RemoveLagHistory(FirstFrame + 1);
|
|
}
|
|
else
|
|
{
|
|
movie.InsertInput(FirstFrame, _log);
|
|
movie.InsertLagHistory(FirstFrame + 1, true);
|
|
}
|
|
|
|
movie.ChangeLog.IsRecording = wasRecording;
|
|
movie.BindMarkersToInput = _bindMarkers;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
} |