use crate::nes::cartridge::base_mapper::BaseMapper;
use crate::nes::cartridge::mapper::{Mapper, MapperCapabilities};
use crate::nes::cartridge::mmc3::MMC3Mapper;
const MAPPER_NUMBER: u16 = 313;
const PRG_INNER_BANK_MASK: usize = 0x0F;
const PRG_OUTER_BANK_SHIFT: usize = 4;
const CHR_INNER_BANK_MASK: usize = 0x7F;
const CHR_OUTER_BANK_SHIFT: usize = 7;
const COUNTER_MASK: u8 = 0x03;
const PRG_BANK_OFFSET_MASK: usize = 0x1FFF;
const CHR_BANK_OFFSET_MASK: usize = 0x03FF;
pub struct Mapper313 {
mmc3: MMC3Mapper,
reset_counter: u8,
hard_reset_pending: bool,
}
impl Mapper313 {
pub fn new(ctx: crate::nes::cartridge::mapper::MapperContext) -> Self {
Self {
mmc3: MMC3Mapper::new_with_irq_mode(ctx.prg_rom, ctx.chr_rom, ctx.mirroring, false),
reset_counter: 0,
hard_reset_pending: false,
}
}
fn outer_prg_bank(&self, inner_bank: usize) -> usize {
((self.reset_counter as usize) << PRG_OUTER_BANK_SHIFT) | (inner_bank & PRG_INNER_BANK_MASK)
}
fn outer_chr_bank(&self, inner_bank: usize) -> usize {
((self.reset_counter as usize) << CHR_OUTER_BANK_SHIFT) | (inner_bank & CHR_INNER_BANK_MASK)
}
}
impl Mapper for Mapper313 {
fn base(&self) -> &BaseMapper {
&self.mmc3.base
}
fn base_mut(&mut self) -> &mut BaseMapper {
&mut self.mmc3.base
}
fn mmc3_delegate(&self) -> Option<&MMC3Mapper> {
Some(&self.mmc3)
}
fn mmc3_delegate_mut(&mut self) -> Option<&mut MMC3Mapper> {
Some(&mut self.mmc3)
}
fn mapper_number(&self) -> u16 {
MAPPER_NUMBER
}
fn read_prg(&self, addr: u16) -> u8 {
if !(0x8000..=0xFFFF).contains(&addr) {
return self.mmc3.read_prg(addr);
}
let page = self.mmc3.raw_prg_8k_page_number(addr) as usize;
let inner_bank = match page {
0xFE => 14,
0xFF => 15,
n => n & PRG_INNER_BANK_MASK,
};
let bank = self.outer_prg_bank(inner_bank);
let offset = (addr as usize) & PRG_BANK_OFFSET_MASK;
self.mmc3.read_prg_at_bank(bank, offset)
}
fn read_prg_open_bus(&self, addr: u16, open_bus: u8) -> u8 {
if !(0x8000..=0xFFFF).contains(&addr) {
return self.mmc3.read_prg_open_bus(addr, open_bus);
}
self.read_prg(addr)
}
fn write_prg(&mut self, addr: u16, value: u8) {
self.mmc3.write_prg(addr, value);
}
fn read_chr(&mut self, addr: u16) -> u8 {
let inner_bank = self.mmc3.raw_chr_1k_bank(addr);
let bank = self.outer_chr_bank(inner_bank);
let offset = (addr as usize) & CHR_BANK_OFFSET_MASK;
self.mmc3.read_chr_1k_at(bank, offset)
}
fn write_chr(&mut self, addr: u16, value: u8) {
let inner_bank = self.mmc3.raw_chr_1k_bank(addr);
let mut bank = self.outer_chr_bank(inner_bank);
let chr_bank_count = self.mmc3.chr_bank_count_1k();
if chr_bank_count != 0 {
bank %= chr_bank_count;
}
let offset = (addr as usize) & CHR_BANK_OFFSET_MASK;
self.mmc3.write_chr_1k_at(bank, offset, value);
}
fn wram_size(&self) -> usize {
self.mmc3.wram_size()
}
fn wram_snapshot(&self) -> Vec<u8> {
self.mmc3.wram_snapshot()
}
fn load_wram_snapshot(&mut self, data: &[u8]) {
self.mmc3.load_wram_snapshot(data);
}
fn registers_snapshot(&self) -> Vec<u8> {
let mut snap = self.mmc3.registers_snapshot();
snap.push(self.reset_counter);
snap
}
fn restore_registers(&mut self, data: &[u8]) {
let Some((&counter, mmc3_data)) = data.split_last() else {
return;
};
self.mmc3.restore_registers(mmc3_data);
self.reset_counter = counter & COUNTER_MASK;
self.hard_reset_pending = false;
}
fn reset(&mut self) {
self.mmc3.reset();
if self.hard_reset_pending {
self.reset_counter = 0;
self.hard_reset_pending = false;
} else {
self.reset_counter = self.reset_counter.wrapping_add(1) & COUNTER_MASK;
}
}
fn initialize_ram(&mut self, mode: crate::nes::console::RamInitMode) {
self.mmc3.initialize_ram(mode);
self.hard_reset_pending = true;
}
fn capabilities(&self) -> MapperCapabilities {
MapperCapabilities {
has_irq: true,
has_chr_banking: true,
has_dynamic_mirroring: true,
has_expansion_audio: false,
max_prg_ram_kb: 8,
prg_bank_size_kb: 8,
chr_bank_size_kb: 1,
..Default::default()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nes::cartridge::NametableLayout;
use crate::nes::cartridge::mapper::{MapperContext, create_mapper};
use crate::nes::cartridge::test_helpers::banked_data;
const PRG_BANKS_8K: usize = 64;
const CHR_BANKS_1K: usize = 256;
fn make_mapper() -> Mapper313 {
Mapper313::new(MapperContext::new_for_test(
MAPPER_NUMBER,
banked_data(8 * 1024, PRG_BANKS_8K),
banked_data(1024, CHR_BANKS_1K),
NametableLayout::Vertical,
))
}
#[test]
fn mapper_313_is_registered_in_factory() {
let result = create_mapper(MapperContext::new_for_test(
MAPPER_NUMBER,
banked_data(8 * 1024, PRG_BANKS_8K),
banked_data(1024, CHR_BANKS_1K),
NametableLayout::Vertical,
));
assert!(result.is_ok(), "Mapper 313 must be registered in factory");
}
#[test]
fn power_on_counter_is_zero() {
let mapper = make_mapper();
assert_eq!(
mapper.reset_counter, 0,
"reset_counter must be 0 at power-on"
);
}
#[test]
fn power_on_prg_uses_mmc3_inner_bank_unmodified() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 0x06);
mapper.write_prg(0x8001, 5);
assert_eq!(
mapper.read_prg(0x8000),
5,
"With counter=0, MMC3 inner bank 5 maps to outer bank 5"
);
}
#[test]
fn soft_reset_increments_counter_to_1() {
let mut mapper = make_mapper();
mapper.reset(); assert_eq!(
mapper.reset_counter, 1,
"Soft reset should increment counter from 0 to 1"
);
}
#[test]
fn soft_reset_wraps_counter_after_3() {
let mut mapper = make_mapper();
for _ in 0..4 {
mapper.reset();
}
assert_eq!(
mapper.reset_counter, 0,
"Four soft resets must wrap counter back to 0"
);
}
#[test]
fn prg_outer_bank_shifts_with_counter() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 0x06);
mapper.write_prg(0x8001, 5);
let bank_before = mapper.read_prg(0x8000);
mapper.reset(); let bank_after = mapper.read_prg(0x8000);
assert_ne!(
bank_before, bank_after,
"Soft reset must shift PRG outer bank via counter"
);
assert_eq!(bank_before, 5, "Before soft reset, outer bank should be 5");
assert_eq!(
bank_after, 21,
"After soft reset (counter=1), outer bank should be 21"
);
}
#[test]
fn chr_outer_bank_shifts_with_counter() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 0x02); mapper.write_prg(0x8001, 5); let chr_before = mapper.read_chr(0x1000);
mapper.reset(); let chr_after = mapper.read_chr(0x1000);
assert_ne!(
chr_before, chr_after,
"Soft reset must shift CHR outer bank via counter"
);
assert_eq!(chr_before, 5, "Before soft reset, CHR bank should be 5");
assert_eq!(
chr_after, 133,
"After soft reset (counter=1), CHR bank should be 133"
);
}
#[test]
fn hard_reset_resets_counter_to_zero() {
let mut mapper = make_mapper();
mapper.reset(); mapper.reset(); assert_eq!(
mapper.reset_counter, 2,
"Counter should be 2 before hard reset"
);
mapper.initialize_ram(crate::nes::console::RamInitMode::Zero);
mapper.reset(); assert_eq!(
mapper.reset_counter, 0,
"Hard reset must reset counter to 0"
);
}
#[test]
fn subsequent_soft_reset_after_hard_reset_increments_to_1() {
let mut mapper = make_mapper();
mapper.reset(); mapper.initialize_ram(crate::nes::console::RamInitMode::Zero);
mapper.reset(); mapper.reset(); assert_eq!(
mapper.reset_counter, 1,
"Soft reset after hard reset should go to counter=1"
);
}
#[test]
fn prg_inner_bank_uses_only_lower_4_bits() {
let mut mapper = make_mapper();
mapper.reset(); mapper.write_prg(0x8000, 0x06);
mapper.write_prg(0x8001, 0x1F); assert_eq!(
mapper.read_prg(0x8000),
31,
"PRG inner bank uses only lower 4 bits; outer = (counter<<4)|inner4"
);
}
#[test]
fn chr_inner_bank_uses_only_lower_7_bits() {
let mut mapper = make_mapper();
mapper.reset(); mapper.write_prg(0x8000, 0x02);
mapper.write_prg(0x8001, 0x7F);
assert_eq!(
mapper.read_chr(0x1000),
255,
"CHR inner bank uses only lower 7 bits; outer = (counter<<7)|inner7"
);
}
#[test]
fn mirroring_control_is_delegated_to_mmc3() {
let mut mapper = make_mapper();
assert_eq!(mapper.get_mirroring(), NametableLayout::Vertical);
mapper.write_prg(0xA000, 1);
assert_eq!(mapper.get_mirroring(), NametableLayout::Horizontal);
}
#[test]
fn irq_signalling_is_delegated_to_mmc3() {
let mut mapper = make_mapper();
mapper.write_prg(0xC000, 1);
mapper.write_prg(0xC001, 0);
mapper.write_prg(0xE001, 0);
for _ in 0..2 {
mapper.ppu_address_changed(0x0FFF);
for _ in 0..3 {
mapper.cpu_cycle();
}
mapper.ppu_address_changed(0x1000);
}
assert!(
mapper.irq_pending(),
"Mapper 313 must pass MMC3 IRQ through"
);
}
#[test]
fn snapshot_restore_preserves_counter() {
let mut mapper = make_mapper();
mapper.reset(); mapper.write_prg(0x8000, 0x06);
mapper.write_prg(0x8001, 5);
let snap = mapper.registers_snapshot();
let mut restored = make_mapper();
restored.restore_registers(&snap);
assert_eq!(restored.reset_counter, 1, "Restored counter should be 1");
assert_eq!(
restored.read_prg(0x8000),
21,
"Restored PRG bank should be 21 (counter=1, R6=5)"
);
}
#[test]
fn restore_with_short_data_is_noop() {
let mut mapper = make_mapper();
mapper.reset(); mapper.restore_registers(&[]);
assert_eq!(
mapper.reset_counter, 1,
"Empty restore data must not change state"
);
}
#[test]
fn capabilities_report_irq_and_chr_banking() {
let mapper = make_mapper();
let caps = mapper.capabilities();
assert!(caps.has_irq, "Mapper 313 must report IRQ capability");
assert!(caps.has_chr_banking, "Mapper 313 must report CHR banking");
assert!(
caps.has_dynamic_mirroring,
"Mapper 313 must report dynamic mirroring"
);
assert!(
!caps.has_expansion_audio,
"Mapper 313 has no expansion audio"
);
assert_eq!(caps.prg_bank_size_kb, 8);
assert_eq!(caps.chr_bank_size_kb, 1);
}
}