jevsnes.git / packages / replay / src / header.rs
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}