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> _history = new List>(); private readonly TasMovie _movie; private int _maxSteps = 100; private int _totalSteps; private bool _recordingBatch; public List Names { get; } = new List(); 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); } } } } /// /// 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 /// 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(); } } /// /// All changes made between calling Begin and End will be one Undo. /// If already recording in a batch, calls EndBatch. /// /// The name of the batch /// If set and a batch is in progress, a new batch will not be created. /// Returns true if a new batch was started; otherwise false. 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; } /// /// Ends the current undo batch. Future changes will be one undo each. /// If not already recording a batch, does nothing. /// public void EndBatch() { if (!IsRecording || !_recordingBatch) { return; } _recordingBatch = false; List 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; } } /// /// Undoes the most recent action batch, if any exist. /// /// Returns the frame which the movie needs to rewind to. public int Undo() { if (UndoIndex == -1) { return _movie.InputLogLength; } List 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; } /// /// Redoes the most recent undo, if any exist. /// /// Returns the frame which the movie needs to rewind to. public int Redo() { if (UndoIndex == _history.Count - 1) { return _movie.InputLogLength; } UndoIndex++; List 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(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 _oldLog; private List _newLog; private readonly bool _bindMarkers; public MovieAction(int firstFrame, int lastFrame, TasMovie movie) { FirstFrame = firstFrame; LastFrame = lastFrame; _oldLog = new List(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(); 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 _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(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(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 }