feat(replay): add replay timeline
This commit is contained in:
1 parent
fe85a30407
commit
7317326166
1 file changed
+140
@@ -0,0 +1,140 @@
|
|||||||
|
using System;
|
||||||
|
using Chip8.Core;
|
||||||
|
|
||||||
|
namespace Chip8.Replay;
|
||||||
|
|
||||||
|
public sealed class Timeline
|
||||||
|
{
|
||||||
|
public const int defaultKeyframeInterval = 8;
|
||||||
|
public const int defaultCapacity = 256;
|
||||||
|
|
||||||
|
public Machine machine { get; }
|
||||||
|
public InputMovie movie { get; }
|
||||||
|
public RewindBuffer rewind { get; }
|
||||||
|
public int keyframeInterval { get; }
|
||||||
|
public int currentFrame { get; private set; }
|
||||||
|
|
||||||
|
int nextKeyframeFrame;
|
||||||
|
|
||||||
|
Timeline(Machine machine, InputMovie movie, int keyframeInterval, int capacity)
|
||||||
|
{
|
||||||
|
this.machine = machine;
|
||||||
|
this.movie = movie;
|
||||||
|
this.keyframeInterval = keyframeInterval;
|
||||||
|
rewind = new RewindBuffer(capacity);
|
||||||
|
currentFrame = 0;
|
||||||
|
nextKeyframeFrame = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Timeline forMovie(InputMovie movie, byte[] rom,
|
||||||
|
int keyframeInterval = defaultKeyframeInterval, int capacity = defaultCapacity)
|
||||||
|
{
|
||||||
|
if (keyframeInterval < 1)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(keyframeInterval));
|
||||||
|
}
|
||||||
|
var machine = MachineBuilder.fromMovie(movie, rom);
|
||||||
|
return new Timeline(machine, movie, keyframeInterval, capacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Timeline forMachine(Machine machine, InputMovie movie,
|
||||||
|
int keyframeInterval = defaultKeyframeInterval, int capacity = defaultCapacity)
|
||||||
|
{
|
||||||
|
if (keyframeInterval < 1)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(keyframeInterval));
|
||||||
|
}
|
||||||
|
return new Timeline(machine, movie, keyframeInterval, capacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int instructionsPerFrame => movie.instructionsPerFrame;
|
||||||
|
|
||||||
|
public void advanceFrame()
|
||||||
|
{
|
||||||
|
maybeRecordKeyframe();
|
||||||
|
stepForwardOnce();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void advanceFrames(int count)
|
||||||
|
{
|
||||||
|
for (int frame = 0; frame < count; frame++)
|
||||||
|
{
|
||||||
|
advanceFrame();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void advanceTo(int frame)
|
||||||
|
{
|
||||||
|
if (frame < currentFrame)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(frame), "advanceTo cannot move backward; use rewindTo");
|
||||||
|
}
|
||||||
|
while (currentFrame < frame)
|
||||||
|
{
|
||||||
|
advanceFrame();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void rewindTo(int frame)
|
||||||
|
{
|
||||||
|
if (frame < 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(frame));
|
||||||
|
}
|
||||||
|
if (frame > currentFrame)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(frame), "rewindTo cannot move forward; use advanceTo");
|
||||||
|
}
|
||||||
|
if (frame == currentFrame)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TimelineSnapshot? keyframe = rewind.nearestAtOrBefore(frame);
|
||||||
|
if (keyframe is null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"frame " + frame + " is older than the retained rewind window (oldest keyframe "
|
||||||
|
+ rewind.oldestFrame + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
machine.loadState(keyframe.state);
|
||||||
|
currentFrame = keyframe.frameIndex;
|
||||||
|
|
||||||
|
while (currentFrame < frame)
|
||||||
|
{
|
||||||
|
stepForwardOnce();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool stepBack()
|
||||||
|
{
|
||||||
|
if (currentFrame == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
rewindTo(currentFrame - 1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] currentState() => machine.saveState();
|
||||||
|
|
||||||
|
public ReplayOutcome outcome() => ReplayOutcome.capture(machine, currentFrame);
|
||||||
|
|
||||||
|
void stepForwardOnce()
|
||||||
|
{
|
||||||
|
KeyMask injected = movie.keysAt(currentFrame);
|
||||||
|
injected.applyTo(machine.keypad);
|
||||||
|
machine.runFrame(instructionsPerFrame);
|
||||||
|
currentFrame++;
|
||||||
|
}
|
||||||
|
|
||||||
|
void maybeRecordKeyframe()
|
||||||
|
{
|
||||||
|
if (currentFrame >= nextKeyframeFrame)
|
||||||
|
{
|
||||||
|
rewind.store(currentFrame, machine.saveState());
|
||||||
|
nextKeyframeFrame = currentFrame + keyframeInterval;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user