Skip to content

Commit 31b64ea

Browse files
committed
implement a rudimentary multitrack recording feature
1 parent 575cd67 commit 31b64ea

15 files changed

Lines changed: 665 additions & 58 deletions

File tree

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
#nullable enable
2+
3+
using System.Collections.Generic;
4+
using BizHawk.Emulation.Common;
5+
6+
namespace BizHawk.Client.Common
7+
{
8+
/// <summary>
9+
/// Used to enable recording a subset of a controller's buttons, while keeping the existing inputs for the other buttons.
10+
/// Also used to allow TAStudio to clear (or otherwise edit) a subset of input columns.
11+
/// </summary>
12+
internal class MultitrackAdapter : IController
13+
{
14+
/// <summary>
15+
/// Input states in this definition will come from <see cref="ActiveSource"/>. All others will come from <see cref="BackingSource"/>.
16+
/// </summary>
17+
public ControllerDefinition ActiveDefinition { get; set; }
18+
19+
public IController ActiveSource { get; set; }
20+
21+
public IController BackingSource { get; set; }
22+
23+
public ControllerDefinition Definition => BackingSource.Definition;
24+
25+
public MultitrackAdapter(IController active, IController backing, ControllerDefinition activeDefinition)
26+
{
27+
ActiveSource = active;
28+
BackingSource = backing;
29+
ActiveDefinition = activeDefinition;
30+
}
31+
32+
public bool IsPressed(string button)
33+
{
34+
if (ActiveDefinition.BoolButtons.Contains(button))
35+
{
36+
return ActiveSource.IsPressed(button);
37+
}
38+
else
39+
{
40+
return BackingSource.IsPressed(button);
41+
}
42+
}
43+
public int AxisValue(string name)
44+
{
45+
if (ActiveDefinition.Axes.ContainsKey(name))
46+
{
47+
return ActiveSource.AxisValue(name);
48+
}
49+
else
50+
{
51+
return BackingSource.AxisValue(name);
52+
}
53+
}
54+
55+
public IReadOnlyCollection<(string Name, int Strength)> GetHapticsSnapshot() => throw new NotImplementedException(); // don't use this
56+
public void SetHapticChannelStrength(string name, int strength) => throw new NotImplementedException(); // don't use this
57+
}
58+
}

src/BizHawk.Client.Common/movie/bk2/Bk2Movie.cs

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,16 @@ namespace BizHawk.Client.Common
55
{
66
public partial class Bk2Movie : BasicMovieInfo, IMovie
77
{
8-
private Bk2Controller _adapter;
8+
private IController _defaultValueController;
9+
protected IController DefaultValueController
10+
{
11+
get
12+
{
13+
// LogKey isn't available at construction time, so we have to create this instance when it is accessed.
14+
_defaultValueController ??= new Bk2Controller(Session.MovieController.Definition, LogKey);
15+
return _defaultValueController;
16+
}
17+
}
918

1019
public Bk2Movie(IMovieSession session, string filename) : base(filename)
1120
{
@@ -18,6 +27,17 @@ public virtual void Attach(IEmulator emulator)
1827
Emulator = emulator;
1928
}
2029

30+
private ControllerDefinition/*?*/ _activeControllerInputs = null;
31+
public ControllerDefinition/*?*/ ActiveControllerInputs
32+
{
33+
get => _activeControllerInputs;
34+
set
35+
{
36+
value?.AssertImmutable();
37+
_activeControllerInputs = value;
38+
}
39+
}
40+
2141
protected bool IsAttached() => Emulator != null;
2242

2343
public IEmulator Emulator { get; private set; }
@@ -48,6 +68,11 @@ public void CopyLog(IEnumerable<string> log)
4868

4969
public void AppendFrame(IController source)
5070
{
71+
if (ActiveControllerInputs != null)
72+
{
73+
source = new MultitrackAdapter(source, new Bk2Controller(Session.MovieController.Definition, LogKey), ActiveControllerInputs);
74+
}
75+
5176
Log.Add(Bk2LogEntryGenerator.GenerateLogEntry(source));
5277
Changes = true;
5378
}
@@ -62,15 +87,24 @@ public virtual void RecordFrame(int frame, IController source)
6287
}
6388
}
6489

