use crate::nes::cartridge::NametableLayout;
use crate::nes::cartridge::base_mapper::BaseMapper;
use crate::nes::cartridge::{Mapper, MapperCapabilities};
pub struct Mapper128 {
base: BaseMapper,
locked: bool,
outer_bank: u8,
prg_mode: bool,
nrom_256: bool,
inner_bank: u8,
}
impl Mapper128 {
const MAPPER_NUMBER: u16 = 128;
pub fn new(ctx: crate::nes::cartridge::mapper::MapperContext) -> Self {
let capabilities = Self::capabilities_for_mapper();
let mut base = BaseMapper::new(&ctx, capabilities);
base.configure_prg_banking(0x4000); let mut mapper = Self {
base,
locked: false,
outer_bank: 0,
prg_mode: false,
nrom_256: false,
inner_bank: 0,
};
mapper.update_prg_banks();
mapper
}
fn capabilities_for_mapper() -> MapperCapabilities {
MapperCapabilities {
has_irq: false,
has_chr_banking: false,
has_dynamic_mirroring: true,
has_expansion_audio: false,
max_prg_ram_kb: 0,
prg_bank_size_kb: 16,
chr_bank_size_kb: 8,
..Default::default()
}
}
fn update_prg_banks(&mut self) {
let base_bank = (self.outer_bank as i16) * 8;
if self.prg_mode {
if self.nrom_256 {
let even = base_bank + (self.inner_bank as i16 & 0x06);
self.base.select_prg_page(0, even);
self.base.select_prg_page(1, even | 1);
} else {
let bank = base_bank + self.inner_bank as i16;
self.base.select_prg_page(0, bank);
self.base.select_prg_page(1, bank);
}
} else if self.nrom_256 {
let inner = base_bank + (self.inner_bank as i16 & 0x06);
self.base.select_prg_page(0, inner);
self.base.select_prg_page(1, base_bank + 7);
} else {
let bank = base_bank + self.inner_bank as i16;
self.base.select_prg_page(0, bank);
self.base.select_prg_page(1, base_bank + 7);
}
}
}
impl Mapper for Mapper128 {
fn base(&self) -> &BaseMapper {
&self.base
}
fn base_mut(&mut self) -> &mut BaseMapper {
&mut self.base
}
fn read_prg(&self, addr: u16) -> u8 {
self.base.read_prg_rom(addr)
}
fn write_prg(&mut self, addr: u16, value: u8) {
if !(0x8000..=0xFFFF).contains(&addr) {
return;
}
self.inner_bank = value & 0x07;
if !self.locked {
self.locked = (addr & 0x2000) != 0;
self.outer_bank = (((addr >> 6) & 0x0C) | ((addr >> 5) & 0x03)) as u8;
self.prg_mode = (addr & 0x0080) != 0;
self.nrom_256 = (addr & 0x0001) != 0;
let mirroring = if (addr & 0x0002) != 0 {
NametableLayout::Horizontal
} else {
NametableLayout::Vertical
};
self.base.set_mirroring(mirroring);
}
self.update_prg_banks();
}
fn read_chr(&mut self, addr: u16) -> u8 {
self.base.read_chr(addr)
}
fn write_chr(&mut self, addr: u16, value: u8) {
self.base.write_chr(addr, value);
}
fn mapper_number(&self) -> u16 {
Self::MAPPER_NUMBER
}
fn wram_size(&self) -> usize {
0
}
fn registers_snapshot(&self) -> Vec<u8> {
let mut snap = self.base.banking_snapshot();
snap.push(
(if self.locked { 0x80 } else { 0 })
| (self.outer_bank << 3)
| (if self.prg_mode { 0x04 } else { 0 })
| (if self.nrom_256 { 0x02 } else { 0 }),
);
snap.push(self.inner_bank);
snap
}
fn restore_registers(&mut self, data: &[u8]) {
if data.len() >= 2 {
let (base_data, extra) = data.split_at(data.len() - 2);
let flags = extra[0];
self.locked = (flags & 0x80) != 0;
self.outer_bank = (flags >> 3) & 0x0F;
self.prg_mode = (flags & 0x04) != 0;
self.nrom_256 = (flags & 0x02) != 0;
self.inner_bank = extra[1] & 0x07;
self.base.restore_banking(base_data);
self.update_prg_banks();
} else {
self.base.restore_banking(data);
}
}
fn capabilities(&self) -> MapperCapabilities {
Self::capabilities_for_mapper()
}
}
#[cfg(test)]
mod tests {
use crate::nes::cartridge::NametableLayout;
use crate::nes::cartridge::mapper::{Mapper, MapperContext, create_mapper};
use crate::nes::cartridge::test_helpers::banked_data;
const PRG_16K_BANKS: usize = 128;
fn make_mapper() -> Box<dyn Mapper> {
let prg = banked_data(16 * 1024, PRG_16K_BANKS);
let chr = vec![]; create_mapper(MapperContext::new_for_test(
128,
prg,
chr,
NametableLayout::Vertical,
))
.expect("Mapper 128 should be implemented")
}
fn latch_write(mapper: &mut Box<dyn Mapper>, addr: u16, value: u8) {
mapper.write_prg(addr, value);
}
fn build_addr(lock: bool, outer: u8, prg_mode: bool, mirror_h: bool, nrom256: bool) -> u16 {
let mut addr: u16 = 0x8000;
if lock {
addr |= 0x2000;
}
addr |= ((outer as u16 & 0x0C) << 6) | ((outer as u16 & 0x03) << 5);
if prg_mode {
addr |= 0x0080;
}
if mirror_h {
addr |= 0x0002;
}
if nrom256 {
addr |= 0x0001;
}
addr
}
#[test]
fn mapper_128_is_registered_in_factory() {
let result = create_mapper(MapperContext::new_for_test(
128,
banked_data(16 * 1024, PRG_16K_BANKS),
vec![],
NametableLayout::Vertical,
));
assert!(
result.is_ok(),
"Mapper 128 must be registered in the factory"
);
}
#[test]
fn unrom_default_c000_is_fixed_bank_7() {
let mapper = make_mapper();
assert_eq!(
mapper.read_prg(0xC000),
7,
"$C000 must be fixed to inner bank 7 in UNROM mode"
);
}
#[test]
fn unrom_inner_bank_selects_8000() {
let mut mapper = make_mapper();
latch_write(&mut mapper, 0x8000, 3);
assert_eq!(
mapper.read_prg(0x8000),
3,
"$8000 must follow inner bank PPP in UNROM mode"
);
}
#[test]
fn unrom_c000_stays_fixed_when_inner_changes() {
let mut mapper = make_mapper();
latch_write(&mut mapper, 0x8000, 5);
assert_eq!(
mapper.read_prg(0xC000),
7,
"$C000 must remain fixed bank 7 regardless of inner bank"
);
}
#[test]
fn outer_bank_shifts_both_halves() {
let mut mapper = make_mapper();
let addr = build_addr(false, 2, false, false, false);
latch_write(&mut mapper, addr, 0);
assert_eq!(
mapper.read_prg(0x8000),
16,
"$8000 in outer bank 2 = bank 16"
);
assert_eq!(
mapper.read_prg(0xC000),
23,
"$C000 in outer bank 2 = bank 23 (fixed 7)"
);
}
#[test]
fn outer_bank_inner_bank_combine() {
let mut mapper = make_mapper();
let addr = build_addr(false, 3, false, false, false);
latch_write(&mut mapper, addr, 5);
assert_eq!(mapper.read_prg(0x8000), 29, "outer=3, inner=5 → bank 29");
}
#[test]
fn nrom128_mirrors_inner_bank() {
let mut mapper = make_mapper();
let addr = build_addr(false, 0, true, false, false);
latch_write(&mut mapper, addr, 4);
assert_eq!(mapper.read_prg(0x8000), 4, "NROM-128 $8000 = inner 4");
assert_eq!(
mapper.read_prg(0xC000),
4,
"NROM-128 $C000 must mirror inner bank"
);
}
#[test]
fn nrom256_maps_consecutive_32k() {
let mut mapper = make_mapper();
let addr = build_addr(false, 0, true, false, true);
latch_write(&mut mapper, addr, 4);
assert_eq!(
mapper.read_prg(0x8000),
4,
"NROM-256 $8000 = inner & ~1 = 4"
);
assert_eq!(
mapper.read_prg(0xC000),
5,
"NROM-256 $C000 = (inner & ~1) | 1 = 5"
);
}
#[test]
fn nrom256_clears_inner_bit0() {
let mut mapper = make_mapper();
let addr = build_addr(false, 0, true, false, true);
latch_write(&mut mapper, addr, 5);
assert_eq!(
mapper.read_prg(0x8000),
4,
"NROM-256 must clear bit 0: inner 5 → bank 4"
);
assert_eq!(mapper.read_prg(0xC000), 5, "NROM-256 $C000 = 5");
}
#[test]
fn lock_prevents_address_latch_change() {
let mut mapper = make_mapper();
let addr = build_addr(true, 1, false, false, false);
latch_write(&mut mapper, addr, 0);
assert_eq!(mapper.read_prg(0x8000), 8, "Outer=1 → bank 8");
let addr2 = build_addr(false, 2, false, false, false);
latch_write(&mut mapper, addr2, 0);
assert_eq!(
mapper.read_prg(0x8000),
8,
"Address latch must be locked: outer stays 1"
);
}
#[test]
fn lock_allows_data_latch_change() {
let mut mapper = make_mapper();
let addr = build_addr(true, 0, false, false, false);
latch_write(&mut mapper, addr, 0);
assert_eq!(mapper.read_prg(0x8000), 0);
latch_write(&mut mapper, 0x8000, 3);
assert_eq!(
mapper.read_prg(0x8000),
3,
"Data latch (inner bank) must remain changeable when locked"
);
}
#[test]
fn mirroring_defaults_to_vertical() {
let mapper = make_mapper();
assert_eq!(
mapper.base().mirroring(),
NametableLayout::Vertical,
"Default mirroring must be vertical"
);
}
#[test]
fn mirroring_switches_to_horizontal() {
let mut mapper = make_mapper();
let addr = build_addr(false, 0, false, true, false);
latch_write(&mut mapper, addr, 0);
assert_eq!(
mapper.base().mirroring(),
NametableLayout::Horizontal,
"M=1 must set horizontal mirroring"
);
}
#[test]
fn chr_ram_is_writable_and_readable() {
let mut mapper = make_mapper();
mapper.write_chr(0x0100, 0xAB);
assert_eq!(
mapper.read_chr(0x0100),
0xAB,
"CHR-RAM must be readable/writable"
);
}
#[test]
fn snapshot_roundtrip_preserves_state() {
let mut mapper = make_mapper();
let addr = build_addr(true, 5, true, true, true);
latch_write(&mut mapper, addr, 6);
let snap = mapper.registers_snapshot();
let mut mapper2 = make_mapper();
mapper2.restore_registers(&snap);
assert_eq!(
mapper.read_prg(0x8000),
mapper2.read_prg(0x8000),
"Snapshot round-trip must preserve PRG mapping"
);
assert_eq!(
mapper.read_prg(0xC000),
mapper2.read_prg(0xC000),
"Snapshot round-trip must preserve $C000 mapping"
);
assert_eq!(
mapper2.base().mirroring(),
NametableLayout::Horizontal,
"Snapshot must preserve mirroring"
);
let prg_8000_before = mapper2.read_prg(0x8000);
let prg_c000_before = mapper2.read_prg(0xC000);
let mirroring_before = mapper2.base().mirroring();
let different_addr = build_addr(false, 2, false, false, false);
latch_write(&mut mapper2, different_addr, 6);
assert_eq!(
mapper2.read_prg(0x8000),
prg_8000_before,
"Snapshot must preserve locked state for $8000 mapping"
);
assert_eq!(
mapper2.read_prg(0xC000),
prg_c000_before,
"Snapshot must preserve locked state for $C000 mapping"
);
assert_eq!(
mapper2.base().mirroring(),
mirroring_before,
"Snapshot must preserve locked state for mirroring"
);
}
}