From a071cd80db68fe7f8d92862cd447073388b3dc94 Mon Sep 17 00:00:00 2001 From: Steven Hugg Date: Thu, 23 Aug 2018 18:52:56 -0400 Subject: [PATCH] now we can live replay --- doc/notes.txt | 3 --- src/recorder.ts | 53 ++++++++++++++++++++++--------------- src/ui.ts | 12 +++++++-- test/cli/testplatforms.js | 14 ++++++++-- test/roms/nes/shoot2.c.rom | Bin 0 -> 40976 bytes 5 files changed, 53 insertions(+), 29 deletions(-) create mode 100644 test/roms/nes/shoot2.c.rom diff --git a/doc/notes.txt b/doc/notes.txt index c6369f24..a56e4484 100644 --- a/doc/notes.txt +++ b/doc/notes.txt @@ -51,10 +51,7 @@ TODO: - tools (memory, disasm) use debugging state - text log debugging script - NES crt should mark raster pos when debugging -- make sure we don't store files in local storage unnecc. -- state buffer/replay - intro/help text for each platform -- make sure controls work with replay feature (we'll have to save control state every frame) - vscode/atom extension? - navigator.getGamepads diff --git a/src/recorder.ts b/src/recorder.ts index 84a63c7d..9925e1fb 100644 --- a/src/recorder.ts +++ b/src/recorder.ts @@ -24,7 +24,8 @@ export class StateRecorderImpl implements EmuRecorder { this.checkpoints = []; this.framerecs = []; this.frameCount = 0; - this.lastSeekFrame = -1; + this.lastSeekFrame = 0; + if (this.callbackStateChanged) this.callbackStateChanged(); } frameRequested() : boolean { @@ -32,32 +33,35 @@ export class StateRecorderImpl implements EmuRecorder { if (this.checkpoints.length >= this.maxCheckpoints) { return false; } - // record the control state, if available - if (this.platform.saveControlsState) { - this.framerecs.push({ - controls:this.platform.saveControlsState(), - seed:getNoiseSeed() - }); + var controls = { + controls:this.platform.saveControlsState(), + seed:getNoiseSeed() + }; + var requested = false; + // are we replaying? then we don't need to save a frame, just replace controls + if (this.lastSeekFrame < this.frameCount) { + this.loadControls(this.lastSeekFrame); + } else { + // record the control state, if available + if (this.platform.saveControlsState) { + this.framerecs.push(controls); + } + // time to save next frame? + requested = (this.frameCount++ % this.checkpointInterval) == 0; } - // pick up where we left off, if we used the seek function - if (this.lastSeekFrame >= 0) { - this.frameCount = this.lastSeekFrame; - this.lastSeekFrame = -1; - // truncate buffers - this.checkpoints = this.checkpoints.slice(0, Math.floor((this.frameCount + this.checkpointInterval - 1) / this.checkpointInterval)); - this.framerecs = this.framerecs.slice(0, this.frameCount); - } - // time to save next frame? - if (this.callbackStateChanged) { - this.callbackStateChanged(); - } - return (this.frameCount++ % this.checkpointInterval) == 0; + this.lastSeekFrame++; + if (this.callbackStateChanged) this.callbackStateChanged(); + return requested; } numFrames() : number { return this.frameCount; } + currentFrame() : number { + return this.lastSeekFrame; + } + recordFrame(state : EmuState) { this.checkpoints.push(state); } @@ -79,8 +83,7 @@ export class StateRecorderImpl implements EmuRecorder { this.platform.loadState(state); while (frame < seekframe) { if (frame < this.framerecs.length) { - this.platform.loadControlsState(this.framerecs[frame].controls); - setNoiseSeed(this.framerecs[frame].seed); + this.loadControls(frame); } frame++; this.platform.advance(frame < seekframe); // TODO: infinite loop? @@ -91,4 +94,10 @@ export class StateRecorderImpl implements EmuRecorder { return 0; } } + + loadControls(frame : number) { + if (this.platform.loadControlsState) + this.platform.loadControlsState(this.framerecs[frame].controls); + setNoiseSeed(this.framerecs[frame].seed); + } } diff --git a/src/ui.ts b/src/ui.ts index d49a9329..da260b02 100644 --- a/src/ui.ts +++ b/src/ui.ts @@ -406,6 +406,7 @@ function setCompileOutput(data: WorkerResult) { if (rom) { // TODO instanceof Uint8Array) { try { clearBreakpoint(); // so we can replace memory (TODO: change toolbar btn) + _resetRecording(); platform.loadROM(getCurrentPresetTitle(), rom); if (!userPaused) _resume(); current_output = rom; @@ -550,6 +551,7 @@ function clearBreakpoint() { } function resetAndDebug() { + _disableRecording(); if (platform.setupDebug && platform.readAddress) { // TODO?? clearBreakpoint(); _resume(); @@ -693,6 +695,12 @@ function _disableRecording() { } } +function _resetRecording() { + if (recorderActive) { + stateRecorder.reset(); + } +} + function _enableRecording() { stateRecorder.reset(); platform.setRecorder(stateRecorder); @@ -790,8 +798,8 @@ function setupReplaySlider() { stateRecorder.callbackStateChanged = () => { replayslider.attr('min', 1); replayslider.attr('max', stateRecorder.numFrames()); - replayslider.val(stateRecorder.numFrames()); - updateFrameNo(stateRecorder.numFrames()); + replayslider.val(stateRecorder.currentFrame()); + updateFrameNo(stateRecorder.currentFrame()); }; replayslider.on('input', sliderChanged); replayslider.on('change', sliderChanged); diff --git a/test/cli/testplatforms.js b/test/cli/testplatforms.js index 4add131e..0a281689 100644 --- a/test/cli/testplatforms.js +++ b/test/cli/testplatforms.js @@ -24,15 +24,16 @@ global.Log = require('tss/js/Log.js').Log; includeInThisContext('tss/js/tss/PsgDeviceChannel.js'); includeInThisContext('tss/js/tss/MasterChannel.js'); includeInThisContext('tss/js/tss/AudioLooper.js'); - -var jsnes = require("jsnes/jsnes.min.js"); +includeInThisContext("jsnes/jsnes.min.js"); var emu = require('gen/emu.js'); +var Keys = emu.Keys; var audio = require('gen/audio.js'); var recorder = require('gen/recorder.js'); var _vicdual = require('gen/platform/vicdual.js'); var _apple2 = require('gen/platform/apple2.js'); var _vcs = require('gen/platform/vcs.js'); +var _nes = require('gen/platform/nes.js'); // @@ -108,5 +109,14 @@ describe('Platform Replay', () => { assert.equal(platform.saveState().p.SA, 0xff ^ 0x40); }); + it('Should run nes', () => { + var platform = testPlatform('nes', 'shoot2.c.rom', 70, (platform, frameno) => { + if (frameno == 60) { + keycallback(Keys.VK_Z.c, Keys.VK_Z.c, 1); + } + }); + assert.equal(65, platform.saveControlsState().c1[0]); + }); + }); diff --git a/test/roms/nes/shoot2.c.rom b/test/roms/nes/shoot2.c.rom new file mode 100644 index 0000000000000000000000000000000000000000..abcfde4021235b1dccfdd213ee6fa0e1d21b94d0 GIT binary patch literal 40976 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 literal 0 HcmV?d00001