65-
SetFrameAt(frame, Bk2LogEntryGenerator.GenerateLogEntry(source));
66-
67-
Changes = true;
90+
PokeFrame(frame, source);
6891
}
6992

7093
public virtual void Truncate(int frame)
7194
{
7295
if (frame < Log.Count)
7396
{
97+
if (ActiveControllerInputs != null)
98+
{
99+
for (int i = frame; i < Log.Count; i++)
100+
PokeFrame(i, DefaultValueController);
101+
string defaultEntry = Bk2LogEntryGenerator.EmptyEntry(DefaultValueController);
102+
int firstDefault = Log.Count;
103+
while (firstDefault > frame && Log[firstDefault - 1] == defaultEntry)
104+
firstDefault--;
105+
frame = firstDefault;
106+
}
107+
74108
Log.RemoveRange(frame, Log.Count - frame);
75109
Changes = true;
76110
}
@@ -80,20 +114,28 @@ public IMovieController GetInputState(int frame)
80114
{
81115
if (frame < FrameCount && frame >= -1)
82116
{
83-
_adapter ??= new Bk2Controller(Session.MovieController.Definition, LogKey);
84-
_adapter.SetFromMnemonic(frame >= 0 ? Log[frame] : Bk2LogEntryGenerator.EmptyEntry(_adapter));
85-
return _adapter;
117+
Bk2Controller controller = new(Session.MovieController.Definition, LogKey);
118+
controller.SetFromMnemonic(frame >= 0 ? Log[frame] : Bk2LogEntryGenerator.EmptyEntry(controller));
119+
return controller;
86120
}
87121

88122
return null;
89123
}
90124

91125
public virtual void PokeFrame(int frame, IController source)
92126
{
127+
if (ActiveControllerInputs != null)
128+
{
129+
source = new MultitrackAdapter(source, GetInputState(frame) ?? DefaultValueController, ActiveControllerInputs);
130+
}
131+
93132
SetFrameAt(frame, Bk2LogEntryGenerator.GenerateLogEntry(source));
94133
Changes = true;
95134
}
96135

136+
/// <summary>
137+
/// Does not use <see cref="ActiveControllerInputs"/>.
138+
/// </summary>
97139
protected void SetFrameAt(int frameNum, string frame)
98140
{
99141
if (Log.Count > frameNum)

src/BizHawk.Client.Common/movie/interfaces/IMovie.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@ public enum MovieMode
3333
// TODO: consider other event handlers, switching modes?
3434
public interface IMovie : IBasicMovieInfo
3535
{
36+
/// <summary>
37+
/// When not null, methods that edit the movie will only affect buttons or axis values present in this definition,
38+
/// with the exceoption of adding or removing default-value frames at the end of the movie.
39+
/// The instance must be made immutable before assignment.
40+
/// </summary>
41+
ControllerDefinition/*?*/ ActiveControllerInputs { get; set; }
42+
3643
/// <summary>
3744
/// Gets the current movie mode
3845
/// </summary>
@@ -161,6 +168,7 @@ public interface IMovie : IBasicMovieInfo
161168

162169
/// <summary>
163170
/// Instructs the movie to remove all input from its input log starting with the input at frame.
171+
/// If <see cref="ActiveControllerInputs"/> is not null, this might not actually change the length of the movie.
164172
/// </summary>
165173
/// <param name="frame">The frame at which to truncate</param>
166174
void Truncate(int frame);

src/BizHawk.Client.Common/movie/interfaces/ITasMovie.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,12 @@ public interface ITasMovie : IMovie, INotifyPropertyChanged, IDisposable
5555

5656
/// <summary>
5757
/// Remove all frames between removeStart and removeUpTo (excluding removeUpTo).
58+
/// If <see cref="IMovie.ActiveControllerInputs"/> is not null, this will not actually change the length of the movie.
5859
/// </summary>
5960
/// <param name="removeStart">The first frame to remove.</param>
6061
/// <param name="removeUpTo">The frame after the last frame to remove.</param>
6162
void RemoveFrames(int removeStart, int removeUpTo);
62-
void SetFrame(int frame, string source);
63+
void PokeFrame(int frame, string source);
6364

6465
void LoadBranch(TasBranch branch);
6566

src/BizHawk.Client.Common/movie/tasproj/TasMovie.Editing.cs

Lines changed: 64 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ internal partial class TasMovie
2121
public override void RecordFrame(int frame, IController source)
2222
{
2323
ChangeLog.AddGeneralUndo(frame, frame, $"Record Frame: {frame}");
24-
SetFrameAt(frame, Bk2LogEntryGenerator.GenerateLogEntry(source));
24+
base.PokeFrame(frame, source);
2525
ChangeLog.SetGeneralRedo();
2626

2727
LagLog[frame] = _inputPollable.IsLagFrame;
@@ -32,7 +32,7 @@ public override void RecordFrame(int frame, IController source)
3232

3333
public override void Truncate(int frame)
3434
{
35-
if (frame >= Log.Count - 1) return;
35+
if (frame >= Log.Count) return;
3636

3737
bool endBatch = ChangeLog.BeginNewBatch($"Truncate Movie: {frame}", true);
3838

@@ -56,22 +56,17 @@ public override void PokeFrame(int frame, IController source)
5656
InvalidateAfter(frame);
5757
}
5858

59-
public void SetFrame(int frame, string source)
59+
public void PokeFrame(int frame, string source)
6060
{
61-
ChangeLog.AddGeneralUndo(frame, frame, $"Set Frame At: {frame}");
62-
SetFrameAt(frame, source);
63-
ChangeLog.SetGeneralRedo();
64-
65-
InvalidateAfter(frame);
61+
Bk2Controller controller = new(Session.MovieController.Definition, LogKey);
62+
controller.SetFromMnemonic(source);
63+
PokeFrame(frame, controller);
6664
}
6765

6866
public void ClearFrame(int frame)
6967
{
70-
string empty = Bk2LogEntryGenerator.EmptyEntry(Session.MovieController);
71-
if (GetInputLogEntry(frame) == empty) return;
72-
7368
ChangeLog.AddGeneralUndo(frame, frame, $"Clear Frame: {frame}");
74-
SetFrameAt(frame, empty);
69+
base.PokeFrame(frame, DefaultValueController);
7570
ChangeLog.SetGeneralRedo();
7671

7772
InvalidateAfter(frame);
@@ -118,6 +113,7 @@ public void RemoveFrames(ICollection<int> frames)
118113

119114
public void RemoveFrames(int removeStart, int removeUpTo)
120115
{
116+
// TODO: column limiting
121117
bool endBatch = ChangeLog.BeginNewBatch($"Remove frames {removeStart}-{removeUpTo - 1}", true);
122118
if (BindMarkersToInput)
123119
{
@@ -133,12 +129,22 @@ public void RemoveFrames(int removeStart, int removeUpTo)
133129
// Log.GetRange() might be preferrable, but Log's type complicates that.
134130
string[] removedInputs = new string[removeUpTo - removeStart];
135131
Log.CopyTo(removeStart, removedInputs, 0, removedInputs.Length);
136-
Log.RemoveRange(removeStart, removeUpTo - removeStart);
132+
133+
if (ActiveControllerInputs == null)
134+
{
135+
Log.RemoveRange(removeStart, removeUpTo - removeStart);
136+
}
137+
else
138+
{
139+
int count = removeUpTo - removeStart;
140+
for (int i = removeStart; i < Log.Count; i++)
141+
base.PokeFrame(i, GetInputState(i + count) ?? DefaultValueController);
142+
}
137143

138144
ChangeLog.AddRemoveFrames(
139145
removeStart,
140-
removeUpTo,
141146
removedInputs.ToList(),
147+
ActiveControllerInputs,
142148
BindMarkersToInput
143149
);
144150
if (endBatch) ChangeLog.EndBatch();
@@ -155,9 +161,29 @@ public void InsertInput(int frame, string inputState)
155161
public void InsertInput(int frame, IEnumerable<string> inputLog)
156162
{
157163
var inputLogCopy = inputLog.ToList();
158-
Log.InsertRange(frame, inputLogCopy);
164+
if (ActiveControllerInputs == null)
165+
{
166+
Log.InsertRange(frame, inputLogCopy);
167+
}
168+
else
169+
{
170+
int count = inputLogCopy.Count;
171+
// add empty frames at the end
172+
Log.AddRange(Enumerable.Repeat(Bk2LogEntryGenerator.EmptyEntry(Session.MovieController), count));
173+
// shift inputs to future frames
174+
for (int i = Log.Count - 1; i >= frame + count; i--)
175+
base.PokeFrame(i, GetInputState(i - count));
176+
// write the new inputs
177+
Bk2Controller controller = new(Session.MovieController.Definition, LogKey);
178+
for (int i = 0; i < count; i++)
179+
{
180+
controller.SetFromMnemonic(inputLogCopy[i]);
181+
base.PokeFrame(frame + i, controller);
182+
}
183+
}
184+
159185
ShiftBindedMarkers(frame, inputLogCopy.Count);
160-
ChangeLog.AddInsertInput(frame, inputLogCopy, BindMarkersToInput, $"Insert {inputLogCopy.Count} frame(s) at {frame}");
186+
ChangeLog.AddInsertInput(frame, inputLogCopy, ActiveControllerInputs, BindMarkersToInput, $"Insert {inputLogCopy.Count} frame(s) at {frame}");
161187
InvalidateAfter(frame);
162188
}
163189

@@ -175,6 +201,7 @@ public void InsertInput(int frame, IEnumerable<IController> inputStates)
175201

176202
public void CopyOverInput(int frame, IEnumerable<IController> inputStates)
177203
{
204+
// TODO: column limiting
178205
var states = inputStates.ToList();
179206

180207
bool endBatch = ChangeLog.BeginNewBatch($"Copy Over Input: {frame}", true);
@@ -187,7 +214,10 @@ public void CopyOverInput(int frame, IEnumerable<IController> inputStates)
187214
ChangeLog.AddGeneralUndo(frame, frame + states.Count - 1, $"Copy Over Input: {frame}");
188215
for (int i = 0; i < states.Count; i++)
189216
{
190-
Log[frame + i] = Bk2LogEntryGenerator.GenerateLogEntry(states[i]);
217+
IController controller = states[i];
218+
if (ActiveControllerInputs != null)
219+
controller = new MultitrackAdapter(controller, GetInputState(frame + i), ActiveControllerInputs);
220+
Log[frame + i] = Bk2LogEntryGenerator.GenerateLogEntry(controller);
191221
}
192222
int firstChangedFrame = ChangeLog.SetGeneralRedo();
193223

@@ -203,9 +233,24 @@ public void InsertEmptyFrame(int frame, int count = 1)
203233
{
204234
frame = Math.Min(frame, Log.Count);
205235

206-
Log.InsertRange(frame, Enumerable.Repeat(Bk2LogEntryGenerator.EmptyEntry(Session.MovieController), count));
236+
if (ActiveControllerInputs == null)
237+
{
238+
Log.InsertRange(frame, Enumerable.Repeat(Bk2LogEntryGenerator.EmptyEntry(Session.MovieController), count));
239+
}
240+
else
241+
{
242+
// add empty frames at the end
243+
Log.AddRange(Enumerable.Repeat(Bk2LogEntryGenerator.EmptyEntry(Session.MovieController), count));
244+
// shift inputs to future frames
245+
for (int i = Log.Count - 1; i >= frame + count; i--)
246+
base.PokeFrame(i, GetInputState(i - count));
247+
// clear frames
248+
for (int i = frame; i < frame + count; i++)
249+
base.PokeFrame(i, DefaultValueController);
250+
}
251+
207252
ShiftBindedMarkers(frame, count);
208-
ChangeLog.AddInsertFrames(frame, count, BindMarkersToInput, $"Insert {count} empty frame(s) at {frame}");
253+
ChangeLog.AddInsertFrames(frame, count, ActiveControllerInputs, BindMarkersToInput, $"Insert {count} empty frame(s) at {frame}");
209254

210255
InvalidateAfter(frame);
211256
}
@@ -214,7 +259,6 @@ private void ExtendMovieForEdit(int numFrames)
214259
{
215260
int oldLength = InputLogLength;
216261

217-
// account for autohold TODO: What about auto-fire?
218262
string inputs = Bk2LogEntryGenerator.GenerateLogEntry(Session.StickySource);
219263
for (int i = 0; i < numFrames; i++)
220264
{

0 commit comments

Comments
 (0)