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
21 changes: 21 additions & 0 deletions crates/rustynes-core/src/movie.rs
Original file line number Diff line number Diff line change
Expand Up @@ -800,6 +800,27 @@ impl MovieRecorder {
/// frame *before* [`Nes::run_frame`], after the frontend has applied its
/// `set_buttons` calls — this captures exactly the inputs the upcoming
/// frame consumes.
///
/// # Two ports only, including under a Four Score
///
/// [`FrameInput`] models ports 0 and 1, so this reads `nes.buttons(0)` and
/// `nes.buttons(1)` and **nothing else**. The core itself carries four —
/// the frontend calls `set_buttons(2)` / `set_buttons(3)` whenever the Four
/// Score adapter is active — so recording a four-player session captures
/// half of what drove it, and replaying that movie diverges from the run it
/// came from.
///
/// Stated here rather than left to be discovered, because the failure is
/// silent at record time: nothing about a `.rnm` says which ports it could
/// not hold, and the divergence only appears on playback. The frontend's
/// Replay panel says so where the topology is displayed, and
/// `Movie::verify`'s attestation catches it after the fact.
///
/// Widening [`FrameInput`] is a `.rnm` format epoch change (ADR 0028), not
/// an additive one, which is why this is a documented limit rather than a
/// fix. The `.fm2` importer already takes the same position for the same
/// reason — it keeps pads 1 and 2, drops 3 and 4, and preserves the
/// `fourscore` flag so the caller is not silently misled.
pub fn capture(&mut self, nes: &Nes) {
self.frames.push(FrameInput {
p1: nes.buttons(0),
Expand Down
20 changes: 20 additions & 0 deletions crates/rustynes-frontend/src/debugger/replay_panel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,26 @@ pub fn show(
ui.end_row();
});

// The adapter row above says "Four Score (P1..P4)", which is true of
// the CONSOLE and not of the movie. `FrameInput` holds two ports, so
// `MovieRecorder::capture` records P1 and P2 and drops P3 and P4 --
// a four-player recording replays as a different run.
//
// Surfaced right under the claim it qualifies, because the failure is
// silent at record time: nothing about a `.rnm` says which ports it
// could not hold, and the divergence only shows up on playback.
if info.four_score {
ui.colored_label(
ui.visuals().warn_fg_color,
"Movies record P1 + P2 only — P3/P4 input is NOT captured.",
)
.on_hover_text(
"The `.rnm` format stores two ports. A recording made with \
the Four Score active will not replay the same run. \
Widening it is a movie-format epoch change (ADR 0028).",
);
}

ui.separator();

// --- Controls ---
Expand Down
14 changes: 14 additions & 0 deletions docs/creator-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,20 @@ what makes movie replay exact — see [`testing-strategy.md`](testing-strategy.m
for the determinism contract. The `.rnm` deserializer is hardened against
malformed input (bounded allocations; fuzz-tested, see `fuzz/`).

**Movies record two ports.** `FrameInput` models P1 and P2, so a recording made
with the **Four Score** adapter active captures half of what drove the run and
replays as a different one. The console carries four ports and the emulator
drives all four; the movie format does not. This is a documented limit rather
than a defect being worked around: widening `FrameInput` is a `.rnm` format epoch
change (ADR 0028), not an additive one, and the `.fm2` importer takes the same
position — it keeps pads 1 and 2, drops 3 and 4, and preserves the `fourscore`
flag so the caller is not misled.

The failure is silent at record time, so it is surfaced where the claim is made:
the Replay panel prints the caveat directly under the "Four Score (P1..P4)"
topology row, and `rustynes verify` catches a divergent replay after the fact
because its attestation folds in the video the run produced, not just the input.

## Scripting (Lua)

An embedded Lua engine exposes an emulation API (memory peek/poke, frame hooks,
Expand Down