use crate::cartridge::base_mapper::BaseMapper;
use crate::cartridge::mmc3::MMC3Mapper;
use crate::cartridge::{Mapper, MapperCapabilities};
pub struct Mapper49 {
pub(crate) mmc3: MMC3Mapper,
block: u8, page: u8, mmc3_mode: bool,
}
impl Mapper49 {
const MAPPER_NUMBER: u8 = 49;
const PRG_BANK_SIZE: usize = 0x2000; const PRG_BANK_MASK: usize = Self::PRG_BANK_SIZE - 1;
const CHR_1K_BANK_SIZE: usize = 0x0400; const CHR_BANK_MASK: usize = Self::CHR_1K_BANK_SIZE - 1;
pub fn new(ctx: super::mapper::MapperContext) -> Self {
let prg_rom = ctx.prg_rom;
let chr_rom = ctx.chr_rom;
let mirroring = ctx.mirroring;
Self {
mmc3: MMC3Mapper::new_with_irq_mode(prg_rom, chr_rom, mirroring, false),
block: 0,
page: 0,
mmc3_mode: false,
}
}
fn fixed_prg_base_bank(&self) -> usize {
(self.block as usize) * 16 + (self.page as usize) * 4
}
fn apply_chr_block(&self, raw_1k_bank: usize) -> usize {
(raw_1k_bank & 0x7F) | ((self.block as usize) << 7)
}
}
impl Mapper for Mapper49 {
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 read_prg(&self, addr: u16) -> u8 {
if !(0x8000..=0xFFFF).contains(&addr) {
return 0;
}
let offset = (addr as usize) & Self::PRG_BANK_MASK;
let bank = if self.mmc3_mode {
let raw = self.mmc3.mapped_prg_bank(addr);
(raw & 0x0F) | ((self.block as usize) << 4)
} else {
let slot = ((addr as usize) >> 13) & 0x03; self.fixed_prg_base_bank() + slot
};
self.mmc3.read_prg_at_bank(bank, offset)
}
fn write_prg(&mut self, addr: u16, value: u8) {
if (0x6000..=0x7FFF).contains(&addr) {
if self.mmc3.is_prg_ram_writable() {
self.block = (value >> 6) & 0x03;
self.page = (value >> 4) & 0x03;
self.mmc3_mode = (value & 0x01) != 0;
}
} else {
self.mmc3.write_prg(addr, value);
}
}
fn read_chr(&mut self, addr: u16) -> u8 {
let raw = self.mmc3.mapped_chr_1k_bank(addr);
let bank = self.apply_chr_block(raw);
let offset = (addr as usize) & Self::CHR_BANK_MASK;
self.mmc3.read_chr_1k_at(bank, offset)
}
fn write_chr(&mut self, addr: u16, value: u8) {
let raw = self.mmc3.mapped_chr_1k_bank(addr);
let bank = self.apply_chr_block(raw);
let offset = (addr as usize) & Self::CHR_BANK_MASK;
self.mmc3.write_chr_1k_at(bank, offset, value);
}
fn mapper_number(&self) -> u16 {
u16::from(Self::MAPPER_NUMBER)
}
fn registers_snapshot(&self) -> Vec<u8> {
let mut snap = self.mmc3.registers_snapshot();
snap.push(self.block | (self.page << 2) | (self.mmc3_mode as u8) << 4);
snap
}
fn restore_registers(&mut self, data: &[u8]) {
if let Some((&packed, mmc3_data)) = data.split_last() {
self.block = packed & 0x03;
self.page = (packed >> 2) & 0x03;
self.mmc3_mode = (packed >> 4) & 0x01 != 0;
self.mmc3.restore_registers(mmc3_data);
}
}
fn capabilities(&self) -> MapperCapabilities {
MapperCapabilities {
has_irq: true,
has_chr_banking: true,
has_dynamic_mirroring: true,
has_expansion_audio: false,
max_prg_ram_kb: 0,
prg_bank_size_kb: 8,
chr_bank_size_kb: 1,
..Default::default()
}
}
}
#[cfg(test)]
mod tests {
use crate::cartridge::NametableLayout;
use crate::cartridge::mapper::{Mapper, MapperContext, create_mapper};
use crate::cartridge::test_helpers::banked_data;
const PRG_BANKS: usize = 64;
fn make_mapper() -> Box<dyn Mapper> {
let prg = banked_data(8 * 1024, PRG_BANKS);
let chr = banked_data(1024, 256);
create_mapper(MapperContext::new_for_test(
49,
prg,
chr,
NametableLayout::Vertical,
))
.expect("Mapper 49 should be implemented")
}
#[test]
fn mapper_49_is_registered_in_factory() {
let result = create_mapper(MapperContext::new_for_test(
49,
banked_data(8 * 1024, PRG_BANKS),
banked_data(1024, 256),
NametableLayout::Vertical,
));
assert!(
result.is_ok(),
"Mapper 49 must be registered in the factory"
);
}
#[test]
fn fixed_mode_block0_page0_maps_banks_0_3() {
let mapper = make_mapper();
assert_eq!(mapper.read_prg(0x8000), 0, "$8000 should read bank 0");
assert_eq!(mapper.read_prg(0xA000), 1, "$A000 should read bank 1");
assert_eq!(mapper.read_prg(0xC000), 2, "$C000 should read bank 2");
assert_eq!(mapper.read_prg(0xE000), 3, "$E000 should read bank 3");
}
#[test]
fn fixed_mode_block0_page1_maps_banks_4_7() {
let mut mapper = make_mapper();
mapper.write_prg(0xA001, 0x80); mapper.write_prg(0x6000, 0x10);
assert_eq!(mapper.read_prg(0x8000), 4, "$8000 block0 page1 = bank 4");
assert_eq!(mapper.read_prg(0xE000), 7, "$E000 block0 page1 = bank 7");
}
#[test]
fn fixed_mode_block1_page0_maps_banks_16_19() {
let mut mapper = make_mapper();
mapper.write_prg(0xA001, 0x80);
mapper.write_prg(0x6000, 0x40);
assert_eq!(mapper.read_prg(0x8000), 16, "$8000 block1 page0 = bank 16");
assert_eq!(mapper.read_prg(0xE000), 19, "$E000 block1 page0 = bank 19");
}
#[test]
fn fixed_mode_block2_page3_maps_banks_44_47() {
let mut mapper = make_mapper();
mapper.write_prg(0xA001, 0x80);
mapper.write_prg(0x6000, 0xB0);
assert_eq!(mapper.read_prg(0x8000), 44, "$8000 block2 page3 = bank 44");
assert_eq!(mapper.read_prg(0xE000), 47, "$E000 block2 page3 = bank 47");
}
#[test]
fn mmc3_mode_block0_fixed_last_bank() {
let mut mapper = make_mapper();
mapper.write_prg(0xA001, 0x80);
mapper.write_prg(0x6000, 0x01);
assert_eq!(
mapper.read_prg(0xE000),
15,
"MMC3 mode block0 fixed-last must be bank 15"
);
}
#[test]
fn mmc3_mode_block1_r6_bank_3() {
let mut mapper = make_mapper();
mapper.write_prg(0xA001, 0x80);
mapper.write_prg(0x6000, 0x41); mapper.write_prg(0x8000, 0b0000_0110); mapper.write_prg(0x8001, 3); assert_eq!(
mapper.read_prg(0x8000),
19,
"MMC3 mode block1 R6=3 must map to bank 19"
);
}
#[test]
fn mmc3_mode_block2_fixed_last_bank() {
let mut mapper = make_mapper();
mapper.write_prg(0xA001, 0x80);
mapper.write_prg(0x6000, 0x81); assert_eq!(
mapper.read_prg(0xE000),
47,
"MMC3 mode block2 fixed-last must be bank 47"
);
}
#[test]
fn outer_reg_not_writable_when_prg_ram_disabled() {
let mut mapper = make_mapper();
mapper.write_prg(0xA001, 0x00); mapper.write_prg(0x6000, 0x41); assert_eq!(
mapper.read_prg(0xE000),
3,
"Block register must not change when PRG-RAM disabled"
);
}
#[test]
fn chr_block0_default() {
let mut mapper = make_mapper();
mapper.write_prg(0xA001, 0x80);
mapper.write_prg(0x6000, 0x01); mapper.write_prg(0x8000, 0b0000_0010); mapper.write_prg(0x8001, 5); assert_eq!(mapper.read_chr(0x1000), 5, "CHR block0 R2=5 = bank 5");
}
#[test]
fn chr_block1_shifts_bank_by_128() {
let mut mapper = make_mapper();
mapper.write_prg(0xA001, 0x80);
mapper.write_prg(0x6000, 0x41); mapper.write_prg(0x8000, 0b0000_0010); mapper.write_prg(0x8001, 5); assert_eq!(
mapper.read_chr(0x1000),
133,
"CHR block1 R2=5 must be bank 133"
);
}
#[test]
fn chr_block_applies_in_fixed_prg_mode_too() {
let mut mapper = make_mapper();
mapper.write_prg(0xA001, 0x80);
mapper.write_prg(0x6000, 0x40); mapper.write_prg(0x8000, 0b0000_0010); mapper.write_prg(0x8001, 5); assert_eq!(
mapper.read_chr(0x1000),
133,
"CHR block applies even in fixed PRG mode"
);
}
}