BizHawk/BizHawk.Client.Common/movie/tasproj/TasMovie.History.cs

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
}