One recorded emulated frame: the pad the console held, the human's pad, the work-RAM bytes the bot's tick wrote this frame, and, on the frame a goal choice happened, which option was used.
Per research/replay-timeline.md's Findings (2026-09-21): the pad alone
does not reproduce a run, because the bot's cheats (full health, bombs,
arrows, hammer, lamp - alttp.c:20-27) write work RAM every frame, not
only the pad. So a [FrameEntry] carries both.
10use bincode::{Decode, Encode};
Which goal-choice option a decision used, on the frame the hook ran.
Logging the PICK (not just what Jev answered) is deliberate: the pick
also records the wall-clock rate breaker's own fallbacks, which a replay
has no way to reproduce by asking again (research/replay-timeline.md,
"Log the pick, not just Jev's answer").
This is goal-choice-specific on purpose, for now: a packages/decisions
crate is landing that replaces zbanks-jev with one Decision trait and
a kind on every decision record, covering more than the goal choice.
Once it lands, FrameEntry::decision should generalize to (kind, answer) so replay can look any decision kind up by frame, not just this
one - tracked here rather than acted on early, since the crate it
depends on does not exist yet.
The chooser returned None, or was never asked: upstream's own pick
(index 0, packages/zbanks' choose_goal).
29 Upstream,
One entry per emulated frame, in order, in a branch's log
(crate::log). Bincode-encoded; wram_diff is empty on nearly every
frame (crate::keyframe's doc comment has the measured size).
What the console held this frame - the bot's returned pad, Snes9x's
joypad layout (zbanks::PAD_BITS). This is what [crate::seek]
replays: it is the actual output, unlike human_pad, which upstream
itself may have overridden (zbanks::apply_pad's doc comment).
43 pub console_pad: u16,
What the human held (keyboard plus any MCP press), before the bot
saw it. Not used to replay - the bot already turned it into
console_pad - kept so the recording says what a person actually did.
47 pub human_pad: u16,
(offset from $7E0000, new byte) for every work-RAM byte the bot's
tick changed this frame, in ascending offset order.
50 pub wram_diff: Vec<(u32, u8)>,
Every work-RAM byte that differs between before and after, as
(offset from $7E0000, new value), ascending. Pure and small enough to
unit-test without a console: the caller (the recorder, or the
determinism probe) supplies two work-RAM snapshots taken immediately
before and immediately after one zbanks::Bot::tick - the window in
which the bot's cheats run and nothing else touches RAM.
Apply a diff to a mutable WRAM buffer - the inverse of taking one. Used
by [crate::seek] to reproduce a bot's cheat writes without the bot.
80#[cfg(test)] 81mod tests { 82 use super::*; 83 84 #[test] 85 fn no_diff_when_nothing_changed() { 86 let ram = [1u8, 2, 3, 4, 5]; 87 assert_eq!(diff_wram(&ram, &ram), Vec::<(u32, u8)>::new()); 88 } 89 90 #[test] 91 fn diff_names_every_changed_offset_ascending() { 92 let before = [0u8, 1, 2, 3, 4, 5, 6]; 93 let after = [0u8, 9, 2, 3, 8, 5, 7]; 94 assert_eq!(diff_wram(&before, &after), vec![(1, 9), (4, 8), (6, 7)]); 95 } 96 97 #[test] 98 fn apply_is_the_inverse_of_diff() { 99 let before = vec![10u8; 64]; 100 let mut after = before.clone(); 101 after[3] = 99; 102 after[40] = 200; 103 let diff = diff_wram(&before, &after); 104 let mut reconstructed = before.clone(); 105 apply_wram_diff(&mut reconstructed, &diff); 106 assert_eq!(reconstructed, after); 107 } 108 109 #[test] 110 fn a_frame_entry_round_trips_through_bincode() { 111 let entry = FrameEntry { 112 console_pad: 0x8421, 113 human_pad: 0x0040, 114 wram_diff: vec![(0x10, 5), (0xF340, 0x63)], 115 decision: Some(Pick::Index(3)), 116 }; 117 let config = bincode::config::standard(); 118 let bytes = bincode::encode_to_vec(&entry, config).expect("encode"); 119 let (back, _): (FrameEntry, usize) = bincode::decode_from_slice(&bytes, config).expect("decode"); 120 assert_eq!(back, entry); 121 122 let plain = FrameEntry { console_pad: 0, human_pad: 0, wram_diff: Vec::new(), decision: None }; 123 let bytes = bincode::encode_to_vec(&plain, config).expect("encode"); 124 let (back, _): (FrameEntry, usize) = bincode::decode_from_slice(&bytes, config).expect("decode"); 125 assert_eq!(back, plain); 126 127 let bytes = bincode::encode_to_vec(&FrameEntry { decision: Some(Pick::Upstream), ..plain.clone() }, config).unwrap(); 128 let (back, _): (FrameEntry, usize) = bincode::decode_from_slice(&bytes, config).unwrap(); 129 assert_eq!(back.decision, Some(Pick::Upstream)); 130 } 131}