use crate::nes::cartridge::BaseMapper;
use crate::nes::cartridge::Mapper;
use crate::nes::cartridge::MapperCapabilities;
use crate::nes::cartridge::NametableLayout;
const CHR_ROM_PAGE_SIZE: usize = 4 * 1024;
const CHR_RAM_SIZE: usize = 2 * 1024;
pub struct IremLrog017Mapper {
base: BaseMapper,
chr_bank: u8,
chr_ram: [u8; CHR_RAM_SIZE],
}
impl IremLrog017Mapper {
pub fn new(ctx: crate::nes::cartridge::mapper::MapperContext) -> Self {
let capabilities = MapperCapabilities {
has_chr_banking: true,
chr_bank_size_kb: 4,
..Default::default()
};
let mut base = BaseMapper::new(&ctx, capabilities);
base.set_mirroring(NametableLayout::SingleScreenLower);
Self {
base,
chr_bank: 0,
chr_ram: [0; CHR_RAM_SIZE],
}
}
#[inline]
fn read_chr_rom_page(&self, offset: usize) -> u8 {
let chr_size = self.base.chr_size();
if chr_size == 0 {
return 0;
}
let bank_count = chr_size.div_ceil(CHR_ROM_PAGE_SIZE);
if bank_count == 0 {
return 0;
}
let effective_bank = (self.chr_bank as usize) % bank_count;
let index = effective_bank * CHR_ROM_PAGE_SIZE + offset;
self.base.read_chr_at_index(index)
}
}
impl Mapper for IremLrog017Mapper {
fn base(&self) -> &BaseMapper {
&self.base
}
fn base_mut(&mut self) -> &mut BaseMapper {
&mut self.base
}
fn write_prg(&mut self, addr: u16, value: u8) {
if (0x8000..=0xFFFF).contains(&addr) {
self.chr_bank = value & 0x0F;
}
}
fn read_chr(&mut self, addr: u16) -> u8 {
match addr {
0x0000..=0x0FFF => self.read_chr_rom_page(addr as usize),
0x1000..=0x1FFF => self.chr_ram[(addr - 0x1000) as usize & 0x7FF],
_ => 0,
}
}
fn write_chr(&mut self, addr: u16, value: u8) {
match addr {
0x0000..=0x0FFF => {}
0x1000..=0x1FFF => self.chr_ram[(addr - 0x1000) as usize & 0x7FF] = value,
_ => {}
}
}
fn mapper_number(&self) -> u16 {
77
}
fn registers_snapshot(&self) -> Vec<u8> {
vec![self.chr_bank]
}
fn restore_registers(&mut self, data: &[u8]) {
if let Some(&bank) = data.first() {
self.chr_bank = bank & 0x0F;
}
}
fn chr_ram_snapshot(&self) -> Vec<u8> {
self.chr_ram.to_vec()
}
fn restore_chr_ram(&mut self, data: &[u8]) {
let len = data.len().min(CHR_RAM_SIZE);
self.chr_ram[..len].copy_from_slice(&data[..len]);
}
fn initialize_ram(&mut self, mode: crate::nes::console::RamInitMode) {
self.base_mut().initialize_ram(mode);
crate::nes::console::initialize_ram(&mut self.chr_ram, mode);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nes::cartridge::mapper::{MapperContext, create_mapper};
use crate::nes::cartridge::test_helpers::banked_data;
fn make_mapper() -> IremLrog017Mapper {
let prg = vec![0u8; 32 * 1024];
let chr = banked_data(4 * 1024, 4);
IremLrog017Mapper::new(MapperContext::new_for_test(
77,
prg,
chr,
NametableLayout::Horizontal, ))
}
#[test]
fn mapper_77_is_registered() {
let result = create_mapper(MapperContext::new_for_test(
77,
vec![0u8; 32 * 1024],
banked_data(4 * 1024, 4),
NametableLayout::Horizontal,
));
assert!(
result.is_ok(),
"Mapper 77 must be registered in the factory"
);
}
#[test]
fn chr_rom_bank_0_selected_on_startup() {
let mapper = make_mapper();
assert_eq!(mapper.read_chr_rom_page(0x000), 0);
assert_eq!(mapper.read_chr_rom_page(0xFFF), 0);
}
#[test]
fn write_to_8000_selects_chr_rom_bank() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 0x00);
assert_eq!(mapper.read_chr(0x0000), 0, "bank 0 at $0000");
mapper.write_prg(0x8000, 0x01);
assert_eq!(mapper.read_chr(0x0000), 1, "bank 1 at $0000");
mapper.write_prg(0x8000, 0x02);
assert_eq!(mapper.read_chr(0x0000), 2, "bank 2 at $0000");
mapper.write_prg(0x8000, 0x03);
assert_eq!(mapper.read_chr(0x0000), 3, "bank 3 at $0000");
}
#[test]
fn write_anywhere_in_prg_range_updates_chr_bank() {
let mut mapper = make_mapper();
mapper.write_prg(0xFFFF, 0x02);
assert_eq!(
mapper.read_chr(0x0000),
2,
"write to $FFFF should select bank 2"
);
mapper.write_prg(0xC000, 0x01);
assert_eq!(
mapper.read_chr(0x0000),
1,
"write to $C000 should select bank 1"
);
}
#[test]
fn register_masks_lower_4_bits_only() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 0xF1); assert_eq!(
mapper.read_chr(0x0000),
1,
"only bits [3:0] should select the bank"
);
}
#[test]
fn bank_wraps_when_exceeding_available_banks() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 0x04);
assert_eq!(mapper.read_chr(0x0000), 0, "bank 4 should wrap to bank 0");
mapper.write_prg(0x8000, 0x05);
assert_eq!(mapper.read_chr(0x0000), 1, "bank 5 should wrap to bank 1");
}
#[test]
fn chr_ram_at_1000_is_readable_and_writable() {
let mut mapper = make_mapper();
mapper.write_chr(0x1000, 0xAB);
assert_eq!(
mapper.read_chr(0x1000),
0xAB,
"$1000 CHR-RAM write should persist"
);
mapper.write_chr(0x17FF, 0xCD);
assert_eq!(
mapper.read_chr(0x17FF),
0xCD,
"$17FF CHR-RAM write should persist"
);
}
#[test]
fn chr_ram_writes_do_not_affect_chr_rom_page() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 0x01);
let bank1_val = mapper.read_chr(0x0000);
mapper.write_chr(0x1000, 0xFF);
assert_eq!(
mapper.read_chr(0x0000),
bank1_val,
"CHR-RAM write must not alter CHR-ROM reads"
);
}
#[test]
fn chr_rom_page_is_read_only() {
let mut mapper = make_mapper();
let original = mapper.read_chr(0x0000);
mapper.write_chr(0x0000, 0xFF);
assert_eq!(
mapper.read_chr(0x0000),
original,
"writes to CHR-ROM region must be ignored"
);
}
#[test]
fn mirroring_is_single_screen_lower() {
let mapper = make_mapper();
assert_eq!(
mapper.get_mirroring(),
NametableLayout::SingleScreenLower,
"mapper 77 uses fixed one-screen lower mirroring"
);
}
#[test]
fn snapshot_restore_roundtrip_preserves_chr_bank() {
let mut mapper = make_mapper();
mapper.write_prg(0x8000, 0x03);
let snap = mapper.registers_snapshot();
let mut restored = make_mapper();
restored.restore_registers(&snap);
assert_eq!(
restored.read_chr(0x0000),
3,
"restored mapper should use the same CHR bank"
);
}
#[test]
fn chr_ram_snapshot_restore_roundtrip() {
let mut mapper = make_mapper();
mapper.write_chr(0x1000, 0xDE);
mapper.write_chr(0x17FF, 0xAD);
let snap = mapper.chr_ram_snapshot();
let mut restored = make_mapper();
restored.restore_chr_ram(&snap);
assert_eq!(
restored.read_chr(0x1000),
0xDE,
"CHR-RAM byte at $1000 should be restored"
);
assert_eq!(
restored.read_chr(0x17FF),
0xAD,
"CHR-RAM byte at $17FF should be restored"
);
}
}