Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 47 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,52 @@ The window's ring is not this one: it keeps a state every *other* frame, which h
doubles the history for the memory, and gives back two frames per display tick so the rewind runs at
twice speed. The REPL stays at one so that `rewind N` means N frames.

### Recording a session, and playing it back

`--record FILE` writes a `.mnm` movie of the run: where it started, one button mask per finished
frame, and a sparse list of the frames Reset was pressed at. `--play FILE` plays one instead of a
schedule. A replay is byte-identical, which is the whole claim and the thing to check after touching
any of it:

```sh
java -jar $JAR --headless --rom ROM.nes --frames 900 --input 60/40x3:start --reset-at 500 \
--record take.mnm --save-state a.mn
java -jar $JAR --headless --rom ROM.nes --play take.mnm --save-state b.mn
cmp a.mn b.mn # byte-identical end state
```

**A rewind is not in the movie.** Rewinding while recording drops the frames that were taken back,
so a movie holds the timeline that was finally played and a replay never re-enacts the revert. That
composes out of the rewind claim above: a rewound machine is byte for byte the machine that never
went forward, so there is nothing lost by truncating the log to match.

`--play` is the input, so it refuses `--record`, `--input`, `--input-file`, `--reset-at`, `--genie`,
`--load-state`, `--sram-in` and `--interactive` -- each of those would be a second answer to a
question the movie has already answered. It defaults `--frames` to the movie's own length; asking for
more runs past the end with nothing held down.

`record`, `record start` and `record stop [PATH]` do the same inside an interactive session -- the
shape of `rewind` rather than of `hack`, since the interesting form is the one that takes a file.
Mutating `genie`/`ungenie`/`genie clear` are refused while recording, because a movie pins the codes
at the moment it starts and a file naming one set that was played against another cannot be replayed.

**`run.record` and `run.replay` join the comparability checklist**, beside `run.state`, `run.region`,
`run.hacks` and `run.genie`. Both are always present with explicit nulls. A run that started at power
on records a movie that starts there and carries no state at all; anything else -- a `--load-state`,
a `--sram-in`, a loaded state mid-session, or a rewind that went back past the start of the recording
-- puts a save state inside the file, and `run.replay.anchored` is what says so.
`run.state.startedFromPowerOn` is false for a replay of one of those.

The Game Genie codes ride inside the movie and are put back on replay. They have to: a cheated
cartridge is byte for byte an honest one, so `cart.sha256` cannot tell them apart and nothing else in
the file would.

The desktop has **Machine > Record Movie... / Play Movie...**. While either is running the pad is
latched once a frame on the emulation thread rather than reaching the controller the moment a key
moves, and Power Cycle, Region and the Game Genie item are greyed out -- the first two would build a
new machine and take the recorder with it. Rewinding during playback stops it and hands the game
back.

### Running a romhack

`--patch FILE` applies an IPS patch to the ROM before anything reads it as a cartridge. Repeatable,
Expand Down Expand Up @@ -313,7 +359,7 @@ Four Maven modules, and the arrows between them only point one way.
mynes-core/ depends on nothing
mynes/ the console: CPU, PPU, APU, BUS, MMU, VRAM, Cart, Region, controllers
mynes/mappers/ mappers 0 to 4
mynes/state/ save states and battery .sav files
mynes/state/ save states, battery .sav files, and .mnm session recordings
mynes/debug/ the disassembler and the breakpoints, shared by the window and the REPL
mynes/cheat/ Game Genie codes, and the device MMU asks on every read of PRG ROM
mynes/video/ colour indices to pixels: the overscan crop and the frame renderer
Expand Down
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,13 @@ and there is no patched copy of it to keep anywhere. A patched game keeps its ow
battery file, named after the patch rather than the ROM, so an afternoon with a hack cannot write
over fifty hours of the original. `--patch` does the same thing from the command line.

**Session recordings**, from **Machine > Record Movie...** and **Machine > Play Movie...**. A movie
is not a video: it is where the run started, one button mask per frame, and the frames Reset was
pressed at, which comes to a few hundred bytes a minute. Playing one back reproduces the session byte
for byte, because nothing in the machine reads a clock or a random number. Rewinding while recording
drops the frames you took back, so a movie holds the run you finally played rather than the one you
undid. `--record` and `--play` do the same from the command line.

**Save states and battery saves**, and a **headless mode** for running with no window at all. Both
have a section of their own below.

Expand Down Expand Up @@ -259,6 +266,24 @@ The difference is worth keeping in mind. A save state is a bookmark, and losing
minutes. A `.sav` is fifty hours of Zelda, which is why that one is written to a temporary file and
moved into place, so that a crash halfway through cannot take both it and its replacement.

**Movies** are the third file here and the odd one out, because they hold no picture of the machine
at all. A `.mnm` is where the run started, one byte per frame saying what was held down, and a list
of the frames Reset was pressed at -- ninety seconds of play is about five kilobytes before it is
compressed. It can be that small only because the console is deterministic: the same cartridge given
the same buttons on the same frames arrives at the same bytes, so playing one back reproduces the
session exactly rather than approximately.

Recording from power on carries no state at all, which makes a movie of a whole playthrough something
you can hand to anybody with the same ROM. Starting one part way through puts a save state inside the
file to begin from, since there is otherwise nothing to say where the beginning was. Game Genie codes
travel inside the movie and are put back on playback -- they have to, because a cheated cartridge is
byte for byte an honest one and nothing else in the file could say so.

Rewinding while recording drops the frames you took back rather than appending them, so a movie is
the timeline you finally played and a replay never re-enacts the revert. Power Cycle and Region are
greyed out while a movie is running, since both build a new machine and the recording would go with
it.

## Headless mode

The emulator also runs with nobody watching: no window, no sound card. That is useful from a script,
Expand Down Expand Up @@ -318,6 +343,12 @@ seconds to start up, the jar about a third of one.
so `cart.sha256` is the plain one and `run.genie` is the only thing in the report that tells a
cheated run from an honest one. Which also means a save state taken with codes in will load into a
machine with them out, without a word of complaint.
- **`--record FILE` and `--play FILE`** write and replay a session. `--play` is the input, so it
refuses `--input`, `--reset-at`, `--genie`, `--load-state` and the rest of what a movie already
says, and it defaults `--frames` to the movie's own length -- ask for more and the run carries on
past the end with nothing held down. `run.record` and `run.replay` in the report join the list of
things to check before diffing two runs. `record start` and `record stop` are commands in the
interactive session as well.
- **`--interactive`** reads commands on standard input and answers each with a line of JSON, for
when you do not yet know the question well enough to write it down. It is also where the debugger
lives without a window: `break`, `watch`, `step` and `disasm`, with `run` reporting back what
Expand Down
Loading