#[cfg(test)]
use std::cell::RefCell;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
#[cfg(test)]
use std::rc::Rc;
use std::{error, fmt};
use crate::app_context::IntoSharedAppContext;
#[cfg(test)]
use crate::cartridge::NametableLayout;
use crate::cartridge::mapper::MapperContext;
use crate::cartridge::{Mapper, TimingMode};
#[derive(Debug)]
pub enum CartridgeError {
InvalidHeader,
FileTooSmall { expected: usize, actual: usize },
UnsupportedMapper(u16),
Io(io::Error),
}
const SAVE_FILE_EXTENSION: &str = "sav";
const STATE_FILE_EXTENSION: &str = "state";
impl fmt::Display for CartridgeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidHeader => {
write!(f, "Invalid iNES header: expected 'NES\\x1A' magic bytes")
}
Self::FileTooSmall { expected, actual } => {
write!(
f,
"File too small: expected at least {expected} bytes, got {actual}"
)
}
Self::UnsupportedMapper(mapper) => write!(f, "Unsupported mapper: {mapper}"),
Self::Io(err) => write!(f, "{err}"),
}
}
}
impl error::Error for CartridgeError {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match self {
Self::Io(err) => Some(err),
_ => None,
}
}
}
impl From<io::Error> for CartridgeError {
fn from(err: io::Error) -> Self {
Self::Io(err)
}
}
pub struct Cartridge {
mapper: Box<dyn Mapper>,
crc32: u32,
rom_timing_mode: TimingMode,
rom_path: Option<PathBuf>,
save_path: Option<PathBuf>,
battery_backed_prg_ram: bool,
trainer: Option<Vec<u8>>,
}
impl Cartridge {
fn map_parse_error(err: crate::cartridge::ines::RomParseError) -> CartridgeError {
match err {
crate::cartridge::ines::RomParseError::InvalidHeader => CartridgeError::InvalidHeader,
crate::cartridge::ines::RomParseError::FileTooSmall { expected, actual } => {
CartridgeError::FileTooSmall { expected, actual }
}
}
}
fn create_mapper(
parsed: &crate::cartridge::ines::ParsedRom,
) -> Result<Box<dyn Mapper>, CartridgeError> {
let context = MapperContext::from_parsed_rom(parsed);
let mapper_number = context.mapper;
crate::cartridge::mapper::create_mapper(context).map_err(|err| {
if err.kind() == io::ErrorKind::Unsupported {
CartridgeError::UnsupportedMapper(mapper_number)
} else {
CartridgeError::Io(err)
}
})
}
fn can_persist_save_ram(&self) -> bool {
self.battery_backed_prg_ram && self.save_path.is_some()
}
fn save_temp_path(save_path: &Path) -> PathBuf {
let mut temp_path = save_path.to_path_buf();
temp_path.set_extension(format!("sav.tmp.{}", std::process::id()));
temp_path
}
fn write_save_data(save_path: &Path, data: &[u8]) -> io::Result<()> {
if let Some(parent) = save_path.parent() {
fs::create_dir_all(parent)?;
}
let temp_path = Self::save_temp_path(save_path);
fs::write(&temp_path, data)?;
if save_path.exists() {
let _ = fs::remove_file(save_path);
}
fs::rename(temp_path, save_path)
}
fn load_save_data(&mut self, save_path: &Path) -> io::Result<()> {
if !save_path.exists() {
return Ok(());
}
let save_data = fs::read(save_path)?;
self.mapper_mut().load_wram_snapshot(&save_data);
Ok(())
}
pub fn load_from_file<P: AsRef<Path>, C: IntoSharedAppContext>(
data: &[u8],
path: P,
app_context: C,
) -> Result<Self, CartridgeError> {
let app_context = app_context.into_shared();
let rom_path = path.as_ref().to_path_buf();
let ctx = app_context.borrow();
let rom_db = ctx.rom_db();
let parsed = crate::cartridge::ParsedRom::parse(data, Some(rom_db))
.map_err(Self::map_parse_error)?;
crate::debugging::log_info(format!(
"Loaded rom with CRC32: {:08X}, mapper={}, submapper={}, PRG-ROM={}KB, CHR-ROM={}KB",
parsed.crc32,
parsed.header.mapper,
parsed.header.submapper,
parsed.header.prg_rom_size_bytes / 1024,
parsed.header.chr_rom_size_bytes / 1024
));
let mut cart = Self {
mapper: Self::create_mapper(&parsed)?,
crc32: parsed.crc32,
rom_timing_mode: parsed.header.timing_mode.normalize_rom_timing_mode(),
save_path: Some(rom_path.with_extension(SAVE_FILE_EXTENSION)),
rom_path: Some(rom_path),
battery_backed_prg_ram: parsed.header.battery_backed_prg_ram,
trainer: parsed.trainer,
};
cart.load_save_ram_from_disk()?;
Ok(cart)
}
pub fn state_path(&self) -> Option<PathBuf> {
self.rom_path
.as_ref()
.map(|path| path.with_extension(STATE_FILE_EXTENSION))
}
pub fn debug_path(&self) -> Option<PathBuf> {
self.rom_path
.as_ref()
.map(|path| path.with_extension("debug"))
}
pub fn save_ram(&self) -> io::Result<()> {
if !self.can_persist_save_ram() {
return Ok(());
}
let Some(save_path) = self.save_path.as_ref() else {
return Ok(());
};
let prg_ram = self.mapper().wram_snapshot();
Self::write_save_data(save_path, &prg_ram)
}
fn load_save_ram_from_disk(&mut self) -> io::Result<()> {
if !self.can_persist_save_ram() {
return Ok(());
}
let Some(save_path) = self.save_path.as_ref() else {
return Ok(());
};
let save_path = save_path.clone();
self.load_save_data(&save_path)
}
pub fn mapper(&self) -> &dyn Mapper {
&*self.mapper
}
pub fn mapper_mut(&mut self) -> &mut dyn Mapper {
&mut *self.mapper
}
pub fn reset(&mut self) {
self.mapper.reset();
}
pub fn initialize_ram(&mut self, mode: crate::console::RamInitMode) {
self.mapper.initialize_ram(mode);
}
pub fn crc32(&self) -> u32 {
self.crc32
}
pub fn rom_timing_mode(&self) -> TimingMode {
self.rom_timing_mode
}
pub fn trainer(&self) -> Option<&[u8]> {
self.trainer.as_deref()
}
pub fn has_trainer(&self) -> bool {
self.trainer.is_some()
}
#[cfg(test)]
pub fn from_parts(prg_rom: Vec<u8>, chr_rom: Vec<u8>, mirroring: NametableLayout) -> Self {
use crate::cartridge::nrom::NROMMapper;
let crc32 = crate::cartridge::calculate_rom_crc32(&prg_rom, &chr_rom);
let mapper = Box::new(NROMMapper::new(MapperContext::new_for_test(
0, prg_rom, chr_rom, mirroring,
)));
Self {
mapper,
crc32,
rom_timing_mode: TimingMode::Ntsc,
rom_path: None,
save_path: None,
battery_backed_prg_ram: false,
trainer: None,
}
}
#[cfg(test)]
pub fn from_mapper_for_test(mapper: Box<dyn Mapper>) -> Self {
Self {
mapper,
crc32: 0,
rom_timing_mode: TimingMode::Ntsc,
rom_path: None,
save_path: None,
battery_backed_prg_ram: false,
trainer: None,
}
}
#[cfg(test)]
pub fn set_crc32_for_test(&mut self, crc32: u32) {
self.crc32 = crc32;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::app_context::AppContext;
use std::path::{Path, PathBuf};
const INES_HEADER_SIZE: usize = 16;
const TRAINER_SIZE: usize = 512;
const PRG_ROM_BANK_SIZE: usize = 16 * 1024;
const CHR_ROM_BANK_SIZE: usize = 8 * 1024;
const PRG_FILL_BYTE: u8 = 0xAA;
const CHR_FILL_BYTE: u8 = 0xBB;
fn build_ines_header(
prg_rom_banks: u8,
chr_rom_banks: u8,
flags6: u8,
flags7: u8,
flags8: u8,
flags9: u8,
flags10: u8,
) -> Vec<u8> {
let mut header = vec![
b'N',
b'E',
b'S',
0x1A,
prg_rom_banks,
chr_rom_banks,
flags6,
flags7,
flags8,
flags9,
flags10,
];
header.resize(INES_HEADER_SIZE, 0);
header
}
fn append_prg_and_chr_pattern(rom: &mut Vec<u8>, prg_rom_banks: u8, chr_rom_banks: u8) {
let prg_size = prg_rom_banks as usize * PRG_ROM_BANK_SIZE;
rom.extend(vec![PRG_FILL_BYTE; prg_size]);
let chr_size = chr_rom_banks as usize * CHR_ROM_BANK_SIZE;
rom.extend(vec![CHR_FILL_BYTE; chr_size]);
}
fn remove_file_if_exists(path: &Path) {
let _ = std::fs::remove_file(path);
}
fn remove_files_if_exist(paths: &[&Path]) {
for path in paths {
remove_file_if_exists(path);
}
}
fn create_test_rom(
prg_rom_banks: u8,
chr_rom_banks: u8,
flags6: u8,
include_trainer: bool,
) -> Vec<u8> {
let mut rom = build_ines_header(prg_rom_banks, chr_rom_banks, flags6, 0, 0, 0, 0);
if include_trainer {
rom.extend(vec![0x00; TRAINER_SIZE]);
}
append_prg_and_chr_pattern(&mut rom, prg_rom_banks, chr_rom_banks);
rom
}
fn create_test_rom_with_flags9(
prg_rom_banks: u8,
chr_rom_banks: u8,
flags6: u8,
flags7: u8,
flags9: u8,
include_trainer: bool,
) -> Vec<u8> {
let mut rom = build_ines_header(prg_rom_banks, chr_rom_banks, flags6, flags7, 0, flags9, 0);
if include_trainer {
rom.extend(vec![0x00; TRAINER_SIZE]);
}
append_prg_and_chr_pattern(&mut rom, prg_rom_banks, chr_rom_banks);
rom
}
fn create_test_rom_with_mapper(
prg_rom_banks: u8,
chr_rom_banks: u8,
mapper_number: u8,
battery_backed: bool,
prg_ram_banks_8k: u8,
) -> Vec<u8> {
let mut flags6 = (mapper_number & 0x0F) << 4;
if battery_backed {
flags6 |= 0x02; }
let flags7 = mapper_number & 0xF0;
let mut rom = build_ines_header(
prg_rom_banks,
chr_rom_banks,
flags6,
flags7,
prg_ram_banks_8k,
0,
0,
);
append_prg_and_chr_pattern(&mut rom, prg_rom_banks, chr_rom_banks);
rom
}
fn unique_temp_path(filename: &str) -> PathBuf {
let mut path = std::env::temp_dir();
let nonce = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
path.push(format!("neser-{nonce}-{filename}"));
path
}
fn load_cartridge_from_disk<C: IntoSharedAppContext>(path: &Path, app_context: C) -> Cartridge {
let data = std::fs::read(path).expect("reading ROM from disk should succeed");
Cartridge::load_from_file(&data, path, app_context).expect("load_from_file should succeed")
}
fn load_cartridge_from_bytes(data: &[u8]) -> Result<Cartridge, CartridgeError> {
let app_context = Rc::new(RefCell::new(AppContext::new()));
Cartridge::load_from_file(
data,
unique_temp_path("in_memory_test.nes"),
app_context.clone(),
)
}
#[test]
fn test_load_from_file_parses_ines_rom() {
let rom_data = create_test_rom(1, 1, 0x01, false);
let path = unique_temp_path("test_load_from_file.nes");
std::fs::write(&path, &rom_data).expect("writing temp ROM should succeed");
let app_context = AppContext::new();
let cart = load_cartridge_from_disk(&path, &app_context);
assert_eq!(cart.mapper().get_mirroring(), NametableLayout::Vertical);
remove_file_if_exists(&path);
}
#[test]
fn test_load_from_file_loads_save_ram_from_sav_with_same_basename() {
let rom_data = create_test_rom(1, 1, 0x02, false); let rom_path = unique_temp_path("save_ram_load_test.nes");
std::fs::write(&rom_path, &rom_data).expect("writing temp ROM should succeed");
let sav_path = rom_path.with_extension("sav");
let mut sav = vec![0u8; 0x2000]; sav[0] = 0x42;
sav[0x1FFF] = 0x99;
std::fs::write(&sav_path, &sav).expect("writing temp SAV should succeed");
let app_context = AppContext::new();
let cart = load_cartridge_from_disk(&rom_path, &app_context);
assert_eq!(cart.mapper().read_prg(0x6000), 0x42);
assert_eq!(cart.mapper().read_prg(0x7FFF), 0x99);
remove_files_if_exist(&[&rom_path, &sav_path]);
}
#[test]
fn test_save_ram_writes_to_sav_with_same_basename() {
let rom_data = create_test_rom(1, 1, 0x02, false);
let rom_path = unique_temp_path("save_ram_save_test.nes");
std::fs::write(&rom_path, &rom_data).expect("writing temp ROM should succeed");
let app_context = AppContext::new();
let mut cart = load_cartridge_from_disk(&rom_path, &app_context);
cart.mapper_mut().write_prg(0x6000, 0xAB);
cart.mapper_mut().write_prg(0x7FFF, 0xCD);
cart.save_ram().expect("save_ram should succeed");
let sav_path = rom_path.with_extension("sav");
let sav = std::fs::read(&sav_path).expect("SAV should be written");
assert_eq!(sav.len(), 0x2000);
assert_eq!(sav[0], 0xAB);
assert_eq!(sav[0x1FFF], 0xCD);
remove_files_if_exist(&[&rom_path, &sav_path]);
}
#[test]
fn test_load_simple_rom() {
let rom_data = create_test_rom(1, 1, 0, false);
let mut cartridge = load_cartridge_from_bytes(&rom_data).unwrap();
assert_eq!(cartridge.mapper().read_prg(0x8000), 0xAA);
assert_eq!(cartridge.mapper_mut().read_chr(0x0000), 0xBB);
assert!(cartridge.trainer().is_none());
}
#[test]
fn test_load_rom_with_trainer_stores_trainer_data() {
let mut rom = vec![
b'N', b'E', b'S', 0x1A, 1, 1, 0x04, 0, 0, 0, 0, 0, 0, 0, 0, 0, ];
let trainer_pattern: Vec<u8> = (0..512).map(|i| (i + 100) as u8).collect();
rom.extend(&trainer_pattern);
rom.extend(vec![0xAA; 16 * 1024]);
rom.extend(vec![0xBB; 8 * 1024]);
let mut cartridge = load_cartridge_from_bytes(&rom).unwrap();
let trainer = cartridge.trainer().expect("Trainer should be present");
assert_eq!(trainer.len(), 512);
for (i, &byte) in trainer.iter().enumerate() {
assert_eq!(byte, (i + 100) as u8);
}
assert_eq!(cartridge.mapper().read_prg(0x8000), 0xAA);
assert_eq!(cartridge.mapper_mut().read_chr(0x0000), 0xBB);
}
#[test]
fn test_load_rom_without_trainer_has_none() {
let rom_data = create_test_rom(1, 1, 0, false);
let cartridge = load_cartridge_from_bytes(&rom_data).unwrap();
assert!(cartridge.trainer().is_none());
}
#[test]
fn test_load_rom_with_trainer() {
let rom_data = create_test_rom(1, 1, 0x04, true);
let mut cartridge = load_cartridge_from_bytes(&rom_data).unwrap();
assert_eq!(cartridge.mapper().read_prg(0x8000), 0xAA);
assert_eq!(cartridge.mapper_mut().read_chr(0x0000), 0xBB);
}
#[test]
fn test_load_rom_multiple_banks() {
let rom_data = create_test_rom(2, 4, 0, false);
let mut cartridge = load_cartridge_from_bytes(&rom_data).unwrap();
assert_eq!(cartridge.mapper().read_prg(0x8000), 0xAA);
assert_eq!(cartridge.mapper().read_prg(0xFFFF), 0xAA);
assert_eq!(cartridge.mapper_mut().read_chr(0x0000), 0xBB);
assert_eq!(cartridge.mapper_mut().read_chr(0x1FFF), 0xBB);
}
#[test]
fn test_rom_timing_mode_defaults_to_ntsc() {
let rom_data = create_test_rom_with_flags9(1, 1, 0, 0, 0, false);
let cartridge = load_cartridge_from_bytes(&rom_data).unwrap();
assert_eq!(cartridge.rom_timing_mode(), TimingMode::Ntsc);
}
#[test]
fn test_rom_timing_mode_parses_pal_flag() {
let rom_data = create_test_rom_with_flags9(1, 1, 0, 0, 0x01, false);
let cartridge = load_cartridge_from_bytes(&rom_data).unwrap();
assert_eq!(cartridge.rom_timing_mode(), TimingMode::Pal);
}
#[test]
fn test_rom_timing_mode_nes2_timing_mode_zero_is_ntsc() {
let rom_data = create_test_rom_with_flags9(1, 1, 0, 0x08, 0x01, false);
let cartridge = load_cartridge_from_bytes(&rom_data).unwrap();
assert_eq!(cartridge.rom_timing_mode(), TimingMode::Ntsc);
}
#[test]
fn test_rom_timing_mode_nes2_uses_header_timing_mode() {
let mut rom_data = create_test_rom_with_flags9(1, 1, 0, 0x08, 0x00, false);
rom_data[12] = 0x01; let cartridge = load_cartridge_from_bytes(&rom_data).unwrap();
assert_eq!(cartridge.rom_timing_mode(), TimingMode::Pal);
}
#[test]
fn test_timing_mode_to_rom_timing_mode_maps_non_ntsc_pal_to_unknown() {
let tv = crate::cartridge::TimingMode::MultiRegion.normalize_rom_timing_mode();
assert!(matches!(tv, TimingMode::Unknown(_)));
}
#[test]
fn test_invalid_header() {
let mut rom_data = vec![b'X', b'Y', b'Z', 0x1A];
rom_data.extend(vec![0; 12]);
let result = load_cartridge_from_bytes(&rom_data);
assert!(matches!(result, Err(CartridgeError::InvalidHeader)));
}
#[test]
fn test_file_too_small() {
let rom_data = create_test_rom(2, 1, 0, false);
let truncated = &rom_data[0..100];
let result = load_cartridge_from_bytes(truncated);
if let Err(CartridgeError::FileTooSmall { expected, actual }) = result {
assert_eq!(expected, 40_976);
assert_eq!(actual, 100);
} else {
panic!("Expected FileTooSmall error");
}
}
#[test]
fn test_empty_data() {
let result = load_cartridge_from_bytes(&[]);
if let Err(CartridgeError::FileTooSmall { expected, actual }) = result {
assert_eq!(expected, 16);
assert_eq!(actual, 0);
} else {
panic!("Expected FileTooSmall error");
}
}
#[test]
fn test_unsupported_mapper() {
let rom_data = create_test_rom_with_mapper(1, 1, 0xFC, false, 1);
let result = load_cartridge_from_bytes(&rom_data);
assert!(matches!(
result,
Err(CartridgeError::UnsupportedMapper(0xFC))
));
}
#[test]
fn test_unsupported_nes2_mapper_does_not_truncate() {
let mut rom_data = vec![
b'N', b'E', b'S', 0x1A, 1, 1, 0x00, 0x08, 0x01, 0x00, 0x00, 0, 0, 0, 0, 0, ];
rom_data.extend(vec![0xAA; 16 * 1024]);
rom_data.extend(vec![0xBB; 8 * 1024]);
let result = load_cartridge_from_bytes(&rom_data);
assert!(matches!(
result,
Err(CartridgeError::UnsupportedMapper(0x100))
));
}
#[test]
fn test_horizontal_mirroring() {
let rom_data = create_test_rom(1, 1, 0x00, false); let cartridge = load_cartridge_from_bytes(&rom_data).unwrap();
assert!(matches!(
cartridge.mapper().get_mirroring(),
NametableLayout::Horizontal
));
}
#[test]
fn test_vertical_mirroring() {
let rom_data = create_test_rom(1, 1, 0x01, false); let cartridge = load_cartridge_from_bytes(&rom_data).unwrap();
assert!(matches!(
cartridge.mapper().get_mirroring(),
NametableLayout::Vertical
));
}
#[test]
fn test_four_screen_mirroring() {
let rom_data = create_test_rom(1, 1, 0x08, false); let cartridge = load_cartridge_from_bytes(&rom_data).unwrap();
assert!(matches!(
cartridge.mapper().get_mirroring(),
NametableLayout::FourScreen
));
}
#[test]
fn test_four_screen_overrides_vertical() {
let rom_data = create_test_rom(1, 1, 0x09, false); let cartridge = load_cartridge_from_bytes(&rom_data).unwrap();
assert!(matches!(
cartridge.mapper().get_mirroring(),
NametableLayout::FourScreen
));
}
#[test]
fn test_mirroring_bit_0_horizontal() {
let rom_data = create_test_rom(1, 1, 0b0000_0000, false);
let cartridge = load_cartridge_from_bytes(&rom_data).unwrap();
assert!(matches!(
cartridge.mapper().get_mirroring(),
NametableLayout::Horizontal
));
}
#[test]
fn test_mirroring_bit_0_vertical() {
let rom_data = create_test_rom(1, 1, 0b0000_0001, false);
let cartridge = load_cartridge_from_bytes(&rom_data).unwrap();
assert!(matches!(
cartridge.mapper().get_mirroring(),
NametableLayout::Vertical
));
}
#[test]
fn test_mirroring_bit_3_four_screen() {
let rom_data = create_test_rom(1, 1, 0b0000_1000, false);
let cartridge = load_cartridge_from_bytes(&rom_data).unwrap();
assert!(matches!(
cartridge.mapper().get_mirroring(),
NametableLayout::FourScreen
));
}
#[test]
fn test_mirroring_with_other_flags_set() {
let rom_data = create_test_rom(1, 1, 0b0000_0110, true);
let cartridge = load_cartridge_from_bytes(&rom_data).unwrap();
assert!(matches!(
cartridge.mapper().get_mirroring(),
NametableLayout::Horizontal
));
}
#[test]
fn test_mirroring_with_trainer_and_vertical() {
let rom_data = create_test_rom(1, 1, 0b0000_0101, true);
let cartridge = load_cartridge_from_bytes(&rom_data).unwrap();
assert!(matches!(
cartridge.mapper().get_mirroring(),
NametableLayout::Vertical
));
}
#[test]
fn test_mmc3_load_save_ram_with_prg_ram_disabled() {
let rom_data = create_test_rom_with_mapper(2, 2, 4, true, 1);
let rom_path = unique_temp_path("mmc3_disabled_ram_test.nes");
std::fs::write(&rom_path, &rom_data).expect("writing temp ROM should succeed");
let sav_path = rom_path.with_extension("sav");
let mut sav = vec![0u8; 0x2000]; sav[0] = 0x42;
sav[0x1FFF] = 0x99;
std::fs::write(&sav_path, &sav).expect("writing temp SAV should succeed");
let app_context = AppContext::new();
let mut cart = load_cartridge_from_disk(&rom_path, &app_context);
cart.mapper_mut().write_prg(0xA001, 0b0000_0000);
assert_eq!(cart.mapper().read_prg(0x6000), 0x00);
assert_eq!(cart.mapper().read_prg(0x7FFF), 0x00);
let snapshot = cart.mapper().wram_snapshot();
assert_eq!(snapshot[0], 0x42);
assert_eq!(snapshot[0x1FFF], 0x99);
remove_files_if_exist(&[&rom_path, &sav_path]);
}
#[test]
fn test_mmc3_save_ram_with_prg_ram_disabled() {
let rom_data = create_test_rom_with_mapper(2, 2, 4, true, 1);
let rom_path = unique_temp_path("mmc3_disabled_save_test.nes");
std::fs::write(&rom_path, &rom_data).expect("writing temp ROM should succeed");
let app_context = AppContext::new();
let mut cart = load_cartridge_from_disk(&rom_path, &app_context);
cart.mapper_mut().write_prg(0x6000, 0xAB);
cart.mapper_mut().write_prg(0x7FFF, 0xCD);
cart.mapper_mut().write_prg(0xA001, 0b0000_0000);
assert_eq!(cart.mapper().read_prg(0x6000), 0x00);
cart.save_ram().expect("save_ram should succeed");
let sav_path = rom_path.with_extension("sav");
let sav = std::fs::read(&sav_path).expect("SAV should be written");
assert_eq!(sav.len(), 0x2000);
assert_eq!(sav[0], 0xAB);
assert_eq!(sav[0x1FFF], 0xCD);
remove_files_if_exist(&[&rom_path, &sav_path]);
}
#[test]
fn test_mmc3_save_ram_with_write_protect() {
let rom_data = create_test_rom_with_mapper(2, 2, 4, true, 1);
let rom_path = unique_temp_path("mmc3_write_protect_test.nes");
std::fs::write(&rom_path, &rom_data).expect("writing temp ROM should succeed");
let app_context = AppContext::new();
let mut cart = load_cartridge_from_disk(&rom_path, &app_context);
cart.mapper_mut().write_prg(0x6000, 0xAB);
cart.mapper_mut().write_prg(0xA001, 0b1100_0000);
cart.mapper_mut().write_prg(0x6000, 0xDD);
assert_eq!(cart.mapper().read_prg(0x6000), 0xAB);
cart.save_ram().expect("save_ram should succeed");
let sav_path = rom_path.with_extension("sav");
let sav = std::fs::read(&sav_path).expect("SAV should be written");
assert_eq!(sav[0], 0xAB);
remove_files_if_exist(&[&rom_path, &sav_path]);
}
#[test]
fn test_mmc5_save_ram_with_banked_wram() {
let rom_data = create_test_rom_with_mapper(2, 2, 5, true, 2);
let rom_path = unique_temp_path("mmc5_banked_wram_test.nes");
std::fs::write(&rom_path, &rom_data).expect("writing temp ROM should succeed");
let app_context = AppContext::new();
let mut cart = load_cartridge_from_disk(&rom_path, &app_context);
cart.mapper_mut().write_prg(0x5113, 0); cart.mapper_mut().write_prg(0x6000, 0xAA);
cart.mapper_mut().write_prg(0x7FFF, 0xBB);
cart.mapper_mut().write_prg(0x5113, 1); cart.mapper_mut().write_prg(0x6000, 0xCC);
cart.mapper_mut().write_prg(0x7FFF, 0xDD);
cart.save_ram().expect("save_ram should succeed");
let sav_path = rom_path.with_extension("sav");
let sav = std::fs::read(&sav_path).expect("SAV should be written");
assert_eq!(sav.len(), 0x4000);
assert_eq!(sav[0x0000], 0xAA);
assert_eq!(sav[0x1FFF], 0xBB);
assert_eq!(sav[0x2000], 0xCC);
assert_eq!(sav[0x3FFF], 0xDD);
remove_files_if_exist(&[&rom_path, &sav_path]);
}
#[test]
fn test_mmc5_load_save_ram_with_banked_wram() {
let rom_data = create_test_rom_with_mapper(2, 2, 5, true, 2);
let rom_path = unique_temp_path("mmc5_load_banked_wram_test.nes");
std::fs::write(&rom_path, &rom_data).expect("writing temp ROM should succeed");
let sav_path = rom_path.with_extension("sav");
let mut sav = vec![0u8; 0x4000]; sav[0x0000] = 0xAA; sav[0x1FFF] = 0xBB; sav[0x2000] = 0xCC; sav[0x3FFF] = 0xDD; std::fs::write(&sav_path, &sav).expect("writing temp SAV should succeed");
let app_context = AppContext::new();
let mut cart = load_cartridge_from_disk(&rom_path, &app_context);
cart.mapper_mut().write_prg(0x5113, 0); assert_eq!(cart.mapper().read_prg(0x6000), 0xAA);
assert_eq!(cart.mapper().read_prg(0x7FFF), 0xBB);
cart.mapper_mut().write_prg(0x5113, 1); assert_eq!(cart.mapper().read_prg(0x6000), 0xCC);
assert_eq!(cart.mapper().read_prg(0x7FFF), 0xDD);
remove_files_if_exist(&[&rom_path, &sav_path]);
}
}