jevsnes.git / packages / replay / src / header.rs

What every branch in a recording has in common: the machine it all starts from, and the bot options that shaped what happened - the equivalent of BizHawk's SyncSettings.json plus its savestate-anchored CoreState (research/replay-timeline.md).

The ROM bytes themselves are NOT stored here: they are a fixed 1 MB file already on disk (roms/*.sfc, gitignored) that every caller already has open, so storing a second copy per recording would only spend disk this project is asked to keep bounded. Console::restore already refuses a snapshot taken of a different cartridge by CRC, which is the check that matters.

13use std::path::Path;
14use std::{fs, io};
16use bincode::{Decode, Encode};

2 (2026-09-22): keyframes are Console::snapshot_fixed, not Console::snapshot. crate::storage::remove_other_formats deletes older recordings rather than reading them.

21pub const FORMAT_VERSION: u32 = 2;
23#[derive(Debug, Clone, Encode, Decode, PartialEq)]
24pub struct RunHeader {
25    pub format_version: u32,

console::Console::snapshot() of the machine this recording starts from - frame 0, shared by every branch in the tree.

28    pub starting_snapshot: Vec<u8>,

The map the bot imported at the start of this recording (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}
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}