lemur/src/emulator/cart.rs

171 lines
5.2 KiB
Rust

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<u8>,
sram_file: File,
pub sram: Vec<u8>,
pub info: Arc<GameInfo>,
watcher: Option<notify::RecommendedWatcher>,
changed: Arc<AtomicBool>,
}
impl Cart {
pub fn load(file_path: &Path, sim_id: SimId, watch: bool) -> Result<Self> {
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<AtomicBool>) -> Option<notify::RecommendedWatcher> {
let file_path = file_path.to_path_buf();
let mut watcher =
notify::recommended_watcher(move |event: Result<notify::Event, notify::Error>| {
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<u8>, 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<u8>, 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<Elf: object::read::elf::FileHeader<Endian = object::Endianness>>(
header: &Elf,
data: &[u8],
) -> Option<Vec<u8>> {
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"),
}
}