use crate::nes::cartridge::base_mapper::BaseMapper;
use crate::nes::cartridge::mapper::{Mapper, MapperCapabilities};
const MAPPER_NUMBER: u16 = 337;
const PRG_BANK_SIZE_BYTES: usize = 16 * 1024;
const CHR_BANK_SIZE_BYTES: usize = 4 * 1024;
const CTRL_MASK: u8 = 0x0F;
const CTRL_NROM_BIT: u8 = 0x08;
const CTRL_MIRROR_BIT: u8 = 0x04;
const CTRL_OUTER_MASK: u8 = 0x03;
const REGISTERS_SNAPSHOT_LEN: usize = 3;
pub struct Mapper337 {
base: BaseMapper,
prg_chr_0: u8,
prg_chr_1: u8,
ctrl: u8,
}
impl Mapper337 {
pub fn new(ctx: crate::nes::cartridge::mapper::MapperContext) -> Self {
let capabilities = MapperCapabilities {
has_dynamic_mirroring: true,
has_chr_banking: true,
prg_bank_size_kb: 16,
chr_bank_size_kb: 4,
max_prg_ram_kb: 0,
..Default::default()
};
let mut base = BaseMapper::new(&ctx, capabilities);
base.configure_prg_banking(PRG_BANK_SIZE_BYTES);
base.configure_chr_banking(CHR_BANK_SIZE_BYTES);
let mut mapper = Self {
base,
prg_chr_0: 0,
prg_chr_1: 0,
ctrl: 0,
};
mapper.apply_state(0, 0, 0);
mapper
}
fn prg_bank_8000(prg_chr_0: u8, ctrl: u8) -> i16 {
let outer = ((ctrl & CTRL_OUTER_MASK) as i16) << 3;
if ctrl & CTRL_NROM_BIT != 0 {
outer | ((prg_chr_0 & 0x06) as i16)
} else {
outer | ((prg_chr_0 & 0x07) as i16)
}
}
fn prg_bank_c000(prg_chr_0: u8, ctrl: u8) -> i16 {
let outer = ((ctrl & CTRL_OUTER_MASK) as i16) << 3;
if ctrl & CTRL_NROM_BIT != 0 {
outer | ((prg_chr_0 & 0x06) as i16) | 1
} else {
outer | 7
}
}
fn chr_bank_0000(prg_chr_0: u8, ctrl: u8) -> i16 {
let outer_chr = ((ctrl & CTRL_OUTER_MASK) as i16) << 5;
outer_chr | ((prg_chr_0 >> 3) as i16)
}
fn chr_bank_1000(prg_chr_1: u8, ctrl: u8) -> i16 {
let outer_chr = ((ctrl & CTRL_OUTER_MASK) as i16) << 5;
outer_chr | ((prg_chr_1 >> 3) as i16)
}
fn apply_state(&mut self, prg_chr_0: u8, prg_chr_1: u8, ctrl: u8) {
self.prg_chr_0 = prg_chr_0;
self.prg_chr_1 = prg_chr_1;
self.ctrl = ctrl;
self.base
.select_prg_page(0, Self::prg_bank_8000(prg_chr_0, ctrl));
self.base
.select_prg_page(1, Self::prg_bank_c000(prg_chr_0, ctrl));
self.base
.select_chr_page(0, Self::chr_bank_0000(prg_chr_0, ctrl));
self.base
.select_chr_page(1, Self::chr_bank_1000(prg_chr_1, ctrl));
self.base.set_mirroring_hv((ctrl & CTRL_MIRROR_BIT) != 0);
}
}
impl Mapper for Mapper337 {
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) {
match addr {
0xA000..=0xBFFF => self.apply_state(value, self.prg_chr_1, self.ctrl),
0xC000..=0xDFFF => self.apply_state(self.prg_chr_0, value, self.ctrl),
0xE000..=0xFFFF => self.apply_state(self.prg_chr_0, self.prg_chr_1, value & CTRL_MASK),
_ => {}
}
}
fn registers_snapshot(&self) -> Vec<u8> {
vec![self.prg_chr_0, self.prg_chr_1, self.ctrl]
}
fn restore_registers(&mut self, data: &[u8]) {
if data.len() < REGISTERS_SNAPSHOT_LEN {
return;
}
self.apply_state(data[0], data[1], data[2] & CTRL_MASK);
}
fn reset(&mut self) {
self.apply_state(0, 0, 0);
}
}
#[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_16K: usize = 48;
const CHR_BANKS_4K: usize = 96;
fn make_mapper() -> Mapper337 {
Mapper337::new(MapperContext::new_for_test(
MAPPER_NUMBER,
banked_data(PRG_BANK_SIZE_BYTES, PRG_BANKS_16K),
banked_data(CHR_BANK_SIZE_BYTES, CHR_BANKS_4K),
NametableLayout::Vertical,
))
}
#[test]
fn mapper_337_is_registered_in_factory() {
let result = create_mapper(MapperContext::new_for_test(
MAPPER_NUMBER,
banked_data(PRG_BANK_SIZE_BYTES, PRG_BANKS_16K),
banked_data(CHR_BANK_SIZE_BYTES, CHR_BANKS_4K),
NametableLayout::Vertical,
));
assert!(result.is_ok(), "Mapper 337 must be registered in factory");
}
#[test]
fn power_on_prg_8000_is_bank_0() {
let mapper = make_mapper();
assert_eq!(
mapper.read_prg(0x8000),
0,
"$8000 should map to PRG bank 0 at power-on"
);
}
#[test]
fn power_on_prg_c000_is_bank_7() {
let mapper = make_mapper();
assert_eq!(
mapper.read_prg(0xC000),
7,
"$C000 should map to PRG bank 7 at power-on"
);
}
#[test]
fn power_on_chr_0000_is_bank_0() {
let mut mapper = make_mapper();
assert_eq!(
mapper.read_chr(0x0000),
0,
"CHR $0000 should be bank 0 at power-on"
);
}
#[test]
fn power_on_chr_1000_is_bank_0() {
let mut mapper = make_mapper();
assert_eq!(
mapper.read_chr(0x1000),
0,
"CHR $1000 should be bank 0 at power-on"
);
}
#[test]
fn power_on_mirroring_is_vertical() {
let mapper = make_mapper();
assert_eq!(
mapper.get_mirroring(),
NametableLayout::Vertical,
"mirroring should be vertical at power-on"
);
}
#[test]
fn normal_mode_prg_8000_follows_prgchr0_bits_2_0() {
let mut mapper = make_mapper();
mapper.write_prg(0xA000, 5);
assert_eq!(mapper.read_prg(0x8000), 5, "$8000 should be PRG bank 5");
}
#[test]
fn normal_mode_prg_c000_is_fixed_to_last_in_outer_group() {
let mut mapper = make_mapper();
mapper.write_prg(0xA000, 3); assert_eq!(
mapper.read_prg(0xC000),
7,
"$C000 should always be bank 7 in normal mode"
);
}
#[test]
fn normal_mode_prg_8000_uses_only_low_3_bits_of_prgchr0() {
let mut mapper = make_mapper();
mapper.write_prg(0xA000, 0xF8);
assert_eq!(
mapper.read_prg(0x8000),
0,
"$8000 should use only bits[2:0]"
);
}
#[test]
fn outer_bank_shifts_prg_window_by_8_banks() {
let mut mapper = make_mapper();
mapper.write_prg(0xA000, 2); mapper.write_prg(0xE000, 1); assert_eq!(
mapper.read_prg(0x8000),
10,
"$8000 should be bank 10 with outer=1, prgchr0=2"
);
assert_eq!(
mapper.read_prg(0xC000),
15,
"$C000 should be bank 15 with outer=1"
);
}
#[test]
fn ctrl_outer_bits_only_use_bits_1_0() {
let mut mapper = make_mapper();
mapper.write_prg(0xE000, 0xFF); mapper.write_prg(0xA000, 0); assert_eq!(
mapper.read_prg(0x8000),
24,
"$8000 should use only ctrl[1:0] for outer bank"
);
}
#[test]
fn nrom_mode_both_windows_are_consecutive_even_odd_pair() {
let mut mapper = make_mapper();
mapper.write_prg(0xA000, 4);
mapper.write_prg(0xE000, 0x08);
assert_eq!(
mapper.read_prg(0x8000),
4,
"$8000 should be even bank 4 in NROM mode"
);
assert_eq!(
mapper.read_prg(0xC000),
5,
"$C000 should be odd bank 5 in NROM mode"
);
}
#[test]
fn nrom_mode_odd_bit_of_prgchr0_is_ignored_for_bank_base() {
let mut mapper = make_mapper();
mapper.write_prg(0xA000, 7);
mapper.write_prg(0xE000, 0x08);
assert_eq!(
mapper.read_prg(0x8000),
6,
"$8000 should be bank 6 (even); bit0 of prgchr[0] is ignored"
);
assert_eq!(mapper.read_prg(0xC000), 7, "$C000 should be bank 7 (odd)");
}
#[test]
fn nrom_mode_with_outer_bank_2() {
let mut mapper = make_mapper();
mapper.write_prg(0xA000, 2);
mapper.write_prg(0xE000, 0x0A);
assert_eq!(
mapper.read_prg(0x8000),
18,
"$8000 should be bank 18 in NROM mode with outer=2"
);
assert_eq!(
mapper.read_prg(0xC000),
19,
"$C000 should be bank 19 in NROM mode with outer=2"
);
}
#[test]
fn chr_0000_bank_comes_from_prgchr0_bits_7_3() {
let mut mapper = make_mapper();
mapper.write_prg(0xA000, 0x18); assert_eq!(mapper.read_chr(0x0000), 3, "CHR $0000 bank should be 3");
}
#[test]
fn chr_1000_bank_comes_from_prgchr1_bits_7_3() {
let mut mapper = make_mapper();
mapper.write_prg(0xC000, 0x28); assert_eq!(mapper.read_chr(0x1000), 5, "CHR $1000 bank should be 5");
}
#[test]
fn chr_outer_bank_extends_both_chr_windows() {
let mut mapper = make_mapper();
mapper.write_prg(0xA000, 0x10); mapper.write_prg(0xC000, 0x20); mapper.write_prg(0xE000, 0x01); assert_eq!(
mapper.read_chr(0x0000),
34,
"CHR $0000 should be bank 34 with outer=1"
);
assert_eq!(
mapper.read_chr(0x1000),
36,
"CHR $1000 should be bank 36 with outer=1"
);
}
#[test]
fn chr_windows_are_independent() {
let mut mapper = make_mapper();
mapper.write_prg(0xA000, 0x08); mapper.write_prg(0xC000, 0x40); assert_eq!(mapper.read_chr(0x0000), 1);
assert_eq!(mapper.read_chr(0x1000), 8);
}
#[test]
fn ctrl_bit2_set_selects_horizontal_mirroring() {
let mut mapper = make_mapper();
mapper.write_prg(0xE000, 0x04); assert_eq!(
mapper.get_mirroring(),
NametableLayout::Horizontal,
"ctrl bit2=1 should select horizontal mirroring"
);
}
#[test]
fn ctrl_bit2_clear_selects_vertical_mirroring() {
let mut mapper = make_mapper();
mapper.write_prg(0xE000, 0x04); mapper.write_prg(0xE000, 0x00); assert_eq!(
mapper.get_mirroring(),
NametableLayout::Vertical,
"ctrl bit2=0 should select vertical mirroring"
);
}
#[test]
fn writes_to_8000_9fff_are_ignored() {
let mut mapper = make_mapper();
mapper.write_prg(0xA000, 3); mapper.write_prg(0x8000, 0xFF); mapper.write_prg(0x9FFF, 0xFF);
assert_eq!(mapper.read_prg(0x8000), 3, "prgchr[0] should be unchanged");
}
#[test]
fn writes_below_8000_are_ignored() {
let mut mapper = make_mapper();
mapper.write_prg(0x4020, 0xFF);
mapper.write_prg(0x6000, 0xFF);
assert_eq!(
mapper.read_prg(0x8000),
0,
"banks should remain at power-on"
);
}
#[test]
fn a000_bfff_writes_update_prgchr0_not_prgchr1() {
let mut mapper = make_mapper();
mapper.write_prg(0xC000, 0x20); mapper.write_prg(0xA000, 0x08); assert_eq!(mapper.read_chr(0x0000), 1);
assert_eq!(
mapper.read_chr(0x1000),
4,
"prgchr[1] unchanged by A000 write"
);
}
#[test]
fn c000_dfff_writes_update_prgchr1_not_prgchr0() {
let mut mapper = make_mapper();
mapper.write_prg(0xA000, 0x08); mapper.write_prg(0xC000, 0x20); assert_eq!(
mapper.read_chr(0x0000),
1,
"prgchr[0] unchanged by C000 write"
);
assert_eq!(mapper.read_chr(0x1000), 4);
}
#[test]
fn ctrl_high_nibble_bits_are_masked() {
let mut mapper = make_mapper();
mapper.write_prg(0xE000, 0xF4);
assert_eq!(
mapper.get_mirroring(),
NametableLayout::Horizontal,
"only low 4 bits of ctrl are stored"
);
assert_eq!(
mapper.read_prg(0xC000),
7,
"$C000 fixed at 7 in normal mode"
);
}
#[test]
fn snapshot_restore_preserves_all_registers() {
let mut mapper = make_mapper();
mapper.write_prg(0xA000, 0x15); mapper.write_prg(0xC000, 0x38); mapper.write_prg(0xE000, 0x09); let snap = mapper.registers_snapshot();
let mut restored = make_mapper();
restored.restore_registers(&snap);
assert_eq!(
restored.read_prg(0x8000),
12,
"restored $8000 bank should be 12"
);
assert_eq!(
restored.read_prg(0xC000),
13,
"restored $C000 bank should be 13"
);
assert_eq!(
restored.read_chr(0x0000),
34,
"restored CHR $0000 should be bank 34"
);
assert_eq!(
restored.read_chr(0x1000),
39,
"restored CHR $1000 should be bank 39"
);
}
#[test]
fn restore_registers_masks_ctrl_high_nibble() {
let mut mapper = make_mapper();
mapper.restore_registers(&[0x00, 0x00, 0xF4]);
assert_eq!(
mapper.get_mirroring(),
NametableLayout::Horizontal,
"ctrl high nibble must be stripped on restore"
);
assert_eq!(
mapper.read_prg(0xC000),
7,
"$C000 must be in normal mode (bit3=0) after restore"
);
}
#[test]
fn reset_restores_power_on_state() {
let mut mapper = make_mapper();
mapper.write_prg(0xA000, 0x15);
mapper.write_prg(0xC000, 0x38);
mapper.write_prg(0xE000, 0x09);
mapper.reset();
assert_eq!(mapper.read_prg(0x8000), 0);
assert_eq!(mapper.read_prg(0xC000), 7);
assert_eq!(mapper.read_chr(0x0000), 0);
assert_eq!(mapper.read_chr(0x1000), 0);
assert_eq!(mapper.get_mirroring(), NametableLayout::Vertical);
}
#[test]
fn capabilities_match_specification() {
let mapper = make_mapper();
let caps = mapper.capabilities();
assert!(!caps.has_irq);
assert!(!caps.has_expansion_audio);
assert!(caps.has_dynamic_mirroring);
assert!(caps.has_chr_banking);
assert_eq!(caps.prg_bank_size_kb, 16);
assert_eq!(caps.chr_bank_size_kb, 4);
assert_eq!(caps.max_prg_ram_kb, 0);
}
}