use anyhow::Result; use notify::Watcher; use rand::Rng; use std::{ fs::{self, File}, io::{Read, Seek as _, SeekFrom, Write as _}, path::{Path, PathBuf}, sync::{Arc, atomic::AtomicBool}, }; use crate::emulator::{SimId, game_info::GameInfo, shrooms_vb_util::rom_from_isx}; pub struct Cart { pub file_path: PathBuf, pub rom: Vec, sram_file: File, pub sram: Vec, pub info: Arc, watcher: Option, changed: Arc, } impl Cart { pub fn load(file_path: &Path, sim_id: SimId, watch: bool) -> Result { let rom = fs::read(file_path)?; let (rom, info) = try_parse_isx(file_path, &rom) .or_else(|| try_parse_elf(file_path, &rom)) .unwrap_or_else(|| (rom, GameInfo::empty(file_path))); let mut sram_file = File::options() .read(true) .write(true) .create(true) .truncate(false) .open(sram_path(file_path, sim_id))?; let sram = if sram_file.metadata()?.len() == 0 { // new SRAM file, randomize the contents let mut sram = vec![0; 16 * 1024]; let mut rng = rand::rng(); for dst in sram.iter_mut().step_by(2) { *dst = rng.random(); } sram } else { let mut sram = Vec::with_capacity(16 * 1024); sram_file.read_to_end(&mut sram)?; sram }; let changed = Arc::new(AtomicBool::new(false)); let watcher = if watch { build_watcher(file_path, changed.clone()) } else { None }; Ok(Cart { file_path: file_path.to_path_buf(), rom, sram_file, sram, info: Arc::new(info), watcher, changed, }) } pub fn save_sram(&mut self) -> Result<()> { self.sram_file.seek(SeekFrom::Start(0))?; self.sram_file.write_all(&self.sram)?; Ok(()) } pub fn changed(&self) -> bool { self.changed.load(std::sync::atomic::Ordering::Relaxed) } pub fn is_watching(&self) -> bool { self.watcher.is_some() } pub fn restart_watching(&mut self) { self.changed = Arc::new(AtomicBool::new(false)); if let Some(mut watcher) = self.watcher.take() { let _ = watcher.unwatch(&self.file_path); }; self.watcher = build_watcher(&self.file_path, self.changed.clone()); } pub fn stop_watching(&mut self) { self.changed = Arc::new(AtomicBool::new(false)); self.watcher = None; } } fn build_watcher(file_path: &Path, changed: Arc) -> Option { let file_path = file_path.to_path_buf(); let mut watcher = notify::recommended_watcher(move |event: Result| { let Ok(e) = event else { return; }; let modified = !matches!( e.kind, notify::EventKind::Access(_) | notify::EventKind::Modify(notify::event::ModifyKind::Metadata(_)) ); if modified { changed.store(true, std::sync::atomic::Ordering::Relaxed); } }) .ok()?; watcher .watch(&file_path, notify::RecursiveMode::NonRecursive) .ok()?; Some(watcher) } fn try_parse_isx(file_path: &Path, data: &[u8]) -> Option<(Vec, GameInfo)> { let rom = rom_from_isx(data)?; let info = GameInfo::from_isx(file_path, data); Some((rom, info)) } fn try_parse_elf(file_path: &Path, data: &[u8]) -> Option<(Vec, GameInfo)> { use object::read::elf::FileHeader; let program = match object::FileKind::parse(data).ok()? { object::FileKind::Elf32 => { let header = object::elf::FileHeader32::parse(data).ok()?; parse_elf_program(header, data)? } object::FileKind::Elf64 => { let header = object::elf::FileHeader64::parse(data).ok()?; parse_elf_program(header, data)? } _ => return None, }; let info = GameInfo::from_elf(file_path, data).unwrap_or_else(|_| GameInfo::empty(file_path)); Some((program, info)) } fn parse_elf_program>( header: &Elf, data: &[u8], ) -> Option> { use object::read::elf::ProgramHeader; let endian = header.endian().ok()?; let mut bytes = vec![]; let mut pstart = None; for phdr in header.program_headers(endian, data).ok()? { let pma = phdr.p_paddr(endian).into(); if pma < 0x07000000 || phdr.p_filesz(endian).into() == 0 { continue; } let start = pstart.unwrap_or(pma); pstart = Some(start); bytes.resize((pma - start) as usize, 0); let data = phdr.data(endian, data).ok()?; bytes.extend_from_slice(data); } Some(bytes) } fn sram_path(file_path: &Path, sim_id: SimId) -> PathBuf { match sim_id { SimId::Player1 => file_path.with_extension("p1.sram"), SimId::Player2 => file_path.with_extension("p2.sram"), } }