1//! What every branch in a recording has in common: the machine it all starts 2//! from, and the bot options that shaped what happened - the equivalent of 3//! BizHawk's `SyncSettings.json` plus its savestate-anchored `CoreState` 4//! (`research/replay-timeline.md`). 5//! 6//! The ROM bytes themselves are NOT stored here: they are a fixed 1 MB file 7//! already on disk (`roms/*.sfc`, gitignored) that every caller already has 8//! open, so storing a second copy per recording would only spend disk this 9//! project is asked to keep bounded. `Console::restore` already refuses a 10//! snapshot taken of a different cartridge by CRC, which is the check that 11//! matters. 12 13use std::path::Path; 14use std::{fs, io}; 15 16use bincode::{Decode, Encode}; 17 18/// 2 (2026-09-22): keyframes are `Console::snapshot_fixed`, not 19/// `Console::snapshot`. `crate::storage::remove_other_formats` deletes older 20/// recordings rather than reading them. 21pub const FORMAT_VERSION: u32 = 2; 22 23#[derive(Debug, Clone, Encode, Decode, PartialEq)] 24pub struct RunHeader { 25 pub format_version: u32, 26 /// `console::Console::snapshot()` of the machine this recording starts 27 /// from - frame 0, shared by every branch in the tree. 28 pub starting_snapshot: Vec<u8>, 29 /// The map the bot imported at the start of this recording 30 /// (`map.19.txt`'s bytes), empty if there was none. 31 pub imported_map: Vec<u8>, 32 pub retry_given_up: bool, 33 pub jev_enabled: bool, 34} 35 36impl RunHeader { 37 pub fn new(starting_snapshot: Vec<u8>, imported_map: Vec<u8>, retry_given_up: bool, jev_enabled: bool) -> Self { 38 Self { format_version: FORMAT_VERSION, starting_snapshot, imported_map, retry_given_up, jev_enabled } 39 } 40 41 pub fn load(path: &Path) -> io::Result<Self> { 42 let bytes = fs::read(path)?; 43 bincode::decode_from_slice(&bytes, bincode::config::standard()) 44 .map(|(h, _)| h) 45 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e)) 46 } 47 48 pub fn save(&self, path: &Path) -> io::Result<()> { 49 let bytes = bincode::encode_to_vec(self, bincode::config::standard()).expect("encoding a header never fails"); 50 let tmp = path.with_extension("bin.part"); 51 fs::write(&tmp, bytes)?; 52 fs::rename(tmp, path) 53 } 54} 55 56#[cfg(test)] 57mod tests { 58 use super::*; 59 60 #[test] 61 fn a_header_round_trips_through_a_file() { 62 let header = RunHeader::new(vec![1, 2, 3, 4, 5], vec![9, 9], true, false); 63 let dir = std::env::temp_dir().join(format!("jev-replay-header-test-{}", std::process::id())); 64 let _ = fs::create_dir_all(&dir); 65 let path = dir.join("header.bin"); 66 header.save(&path).expect("save"); 67 let back = RunHeader::load(&path).expect("load"); 68 assert_eq!(back, header); 69 let _ = fs::remove_dir_all(&dir); 70 } 71}