diff --git a/src/BizHawk.Client.Common/movie/bk2/StringLogs.cs b/src/BizHawk.Client.Common/movie/bk2/StringLogs.cs
index f84e17b7bec..517bee596b1 100644
--- a/src/BizHawk.Client.Common/movie/bk2/StringLogs.cs
+++ b/src/BizHawk.Client.Common/movie/bk2/StringLogs.cs
@@ -37,6 +37,9 @@ public static IStringLog MakeStringLog()
}
}
+ if (newLog.Count != currentLog.Count)
+ return Math.Min(newLog.Count, currentLog.Count);
+
return null;
}
diff --git a/src/BizHawk.Client.Common/movie/interfaces/ITasMovie.cs b/src/BizHawk.Client.Common/movie/interfaces/ITasMovie.cs
index ab9ab063ba5..9ea994305be 100644
--- a/src/BizHawk.Client.Common/movie/interfaces/ITasMovie.cs
+++ b/src/BizHawk.Client.Common/movie/interfaces/ITasMovie.cs
@@ -21,6 +21,11 @@ public interface ITasMovie : IMovie, INotifyPropertyChanged, IDisposable
int LastEditedFrame { get; }
bool LastEditWasRecording { get; }
+ ///
+ /// Called whenever the movie is modified in a way that could invalidate savestates in the movie's state history.
+ /// Called regardless of whether any states were actually invalidated.
+ /// The parameter is the last frame number who's savestate (if it exists) is still valid. That is, the first of the modified frames.
+ ///
Action GreenzoneInvalidated { get; set; }
string DisplayValue(int frame, string buttonName);
@@ -43,10 +48,16 @@ public interface ITasMovie : IMovie, INotifyPropertyChanged, IDisposable
void InsertInput(int frame, IEnumerable inputStates);
void InsertEmptyFrame(int frame, int count = 1);
- int CopyOverInput(int frame, IEnumerable inputStates);
+ void CopyOverInput(int frame, IEnumerable inputStates);
void RemoveFrame(int frame);
void RemoveFrames(ICollection frames);
+
+ ///
+ /// Remove all frames between removeStart and removeUpTo (excluding removeUpTo).
+ ///
+ /// The first frame to remove.
+ /// The frame after the last frame to remove.
void RemoveFrames(int removeStart, int removeUpTo);
void SetFrame(int frame, string source);
@@ -55,5 +66,14 @@ public interface ITasMovie : IMovie, INotifyPropertyChanged, IDisposable
void CopyVerificationLog(IEnumerable log);
bool IsReserved(int frame);
+
+ ///
+ /// Sometimes we will be doing a whole bunch of small operations together. (e.g. large painting undo, large Lua edit)
+ /// This avoids using the greenzone invalidated callback for each one, making things run smoother.
+ /// Note that this is different from undo batching.
+ /// For one, we don't undo batch an undo itself.
+ /// For two, undo batches might span a significant amount of time during which users want to atually see edits or auto-restore in TAStudio.
+ ///
+ void SingleInvalidation(Action action);
}
}
diff --git a/src/BizHawk.Client.Common/movie/tasproj/TasMovie.Editing.cs b/src/BizHawk.Client.Common/movie/tasproj/TasMovie.Editing.cs
index 9f6714414e0..e5bbfeecb4b 100644
--- a/src/BizHawk.Client.Common/movie/tasproj/TasMovie.Editing.cs
+++ b/src/BizHawk.Client.Common/movie/tasproj/TasMovie.Editing.cs
@@ -9,76 +9,60 @@ internal partial class TasMovie
{
public IMovieChangeLog ChangeLog { get; set; }
+ // In each editing method we do things in this order:
+ // 1) Any special logic (such as short-circuit)
+ // 2) Begin an undo batch, if needed.
+ // 3) Edit the movie.
+ // 4) End the undo batch, if needed.
+ // 5) Call InvalidateAfter.
+
+ // InvalidateAfter being last ensures that the GreenzoneInvalidated callback sees all edits.
+
public override void RecordFrame(int frame, IController source)
{
- // RetroEdit: This check is questionable; recording at frame 0 is valid and should be reversible.
- // Also, frame - 1, why?
- // Is the precondition compensating for frame - 1 reindexing?
- if (frame != 0)
- {
- ChangeLog.AddGeneralUndo(frame - 1, frame - 1, $"Record Frame: {frame}");
- }
-
+ ChangeLog.AddGeneralUndo(frame, frame, $"Record Frame: {frame}");
SetFrameAt(frame, Bk2LogEntryGenerator.GenerateLogEntry(source));
-
- Changes = true;
+ ChangeLog.SetGeneralRedo();
LagLog[frame] = _inputPollable.IsLagFrame;
+ LastEditWasRecording = true;
- if (this.IsRecording())
- {
- LastEditWasRecording = true;
- InvalidateAfter(frame);
- }
-
- if (frame != 0)
- {
- ChangeLog.SetGeneralRedo();
- }
+ InvalidateAfter(frame);
}
public override void Truncate(int frame)
{
- bool endBatch = ChangeLog.BeginNewBatch($"Truncate Movie: {frame}", true);
- ChangeLog.AddGeneralUndo(frame, InputLogLength - 1);
+ if (frame >= Log.Count - 1) return;
- if (frame < Log.Count - 1)
- {
- Changes = true;
- }
+ bool endBatch = ChangeLog.BeginNewBatch($"Truncate Movie: {frame}", true);
+ ChangeLog.AddGeneralUndo(frame, InputLogLength - 1);
base.Truncate(frame);
+ ChangeLog.SetGeneralRedo();
- LagLog.RemoveFrom(frame);
- TasStateManager.InvalidateAfter(frame);
- Markers.TruncateAt(frame); // must happen before the invalidation
- GreenzoneInvalidated?.Invoke(frame);
+ Markers.TruncateAt(frame);
- ChangeLog.SetGeneralRedo();
- if (endBatch)
- {
- ChangeLog.EndBatch();
- }
+ if (endBatch) ChangeLog.EndBatch();
+
+ InvalidateAfter(frame);
}
public override void PokeFrame(int frame, IController source)
{
ChangeLog.AddGeneralUndo(frame, frame, $"Set Frame At: {frame}");
-
base.PokeFrame(frame, source);
- InvalidateAfter(frame);
-
ChangeLog.SetGeneralRedo();
+
+ InvalidateAfter(frame);
}
public void SetFrame(int frame, string source)
{
ChangeLog.AddGeneralUndo(frame, frame, $"Set Frame At: {frame}");
-
SetFrameAt(frame, source);
- InvalidateAfter(frame);
-
ChangeLog.SetGeneralRedo();
+
+ InvalidateAfter(frame);
}
public void ClearFrame(int frame)
@@ -87,12 +71,10 @@ public void ClearFrame(int frame)
if (GetInputLogEntry(frame) == empty) return;
ChangeLog.AddGeneralUndo(frame, frame, $"Clear Frame: {frame}");
-
SetFrameAt(frame, empty);
- Changes = true;
+ ChangeLog.SetGeneralRedo();
InvalidateAfter(frame);
- ChangeLog.SetGeneralRedo();
}
private void ShiftBindedMarkers(int frame, int offset)
@@ -110,69 +92,58 @@ public void RemoveFrame(int frame)
public void RemoveFrames(ICollection frames)
{
- if (frames.Count is not 0)
- {
- // Separate the given frames into contiguous blocks
- // and process each block independently
- List framesToDelete = frames
- .Where(fr => fr >= 0 && fr < InputLogLength)
- .Order().ToList();
+ if (frames.Count is 0) return;
+ // Separate the given frames into contiguous blocks
+ // and process each block independently
+ List framesToDelete = frames
+ .Where(fr => fr >= 0 && fr < InputLogLength)
+ .Order().ToList();
+
+ SingleInvalidation(() =>
+ {
int alreadyDeleted = 0;
+ bool endBatch = ChangeLog.BeginNewBatch($"Delete {framesToDelete.Count} frames from {framesToDelete[0]}-{framesToDelete[framesToDelete.Count - 1]}", true);
for (int i = 1; i <= framesToDelete.Count; i++)
{
if (i == framesToDelete.Count || framesToDelete[i] - framesToDelete[i - 1] != 1)
{
- // Each block is logged as an individual ChangeLog entry
RemoveFrames(framesToDelete[alreadyDeleted] - alreadyDeleted, framesToDelete[i - 1] + 1 - alreadyDeleted);
alreadyDeleted = i;
}
}
- }
+ if (endBatch) ChangeLog.EndBatch();
+ });
}
- ///
- /// Remove all frames between removeStart and removeUpTo (excluding removeUpTo).
- ///
- /// The first frame to remove.
- /// The frame after the last frame to remove.
public void RemoveFrames(int removeStart, int removeUpTo)
{
- // Log.GetRange() might be preferrable, but Log's type complicates that.
- string[] removedInputs = new string[removeUpTo - removeStart];
- Log.CopyTo(removeStart, removedInputs, 0, removedInputs.Length);
-
- // Pre-process removed markers for the ChangeLog.
- List removedMarkers = new List();
+ bool endBatch = ChangeLog.BeginNewBatch($"Remove frames {removeStart}-{removeUpTo - 1}", true);
if (BindMarkersToInput)
{
- bool wasRecording = ChangeLog.IsRecording;
- ChangeLog.IsRecording = false;
-
// O(n^2) removal time, but removing many binded markers in a deleted section is probably rare.
- removedMarkers = Markers.Where(m => m.Frame >= removeStart && m.Frame < removeUpTo).ToList();
- foreach (var marker in removedMarkers)
+ List markersToRemove = Markers.Where(m => m.Frame >= removeStart && m.Frame < removeUpTo).ToList();
+ foreach (var marker in markersToRemove)
{
Markers.Remove(marker);
}
-
- ChangeLog.IsRecording = wasRecording;
}
-
- Log.RemoveRange(removeStart, removeUpTo - removeStart);
-
ShiftBindedMarkers(removeUpTo, removeStart - removeUpTo);
- Changes = true;
- InvalidateAfter(removeStart);
+ // Log.GetRange() might be preferrable, but Log's type complicates that.
+ string[] removedInputs = new string[removeUpTo - removeStart];
+ Log.CopyTo(removeStart, removedInputs, 0, removedInputs.Length);
+ Log.RemoveRange(removeStart, removeUpTo - removeStart);
ChangeLog.AddRemoveFrames(
removeStart,
removeUpTo,
removedInputs.ToList(),
- removedMarkers,
- $"Remove frames {removeStart}-{removeUpTo - 1}"
+ BindMarkersToInput
);
+ if (endBatch) ChangeLog.EndBatch();
+
+ InvalidateAfter(removeStart);
}
public void InsertInput(int frame, string inputState)
@@ -184,18 +155,14 @@ public void InsertInput(int frame, string inputState)
public void InsertInput(int frame, IEnumerable inputLog)
{
Log.InsertRange(frame, inputLog);
-
ShiftBindedMarkers(frame, inputLog.Count());
+ ChangeLog.AddInsertInput(frame, inputLog.ToList(), BindMarkersToInput, $"Insert {inputLog.Count()} frame(s) at {frame}");
- Changes = true;
InvalidateAfter(frame);
-
- ChangeLog.AddInsertInput(frame, inputLog.ToList(), $"Insert {inputLog.Count()} frame(s) at {frame}");
}
public void InsertInput(int frame, IEnumerable inputStates)
{
- // ChangeLog is done in the InsertInput call.
var inputLog = new List();
foreach (var input in inputStates)
@@ -206,47 +173,30 @@ public void InsertInput(int frame, IEnumerable inputStates)
InsertInput(frame, inputLog); // Sets the ChangeLog
}
- public int CopyOverInput(int frame, IEnumerable inputStates)
+ public void CopyOverInput(int frame, IEnumerable inputStates)
{
- int firstChangedFrame = -1;
- ChangeLog.BeginNewBatch($"Copy Over Input: {frame}");
-
var states = inputStates.ToList();
+ bool endBatch = ChangeLog.BeginNewBatch($"Copy Over Input: {frame}", true);
+
if (Log.Count < states.Count + frame)
{
- firstChangedFrame = Log.Count;
ExtendMovieForEdit(states.Count + frame - Log.Count);
}
ChangeLog.AddGeneralUndo(frame, frame + states.Count - 1, $"Copy Over Input: {frame}");
-
for (int i = 0; i < states.Count; i++)
{
- if (Log.Count <= frame + i)
- {
- break;
- }
-
- var entry = Bk2LogEntryGenerator.GenerateLogEntry(states[i]);
- if ((firstChangedFrame == -1 || firstChangedFrame > frame + i) && Log[frame + i] != entry)
- {
- firstChangedFrame = frame + i;
- }
-
- Log[frame + i] = entry;
+ Log[frame + i] = Bk2LogEntryGenerator.GenerateLogEntry(states[i]);
}
+ int firstChangedFrame = ChangeLog.SetGeneralRedo();
+
+ if (endBatch) ChangeLog.EndBatch();
- ChangeLog.EndBatch();
- Changes = true;
if (firstChangedFrame != -1)
{
- // TODO: Throw out the undo action if there are no changes.
InvalidateAfter(firstChangedFrame);
}
-
- ChangeLog.SetGeneralRedo();
- return firstChangedFrame;
}
public void InsertEmptyFrame(int frame, int count = 1)
@@ -254,40 +204,30 @@ public void InsertEmptyFrame(int frame, int count = 1)
frame = Math.Min(frame, Log.Count);
Log.InsertRange(frame, Enumerable.Repeat(Bk2LogEntryGenerator.EmptyEntry(Session.MovieController), count));
-
ShiftBindedMarkers(frame, count);
+ ChangeLog.AddInsertFrames(frame, count, BindMarkersToInput, $"Insert {count} empty frame(s) at {frame}");
- Changes = true;
InvalidateAfter(frame);
-
- ChangeLog.AddInsertFrames(frame, count, $"Insert {count} empty frame(s) at {frame}");
}
private void ExtendMovieForEdit(int numFrames)
{
- bool endBatch = ChangeLog.BeginNewBatch("Auto-Extend Movie", true);
int oldLength = InputLogLength;
- ChangeLog.AddGeneralUndo(oldLength, oldLength + numFrames - 1);
- Session.MovieController.SetFrom(Session.StickySource);
-
- // account for autohold. needs autohold pattern to be already recorded in the current frame
+ // account for autohold TODO: What about auto-fire?
+ string inputs = Bk2LogEntryGenerator.GenerateLogEntry(Session.StickySource);
for (int i = 0; i < numFrames; i++)
{
- Log.Add(Bk2LogEntryGenerator.GenerateLogEntry(Session.MovieController));
+ Log.Add(inputs);
}
- Changes = true;
-
- ChangeLog.SetGeneralRedo();
- if (endBatch)
- {
- ChangeLog.EndBatch();
- }
+ ChangeLog.AddExtend(oldLength, numFrames, inputs);
}
public void ToggleBoolState(int frame, string buttonName)
{
+ bool endBatch = ChangeLog.BeginNewBatch($"Toggle {buttonName}: {frame}", true);
+
if (frame >= Log.Count) // Insert blank frames up to this point
{
ExtendMovieForEdit(frame - Log.Count + 1);
@@ -297,43 +237,51 @@ public void ToggleBoolState(int frame, string buttonName)
adapter.SetBool(buttonName, !adapter.IsPressed(buttonName));
Log[frame] = Bk2LogEntryGenerator.GenerateLogEntry(adapter);
- Changes = true;
- InvalidateAfter(frame);
+ ChangeLog.AddBoolToggle(frame, buttonName, !adapter.IsPressed(buttonName));
+
+ if (endBatch) ChangeLog.EndBatch();
- ChangeLog.AddBoolToggle(frame, buttonName, !adapter.IsPressed(buttonName), $"Toggle {buttonName}: {frame}");
+ InvalidateAfter(frame);
}
public void SetBoolState(int frame, string buttonName, bool val)
{
+ bool endBatch = ChangeLog.BeginNewBatch($"Set {buttonName}({(val ? "On" : "Off")}): {frame}", true);
+
+ bool extended = false;
if (frame >= Log.Count) // Insert blank frames up to this point
{
ExtendMovieForEdit(frame - Log.Count + 1);
+ extended = true;
}
var adapter = GetInputState(frame);
var old = adapter.IsPressed(buttonName);
- adapter.SetBool(buttonName, val);
-
- Log[frame] = Bk2LogEntryGenerator.GenerateLogEntry(adapter);
if (old != val)
{
- InvalidateAfter(frame);
- Changes = true;
- ChangeLog.AddBoolToggle(frame, buttonName, old, $"Set {buttonName}({(val ? "On" : "Off")}): {frame}");
+ adapter.SetBool(buttonName, val);
+ Log[frame] = Bk2LogEntryGenerator.GenerateLogEntry(adapter);
+ ChangeLog.AddBoolToggle(frame, buttonName, old);
}
+
+ if (endBatch) ChangeLog.EndBatch();
+
+ if (old != val || extended) InvalidateAfter(frame);
}
public void SetBoolStates(int frame, int count, string buttonName, bool val)
{
+ bool endBatch = ChangeLog.BeginNewBatch($"Set {buttonName}({(val ? "On" : "Off")}): {frame}-{frame + count - 1}", true);
+
+ int firstChangedFrame = -1;
if (Log.Count < frame + count)
{
+ firstChangedFrame = Log.Count;
ExtendMovieForEdit(frame + count - Log.Count);
}
- ChangeLog.AddGeneralUndo(frame, frame + count - 1, $"Set {buttonName}({(val ? "On" : "Off")}): {frame}-{frame + count - 1}");
-
- int changed = -1;
+ ChangeLog.AddGeneralUndo(frame, frame + count - 1);
for (int i = 0; i < count; i++)
{
var adapter = GetInputState(frame + i);
@@ -342,52 +290,56 @@ public void SetBoolStates(int frame, int count, string buttonName, bool val)
Log[frame + i] = Bk2LogEntryGenerator.GenerateLogEntry(adapter);
- if (changed == -1 && old != val)
+ if (firstChangedFrame == -1 && old != val)
{
- changed = frame + i;
+ firstChangedFrame = frame + i;
}
}
+ ChangeLog.SetGeneralRedo();
- if (changed != -1)
- {
- InvalidateAfter(changed);
- Changes = true;
- }
+ if (endBatch) ChangeLog.EndBatch();
- ChangeLog.SetGeneralRedo();
+ if (firstChangedFrame != -1) InvalidateAfter(firstChangedFrame);
}
public void SetAxisState(int frame, string buttonName, int val)
{
+ bool endBatch = ChangeLog.BeginNewBatch($"Set {buttonName}({val}): {frame}", true);
+
+ bool extended = false;
if (frame >= Log.Count) // Insert blank frames up to this point
{
ExtendMovieForEdit(frame - Log.Count + 1);
+ extended = true;
}
var adapter = GetInputState(frame);
var old = adapter.AxisValue(buttonName);
- adapter.SetAxis(buttonName, val);
-
- Log[frame] = Bk2LogEntryGenerator.GenerateLogEntry(adapter);
if (old != val)
{
- InvalidateAfter(frame);
- Changes = true;
- ChangeLog.AddAxisChange(frame, buttonName, old, val, $"Set {buttonName}({val}): {frame}");
+ adapter.SetAxis(buttonName, val);
+ Log[frame] = Bk2LogEntryGenerator.GenerateLogEntry(adapter);
+ ChangeLog.AddAxisChange(frame, buttonName, old, val);
}
+
+ if (endBatch) ChangeLog.EndBatch();
+
+ if (old != val || extended) InvalidateAfter(frame);
}
public void SetAxisStates(int frame, int count, string buttonName, int val)
{
+ bool endBatch = ChangeLog.BeginNewBatch($"Set {buttonName}({val}): {frame}-{frame + count - 1}", true);
+
+ int firstChangedFrame = -1;
if (frame + count >= Log.Count) // Insert blank frames up to this point
{
- ExtendMovieForEdit(frame - Log.Count + 1);
+ firstChangedFrame = Log.Count;
+ ExtendMovieForEdit(frame + count - Log.Count);
}
- ChangeLog.AddGeneralUndo(frame, frame + count - 1, $"Set {buttonName}({val}): {frame}-{frame + count - 1}");
-
- int changed = -1;
+ ChangeLog.AddGeneralUndo(frame, frame + count - 1);
for (int i = 0; i < count; i++)
{
var adapter = GetInputState(frame + i);
@@ -396,19 +348,32 @@ public void SetAxisStates(int frame, int count, string buttonName, int val)
Log[frame + i] = Bk2LogEntryGenerator.GenerateLogEntry(adapter);
- if (changed == -1 && old != val)
+ if (firstChangedFrame == -1 && old != val)
{
- changed = frame + i;
+ firstChangedFrame = frame + i;
}
}
+ ChangeLog.SetGeneralRedo();
- if (changed != -1)
+ if (endBatch) ChangeLog.EndBatch();
+
+ if (firstChangedFrame != -1) InvalidateAfter(firstChangedFrame);
+ }
+
+ private bool _suspendInvalidation;
+ private int _minInvalidationFrame;
+ public void SingleInvalidation(Action action)
+ {
+ bool wasSuspending = _suspendInvalidation;
+ if (!wasSuspending) _minInvalidationFrame = int.MaxValue;
+ _suspendInvalidation = true;
+ try { action(); }
+ finally { _suspendInvalidation = wasSuspending; }
+
+ if (!wasSuspending && _minInvalidationFrame != int.MaxValue)
{
- InvalidateAfter(changed);
- Changes = true;
+ InvalidateAfter(_minInvalidationFrame);
}
-
- ChangeLog.SetGeneralRedo();
}
}
}
diff --git a/src/BizHawk.Client.Common/movie/tasproj/TasMovie.History.cs b/src/BizHawk.Client.Common/movie/tasproj/TasMovie.History.cs
index 3a19ed4d7eb..b87f0cb4a7b 100644
--- a/src/BizHawk.Client.Common/movie/tasproj/TasMovie.History.cs
+++ b/src/BizHawk.Client.Common/movie/tasproj/TasMovie.History.cs
@@ -1,4 +1,5 @@
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
namespace BizHawk.Client.Common
@@ -7,28 +8,58 @@ public interface IMovieChangeLog
{
List Names { get; }
int UndoIndex { get; }
- string NextUndoStepName { get; }
+
+ ///
+ /// Gets or sets a value indicating whether the movie is recording action history.
+ /// 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 disabling the ChangeLog during undo/redo).
+ ///
bool IsRecording { get; set; }
void Clear(int upTo = -1);
+
+ ///
+ /// 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.
bool BeginNewBatch(string name = "", bool keepOldBatch = false);
+
+ ///
+ /// Ends the current undo batch. Future changes will be one undo each.
+ /// If not already recording a batch, does nothing.
+ ///
void EndBatch();
- int Undo();
- int Redo();
+
+ ///
+ /// Combine the last two undo actions, making them part of one batch action.
+ ///
+ void MergeLastActions();
+
+ ///
+ /// Undoes the most recent action batch, if any exist.
+ ///
+ void Undo();
+
+ ///
+ /// Redoes the most recent undo, if any exist.
+ ///
+ void Redo();
bool CanUndo { get; }
bool CanRedo { get; }
- int PreviousUndoFrame { get; }
- int PreviousRedoFrame { get; }
int MaxSteps { get; set; }
- void AddGeneralUndo(int first, int last, string name = "", bool force = false);
- void SetGeneralRedo(bool force = false);
- void AddBoolToggle(int frame, string button, bool oldState, string name = "", bool force = false);
- void AddAxisChange(int frame, string button, int oldState, int newState, string name = "", bool force = false);
- void AddMarkerChange(TasMovieMarker newMarker, int oldPosition = -1, string oldMessage = "", string name = "", bool force = false);
- void AddInputBind(int frame, bool isDelete, string name = "", bool force = false);
- void AddInsertFrames(int frame, int count, string name = "", bool force = false);
- void AddInsertInput(int frame, List newInputs, string name = "", bool force = false);
- void AddRemoveFrames(int removeStart, int removeUpTo, List oldInputs, List removedMarkers, string name = "", bool force = false);
+ void AddGeneralUndo(int first, int last, string name = "");
+ int SetGeneralRedo();
+ void AddBoolToggle(int frame, string button, bool oldState, string name = "");
+ void AddAxisChange(int frame, string button, int oldState, int newState, string name = "");
+ void AddMarkerChange(TasMovieMarker newMarker, int oldPosition = -1, string oldMessage = "", string name = "");
+ void AddInputBind(int frame, bool isDelete, string name = "");
+ void AddInsertFrames(int frame, int count, bool bindMarkers, string name = "");
+ void AddInsertInput(int frame, List newInputs, bool bindMarkers, string name = "");
+ void AddRemoveFrames(int removeStart, int removeUpTo, List oldInputs, bool bindMarkers, string name = "");
+ void AddExtend(int originalLength, int count, string inputs);
}
public class TasMovieChangeLog : IMovieChangeLog
@@ -64,11 +95,6 @@ public int MaxSteps
}
}
- ///
- /// Gets or sets a value indicating whether the movie is recording action history.
- /// 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 disabling the ChangeLog during undo/redo).
- ///
public bool IsRecording { get; set; } = true;
public void Clear(int upTo = -1)
@@ -104,18 +130,13 @@ private void TruncateLog(int from)
if (_recordingBatch)
{
+ // This means we are adding new actions to a batch that was undone while still in progress.
+ // So start a new one.
_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)
@@ -138,7 +159,7 @@ public bool BeginNewBatch(string name = "", bool keepOldBatch = false)
if (ret)
{
- ret = AddMovieAction(name);
+ AddMovieAction(name);
}
_recordingBatch = true;
@@ -146,10 +167,6 @@ public bool BeginNewBatch(string name = "", bool keepOldBatch = false)
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)
@@ -171,114 +188,73 @@ public void EndBatch()
}
}
- ///
- /// Undoes the most recent action batch, if any exist.
- ///
- /// Returns the frame which the movie needs to rewind to.
- public int Undo()
+ public void MergeLastActions()
{
- if (UndoIndex == -1)
- {
- return _movie.InputLogLength;
- }
-
- List batch = _history[UndoIndex];
- for (int i = batch.Count - 1; i >= 0; i--)
- {
- batch[i].Undo(_movie);
- }
+ Debug.Assert(UndoIndex + 1 == _history.Count, "Don't merge the last actions if they aren't the last actions.");
+ Debug.Assert(_history.Count > 1, "Don't merge when there aren't actions to merge.");
+ _history[_history.Count - 2].AddRange(_history[_history.Count - 1]);
+ _history.RemoveAt(_history.Count - 1);
+ Names.RemoveAt(Names.Count - 1);
UndoIndex--;
-
- _recordingBatch = false;
- return batch.TrueForAll(static a => a is MovieActionMarker) ? _movie.InputLogLength : PreviousUndoFrame;
}
- ///
- /// Redoes the most recent undo, if any exist.
- ///
- /// Returns the frame which the movie needs to rewind to.
- public int Redo()
+ public void Undo()
{
- if (UndoIndex == _history.Count - 1)
+ if (UndoIndex == -1)
{
- return _movie.InputLogLength;
+ return;
}
- UndoIndex++;
List batch = _history[UndoIndex];
- foreach (IMovieAction b in batch)
- {
- b.Redo(_movie);
- }
-
- _recordingBatch = false;
- return batch.TrueForAll(static a => a is MovieActionMarker) ? _movie.InputLogLength : PreviousRedoFrame;
- }
-
- public bool CanUndo => UndoIndex > -1;
- public bool CanRedo => UndoIndex < _history.Count - 1;
+ UndoIndex--;
- public string NextUndoStepName
- {
- get
+ bool wasRecording = IsRecording;
+ IsRecording = false;
+ _movie.SingleInvalidation(() =>
{
- if (Names.Count == 0 || UndoIndex < 0)
+ for (int i = batch.Count - 1; i >= 0; i--)
{
- return null;
+ batch[i].Undo(_movie);
}
-
- return Names[UndoIndex];
- }
+ });
+ IsRecording = wasRecording;
}
- public int PreviousUndoFrame
+ public void Redo()
{
- get
+ if (UndoIndex == _history.Count - 1)
{
- 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);
+ return;
}
- }
- public int PreviousRedoFrame
- {
- get
- {
- if (UndoIndex == -1)
- {
- return _movie.InputLogLength;
- }
+ UndoIndex++;
+ List batch = _history[UndoIndex];
- if (_history[UndoIndex].Count == 0)
+ bool wasRecording = IsRecording;
+ IsRecording = false;
+ _movie.SingleInvalidation(() =>
+ {
+ foreach (IMovieAction b in batch)
{
- return _movie.InputLogLength;
+ b.Redo(_movie);
}
-
- return _history[UndoIndex].Min(a => a.FirstFrame);
- }
+ });
+ IsRecording = wasRecording;
}
- private bool AddMovieAction(string name)
+ public bool CanUndo => UndoIndex > -1;
+ public bool CanRedo => UndoIndex < _history.Count - 1;
+
+ private void AddMovieAction(string name)
{
if (UndoIndex + 1 != _history.Count)
{
TruncateLog(UndoIndex + 1);
}
if (name.Length is 0) name = $"Undo step {_totalSteps}";
- bool ret = false;
if (!_recordingBatch)
{
- ret = true;
_history.Add(new List(1));
Names.Add(name);
_totalSteps += 1;
@@ -291,19 +267,16 @@ private bool AddMovieAction(string name)
{
_history.RemoveAt(0);
Names.RemoveAt(0);
- ret = false;
}
}
-
- return ret;
}
// 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)
+ public void AddGeneralUndo(int first, int last, string name = "")
{
- if (IsRecording || force)
+ if (IsRecording)
{
AddMovieAction(name);
LatestBatch.Add(new MovieAction(first, last, _movie));
@@ -311,35 +284,48 @@ public void AddGeneralUndo(int first, int last, string name = "", bool force = f
}
}
- public void SetGeneralRedo(bool force = false)
+ public int SetGeneralRedo()
{
- if (IsRecording || force)
+ if (IsRecording)
{
- ((MovieAction) LatestBatch[_lastGeneral]).SetRedoLog(_movie);
+ Debug.Assert(_lastGeneral == LatestBatch.Count - 1, "GeneralRedo should not see changes from other undo actions.");
+ int changed = ((MovieAction) LatestBatch[_lastGeneral]).SetRedoLog(_movie);
+ if (changed == -1)
+ {
+ LatestBatch.RemoveAt(_lastGeneral);
+ if (LatestBatch.Count == 0 && !_recordingBatch)
+ {
+ // Remove this undo item
+ _recordingBatch = true;
+ EndBatch();
+ }
+ }
+ return changed;
}
+ return -1;
}
- public void AddBoolToggle(int frame, string button, bool oldState, string name = "", bool force = false)
+ public void AddBoolToggle(int frame, string button, bool oldState, string name = "")
{
- if (IsRecording || force)
+ if (IsRecording)
{
AddMovieAction(name);
LatestBatch.Add(new MovieActionFrameEdit(frame, button, oldState, !oldState));
}
}
- public void AddAxisChange(int frame, string button, int oldState, int newState, string name = "", bool force = false)
+ public void AddAxisChange(int frame, string button, int oldState, int newState, string name = "")
{
- if (IsRecording || force)
+ if (IsRecording)
{
AddMovieAction(name);
LatestBatch.Add(new MovieActionFrameEdit(frame, button, oldState, newState));
}
}
- public void AddMarkerChange(TasMovieMarker newMarker, int oldPosition = -1, string oldMessage = "", string name = "", bool force = false)
+ public void AddMarkerChange(TasMovieMarker newMarker, int oldPosition = -1, string oldMessage = "", string name = "")
{
- if (IsRecording || force)
+ if (IsRecording)
{
name = oldPosition == -1
? $"Set Marker at frame {newMarker.Frame}"
@@ -350,39 +336,48 @@ public void AddMarkerChange(TasMovieMarker newMarker, int oldPosition = -1, stri
}
}
- public void AddInputBind(int frame, bool isDelete, string name = "", bool force = false)
+ public void AddInputBind(int frame, bool isDelete, string name = "")
{
- if (IsRecording || force)
+ if (IsRecording)
{
AddMovieAction(name);
LatestBatch.Add(new MovieActionBindInput(_movie, frame, isDelete));
}
}
- public void AddInsertFrames(int frame, int count, string name = "", bool force = false)
+ public void AddInsertFrames(int frame, int count, bool bindMarkers, string name = "")
{
- if (IsRecording || force)
+ if (IsRecording)
{
AddMovieAction(name);
- LatestBatch.Add(new MovieActionInsertFrames(frame, count));
+ LatestBatch.Add(new MovieActionInsertFrames(frame, bindMarkers, count));
}
}
- public void AddInsertInput(int frame, List newInputs, string name = "", bool force = false)
+ public void AddInsertInput(int frame, List newInputs, bool bindMarkers, string name = "")
{
- if (IsRecording || force)
+ if (IsRecording)
{
AddMovieAction(name);
- LatestBatch.Add(new MovieActionInsertFrames(frame, newInputs));
+ LatestBatch.Add(new MovieActionInsertFrames(frame, bindMarkers, newInputs));
}
}
- public void AddRemoveFrames(int removeStart, int removeUpTo, List oldInputs, List removedMarkers, string name = "", bool force = false)
+ public void AddRemoveFrames(int removeStart, int removeUpTo, List oldInputs, bool bindMarkers, string name = "")
{
- if (IsRecording || force)
+ if (IsRecording)
{
AddMovieAction(name);
- LatestBatch.Add(new MovieActionRemoveFrames(removeStart, removeUpTo, oldInputs, removedMarkers));
+ LatestBatch.Add(new MovieActionRemoveFrames(removeStart, removeUpTo, oldInputs, bindMarkers));
+ }
+ }
+
+ public void AddExtend(int originalLength, int count, string inputs)
+ {
+ if (IsRecording)
+ {
+ AddMovieAction("extend movie");
+ LatestBatch.Add(new MovieActionExtend(originalLength, count, inputs));
}
}
}
@@ -425,20 +420,26 @@ public MovieAction(int firstFrame, int lastFrame, ITasMovie movie)
_bindMarkers = movie.BindMarkersToInput;
}
- public void SetRedoLog(ITasMovie movie)
+ /// Returns the first frame that has changed, or -1 if no changes.
+ public int SetRedoLog(ITasMovie movie)
{
_redoLength = Math.Min(LastFrame + 1, movie.InputLogLength) - FirstFrame;
_newLog = new List(_redoLength);
+ int changed = Math.Min(_redoLength, _undoLength);
for (int i = 0; i < _redoLength; i++)
{
- _newLog.Add(movie.GetInputLogEntry(FirstFrame + i));
+ string newEntry = movie.GetInputLogEntry(FirstFrame + i);
+ _newLog.Add(newEntry);
+ if (i < changed && newEntry != _oldLog[i]) changed = i;
}
+
+ if (changed == _redoLength && changed == _undoLength) return -1;
+ else return changed + FirstFrame;
}
public void Undo(ITasMovie movie)
{
- bool wasRecording = movie.ChangeLog.IsRecording;
- movie.ChangeLog.IsRecording = false;
+ bool wasBinding = movie.BindMarkersToInput;
movie.BindMarkersToInput = _bindMarkers;
if (_redoLength != Length)
@@ -455,15 +456,12 @@ public void Undo(ITasMovie movie)
{
movie.SetFrame(FirstFrame + i, _oldLog[i]);
}
-
- movie.ChangeLog.IsRecording = wasRecording;
- movie.BindMarkersToInput = _bindMarkers;
+ movie.BindMarkersToInput = wasBinding;
}
public void Redo(ITasMovie movie)
{
- bool wasRecording = movie.ChangeLog.IsRecording;
- movie.ChangeLog.IsRecording = false;
+ bool wasBinding = movie.BindMarkersToInput;
movie.BindMarkersToInput = _bindMarkers;
if (_undoLength != Length)
@@ -480,9 +478,7 @@ public void Redo(ITasMovie movie)
{
movie.SetFrame(FirstFrame + i, _newLog[i]);
}
-
- movie.ChangeLog.IsRecording = wasRecording;
- movie.BindMarkersToInput = _bindMarkers;
+ movie.BindMarkersToInput = wasBinding;
}
}
@@ -512,9 +508,6 @@ public MovieActionMarker(TasMovieMarker marker, int oldPosition = -1, string old
public void Undo(ITasMovie movie)
{
- bool wasRecording = movie.ChangeLog.IsRecording;
- movie.ChangeLog.IsRecording = false;
-
if (FirstFrame == -1) // Action: Place marker
{
movie.Markers.Remove(movie.Markers.Get(LastFrame));
@@ -528,15 +521,10 @@ public void Undo(ITasMovie movie)
movie.Markers.Move(LastFrame, FirstFrame);
movie.Markers.Get(LastFrame).Message = _oldMessage;
}
-
- movie.ChangeLog.IsRecording = wasRecording;
}
public void Redo(ITasMovie movie)
{
- bool wasRecording = movie.ChangeLog.IsRecording;
- movie.ChangeLog.IsRecording = false;
-
if (FirstFrame == -1) // Action: Place marker
{
movie.Markers.Add(LastFrame, _oldMessage);
@@ -550,8 +538,6 @@ public void Redo(ITasMovie movie)
movie.Markers.Move(FirstFrame, LastFrame);
movie.Markers.Get(LastFrame).Message = _newMessage;
}
-
- movie.ChangeLog.IsRecording = wasRecording;
}
}
@@ -585,9 +571,6 @@ public MovieActionFrameEdit(int frame, string button, int oldS, int newS)
public void Undo(ITasMovie movie)
{
- bool wasRecording = movie.ChangeLog.IsRecording;
- movie.ChangeLog.IsRecording = false;
-
if (_isAxis)
{
movie.SetAxisState(FirstFrame, _buttonName, _oldState);
@@ -596,15 +579,10 @@ public void Undo(ITasMovie movie)
{
movie.SetBoolState(FirstFrame, _buttonName, _oldState == 1);
}
-
- movie.ChangeLog.IsRecording = wasRecording;
}
public void Redo(ITasMovie movie)
{
- bool wasRecording = movie.ChangeLog.IsRecording;
- movie.ChangeLog.IsRecording = false;
-
if (_isAxis)
{
movie.SetAxisState(FirstFrame, _buttonName, _newState);
@@ -613,8 +591,6 @@ public void Redo(ITasMovie movie)
{
movie.SetBoolState(FirstFrame, _buttonName, _newState == 1);
}
-
- movie.ChangeLog.IsRecording = wasRecording;
}
}
@@ -658,9 +634,6 @@ public MovieActionPaint(int startFrame, int endFrame, string button, int newS, I
public void Undo(ITasMovie movie)
{
- bool wasRecording = movie.ChangeLog.IsRecording;
- movie.ChangeLog.IsRecording = false;
-
if (_isAxis)
{
for (int i = 0; i < _oldState.Count; i++)
@@ -675,15 +648,10 @@ public void Undo(ITasMovie movie)
movie.SetBoolState(FirstFrame + i, _buttonName, _oldState[i] == 1);
}
}
-
- movie.ChangeLog.IsRecording = wasRecording;
}
public void Redo(ITasMovie movie)
{
- bool wasRecording = movie.ChangeLog.IsRecording;
- movie.ChangeLog.IsRecording = false;
-
if (_isAxis)
{
movie.SetAxisStates(FirstFrame, LastFrame - FirstFrame + 1, _buttonName, _newState);
@@ -692,8 +660,6 @@ public void Redo(ITasMovie movie)
{
movie.SetBoolStates(FirstFrame, LastFrame - FirstFrame + 1, _buttonName, _newState == 1);
}
-
- movie.ChangeLog.IsRecording = wasRecording;
}
}
@@ -717,8 +683,6 @@ public MovieActionBindInput(ITasMovie movie, int frame, bool isDelete)
public void Undo(ITasMovie movie)
{
- bool wasRecording = movie.ChangeLog.IsRecording;
- movie.ChangeLog.IsRecording = false;
movie.BindMarkersToInput = _bindMarkers;
if (_delete) // Insert
@@ -731,15 +695,11 @@ public void Undo(ITasMovie movie)
movie.RemoveFrame(FirstFrame);
movie.LagLog.RemoveHistoryAt(FirstFrame + 1);
}
-
- movie.ChangeLog.IsRecording = wasRecording;
movie.BindMarkersToInput = _bindMarkers;
}
public void Redo(ITasMovie movie)
{
- bool wasRecording = movie.ChangeLog.IsRecording;
- movie.ChangeLog.IsRecording = false;
movie.BindMarkersToInput = _bindMarkers;
if (_delete)
@@ -752,8 +712,6 @@ public void Redo(ITasMovie movie)
movie.InsertInput(FirstFrame, _log);
movie.LagLog.InsertHistoryAt(FirstFrame + 1, true);
}
-
- movie.ChangeLog.IsRecording = wasRecording;
movie.BindMarkersToInput = _bindMarkers;
}
}
@@ -764,39 +722,42 @@ public class MovieActionInsertFrames : IMovieAction
public int LastFrame { get; }
private readonly int _count;
private readonly bool _onlyEmpty;
+ private readonly bool _bindMarkers;
private readonly List _newInputs;
- public MovieActionInsertFrames(int frame, int count)
+ public MovieActionInsertFrames(int frame, bool bindMarkers, int count)
{
FirstFrame = frame;
LastFrame = frame + count;
_count = count;
_onlyEmpty = true;
+ _bindMarkers = bindMarkers;
}
- public MovieActionInsertFrames(int frame, List newInputs)
+ public MovieActionInsertFrames(int frame, bool bindMarkers, List newInputs)
{
FirstFrame = frame;
LastFrame = frame + newInputs.Count;
_onlyEmpty = false;
_newInputs = newInputs;
+ _bindMarkers = bindMarkers;
}
public void Undo(ITasMovie movie)
{
- bool wasRecording = movie.ChangeLog.IsRecording;
- movie.ChangeLog.IsRecording = false;
+ bool wasBinding = movie.BindMarkersToInput;
+ movie.BindMarkersToInput = _bindMarkers;
movie.RemoveFrames(FirstFrame, LastFrame);
- movie.ChangeLog.IsRecording = wasRecording;
+ movie.BindMarkersToInput = wasBinding;
}
public void Redo(ITasMovie movie)
{
- bool wasRecording = movie.ChangeLog.IsRecording;
- movie.ChangeLog.IsRecording = false;
+ bool wasBinding = movie.BindMarkersToInput;
+ movie.BindMarkersToInput = _bindMarkers;
if (_onlyEmpty)
{
@@ -807,7 +768,7 @@ public void Redo(ITasMovie movie)
movie.InsertInput(FirstFrame, _newInputs);
}
- movie.ChangeLog.IsRecording = wasRecording;
+ movie.BindMarkersToInput = wasBinding;
}
}
@@ -817,36 +778,69 @@ public class MovieActionRemoveFrames : IMovieAction
public int LastFrame { get; }
private readonly List _oldInputs;
- private readonly List _removedMarkers;
+ private readonly bool _bindMarkers;
- public MovieActionRemoveFrames(int removeStart, int removeUpTo, List oldInputs, List removedMarkers)
+ public MovieActionRemoveFrames(int removeStart, int removeUpTo, List oldInputs, bool bindMarkers)
{
FirstFrame = removeStart;
LastFrame = removeUpTo;
_oldInputs = oldInputs;
- _removedMarkers = removedMarkers;
+ _bindMarkers = bindMarkers;
}
public void Undo(ITasMovie movie)
{
- bool wasRecording = movie.ChangeLog.IsRecording;
- movie.ChangeLog.IsRecording = false;
+ bool wasBinding = movie.BindMarkersToInput;
+ movie.BindMarkersToInput = _bindMarkers;
movie.InsertInput(FirstFrame, _oldInputs);
- movie.Markers.AddRange(_removedMarkers);
-
- movie.ChangeLog.IsRecording = wasRecording;
+ movie.BindMarkersToInput = wasBinding;
}
public void Redo(ITasMovie movie)
{
- bool wasRecording = movie.ChangeLog.IsRecording;
- movie.ChangeLog.IsRecording = false;
+ bool wasBinding = movie.BindMarkersToInput;
+ movie.BindMarkersToInput = _bindMarkers;
movie.RemoveFrames(FirstFrame, LastFrame);
- movie.ChangeLog.IsRecording = wasRecording;
+ movie.BindMarkersToInput = wasBinding;
+ }
+ }
+
+
+ public class MovieActionExtend : IMovieAction
+ {
+ public int FirstFrame { get; }
+ public int LastFrame => FirstFrame + _count - 1;
+
+ private int _count;
+ private string _inputs;
+
+ public MovieActionExtend(int currentEndOfMovie, int count, string inputs)
+ {
+ FirstFrame = currentEndOfMovie;
+ _count = count;
+ _inputs = inputs;
+ }
+
+ public void Undo(ITasMovie movie)
+ {
+ bool wasMarkerBound = movie.BindMarkersToInput;
+ movie.BindMarkersToInput = false;
+
+ movie.RemoveFrames(FirstFrame, LastFrame + 1);
+ movie.BindMarkersToInput = wasMarkerBound;
+ }
+
+ public void Redo(ITasMovie movie)
+ {
+ bool wasMarkerBound = movie.BindMarkersToInput;
+ movie.BindMarkersToInput = false;
+
+ movie.InsertInput(FirstFrame, Enumerable.Repeat(_inputs, _count));
+ movie.BindMarkersToInput = wasMarkerBound;
}
}
}
diff --git a/src/BizHawk.Client.Common/movie/tasproj/TasMovie.cs b/src/BizHawk.Client.Common/movie/tasproj/TasMovie.cs
index b0bf0a1268f..28d585cdd22 100644
--- a/src/BizHawk.Client.Common/movie/tasproj/TasMovie.cs
+++ b/src/BizHawk.Client.Common/movie/tasproj/TasMovie.cs
@@ -121,21 +121,24 @@ public override void StartNewRecording()
// Removes lag log and greenzone after this frame
private void InvalidateAfter(int frame)
{
- var anyLagInvalidated = LagLog.RemoveFrom(frame);
- var anyStateInvalidated = TasStateManager.InvalidateAfter(frame);
- GreenzoneInvalidated?.Invoke(frame);
- if (anyLagInvalidated || anyStateInvalidated)
+ if (_suspendInvalidation)
{
- Changes = true;
+ _minInvalidationFrame = Math.Min(_minInvalidationFrame, frame);
+ return;
}
- LastEditedFrame = frame;
- LastEditWasRecording = false; // We can set it here; it's only used in the GreenzoneInvalidated action.
+ LagLog.RemoveFrom(frame);
+ var anyStateInvalidated = TasStateManager.InvalidateAfter(frame);
+ Changes = true;
+ LastEditedFrame = frame;
if (anyStateInvalidated && IsCountingRerecords)
{
Rerecords++;
}
+
+ GreenzoneInvalidated?.Invoke(frame);
+ LastEditWasRecording = false; // We can set it here; it's only used in the GreenzoneInvalidated action.
}
public void InvalidateEntireGreenzone()
@@ -149,7 +152,7 @@ public void InvalidateEntireGreenzone()
///
public string DisplayValue(int frame, string buttonName)
{
- if (_displayCache.Frame != frame)
+ if (_displayCache.Frame != frame || Log.Count == 1)
{
_displayCache.Controller ??= new Bk2Controller(Session.MovieController.Definition, LogKey);
_displayCache.Controller.SetFromMnemonic(Log[frame]);
@@ -293,14 +296,12 @@ public void LoadBranch(TasBranch branch)
Log?.Dispose();
Log = branch.InputLog.Clone();
- InvalidateAfter(divergentPoint ?? branch.InputLog.Count);
-
if (BindMarkersToInput) // pretty critical not to erase them
{
Markers = branch.Markers.DeepClone();
}
- Changes = true;
+ InvalidateAfter(divergentPoint ?? branch.InputLog.Count);
}
public event PropertyChangedEventHandler PropertyChanged;
diff --git a/src/BizHawk.Client.EmuHawk/MainForm.cs b/src/BizHawk.Client.EmuHawk/MainForm.cs
index 91783b6aa80..d96de3b53dc 100644
--- a/src/BizHawk.Client.EmuHawk/MainForm.cs
+++ b/src/BizHawk.Client.EmuHawk/MainForm.cs
@@ -1045,6 +1045,7 @@ private set
InitializeFpsData();
}
+ if (value != _emulatorPaused) Tools.OnPauseToggle(value);
_emulatorPaused = value;
}
}
diff --git a/src/BizHawk.Client.EmuHawk/tools/Lua/Libraries/TAStudioLuaLibrary.cs b/src/BizHawk.Client.EmuHawk/tools/Lua/Libraries/TAStudioLuaLibrary.cs
index be91c0e09fc..b53d2c4472b 100644
--- a/src/BizHawk.Client.EmuHawk/tools/Lua/Libraries/TAStudioLuaLibrary.cs
+++ b/src/BizHawk.Client.EmuHawk/tools/Lua/Libraries/TAStudioLuaLibrary.cs
@@ -314,8 +314,10 @@ public void ApplyInputChanges()
_luaLibsImpl.IsUpdateSupressed = true;
- Tastudio.ApiHawkBatchEdit(() =>
+ Tastudio.StopRecordingOnNextEdit = false;
+ Tastudio.CurrentTasMovie.SingleInvalidation(() =>
{
+ Tastudio.CurrentTasMovie.ChangeLog.BeginNewBatch("tastudio.applyinputchanges");
int size = _changeList.Count;
for (int i = 0; i < size; i++)
@@ -334,17 +336,28 @@ public void ApplyInputChanges()
}
break;
case LuaChangeTypes.InsertFrames:
- Tastudio.InsertNumFrames(_changeList[i].Frame, _changeList[i].Number);
+ Tastudio.CurrentTasMovie.InsertEmptyFrame(_changeList[i].Frame, _changeList[i].Number);
break;
case LuaChangeTypes.DeleteFrames:
- Tastudio.DeleteFrames(_changeList[i].Frame, _changeList[i].Number);
+ int endExclusive = _changeList[i].Frame + _changeList[i].Number;
+ endExclusive = Math.Min(Tastudio.CurrentTasMovie.InputLogLength, endExclusive);
+ if (_changeList[i].Frame < endExclusive)
+ {
+ Tastudio.CurrentTasMovie.RemoveFrames(_changeList[i].Frame, endExclusive);
+ }
break;
case LuaChangeTypes.ClearFrames:
- Tastudio.ClearFrames(_changeList[i].Frame, _changeList[i].Number);
+ endExclusive = _changeList[i].Frame + _changeList[i].Number;
+ endExclusive = Math.Min(Tastudio.CurrentTasMovie.InputLogLength, endExclusive);
+ for (int j = _changeList[i].Frame; j < endExclusive; j++)
+ {
+ Tastudio.CurrentTasMovie.ClearFrame(j);
+ }
break;
}
}
_changeList.Clear();
+ Tastudio.CurrentTasMovie.ChangeLog.EndBatch();
});
_luaLibsImpl.IsUpdateSupressed = false;
@@ -471,7 +484,6 @@ public void RemoveMarker(int frame)
if (marker != null)
{
Tastudio.CurrentTasMovie.Markers.Remove(marker);
- Tastudio.RefreshDialog();
}
}
}
@@ -491,7 +503,6 @@ public void SetMarker(int frame, string message = null)
else
{
Tastudio.CurrentTasMovie.Markers.Add(frame, message1);
- Tastudio.RefreshDialog();
}
}
}
@@ -544,7 +555,7 @@ public void ClearIconCache()
}
[LuaMethodExample("tastudio.ongreenzoneinvalidated( function( currentindex )\r\n\tconsole.log( \"Called whenever the greenzone is invalidated.\" );\r\nend );")]
- [LuaMethod("ongreenzoneinvalidated", "Called whenever the greenzone is invalidated. Your callback can have 1 parameter, which will be the index of the last row before the invalidated ones.")]
+ [LuaMethod("ongreenzoneinvalidated", "Called whenever the movie is modified in a way that could invalidate savestates in the movie's state history. Called regardless of whether any states were actually invalidated. Your callback can have 1 parameter, which will be the last frame before the invalidated ones. That is, the first of the modified frames.")]
public void OnGreenzoneInvalidated(LuaFunction luaf)
{
if (Engaged())
diff --git a/src/BizHawk.Client.EmuHawk/tools/TAStudio/MarkerControl.cs b/src/BizHawk.Client.EmuHawk/tools/TAStudio/MarkerControl.cs
index 0c2b854477c..dd2054df43d 100644
--- a/src/BizHawk.Client.EmuHawk/tools/TAStudio/MarkerControl.cs
+++ b/src/BizHawk.Client.EmuHawk/tools/TAStudio/MarkerControl.cs
@@ -157,7 +157,6 @@ private void RemoveMarkerToolStripMenuItem_Click(object sender, EventArgs e)
if (!MarkerView.AnyRowsSelected) return;
foreach (var i in MarkerView.SelectedRows.Select(index => Markers[index]).ToList()) Markers.Remove(i);
MarkerView.RowCount = Markers.Count;
- Tastudio.RefreshDialog();
}
public void UpdateMarkerCount()
@@ -193,7 +192,6 @@ public void AddMarker(int frame, bool editText = false)
Markers.Add(marker);
var index = Markers.IndexOf(marker);
MarkerView.MakeIndexVisible(index);
- Tastudio.RefreshDialog();
}
public void UpdateTextColumnWidth()
@@ -251,7 +249,6 @@ public void EditMarkerFramePopUp(TasMovieMarker marker)
Markers.Move(marker.Frame, promptValue);
UpdateTextColumnWidth();
UpdateValues();
- Tastudio.RefreshDialog();
}
public void UpdateValues()
diff --git a/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.IToolForm.cs b/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.IToolForm.cs
index 6c09b298233..a5e13e94086 100644
--- a/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.IToolForm.cs
+++ b/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.IToolForm.cs
@@ -21,6 +21,8 @@ public partial class TAStudio : IToolForm
private int _lastRefresh;
private bool _doPause;
+ private bool _extended;
+ private bool _extendNeedsMerge;
private void UpdateProgressBar()
{
@@ -55,7 +57,8 @@ private void UpdateProgressBar()
protected override void UpdateBefore()
{
- if (CurrentTasMovie.IsAtEnd())
+ _extended = CurrentTasMovie.IsAtEnd() && !CurrentTasMovie.IsRecording();
+ if (_extended)
{
CurrentTasMovie.RecordFrame(CurrentTasMovie.Emulator.Frame, MovieSession.StickySource);
}
@@ -108,6 +111,18 @@ protected override void UpdateAfter()
protected override void FastUpdateAfter()
{
+ // Recording multiple frames, or auto-extending the movie, while unpaused should count as a single undo action.
+ // And do this before stopping the seek, which could puase.
+ if (!MainForm.EmulatorPaused && (CurrentTasMovie.IsRecording() || _extended))
+ {
+ if (_extendNeedsMerge) CurrentTasMovie.ChangeLog.MergeLastActions();
+ _extendNeedsMerge = true;
+ }
+ else
+ {
+ _extendNeedsMerge = false;
+ }
+
if (_seekingTo != -1 && Emulator.Frame >= _seekingTo)
{
StopSeeking();
diff --git a/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.ListView.cs b/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.ListView.cs
index d4045de546d..6072887b8f9 100644
--- a/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.ListView.cs
+++ b/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.ListView.cs
@@ -27,7 +27,6 @@ public partial class TAStudio
private int _startRow;
private int _batchEditMinFrame = -1;
private bool _batchEditing;
- private bool _editIsFromLua;
// Editing analog input
private string _axisEditColumn = "";
@@ -378,7 +377,6 @@ private void TasView_ColumnClick(object sender, InputRoll.ColumnClickEventArgs e
if (columnName == FrameColumnName)
{
CurrentTasMovie.Markers.Add(TasView.SelectionEndIndex!.Value, "");
- RefreshDialog();
}
else if (columnName != CursorColumnName)
{
@@ -405,10 +403,13 @@ private void TasView_ColumnClick(object sender, InputRoll.ColumnClickEventArgs e
else
{
BoolPatterns[ControllerType.BoolButtons.IndexOf(buttonName)].Reset();
- foreach (var index in TasView.SelectedRows)
+ CurrentTasMovie.SingleInvalidation(() =>
{
- CurrentTasMovie.SetBoolState(index, buttonName, BoolPatterns[ControllerType.BoolButtons.IndexOf(buttonName)].GetNextValue());
- }
+ foreach (var index in TasView.SelectedRows)
+ {
+ CurrentTasMovie.SetBoolState(index, buttonName, BoolPatterns[ControllerType.BoolButtons.IndexOf(buttonName)].GetNextValue());
+ }
+ });
}
}
else
@@ -784,22 +785,6 @@ private bool EndBatchEdit()
return false;
}
- public void ApiHawkBatchEdit(Action action)
- {
- // This is only caled from Lua.
- _editIsFromLua = true;
- BeginBatchEdit();
- try
- {
- action();
- }
- finally
- {
- EndBatchEdit();
- _editIsFromLua = false;
- }
- }
-
///
/// Disables recording mode, ensures we are in the greenzone, and does autorestore if needed.
/// If a mouse button is down, only tracks the edit so we can do this stuff on mouse up.
@@ -829,11 +814,12 @@ public bool FrameEdited(int frame)
}
else
{
- if (!_editIsFromLua)
+ if (StopRecordingOnNextEdit)
{
// Lua users will want to preserve recording mode.
TastudioPlayMode(true);
}
+ StopRecordingOnNextEdit = true;
if (Emulator.Frame > frame)
{
@@ -867,11 +853,18 @@ public bool FrameEdited(int frame)
RefreshDialog();
return true;
}
- else if (TasView.RowCount != CurrentTasMovie.InputLogLength + 1)
+ else
{
- // Row count must always be kept up to date even if last row is not directly visible.
- TasView.RowCount = CurrentTasMovie.InputLogLength + 1;
- return true;
+ if (_undoForm != null && !_undoForm.IsDisposed)
+ {
+ _undoForm.UpdateValues();
+ }
+ if (TasView.RowCount != CurrentTasMovie.InputLogLength + 1)
+ {
+ // Row count must always be kept up to date even if last row is not directly visible.
+ TasView.RowCount = CurrentTasMovie.InputLogLength + 1;
+ return true;
+ }
}
}
@@ -1099,6 +1092,30 @@ private void TasView_PointedCellChanged(object sender, InputRoll.CellEventArgs e
}
else if (_rightClickFrame != -1)
{
+ FramePaint(frame, startVal, endVal);
+ }
+ // Left-click
+ else if (TasView.IsPaintDown && !string.IsNullOrEmpty(_startBoolDrawColumn))
+ {
+ BoolPaint(frame, startVal, endVal);
+ }
+ else if (TasView.IsPaintDown && !string.IsNullOrEmpty(_startAxisDrawColumn))
+ {
+ AxisPaint(frame, startVal, endVal);
+ }
+
+ CurrentTasMovie.IsCountingRerecords = wasCountingRerecords;
+
+ if (MouseButtonHeld)
+ {
+ TasView.MakeIndexVisible(TasView.CurrentCell.RowIndex.Value); // todo: limit scrolling speed
+ SetTasViewRowCount(); // refreshes
+ }
+ }
+
+ private void FramePaint(int frame, int startVal, int endVal)
+ {
+ CurrentTasMovie.SingleInvalidation(() => {
if (frame > CurrentTasMovie.InputLogLength - _rightClickInput.Length)
{
frame = CurrentTasMovie.InputLogLength - _rightClickInput.Length;
@@ -1202,11 +1219,12 @@ private void TasView_PointedCellChanged(object sender, InputRoll.CellEventArgs e
{
_suppressContextMenu = true;
}
- }
+ });
+ }
- // Left-click
- else if (TasView.IsPaintDown && !string.IsNullOrEmpty(_startBoolDrawColumn))
- {
+ private void BoolPaint(int frame, int startVal, int endVal)
+ {
+ CurrentTasMovie.SingleInvalidation(() => {
CurrentTasMovie.IsCountingRerecords = false;
for (int i = startVal; i <= endVal; i++) // Inclusive on both ends (drawing up or down)
@@ -1227,9 +1245,12 @@ private void TasView_PointedCellChanged(object sender, InputRoll.CellEventArgs e
CurrentTasMovie.SetBoolState(i, _startBoolDrawColumn, setVal); // Notice it uses new row, old column, you can only paint across a single column
}
- }
+ });
+ }
- else if (TasView.IsPaintDown && !string.IsNullOrEmpty(_startAxisDrawColumn))
+ private void AxisPaint(int frame, int startVal, int endVal)
+ {
+ CurrentTasMovie.SingleInvalidation(() =>
{
CurrentTasMovie.IsCountingRerecords = false;
@@ -1250,15 +1271,7 @@ private void TasView_PointedCellChanged(object sender, InputRoll.CellEventArgs e
CurrentTasMovie.SetAxisState(i, _startAxisDrawColumn, setVal); // Notice it uses new row, old column, you can only paint across a single column
}
- }
-
- CurrentTasMovie.IsCountingRerecords = wasCountingRerecords;
-
- if (MouseButtonHeld)
- {
- TasView.MakeIndexVisible(TasView.CurrentCell.RowIndex.Value); // todo: limit scrolling speed
- SetTasViewRowCount(); // refreshes
- }
+ });
}
private void TasView_MouseMove(object sender, MouseEventArgs e)
@@ -1339,6 +1352,7 @@ public void EditAnalogProgrammatically(KeyEventArgs e)
return;
}
+ // TODO: properly handle axis editing batches
BeginBatchEdit();
int value = CurrentTasMovie.GetAxisState(_axisEditRow, _axisEditColumn);
diff --git a/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.MenuItems.cs b/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.MenuItems.cs
index 9abd391c161..2e78e58b4a0 100644
--- a/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.MenuItems.cs
+++ b/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.MenuItems.cs
@@ -277,42 +277,30 @@ private void EditSubMenu_DropDownOpened(object sender, EventArgs e)
GreenzoneICheckSeparator.Visible =
StateHistoryIntegrityCheckMenuItem.Visible =
VersionInfo.DeveloperBuild;
+
+ UndoMenuItem.Enabled = CurrentTasMovie.ChangeLog.CanUndo;
+ RedoMenuItem.Enabled = CurrentTasMovie.ChangeLog.CanRedo;
}
private void UndoMenuItem_Click(object sender, EventArgs e)
{
- if (CurrentTasMovie.ChangeLog.Undo() < Emulator.Frame)
- {
- GoToFrame(CurrentTasMovie.ChangeLog.PreviousUndoFrame);
- }
- else
- {
- RefreshDialog();
- }
-
- // Currently I don't have a way to easily detect when CanUndo changes, so this button should be enabled always.
- // UndoMenuItem.Enabled = CurrentTasMovie.ChangeLog.CanUndo;
- RedoMenuItem.Enabled = CurrentTasMovie.ChangeLog.CanRedo;
+ CurrentTasMovie.ChangeLog.Undo();
+ _extendNeedsMerge = false;
}
private void RedoMenuItem_Click(object sender, EventArgs e)
{
- if (CurrentTasMovie.ChangeLog.Redo() < Emulator.Frame)
- {
- GoToFrame(CurrentTasMovie.ChangeLog.PreviousRedoFrame);
- }
- else
- {
- RefreshDialog();
- }
-
- // Currently I don't have a way to easily detect when CanUndo changes, so this button should be enabled always.
- // UndoMenuItem.Enabled = CurrentTasMovie.ChangeLog.CanUndo;
- RedoMenuItem.Enabled = CurrentTasMovie.ChangeLog.CanRedo;
+ CurrentTasMovie.ChangeLog.Redo();
}
private void ShowUndoHistoryMenuItem_Click(object sender, EventArgs e)
{
+ if (_undoForm != null && !_undoForm.IsDisposed)
+ {
+ // We could just BringToFront, but closing is probably better since the new one will appear in the expected screen location.
+ _undoForm.Close();
+ }
+
_undoForm = new UndoHistoryForm(this) { Owner = this };
_undoForm.Show();
_undoForm.UpdateValues();
@@ -487,18 +475,17 @@ private void CutMenuItem_Click(object sender, EventArgs e)
}
Clipboard.SetDataObject(sb.ToString());
- BeginBatchEdit(); // movie's RemoveFrames may make multiple separate invalidations
CurrentTasMovie.RemoveFrames(list);
- EndBatchEdit();
SetSplicer();
}
}
private void ClearFramesMenuItem_Click(object sender, EventArgs e)
{
- if (TasView.Focused && TasView.AnyRowsSelected)
+ if (!TasView.Focused || !TasView.AnyRowsSelected) return;
+
+ CurrentTasMovie.SingleInvalidation(() =>
{
- BeginBatchEdit();
CurrentTasMovie.ChangeLog.BeginNewBatch($"Clear frames {TasView.SelectionStartIndex}-{TasView.SelectionEndIndex}");
foreach (int frame in TasView.SelectedRows)
{
@@ -506,8 +493,7 @@ private void ClearFramesMenuItem_Click(object sender, EventArgs e)
}
CurrentTasMovie.ChangeLog.EndBatch();
- EndBatchEdit();
- }
+ });
}
private void DeleteFramesMenuItem_Click(object sender, EventArgs e)
@@ -523,9 +509,7 @@ private void DeleteFramesMenuItem_Click(object sender, EventArgs e)
return;
}
- BeginBatchEdit(); // movie's RemoveFrames may make multiple separate invalidations
CurrentTasMovie.RemoveFrames(TasView.SelectedRows.ToArray());
- EndBatchEdit();
SetTasViewRowCount();
SetSplicer();
}
@@ -547,22 +531,28 @@ private void CloneFramesXTimesMenuItem_Click(object sender, EventArgs e)
private void CloneFramesXTimes(int timesToClone)
{
- BeginBatchEdit();
- for (int i = 0; i < timesToClone; i++)
- {
- if (TasView.Focused && TasView.AnyRowsSelected)
- {
- var framesToInsert = TasView.SelectedRows;
- var insertionFrame = Math.Min((TasView.SelectionEndIndex ?? 0) + 1, CurrentTasMovie.InputLogLength);
+ if (!TasView.Focused || !TasView.AnyRowsSelected) return;
- var inputLog = framesToInsert
- .Select(frame => CurrentTasMovie.GetInputLogEntry(frame))
- .ToList();
+ var framesToInsert = TasView.SelectedRows;
+ var insertionFrame = Math.Min((TasView.SelectionEndIndex ?? 0) + 1, CurrentTasMovie.InputLogLength);
+
+ var inputLog = framesToInsert
+ .Select(CurrentTasMovie.GetInputLogEntry)
+ .ToList();
+ CurrentTasMovie.SingleInvalidation(() =>
+ {
+ string batchName = $"Clone {inputLog.Count} frames starting at {TasView.FirstSelectedRowIndex}";
+ if (timesToClone != 1) batchName += $" {timesToClone} times";
+ CurrentTasMovie.ChangeLog.BeginNewBatch(batchName);
+
+ for (int i = 0; i < timesToClone; i++)
+ {
CurrentTasMovie.InsertInput(insertionFrame, inputLog);
}
- }
- EndBatchEdit();
+
+ CurrentTasMovie.ChangeLog.EndBatch();
+ });
}
private void InsertFrameMenuItem_Click(object sender, EventArgs e)
@@ -581,7 +571,7 @@ private void InsertNumFramesMenuItem_Click(object sender, EventArgs e)
using var framesPrompt = new FramesPrompt();
if (framesPrompt.ShowDialogOnScreen().IsOk())
{
- InsertNumFrames(insertionFrame, framesPrompt.Frames);
+ CurrentTasMovie.InsertEmptyFrame(insertionFrame, framesPrompt.Frames);
}
}
}
@@ -622,7 +612,6 @@ private void RemoveMarkersMenuItem_Click(object sender, EventArgs e)
{
CurrentTasMovie.Markers.RemoveAll(m => TasView.IsRowSelected(m.Frame));
MarkerControl.UpdateMarkerCount();
- RefreshDialog();
}
private void ClearGreenzoneMenuItem_Click(object sender, EventArgs e)
diff --git a/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.cs b/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.cs
index b7ddb20d4ca..07712656610 100644
--- a/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.cs
+++ b/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.cs
@@ -64,6 +64,11 @@ public static Icon ToolIcon
[ConfigPersist]
public Font TasViewFont { get; set; } = new Font("Arial", 8.25F, FontStyle.Bold, GraphicsUnit.Point, 0);
+ ///
+ /// This is meant to be used by Lua.
+ ///
+ public bool StopRecordingOnNextEdit = true;
+
public class TAStudioSettings
{
public TAStudioSettings()
@@ -585,7 +590,15 @@ private bool StartNewMovieWrapper(ITasMovie movie, bool isNew)
movie.BindMarkersToInput = Settings.BindMarkersToInput;
movie.GreenzoneInvalidated = (f) => _ = FrameEdited(f);
movie.ChangeLog.MaxSteps = Settings.MaxUndoSteps;
+
movie.PropertyChanged += TasMovie_OnPropertyChanged;
+ System.Collections.Specialized.NotifyCollectionChangedEventHandler refreshOnMarker = (_, _) => RefreshDialog();
+ movie.Markers.CollectionChanged += refreshOnMarker;
+ this.Disposed += (s, e) =>
+ {
+ movie.PropertyChanged -= TasMovie_OnPropertyChanged;
+ movie.Markers.CollectionChanged -= refreshOnMarker;
+ };
SuspendLayout();
WantsToControlStopMovie = false;
@@ -877,6 +890,13 @@ public void TogglePause()
MainForm.TogglePause();
}
+ public override void OnPauseToggle(bool newPauseState)
+ {
+ // Consecutively recorded frames are merged into one undo action, until we pause.
+ // Then a new undo action should be used.
+ if (newPauseState) _extendNeedsMerge = false;
+ }
+
private void SetSplicer()
{
// TODO: columns selected?
@@ -887,45 +907,6 @@ private void SetSplicer()
SplicerStatusLabel.Text = temp;
}
- public void InsertNumFrames(int insertionFrame, int numberOfFrames)
- {
- if (insertionFrame <= CurrentTasMovie.InputLogLength)
- {
- CurrentTasMovie.InsertEmptyFrame(insertionFrame, numberOfFrames);
- }
- }
-
- public void DeleteFrames(int beginningFrame, int numberOfFrames)
- {
- if (beginningFrame < CurrentTasMovie.InputLogLength)
- {
- // movie's RemoveFrames might do multiple separate invalidations
- BeginBatchEdit();
-
- int[] framesToRemove = Enumerable.Range(beginningFrame, numberOfFrames).ToArray();
- CurrentTasMovie.RemoveFrames(framesToRemove);
- SetSplicer();
-
- EndBatchEdit();
- }
- }
-
- public void ClearFrames(int beginningFrame, int numberOfFrames)
- {
- if (beginningFrame < CurrentTasMovie.InputLogLength)
- {
- BeginBatchEdit();
-
- int last = Math.Min(beginningFrame + numberOfFrames, CurrentTasMovie.InputLogLength);
- for (int i = beginningFrame; i < last; i++)
- {
- CurrentTasMovie.ClearFrame(i);
- }
-
- EndBatchEdit();
- }
- }
-
private void Tastudio_Closing(object sender, FormClosingEventArgs e)
{
if (!_initialized)
@@ -1059,7 +1040,7 @@ private void TasView_CellDropped(object sender, InputRoll.CellEventArgs e)
if (e.NewCell?.RowIndex != null && !CurrentTasMovie.Markers.IsMarker(e.NewCell.RowIndex.Value))
{
CurrentTasMovie.Markers.Move(e.OldCell.RowIndex.Value, e.NewCell.RowIndex.Value);
- RefreshDialog();
+ RefreshDialog(); // Marker move might have been rejected so we need to manually refresh.
}
}
diff --git a/src/BizHawk.Client.EmuHawk/tools/TAStudio/UndoHistoryForm.cs b/src/BizHawk.Client.EmuHawk/tools/TAStudio/UndoHistoryForm.cs
index 32892bc021e..77767a8d65f 100644
--- a/src/BizHawk.Client.EmuHawk/tools/TAStudio/UndoHistoryForm.cs
+++ b/src/BizHawk.Client.EmuHawk/tools/TAStudio/UndoHistoryForm.cs
@@ -10,7 +10,7 @@ public partial class UndoHistoryForm : Form
private const string UndoColumnName = "Undo Step";
private readonly TAStudio _tastudio;
- private string _lastUndoAction;
+ private int _lastUndoAction;
private IMovieChangeLog Log => _tastudio.CurrentTasMovie.ChangeLog;
public UndoHistoryForm(TAStudio owner)
@@ -50,14 +50,14 @@ private void HistoryView_QueryItemBkColor(int index, RollColumn column, ref Colo
public void UpdateValues()
{
HistoryView.RowCount = Log.Names.Count;
- if (AutoScrollCheck.Checked && _lastUndoAction != Log.NextUndoStepName)
+ if (AutoScrollCheck.Checked && _lastUndoAction != Log.UndoIndex)
{
- HistoryView.ScrollToIndex(Log.UndoIndex);
+ HistoryView.ScrollToIndex(Math.Max(Log.UndoIndex, 0));
HistoryView.DeselectAll();
HistoryView.SelectRow(Log.UndoIndex - 1, true);
}
- _lastUndoAction = Log.NextUndoStepName;
+ _lastUndoAction = Log.UndoIndex;
HistoryView.Refresh();
}
@@ -71,13 +71,11 @@ private void ClearButton_Click(object sender, EventArgs e)
private void UndoButton_Click(object sender, EventArgs e)
{
_tastudio.UndoExternal();
- _tastudio.RefreshDialog();
}
private void RedoButton_Click(object sender, EventArgs e)
{
_tastudio.RedoExternal();
- _tastudio.RefreshDialog();
}
private int SelectedItem
@@ -85,20 +83,16 @@ private int SelectedItem
private void UndoToHere(int index)
{
- int earliestFrame = int.MaxValue;
- while (Log.UndoIndex > index)
+ _tastudio.CurrentTasMovie.SingleInvalidation(() =>
{
- int frame = Log.Undo();
- if (frame < earliestFrame)
- earliestFrame = frame;
- }
+ while (Log.UndoIndex > index)
+ {
+ // Although we have a reference to the Log, TAStudio needs to do a little extra on undo.
+ _tastudio.UndoExternal();
+ }
+ });
UpdateValues();
-
- // potentially rewind, then update display for TAStudio
- if (_tastudio.Emulator.Frame > earliestFrame)
- _tastudio.GoToFrame(earliestFrame);
- _tastudio.RefreshDialog();
}
private void HistoryView_DoubleClick(object sender, EventArgs e)
@@ -136,20 +130,15 @@ private void UndoHereMenuItem_Click(object sender, EventArgs e)
private void RedoHereMenuItem_Click(object sender, EventArgs e)
{
- int earliestFrame = int.MaxValue;
- while (Log.UndoIndex < SelectedItem)
+ _tastudio.CurrentTasMovie.SingleInvalidation(() =>
{
- int frame = Log.Redo();
- if (earliestFrame < frame)
- earliestFrame = frame;
- }
+ while (Log.UndoIndex < SelectedItem)
+ {
+ Log.Redo();
+ }
+ });
UpdateValues();
-
- // potentially rewind, then update display for TAStudio
- if (_tastudio.Emulator.Frame > earliestFrame)
- _tastudio.GoToFrame(earliestFrame);
- _tastudio.RefreshDialog();
}
private void ClearHistoryToHereMenuItem_Click(object sender, EventArgs e)
diff --git a/src/BizHawk.Client.EmuHawk/tools/ToolFormBase.cs b/src/BizHawk.Client.EmuHawk/tools/ToolFormBase.cs
index cb1f8e64b86..176a21d712b 100644
--- a/src/BizHawk.Client.EmuHawk/tools/ToolFormBase.cs
+++ b/src/BizHawk.Client.EmuHawk/tools/ToolFormBase.cs
@@ -136,5 +136,7 @@ protected override void OnLoad(EventArgs e)
}
public virtual void HandleHotkeyUpdate() { }
+
+ public virtual void OnPauseToggle(bool newPauseState) { }
}
}
diff --git a/src/BizHawk.Client.EmuHawk/tools/ToolManager.cs b/src/BizHawk.Client.EmuHawk/tools/ToolManager.cs
index 1b5a3612411..bec31d413cb 100644
--- a/src/BizHawk.Client.EmuHawk/tools/ToolManager.cs
+++ b/src/BizHawk.Client.EmuHawk/tools/ToolManager.cs
@@ -804,6 +804,17 @@ public void HandleHotkeyUpdate()
}
}
+ public void OnPauseToggle(bool newPauseState)
+ {
+ foreach (var tool in _tools)
+ {
+ if (tool.IsActive && tool is ToolFormBase toolForm)
+ {
+ toolForm.OnPauseToggle(newPauseState);
+ }
+ }
+ }
+
private static readonly IList PossibleToolTypeNames = EmuHawk.ReflectionCache.Types.Select(t => t.AssemblyQualifiedName).ToList();
public bool IsAvailable(Type tool)
diff --git a/src/BizHawk.Tests.Client.Common/BizHawk.Tests.Client.Common.csproj b/src/BizHawk.Tests.Client.Common/BizHawk.Tests.Client.Common.csproj
index 79cc055084d..86e6f698719 100644
--- a/src/BizHawk.Tests.Client.Common/BizHawk.Tests.Client.Common.csproj
+++ b/src/BizHawk.Tests.Client.Common/BizHawk.Tests.Client.Common.csproj
@@ -1,4 +1,4 @@
-
+
net48
diff --git a/src/BizHawk.Tests.Client.Common/Movie/FakeEmulator.cs b/src/BizHawk.Tests.Client.Common/Movie/FakeEmulator.cs
new file mode 100644
index 00000000000..4c2661b4b52
--- /dev/null
+++ b/src/BizHawk.Tests.Client.Common/Movie/FakeEmulator.cs
@@ -0,0 +1,75 @@
+using System.IO;
+
+using BizHawk.Common;
+using BizHawk.Emulation.Common;
+
+namespace BizHawk.Tests.Client.Common.Movie
+{
+ internal class FakeEmulator : IEmulator, IStatable, IInputPollable
+ {
+ private BasicServiceProvider _serviceProvider;
+ public IEmulatorServiceProvider ServiceProvider => _serviceProvider;
+
+ private static readonly ControllerDefinition _cd = new ControllerDefinition("fake controller")
+ {
+ BoolButtons = { "A", "B" },
+ }
+ .AddAxis("Stick", (-100).RangeTo(100), 0)
+ .MakeImmutable();
+
+ static FakeEmulator()
+ {
+ _cd.BuildMnemonicsCache("fake");
+ }
+
+ public ControllerDefinition ControllerDefinition => _cd;
+
+ public int Frame { get; set; }
+
+ public string SystemId => "fake";
+
+ public bool DeterministicEmulation => true;
+
+ public bool AvoidRewind => false;
+
+ public int LagCount { get; set; }
+ public bool IsLagFrame { get; set; }
+
+#pragma warning disable CA1065 // Do not raise exceptions in unexpected locations
+ public IInputCallbackSystem InputCallbacks => throw new NotImplementedException();
+#pragma warning restore CA1065 // Do not raise exceptions in unexpected locations
+
+ public FakeEmulator()
+ {
+ _serviceProvider = new(this);
+ }
+
+ public void Dispose() { }
+ public bool FrameAdvance(IController controller, bool render, bool renderSound = true)
+ {
+ Frame++;
+ return true;
+ }
+
+ public void LoadStateBinary(BinaryReader reader)
+ {
+ Frame = reader.ReadInt32();
+ LagCount = reader.ReadInt32();
+ IsLagFrame = reader.ReadBoolean();
+ }
+
+ public void ResetCounters()
+ {
+ Frame = 0;
+ LagCount = 0;
+ IsLagFrame = false;
+ }
+
+ public void SaveStateBinary(BinaryWriter writer)
+ {
+ writer.Write(Frame);
+ writer.Write(LagCount);
+ writer.Write(IsLagFrame);
+ }
+ }
+}
diff --git a/src/BizHawk.Tests.Client.Common/Movie/FakeMovieSession.cs b/src/BizHawk.Tests.Client.Common/Movie/FakeMovieSession.cs
new file mode 100644
index 00000000000..7d863ec47e9
--- /dev/null
+++ b/src/BizHawk.Tests.Client.Common/Movie/FakeMovieSession.cs
@@ -0,0 +1,57 @@
+using System.Collections.Generic;
+using System.IO;
+
+using BizHawk.Client.Common;
+using BizHawk.Emulation.Common;
+
+namespace BizHawk.Tests.Client.Common.Movie
+{
+ internal class FakeMovieSession : IMovieSession
+ {
+ public IMovieConfig Settings { get; set; }
+
+ public required IMovie Movie { get; set; }
+
+ public bool ReadOnly { get => false; set { } }
+
+ public bool NewMovieQueued => throw new NotImplementedException();
+
+ public string QueuedSyncSettings => throw new NotImplementedException();
+
+ public string QueuedCoreName => throw new NotImplementedException();
+
+ public string QueuedSysID => throw new NotImplementedException();
+
+ public IDictionary UserBag { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+
+
+ public IMovieController MovieController { get; }
+
+ public IController StickySource { get; set; }
+ public IController MovieIn { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+
+ public IInputAdapter MovieOut => throw new NotImplementedException();
+
+ public string BackupDirectory { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+
+ public FakeMovieSession(IEmulator emulator)
+ {
+ Settings = new MovieConfig();
+ StickySource = new Bk2Controller(emulator.ControllerDefinition);
+ MovieController = new Bk2Controller(emulator.ControllerDefinition);
+ }
+
+ public void AbortQueuedMovie() => throw new NotImplementedException();
+ public bool CheckSavestateTimeline(TextReader reader) => throw new NotImplementedException();
+ public IMovieController GenerateMovieController(ControllerDefinition? definition = null, string? logKey = null) => throw new NotImplementedException();
+ public IMovie Get(string path, bool loadMovie = false) => throw new NotImplementedException();
+ public void HandleFrameAfter(bool ignoreMovieEndAction) => throw new NotImplementedException();
+ public void HandleFrameBefore() => throw new NotImplementedException();
+ public bool HandleLoadState(TextReader reader) => throw new NotImplementedException();
+ public void HandleSaveState(TextWriter writer) => throw new NotImplementedException();
+ public void PopupMessage(string message) => throw new NotImplementedException();
+ public void QueueNewMovie(IMovie movie, string systemId, string loadedRomHash, PathEntryCollection pathEntries, IDictionary preferredCores) => throw new NotImplementedException();
+ public void RunQueuedMovie(bool recordMode, IEmulator emulator) => throw new NotImplementedException();
+ public void StopMovie(bool saveChanges = true) => Movie.Stop();
+ }
+}
diff --git a/src/BizHawk.Tests.Client.Common/Movie/MovieUndoTests.cs b/src/BizHawk.Tests.Client.Common/Movie/MovieUndoTests.cs
new file mode 100644
index 00000000000..5254da19946
--- /dev/null
+++ b/src/BizHawk.Tests.Client.Common/Movie/MovieUndoTests.cs
@@ -0,0 +1,440 @@
+using BizHawk.Client.Common;
+
+namespace BizHawk.Tests.Client.Common.Movie
+{
+ [TestClass]
+ public class MovieUndoTests
+ {
+ // Our test looks for the first actually different frame and compares with the value returned by Undo.
+ // Some operations (e.g. RemoveFrames) don't check for which frames were actually edited. (Should they? Would that give bad performance?)
+ // So we should ensure the first frame we touch is actually changed.
+ private void ValidateActionCanUndoAndRedo(ITasMovie movie, Action action, int expectedUndoItems = 1, bool skipUndoIndexCheck = false)
+ {
+ IStringLog originalLog = movie.GetLogEntries().Clone();
+ int originalUndoLength = movie.ChangeLog.UndoIndex;
+ action();
+
+ IStringLog changedLog = movie.GetLogEntries().Clone();
+ int changedUndoLength = movie.ChangeLog.UndoIndex;
+ int firstEditedFrame = originalLog.DivergentPoint(changedLog) ?? movie.InputLogLength;
+
+ if (!skipUndoIndexCheck)
+ Assert.AreEqual(originalUndoLength + expectedUndoItems, changedUndoLength);
+
+ Action? oldInvalidated = movie.GreenzoneInvalidated;
+ int invalidateFrame = int.MaxValue;
+ movie.GreenzoneInvalidated = (f) =>
+ {
+ oldInvalidated?.Invoke(f);
+ invalidateFrame = Math.Min(invalidateFrame, f);
+ };
+
+ // undo
+ for (int i = 0; i < expectedUndoItems; i++)
+ movie.ChangeLog.Undo();
+ Assert.AreEqual(firstEditedFrame, invalidateFrame);
+ Assert.IsNull(originalLog.DivergentPoint(movie.GetLogEntries()));
+
+ // redo
+ invalidateFrame = int.MaxValue;
+ for (int i = 0; i < expectedUndoItems; i++)
+ movie.ChangeLog.Redo();
+ Assert.AreEqual(firstEditedFrame, invalidateFrame);
+ Assert.IsNull(changedLog.DivergentPoint(movie.GetLogEntries()));
+
+ movie.GreenzoneInvalidated = oldInvalidated;
+ }
+
+ [TestMethod]
+ public void SetBool()
+ {
+ ITasMovie movie = TasMovieTests.MakeMovie(5);
+
+ ValidateActionCanUndoAndRedo(movie, () =>
+ {
+ movie.SetBoolState(2, "A", true);
+ });
+ }
+
+ [TestMethod]
+ public void ExtendsMovie()
+ {
+ ITasMovie movie = TasMovieTests.MakeMovie(5);
+
+ ValidateActionCanUndoAndRedo(movie, () =>
+ {
+ movie.SetBoolState(8, "A", true);
+ });
+ movie.ChangeLog.Undo();
+ ValidateActionCanUndoAndRedo(movie, () =>
+ {
+ movie.SetBoolStates(8, 2, "A", true);
+ });
+ movie.ChangeLog.Undo();
+ ValidateActionCanUndoAndRedo(movie, () =>
+ {
+ movie.SetAxisState(8, "Stick", 10);
+ });
+ movie.ChangeLog.Undo();
+ ValidateActionCanUndoAndRedo(movie, () =>
+ {
+ movie.SetAxisStates(8, 2, "Stick", 10);
+ });
+ movie.ChangeLog.Undo();
+ }
+
+ [TestMethod]
+ public void SetAxis()
+ {
+ ITasMovie movie = TasMovieTests.MakeMovie(5);
+
+ ValidateActionCanUndoAndRedo(movie, () =>
+ {
+ movie.SetAxisState(2, "Stick", 20);
+ });
+ }
+
+ [TestMethod]
+ public void InsertFrame()
+ {
+ ITasMovie movie = TasMovieTests.MakeMovie(5);
+ movie.SetBoolState(2, "A", true);
+ movie.SetBoolState(3, "B", true);
+
+ ValidateActionCanUndoAndRedo(movie, () =>
+ {
+ movie.InsertEmptyFrame(3);
+ });
+ }
+
+ [TestMethod]
+ public void DeleteFrame()
+ {
+ ITasMovie movie = TasMovieTests.MakeMovie(5);
+ movie.SetBoolState(2, "A", true);
+ movie.SetBoolState(4, "B", true);
+
+ ValidateActionCanUndoAndRedo(movie, () =>
+ {
+ movie.RemoveFrame(3);
+ });
+ }
+
+ [TestMethod]
+ public void DeleteFrames()
+ {
+ ITasMovie movie = TasMovieTests.MakeMovie(10);
+ movie.SetBoolState(2, "A", true);
+ movie.SetBoolState(4, "B", true);
+
+ // both overloads
+ ValidateActionCanUndoAndRedo(movie, () =>
+ {
+ movie.RemoveFrames(1, 4);
+ });
+ movie.ChangeLog.Undo();
+
+ ValidateActionCanUndoAndRedo(movie, () =>
+ {
+ movie.RemoveFrames([ 1, 2, 3, 5 ]);
+ });
+ }
+
+ [TestMethod]
+ public void CloneFrame()
+ {
+ ITasMovie movie = TasMovieTests.MakeMovie(5);
+ movie.SetBoolState(2, "A", true);
+ movie.SetBoolState(3, "B", true);
+
+ ValidateActionCanUndoAndRedo(movie, () =>
+ {
+ movie.InsertInput(2, movie.GetInputLogEntry(3));
+ });
+ }
+
+ [TestMethod]
+ public void MultipleEdits()
+ {
+ ITasMovie movie = TasMovieTests.MakeMovie(5);
+
+ ValidateActionCanUndoAndRedo(movie, () =>
+ {
+ movie.SetBoolState(2, "A", true);
+ movie.SetBoolState(3, "B", true);
+ }, 2);
+ }
+
+ [TestMethod]
+ public void BatchedEdit()
+ {
+ ITasMovie movie = TasMovieTests.MakeMovie(5);
+
+ ValidateActionCanUndoAndRedo(movie, () =>
+ {
+ movie.ChangeLog.BeginNewBatch();
+ movie.SetBoolState(2, "A", true);
+ movie.SetBoolState(3, "B", true);
+ movie.ChangeLog.EndBatch();
+ });
+ }
+
+ [TestMethod]
+ public void RecordFrameAtEnd()
+ {
+ ITasMovie movie = TasMovieTests.MakeMovie(5);
+
+ ValidateActionCanUndoAndRedo(movie, () =>
+ {
+ Bk2Controller controller = new Bk2Controller(movie.Emulator.ControllerDefinition);
+ controller.SetBool("A", true);
+ movie.RecordFrame(5, controller);
+ });
+ }
+
+ [TestMethod]
+ public void RecordFrameInMiddle()
+ {
+ ITasMovie movie = TasMovieTests.MakeMovie(5);
+
+ ValidateActionCanUndoAndRedo(movie, () =>
+ {
+ Bk2Controller controller = new Bk2Controller(movie.Emulator.ControllerDefinition);
+ controller.SetBool("A", true);
+ movie.RecordFrame(2, controller);
+ });
+ }
+
+ [TestMethod]
+ public void RecordFrameZero()
+ {
+ ITasMovie movie = TasMovieTests.MakeMovie(5);
+
+ ValidateActionCanUndoAndRedo(movie, () =>
+ {
+ Bk2Controller controller = new Bk2Controller(movie.Emulator.ControllerDefinition);
+ controller.SetBool("A", true);
+ movie.RecordFrame(0, controller);
+ });
+ }
+
+ [TestMethod]
+ public void MarkersGetMoved()
+ {
+ ITasMovie movie = TasMovieTests.MakeMovie(5);
+ movie.BindMarkersToInput = true;
+ movie.Markers.Add(3, "a");
+ movie.InsertEmptyFrame(2);
+
+ movie.ChangeLog.Undo();
+ Assert.AreEqual(3, movie.Markers[0].Frame);
+
+ movie.ChangeLog.Redo();
+ Assert.AreEqual(4, movie.Markers[0].Frame);
+ }
+
+ [TestMethod]
+ public void MarkersGetUndeleted()
+ {
+ ITasMovie movie = TasMovieTests.MakeMovie(5);
+ movie.BindMarkersToInput = true;
+ movie.Markers.Add(3, "a");
+ movie.RemoveFrame(3);
+
+ movie.ChangeLog.Undo();
+ Assert.AreEqual(1, movie.Markers.Count);
+ Assert.AreEqual(3, movie.Markers[0].Frame);
+ Assert.AreEqual("a", movie.Markers[0].Message);
+
+ movie.ChangeLog.Redo();
+ Assert.AreEqual(0, movie.Markers.Count);
+ }
+
+ [TestMethod]
+ public void MarkersUnaffectedByMovieExtension()
+ {
+ ITasMovie movie = TasMovieTests.MakeMovie(5);
+ movie.BindMarkersToInput = true;
+ movie.Markers.Add(5, "a");
+ movie.Markers.Add(8, "b");
+ movie.SetBoolState(10, "A", true);
+
+ movie.ChangeLog.Undo();
+ Assert.AreEqual(2, movie.Markers.Count);
+ Assert.AreEqual(5, movie.Markers[0].Frame);
+ Assert.AreEqual(8, movie.Markers[1].Frame);
+
+ movie.ChangeLog.Redo();
+ Assert.AreEqual(2, movie.Markers.Count);
+ Assert.AreEqual(5, movie.Markers[0].Frame);
+ Assert.AreEqual(8, movie.Markers[1].Frame);
+ }
+
+ [TestMethod]
+ public void AllOperationsRespectBatching()
+ {
+ ITasMovie movie = TasMovieTests.MakeMovie(10);
+
+ // Some actions can move markers.
+ movie.Markers.Add(9, "");
+ movie.BindMarkersToInput = true;
+
+ Bk2Controller controllerA = new Bk2Controller(movie.Emulator.ControllerDefinition);
+ controllerA.SetBool("A", true);
+ string entryA = Bk2LogEntryGenerator.GenerateLogEntry(controllerA);
+
+ int beginIndex = 0;
+ TasMovieTests.TestAllOperations(movie,
+ () =>
+ {
+ beginIndex = movie.ChangeLog.UndoIndex;
+ movie.ChangeLog.BeginNewBatch();
+ },
+ () =>
+ {
+ movie.SetFrame(0, entryA);
+ movie.ChangeLog.EndBatch();
+
+ Assert.AreEqual(1, movie.ChangeLog.UndoIndex - beginIndex);
+ });
+ }
+
+ [TestMethod]
+ public void AllOperationsGiveOneUndo()
+ {
+ ITasMovie movie = TasMovieTests.MakeMovie(10);
+
+ // Some actions can move markers.
+ movie.Markers.Add(9, "");
+ movie.BindMarkersToInput = true;
+
+ int beginIndex = 0;
+ TasMovieTests.TestAllOperations(movie,
+ () => beginIndex = movie.ChangeLog.UndoIndex,
+ () => Assert.AreEqual(1, movie.ChangeLog.UndoIndex - beginIndex)
+ );
+ }
+
+ [TestMethod]
+ public void WorkWithFullUndoHistory()
+ {
+ ITasMovie movie = TasMovieTests.MakeMovie(5);
+ movie.ChangeLog.MaxSteps = 3;
+
+ movie.SetBoolState(0, "A", true);
+ movie.SetBoolState(1, "A", true);
+ movie.SetBoolState(2, "A", true);
+
+ ValidateActionCanUndoAndRedo(movie, () =>
+ {
+ movie.SetBoolState(10, "A", true);
+ }, 1, true);
+ }
+
+ [TestMethod]
+ public void UndoingMidBatchRetainsBatchState()
+ {
+ ITasMovie movie = TasMovieTests.MakeMovie(8);
+ movie.ChangeLog.BeginNewBatch();
+
+ movie.SetBoolState(1, "A", true);
+ movie.SetBoolState(2, "A", true);
+ movie.ChangeLog.Undo();
+
+ ValidateActionCanUndoAndRedo(movie, () =>
+ {
+ movie.SetBoolState(3, "A", true);
+ movie.SetBoolState(4, "A", true);
+
+ movie.ChangeLog.EndBatch();
+ }, 1, true);
+
+ Assert.AreEqual(1, movie.ChangeLog.UndoIndex);
+ }
+
+ [TestMethod]
+ public void UndoRedoProduceSingleInvalidation()
+ {
+ ITasMovie movie = TasMovieTests.MakeMovie(5);
+ int invalidations = 0;
+ movie.GreenzoneInvalidated = (_) => invalidations++;
+
+ movie.ChangeLog.BeginNewBatch();
+ movie.SetBoolState(1, "A", true);
+ movie.SetBoolState(2, "B", true);
+ movie.ChangeLog.EndBatch();
+
+ invalidations = 0;
+ movie.ChangeLog.Undo();
+ Assert.AreEqual(1, invalidations);
+
+ invalidations = 0;
+ movie.ChangeLog.Redo();
+ Assert.AreEqual(1, invalidations);
+ }
+
+ [TestMethod]
+ public void InsertRespectsMarkerBinding()
+ {
+ ITasMovie movie = TasMovieTests.MakeMovie(5);
+ movie.Markers.Add(3, "a");
+
+ movie.BindMarkersToInput = true;
+ movie.InsertEmptyFrame(1);
+ movie.BindMarkersToInput = false;
+
+ movie.ChangeLog.Undo();
+ Assert.AreEqual(3, movie.Markers[0].Frame);
+ Assert.IsFalse(movie.BindMarkersToInput);
+
+ movie.ChangeLog.Redo();
+ Assert.AreEqual(4, movie.Markers[0].Frame);
+ Assert.IsFalse(movie.BindMarkersToInput);
+ }
+
+ [TestMethod]
+ public void DeleteRespectsMarkerBinding()
+ {
+ ITasMovie movie = TasMovieTests.MakeMovie(5);
+ movie.Markers.Add(3, "a");
+
+ movie.BindMarkersToInput = true;
+ movie.RemoveFrame(1);
+ movie.BindMarkersToInput = false;
+
+ movie.ChangeLog.Undo();
+ Assert.AreEqual(3, movie.Markers[0].Frame);
+ Assert.IsFalse(movie.BindMarkersToInput);
+
+ movie.ChangeLog.Redo();
+ Assert.AreEqual(2, movie.Markers[0].Frame);
+ Assert.IsFalse(movie.BindMarkersToInput);
+ }
+
+ [TestMethod]
+ public void GeneralRespectsMarkerBinding()
+ {
+ // This was just a silly bug.
+ ITasMovie movie = TasMovieTests.MakeMovie(5);
+ movie.Markers.Add(3, "a");
+
+ Bk2Controller controllerA = new Bk2Controller(movie.Emulator.ControllerDefinition);
+ controllerA.SetBool("A", true);
+
+ movie.BindMarkersToInput = true;
+ movie.PokeFrame(1, controllerA);
+ movie.BindMarkersToInput = false;
+ movie.InsertEmptyFrame(1);
+
+ movie.ChangeLog.Undo();
+ movie.ChangeLog.Undo();
+ Assert.AreEqual(3, movie.Markers[0].Frame);
+ Assert.IsFalse(movie.BindMarkersToInput);
+
+ movie.ChangeLog.Redo();
+ movie.ChangeLog.Redo();
+ Assert.AreEqual(3, movie.Markers[0].Frame);
+ Assert.IsFalse(movie.BindMarkersToInput);
+ }
+ }
+}
diff --git a/src/BizHawk.Tests.Client.Common/Movie/TasMovieTests.cs b/src/BizHawk.Tests.Client.Common/Movie/TasMovieTests.cs
new file mode 100644
index 00000000000..fcb0d9a7f3b
--- /dev/null
+++ b/src/BizHawk.Tests.Client.Common/Movie/TasMovieTests.cs
@@ -0,0 +1,97 @@
+using BizHawk.Client.Common;
+
+namespace BizHawk.Tests.Client.Common.Movie
+{
+ [TestClass]
+ public class TasMovieTests
+ {
+ internal static TasMovie MakeMovie(int numberOfFrames)
+ {
+ FakeEmulator emu = new FakeEmulator();
+ FakeMovieSession session = new(emu) { Movie = null! };
+ TasMovie movie = new(session, "/fake/path");
+ session.Movie = movie;
+
+ movie.Attach(emu);
+ movie.InsertEmptyFrame(0, numberOfFrames);
+
+ return movie;
+ }
+
+ public static void TestAllOperations(ITasMovie movie, Action PreOperation, Action PostOperation)
+ {
+ Bk2Controller controllerEmpty = new Bk2Controller(movie.Emulator.ControllerDefinition);
+ string entryEmpty = Bk2LogEntryGenerator.GenerateLogEntry(controllerEmpty);
+ Bk2Controller controllerA = new Bk2Controller(movie.Emulator.ControllerDefinition);
+ controllerA.SetBool("A", true);
+ string entryA = Bk2LogEntryGenerator.GenerateLogEntry(controllerA);
+
+ // Make sure all operations actually do something.
+ movie.SetBoolState(3, "A", true);
+
+ void TestForSingleOperation(Action a)
+ {
+ PreOperation();
+ a();
+ PostOperation();
+ movie.ChangeLog.Undo();
+ }
+
+ TestForSingleOperation(() => movie.RecordFrame(1, controllerA));
+ TestForSingleOperation(() => movie.Truncate(3));
+ TestForSingleOperation(() => movie.PokeFrame(1, controllerA));
+ TestForSingleOperation(() => movie.SetFrame(1, entryA));
+ TestForSingleOperation(() => movie.ClearFrame(3));
+
+ TestForSingleOperation(() => movie.InsertInput(2, entryA));
+ TestForSingleOperation(() => movie.InsertInput(2, [ entryA, entryEmpty, entryA, entryEmpty ]));
+ TestForSingleOperation(() => movie.InsertInput(2, [ controllerA, controllerEmpty, controllerA, controllerEmpty ]));
+
+ TestForSingleOperation(() => movie.RemoveFrame(2));
+ TestForSingleOperation(() => movie.RemoveFrames(2, 4));
+ TestForSingleOperation(() => movie.RemoveFrames([ 2, 4, 6 ]));
+
+ TestForSingleOperation(() => movie.CopyOverInput(2, [ controllerA, controllerEmpty ]));
+ TestForSingleOperation(() => movie.InsertEmptyFrame(2, 2));
+
+ TestForSingleOperation(() => movie.ToggleBoolState(2, "B"));
+ TestForSingleOperation(() => movie.SetBoolState(3, "B", true));
+ TestForSingleOperation(() => movie.SetBoolStates(3, 2, "B", true));
+
+ TestForSingleOperation(() => movie.SetAxisState(2, "Stick", 10));
+ TestForSingleOperation(() => movie.SetAxisStates(3, 2, "Stick", 20));
+
+ // actions that can also extend the movie
+ TestForSingleOperation(() => movie.CopyOverInput(9, [ controllerA, controllerEmpty, controllerA ]));
+ TestForSingleOperation(() => movie.ToggleBoolState(15, "B"));
+ TestForSingleOperation(() => movie.SetBoolState(15, "B", true));
+ TestForSingleOperation(() => movie.SetBoolStates(15, 2, "B", true));
+ TestForSingleOperation(() => movie.SetAxisState(15, "Stick", 10));
+ TestForSingleOperation(() => movie.SetAxisStates(15, 2, "Stick", 20));
+ }
+
+ [TestMethod]
+ public void AllOperationsFlagChanges()
+ {
+ ITasMovie movie = MakeMovie(10);
+
+ TestAllOperations(movie,
+ movie.ClearChanges,
+ () => Assert.IsTrue(movie.Changes)
+ );
+ }
+
+ [TestMethod]
+ public void AllOperationsProduceSingleInvalidation()
+ {
+ ITasMovie movie = MakeMovie(10);
+ int invalidations = 0;
+ movie.GreenzoneInvalidated = (_) => invalidations++;
+
+ TestAllOperations(movie,
+ () => invalidations = 0,
+ () => Assert.AreEqual(1, invalidations)
+ );
+ }
+ }
+}