use crate::cartridge::base_mapper::BaseMapper;
use crate::cartridge::mapper::{Mapper, MapperCapabilities};
const MAPPER_NUMBER: u16 = 335;
const PRG_BANK_SIZE_BYTES: usize = 16 * 1024;
const CHR_BANK_SIZE_BYTES: usize = 8 * 1024;
const NROM128_BIT: u8 = 0x10;
const PRG_BANK_MASK: u8 = 0x07;
const CHR_BANK_MASK: u8 = 0x0F;
const MIRROR_BIT: u8 = 0x20;
const REGISTERS_SNAPSHOT_LEN: usize = 2;
pub struct Mapper335 {
base: BaseMapper,
prg_reg: u8,
chr_reg: u8,
}
impl Mapper335 {
pub fn new(ctx: super::mapper::MapperContext) -> Self {
let capabilities = MapperCapabilities {
has_dynamic_mirroring: true,
has_chr_banking: true,
prg_bank_size_kb: 16,
chr_bank_size_kb: 8,
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_reg: 0,
chr_reg: 0,
};
mapper.apply_state(0, 0);
mapper
}
fn prg_bank_8000(prg_reg: u8) -> i16 {
if prg_reg & NROM128_BIT != 0 {
(((prg_reg & PRG_BANK_MASK) << 1) | ((prg_reg >> 3) & 1)) as i16
} else {
((prg_reg & PRG_BANK_MASK) as i16) * 2
}
}
fn prg_bank_c000(prg_reg: u8) -> i16 {
if prg_reg & NROM128_BIT != 0 {
(((prg_reg & PRG_BANK_MASK) << 1) | ((prg_reg >> 3) & 1)) as i16
} else {
((prg_reg & PRG_BANK_MASK) as i16) * 2 + 1
}
}
fn apply_state(&mut self, prg_reg: u8, chr_reg: u8) {
self.prg_reg = prg_reg;
self.chr_reg = chr_reg;
self.base.select_prg_page(0, Self::prg_bank_8000(prg_reg));
self.base.select_prg_page(1, Self::prg_bank_c000(prg_reg));
self.base
.select_chr_page(0, (chr_reg & CHR_BANK_MASK) as i16);
self.base.set_mirroring_hv((prg_reg & MIRROR_BIT) != 0);
}
}
impl Mapper for Mapper335 {
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 {
0x8000..=0xBFFF => self.apply_state(self.prg_reg, value),
0xC000..=0xFFFF => self.apply_state(value, self.chr_reg),
_ => {}
}
}
fn registers_snapshot(&self) -> Vec<u8> {
vec![self.prg_reg, self.chr_reg]
}
fn restore_registers(&mut self, data: &[u8]) {
if data.len() < REGISTERS_SNAPSHOT_LEN {
return;
}
self.apply_state(data[0], data[1]);
}
fn reset(&mut self) {
self.apply_state(0, 0);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cartridge::NametableLayout;
use crate::cartridge::mapper::{MapperContext, create_mapper};
use crate::cartridge::test_helpers::banked_data;
const PRG_BANKS_16K: usize = 48;
const CHR_BANKS_8K: usize = 16;
fn make_mapper() -> Mapper335 {
Mapper335::new(MapperContext::new_for_test(
MAPPER_NUMBER,
banked_data(PRG_BANK_SIZE_BYTES, PRG_BANKS_16K),
banked_data(CHR_BANK_SIZE_BYTES, CHR_BANKS_8K),
NametableLayout::Vertical,
))
}
#[test]
fn mapper_335_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_8K),
NametableLayout::Vertical,
));
assert!(result.is_ok(), "Mapper 335 must be registered in factory");
}
#[test]
fn power_on_prg_banks_are_0_and_1_with_vertical_mirroring() {
let mapper = make_mapper();
assert_eq!(mapper.read_prg(0x8000), 0, "$8000 should be PRG bank 0");
assert_eq!(mapper.read_prg(0xC000), 1, "$C000 should be PRG bank 1");
assert_eq!(
mapper.get_mirroring(),
NametableLayout::Vertical,
"mirroring should be vertical at power-on"
);
}
#[test]
fn power_on_chr_bank_is_0() {
let mut mapper = make_mapper();
assert_eq!(
mapper.read_chr(0x0000),
0,
"CHR bank should be 0 at power-on"
);
}
#[test]
fn nrom256_mode_prg_bank_3_gives_pages_6_and_7() {
let mut mapper = make_mapper();
mapper.write_prg(0xC000, 0x03);
assert_eq!(mapper.read_prg(0x8000), 6, "$8000 should be 16KB page 6");
assert_eq!(mapper.read_prg(0xC000), 7, "$C000 should be 16KB page 7");
}
#[test]
fn nrom256_mode_prg_bank_5_gives_pages_10_and_11() {
let mut mapper = make_mapper();
mapper.write_prg(0xC000, 0x05);
assert_eq!(mapper.read_prg(0x8000), 10, "$8000 should be 16KB page 10");
assert_eq!(mapper.read_prg(0xC000), 11, "$C000 should be 16KB page 11");
}
#[test]
fn nrom256_mode_upper_prg_bits_are_ignored() {
let mut mapper = make_mapper();
mapper.write_prg(0xC000, 0x43); assert_eq!(mapper.read_prg(0x8000), 6);
assert_eq!(mapper.read_prg(0xC000), 7);
}
#[test]
fn nrom128_mode_both_windows_use_same_bank() {
let mut mapper = make_mapper();
mapper.write_prg(0xC000, 0x10);
let bank8 = mapper.read_prg(0x8000);
let bankc = mapper.read_prg(0xC000);
assert_eq!(
bank8, bankc,
"$8000 and $C000 must use the same bank in NROM-128"
);
}
#[test]
fn nrom128_bank_formula_uses_bits2_0_and_bit3() {
let mut mapper = make_mapper();
mapper.write_prg(0xC000, 0x1B);
assert_eq!(mapper.read_prg(0x8000), 7, "NROM-128 bank should be 7");
assert_eq!(mapper.read_prg(0xC000), 7, "$C000 should also be bank 7");
}
#[test]
fn nrom128_bank_with_bits2_0_eq_2_and_bit3_set_gives_bank_5() {
let mut mapper = make_mapper();
mapper.write_prg(0xC000, 0x1A);
assert_eq!(mapper.read_prg(0x8000), 5, "NROM-128 bank should be 5");
assert_eq!(mapper.read_prg(0xC000), 5, "$C000 should also be bank 5");
}
#[test]
fn nrom128_bank_with_bits2_0_eq_1_and_bit3_clear_gives_bank_2() {
let mut mapper = make_mapper();
mapper.write_prg(0xC000, 0x11);
assert_eq!(mapper.read_prg(0x8000), 2, "NROM-128 bank should be 2");
assert_eq!(mapper.read_prg(0xC000), 2, "$C000 should also be bank 2");
}
#[test]
fn writes_below_8000_do_not_change_prg_or_chr_banks() {
let mut mapper = make_mapper();
mapper.write_prg(0xC000, 0x03); mapper.write_prg(0x8000, 0x05); mapper.write_prg(0x4020, 0xFF);
mapper.write_prg(0x6000, 0xFF);
mapper.write_prg(0x7FFF, 0xFF);
assert_eq!(
mapper.read_prg(0x8000),
6,
"PRG $8000 should remain 6 after writes below $8000"
);
assert_eq!(
mapper.read_prg(0xC000),
7,
"PRG $C000 should remain 7 after writes below $8000"
);
assert_eq!(
mapper.read_chr(0x0000),
5,
"CHR bank should remain 5 after writes below $8000"
);
}
#[test]
fn writes_to_8000_bfff_change_chr_bank() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 0x05);
assert_eq!(mapper.read_chr(0x0000), 5, "CHR should switch to bank 5");
}
#[test]
fn writes_to_c000_ffff_change_prg_bank() {
let mut mapper = make_mapper();
mapper.write_prg(0xFFFF, 0x02);
assert_eq!(mapper.read_prg(0x8000), 4, "$8000 should be PRG page 4");
assert_eq!(mapper.read_prg(0xC000), 5, "$C000 should be PRG page 5");
}
#[test]
fn chr_write_does_not_affect_prg_banks() {
let mut mapper = make_mapper();
mapper.write_prg(0xC000, 0x03); mapper.write_prg(0x8000, 0x07); assert_eq!(mapper.read_prg(0x8000), 6, "PRG should remain at page 6");
assert_eq!(mapper.read_prg(0xC000), 7, "PRG should remain at page 7");
}
#[test]
fn prg_write_does_not_affect_chr_bank() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 0x0A); mapper.write_prg(0xC000, 0x05); assert_eq!(mapper.read_chr(0x0000), 10, "CHR should remain at bank 10");
}
#[test]
fn chr_bank_register_uses_only_low_4_bits() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 0xF5); assert_eq!(
mapper.read_chr(0x0000),
5,
"CHR should be bank 5 (bits[3:0])"
);
}
#[test]
fn chr_bank_15_is_selectable() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 0x0F);
assert_eq!(
mapper.read_chr(0x0000),
15,
"CHR bank 15 should be selectable"
);
}
#[test]
fn prg_bit5_set_gives_horizontal_mirroring() {
let mut mapper = make_mapper();
mapper.write_prg(0xC000, 0x20); assert_eq!(
mapper.get_mirroring(),
NametableLayout::Horizontal,
"bit5=1 should select horizontal mirroring"
);
}
#[test]
fn prg_bit5_clear_gives_vertical_mirroring() {
let mut mapper = make_mapper();
mapper.write_prg(0xC000, 0x20); mapper.write_prg(0xC000, 0x00); assert_eq!(
mapper.get_mirroring(),
NametableLayout::Vertical,
"bit5=0 should select vertical mirroring"
);
}
#[test]
fn snapshot_restore_preserves_prg_chr_and_mirroring() {
let mut mapper = make_mapper();
mapper.write_prg(0xC000, 0x35);
mapper.write_prg(0x8000, 0x0C); let snap = mapper.registers_snapshot();
let mut restored = make_mapper();
restored.restore_registers(&snap);
assert_eq!(
restored.read_prg(0x8000),
10,
"restored PRG bank should be 10"
);
assert_eq!(
restored.read_prg(0xC000),
10,
"restored PRG $C000 should be 10 (NROM-128)"
);
assert_eq!(
restored.read_chr(0x0000),
12,
"restored CHR bank should be 12"
);
assert_eq!(
restored.get_mirroring(),
NametableLayout::Horizontal,
"restored mirroring should be horizontal"
);
}
#[test]
fn reset_restores_power_on_state() {
let mut mapper = make_mapper();
mapper.write_prg(0xC000, 0x25);
mapper.write_prg(0x8000, 0x0C);
mapper.reset();
assert_eq!(mapper.read_prg(0x8000), 0);
assert_eq!(mapper.read_prg(0xC000), 1);
assert_eq!(mapper.read_chr(0x0000), 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, 8);
assert_eq!(caps.max_prg_ram_kb, 0);
}
}