use crate::nes::cartridge::base_mapper::BaseMapper;
use crate::nes::cartridge::mapper::{Mapper, MapperCapabilities, MapperContext};
const MAPPER_NUMBER: u16 = 309;
const PRG_BANK_SIZE: usize = 8 * 1024;
pub struct Mapper309 {
base: BaseMapper,
prg_bank: u8,
mirroring_h: bool,
}
impl Mapper309 {
pub fn new(mut ctx: MapperContext) -> Self {
ctx.prg_ram_banks_8k = 1;
ctx.prg_ram_size_specified = true;
ctx.chr_rom = vec![];
let capabilities = MapperCapabilities {
has_chr_banking: false,
has_dynamic_mirroring: true,
prg_bank_size_kb: PRG_BANK_SIZE / 1024,
chr_bank_size_kb: 8,
max_prg_ram_kb: 8,
..Default::default()
};
let mut base = BaseMapper::new(&ctx, capabilities);
base.configure_prg_banking(PRG_BANK_SIZE);
let mut mapper = Self {
base,
prg_bank: 0,
mirroring_h: false,
};
mapper.apply_banks();
mapper
}
fn apply_banks(&mut self) {
self.base.select_prg_page(0, self.prg_bank as i16);
self.base.select_prg_page(1, 13);
self.base.select_prg_page(2, 14);
self.base.select_prg_page(3, 15);
self.base.set_mirroring_hv(self.mirroring_h);
}
}
impl Mapper for Mapper309 {
fn base(&self) -> &BaseMapper {
&self.base
}
fn base_mut(&mut self) -> &mut BaseMapper {
&mut self.base
}
fn mapper_number(&self) -> u16 {
MAPPER_NUMBER
}
fn write_prg(&mut self, addr: u16, value: u8) {
if self.base.try_write_prg_ram(addr, value) {
return;
}
match addr & 0xE000 {
0x8000 => {
self.prg_bank = value & 0x0F;
self.apply_banks();
}
0xE000 => {
self.mirroring_h = value & 0x08 != 0;
self.apply_banks();
}
_ => {}
}
}
fn reset(&mut self) {
self.prg_bank = 0;
self.mirroring_h = false;
self.apply_banks();
}
fn registers_snapshot(&self) -> Vec<u8> {
vec![self.prg_bank, self.mirroring_h as u8]
}
fn restore_registers(&mut self, data: &[u8]) {
if data.len() < 2 {
return;
}
self.prg_bank = data[0] & 0x0F;
self.mirroring_h = data[1] & 0x01 != 0;
self.apply_banks();
}
fn wram_size(&self) -> usize {
self.base.wram_size()
}
fn wram_snapshot(&self) -> Vec<u8> {
self.base.wram_snapshot()
}
fn load_wram_snapshot(&mut self, data: &[u8]) {
self.base.load_wram_snapshot(data);
}
fn initialize_ram(&mut self, mode: crate::nes::console::RamInitMode) {
self.base.initialize_ram(mode);
}
}
#[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: usize = 16;
fn make_mapper() -> Mapper309 {
Mapper309::new(MapperContext::new_for_test(
MAPPER_NUMBER,
banked_data(PRG_BANK_SIZE, PRG_BANKS),
vec![],
NametableLayout::Vertical,
))
}
#[test]
fn mapper_309_is_registered_in_factory() {
let result = create_mapper(MapperContext::new_for_test(
MAPPER_NUMBER,
banked_data(PRG_BANK_SIZE, PRG_BANKS),
vec![],
NametableLayout::Vertical,
));
assert!(
result.is_ok(),
"Mapper 309 must be registered in the factory"
);
}
#[test]
fn power_on_slot0_maps_to_bank_0() {
let mapper = make_mapper();
assert_eq!(mapper.read_prg(0x8000), 0, "$8000 → bank 0");
assert_eq!(mapper.read_prg(0x9FFF), 0, "$9FFF → bank 0");
}
#[test]
fn power_on_fixed_banks_13_14_15() {
let mapper = make_mapper();
assert_eq!(mapper.read_prg(0xA000), 13, "$A000 → bank 13");
assert_eq!(mapper.read_prg(0xC000), 14, "$C000 → bank 14");
assert_eq!(mapper.read_prg(0xE000), 15, "$E000 → bank 15");
}
#[test]
fn power_on_mirroring_is_vertical() {
let mapper = make_mapper();
assert_eq!(mapper.get_mirroring(), NametableLayout::Vertical);
}
#[test]
fn write_to_8000_switches_slot0_bank() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 0x05);
assert_eq!(mapper.read_prg(0x8000), 5, "slot0 → bank 5");
}
#[test]
fn write_to_9000_also_switches_slot0_bank() {
let mut mapper = make_mapper();
mapper.write_prg(0x9000, 0x07);
assert_eq!(mapper.read_prg(0x8000), 7, "write to $9000 also works");
}
#[test]
fn prg_bank_register_uses_lower_4_bits_only() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 0xFF); assert_eq!(mapper.read_prg(0x8000), 15, "bank = 0xFF & 0x0F = 15");
}
#[test]
fn fixed_banks_unaffected_by_prg_bank_writes() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 0x0A);
assert_eq!(mapper.read_prg(0xA000), 13, "bank 13 still fixed");
assert_eq!(mapper.read_prg(0xC000), 14, "bank 14 still fixed");
assert_eq!(mapper.read_prg(0xE000), 15, "bank 15 still fixed");
}
#[test]
fn write_to_e000_with_bit3_set_selects_horizontal() {
let mut mapper = make_mapper();
mapper.write_prg(0xE000, 0x08);
assert_eq!(mapper.get_mirroring(), NametableLayout::Horizontal);
}
#[test]
fn write_to_e000_with_bit3_clear_selects_vertical() {
let mut mapper = make_mapper();
mapper.write_prg(0xE000, 0x08); mapper.write_prg(0xE000, 0x00); assert_eq!(mapper.get_mirroring(), NametableLayout::Vertical);
}
#[test]
fn write_to_f000_also_affects_mirroring() {
let mut mapper = make_mapper();
mapper.write_prg(0xF000, 0x08);
assert_eq!(mapper.get_mirroring(), NametableLayout::Horizontal);
}
#[test]
fn chr_ram_is_readable_and_writable() {
let mut mapper = make_mapper();
mapper.write_chr(0x0000, 0x42);
assert_eq!(
mapper.read_chr(0x0000),
0x42,
"CHR-RAM read/write roundtrip"
);
}
#[test]
fn prg_ram_is_readable_and_writable_at_6000() {
let mut mapper = make_mapper();
mapper.write_prg(0x6000, 0xAB);
assert_eq!(mapper.read_prg(0x6000), 0xAB, "PRG-RAM at $6000");
}
#[test]
fn reset_restores_bank_0_and_fixed_banks() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 0x09);
mapper.write_prg(0xE000, 0x08);
mapper.reset();
assert_eq!(mapper.read_prg(0x8000), 0, "bank 0 after reset");
assert_eq!(
mapper.read_prg(0xA000),
13,
"bank 13 still fixed after reset"
);
assert_eq!(
mapper.get_mirroring(),
NametableLayout::Vertical,
"V mirroring after reset"
);
}
#[test]
fn snapshot_restore_preserves_state() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 0x07);
mapper.write_prg(0xE000, 0x08);
let snap = mapper.registers_snapshot();
let mut restored = make_mapper();
restored.restore_registers(&snap);
assert_eq!(restored.read_prg(0x8000), 7, "restored bank 7");
assert_eq!(
restored.get_mirroring(),
NametableLayout::Horizontal,
"restored mirroring"
);
}
#[test]
fn restore_with_short_data_is_noop() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 0x05);
mapper.restore_registers(&[0x00]); assert_eq!(
mapper.read_prg(0x8000),
5,
"state unchanged on short restore"
);
}
#[test]
fn capabilities_match_specification() {
let mapper = make_mapper();
let caps = mapper.capabilities();
assert!(
!caps.has_chr_banking,
"no CHR banking (CHR-RAM is unbanked)"
);
assert!(caps.has_dynamic_mirroring, "dynamic mirroring");
assert!(!caps.has_irq, "no IRQ");
assert_eq!(caps.prg_bank_size_kb, 8);
assert_eq!(caps.chr_bank_size_kb, 8);
assert_eq!(caps.max_prg_ram_kb, 8);
}
}