1//! One recorded emulated frame: the pad the console held, the human's pad, 2//! the work-RAM bytes the bot's tick wrote this frame, and, on the frame a 3//! goal choice happened, which option was used. 4//! 5//! Per `research/replay-timeline.md`'s Findings (2026-09-21): the pad alone 6//! does not reproduce a run, because the bot's cheats (full health, bombs, 7//! arrows, hammer, lamp - `alttp.c:20-27`) write work RAM every frame, not 8//! only the pad. So a [`FrameEntry`] carries both. 9 10use bincode::{Decode, Encode}; 11 12/// Which goal-choice option a decision used, on the frame the hook ran. 13/// Logging the PICK (not just what Jev answered) is deliberate: the pick 14/// also records the wall-clock rate breaker's own fallbacks, which a replay 15/// has no way to reproduce by asking again (`research/replay-timeline.md`, 16/// "Log the pick, not just Jev's answer"). 17/// 18/// This is goal-choice-specific on purpose, for now: a `packages/decisions` 19/// crate is landing that replaces `zbanks-jev` with one `Decision` trait and 20/// a `kind` on every decision record, covering more than the goal choice. 21/// Once it lands, `FrameEntry::decision` should generalize to `(kind, 22/// answer)` so replay can look any decision kind up by frame, not just this 23/// one - tracked here rather than acted on early, since the crate it 24/// depends on does not exist yet. 25#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)] 26pub enum Pick { 27 /// The chooser returned `None`, or was never asked: upstream's own pick 28 /// (index 0, `packages/zbanks`' `choose_goal`). 29 Upstream, 30 /// The chooser returned this index into the options offered. 31 Index(u32), 32} 33 34/// One entry per emulated frame, in order, in a branch's log 35/// (`crate::log`). Bincode-encoded; `wram_diff` is empty on nearly every 36/// frame (`crate::keyframe`'s doc comment has the measured size). 37#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] 38pub struct FrameEntry { 39 /// What the console held this frame - the bot's returned pad, Snes9x's 40 /// joypad layout (`zbanks::PAD_BITS`). This is what [`crate::seek`] 41 /// replays: it is the actual output, unlike `human_pad`, which upstream 42 /// itself may have overridden (`zbanks::apply_pad`'s doc comment). 43 pub console_pad: u16, 44 /// What the human held (keyboard plus any MCP `press`), before the bot 45 /// saw it. Not used to replay - the bot already turned it into 46 /// `console_pad` - kept so the recording says what a person actually did. 47 pub human_pad: u16, 48 /// `(offset from $7E0000, new byte)` for every work-RAM byte the bot's 49 /// tick changed this frame, in ascending offset order. 50 pub wram_diff: Vec<(u32, u8)>, 51 /// `Some` only on the frame a goal choice was made. 52 pub decision: Option<Pick>, 53} 54 55/// Every work-RAM byte that differs between `before` and `after`, as 56/// `(offset from $7E0000, new value)`, ascending. Pure and small enough to 57/// unit-test without a console: the caller (the recorder, or the 58/// determinism probe) supplies two work-RAM snapshots taken immediately 59/// before and immediately after one `zbanks::Bot::tick` - the window in 60/// which the bot's cheats run and nothing else touches RAM. 61pub fn diff_wram(before: &[u8], after: &[u8]) -> Vec<(u32, u8)> { 62 debug_assert_eq!(before.len(), after.len(), "diffing WRAM of two different sizes"); 63 before 64 .iter() 65 .zip(after) 66 .enumerate() 67 .filter(|(_, (b, a))| b != a) 68 .map(|(i, (_, &a))| (i as u32, a)) 69 .collect() 70} 71 72/// Apply a diff to a mutable WRAM buffer - the inverse of taking one. Used 73/// by [`crate::seek`] to reproduce a bot's cheat writes without the bot. 74pub fn apply_wram_diff(wram: &mut [u8], diff: &[(u32, u8)]) { 75 for &(offset, value) in diff { 76 wram[offset as usize] = value; 77 } 78} 79 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}