use crate::nes::cartridge::base_mapper::BaseMapper;
use crate::nes::cartridge::mapper::{Mapper, MapperCapabilities};
pub struct Sunsoft2Mapper {
base: BaseMapper,
prg_bank: u8,
chr_enabled: bool,
}
impl Sunsoft2Mapper {
pub fn new(ctx: crate::nes::cartridge::mapper::MapperContext) -> Self {
let capabilities = MapperCapabilities {
max_prg_ram_kb: 0,
prg_bank_size_kb: 16,
..Default::default()
};
let mut base = BaseMapper::new(&ctx, capabilities);
base.configure_prg_banking(16 * 1024);
base.select_prg_page(1, -1);
base.set_bus_conflicts(true);
let mut mapper = Self {
base,
prg_bank: 0,
chr_enabled: false,
};
mapper.update_prg();
mapper
}
fn update_prg(&mut self) {
self.base.select_prg_page(0, self.prg_bank as i16);
}
}
impl Mapper for Sunsoft2Mapper {
fn base(&self) -> &BaseMapper {
&self.base
}
fn base_mut(&mut self) -> &mut BaseMapper {
&mut self.base
}
fn write_prg(&mut self, addr: u16, value: u8) {
if !(0x8000..=0xFFFF).contains(&addr) {
return;
}
let effective = self.base.apply_bus_conflict(addr, value);
self.prg_bank = (effective >> 4) & 0x07;
self.chr_enabled = (effective & 0x01) != 0;
self.update_prg();
}
fn read_chr(&mut self, addr: u16) -> u8 {
if !self.chr_enabled {
return 0; }
self.base.read_chr(addr)
}
fn write_chr(&mut self, addr: u16, value: u8) {
if !self.chr_enabled {
return; }
self.base.write_chr(addr, value);
}
fn registers_snapshot(&self) -> Vec<u8> {
vec![self.prg_bank, u8::from(self.chr_enabled)]
}
fn restore_registers(&mut self, data: &[u8]) {
if let Some(&bank) = data.first() {
self.prg_bank = bank;
self.update_prg();
}
if let Some(&enabled) = data.get(1) {
self.chr_enabled = enabled != 0;
}
}
fn reset(&mut self) {
self.prg_bank = 0;
self.chr_enabled = false;
self.update_prg();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nes::cartridge::NametableLayout;
use crate::nes::cartridge::mapper::{MapperContext, create_mapper};
const PRG_BANKS: usize = 5;
fn make_prg_rom() -> Vec<u8> {
let bank_size = 16 * 1024;
let mut rom = vec![0xFF_u8; bank_size * PRG_BANKS];
for bank in 0..PRG_BANKS {
rom[bank * bank_size + 0x100] = bank as u8;
}
rom
}
fn make_mapper() -> Sunsoft2Mapper {
Sunsoft2Mapper::new(MapperContext::new_for_test(
93,
make_prg_rom(),
vec![],
NametableLayout::Horizontal,
))
}
fn read_prg_bank(mapper: &Sunsoft2Mapper, window_base: u16) -> u8 {
mapper.read_prg(window_base + 0x100)
}
#[test]
fn mapper_93_is_registered() {
let result = create_mapper(MapperContext::new_for_test(
93,
make_prg_rom(),
vec![],
NametableLayout::Horizontal,
));
assert!(
result.is_ok(),
"Mapper 93 must be registered in the factory"
);
}
#[test]
fn power_on_lower_window_is_bank0() {
let mapper = make_mapper();
assert_eq!(
read_prg_bank(&mapper, 0x8000),
0,
"$8000–$BFFF must map to PRG bank 0 at power-on"
);
}
#[test]
fn power_on_upper_window_is_last_bank() {
let mapper = make_mapper();
let last = (PRG_BANKS - 1) as u8;
assert_eq!(
read_prg_bank(&mapper, 0xC000),
last,
"$C000–$FFFF must be fixed to the last PRG bank at power-on"
);
}
#[test]
fn power_on_chr_ram_is_disabled() {
let mut mapper = make_mapper();
mapper.write_chr(0x0000, 0xAB); assert_eq!(
mapper.read_chr(0x0000),
0,
"CHR-RAM reads must return 0 (open bus) at power-on (E=0)"
);
}
#[test]
fn prg_bank_1_selected_by_bits_6_4() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 0x11);
assert_eq!(
read_prg_bank(&mapper, 0x8000),
1,
"Writing 0x11 must select PRG bank 1 at $8000–$BFFF"
);
}
#[test]
fn prg_bank_3_selected_by_bits_6_4() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 0x31);
assert_eq!(
read_prg_bank(&mapper, 0x8000),
3,
"Writing 0x31 must select PRG bank 3"
);
}
#[test]
fn prg_upper_window_stays_fixed_after_bank_switch() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 0x31); let last = (PRG_BANKS - 1) as u8;
assert_eq!(
read_prg_bank(&mapper, 0xC000),
last,
"$C000–$FFFF must remain fixed to the last bank after a bank switch"
);
}
#[test]
fn prg_bank_covers_full_16kb_window() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 0x31); assert_eq!(mapper.read_prg(0x8000 + 0x100), 3);
assert_eq!(mapper.read_prg(0xBFFF), 0xFF); }
#[test]
fn prg_register_only_responds_to_8000_ffff() {
let mut mapper = make_mapper();
mapper.write_prg(0x7FFF, 0x31); assert_eq!(
read_prg_bank(&mapper, 0x8000),
0,
"Write below $8000 must not affect PRG bank"
);
}
#[test]
fn chr_ram_enabled_when_bit0_is_1() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 0x01);
mapper.write_chr(0x0000, 0xAB);
assert_eq!(
mapper.read_chr(0x0000),
0xAB,
"CHR-RAM must be readable and writable when E=1"
);
}
#[test]
fn chr_ram_disabled_when_bit0_is_0() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 0x01); mapper.write_chr(0x0000, 0xAB);
mapper.write_prg(0x8000, 0x00); assert_eq!(
mapper.read_chr(0x0000),
0,
"CHR-RAM reads must return 0 after disabling E bit"
);
}
#[test]
fn chr_writes_ignored_when_disabled() {
let mut mapper = make_mapper();
mapper.write_chr(0x0100, 0x55);
mapper.write_prg(0x8000, 0x01);
assert_eq!(
mapper.read_chr(0x0100),
0,
"CHR-RAM write while disabled must be discarded"
);
}
#[test]
fn chr_ram_covers_full_8kb_window() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 0x01); mapper.write_chr(0x0000, 0x11);
mapper.write_chr(0x1FFF, 0x22);
assert_eq!(mapper.read_chr(0x0000), 0x11);
assert_eq!(mapper.read_chr(0x1FFF), 0x22);
}
#[test]
fn bus_conflicts_apply_to_register_writes() {
let bank_size = 16 * 1024;
let mut prg_rom = vec![0xFF_u8; bank_size * PRG_BANKS];
prg_rom[0] = 0x10;
for bank in 0..PRG_BANKS {
prg_rom[bank * bank_size + 0x100] = bank as u8;
}
let mut mapper = Sunsoft2Mapper::new(MapperContext::new_for_test(
93,
prg_rom,
vec![],
NametableLayout::Horizontal,
));
mapper.write_prg(0x8000, 0x50);
assert_eq!(
read_prg_bank(&mapper, 0x8000),
1,
"Bus conflict must AND written value with PRG-ROM byte"
);
}
#[test]
fn mirroring_fixed_from_header() {
let mapper = make_mapper();
assert_eq!(
mapper.get_mirroring(),
NametableLayout::Horizontal,
"Mirroring must be fixed from header"
);
}
#[test]
fn mirroring_not_changed_by_register_writes() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 0xFF);
assert_eq!(
mapper.get_mirroring(),
NametableLayout::Horizontal,
"Mirroring must not change after register write"
);
}
#[test]
fn irq_never_pending() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 0xFF);
assert!(!mapper.irq_pending(), "Mapper 93 must never assert IRQ");
}
#[test]
fn registers_snapshot_round_trips() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 0x31);
let snap = mapper.registers_snapshot();
let mut restored = make_mapper();
restored.restore_registers(&snap);
assert_eq!(
read_prg_bank(&restored, 0x8000),
3,
"Restored mapper must map PRG bank 3"
);
restored.write_chr(0x0000, 0xDE);
assert_eq!(
restored.read_chr(0x0000),
0xDE,
"Restored mapper must have CHR-RAM enabled"
);
}
#[test]
fn registers_snapshot_preserves_chr_disabled_state() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 0x20);
let snap = mapper.registers_snapshot();
let mut restored = make_mapper();
restored.restore_registers(&snap);
assert_eq!(
restored.read_chr(0x0000),
0,
"Restored mapper must have CHR-RAM disabled when E=0 was snapshotted"
);
}
#[test]
fn reset_returns_to_power_on_state() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 0x31); mapper.write_chr(0x0000, 0x42);
mapper.reset();
assert_eq!(
read_prg_bank(&mapper, 0x8000),
0,
"PRG bank must be 0 after reset"
);
assert_eq!(
mapper.read_chr(0x0000),
0,
"CHR-RAM must return 0 (disabled) after reset"
);
}
}