use super::cartridge::GbCartridge;
pub struct Mbc5 {
rom: Vec<u8>,
ram: Vec<u8>,
rom_bank: u16,
ram_bank: u8,
ram_enabled: bool,
has_rumble: bool,
}
impl Mbc5 {
pub fn new(rom: Vec<u8>, ram: Vec<u8>, has_rumble: bool) -> Self {
Self {
rom,
ram,
rom_bank: 1,
ram_bank: 0,
ram_enabled: false,
has_rumble,
}
}
fn rom_bank_count(&self) -> usize {
(self.rom.len() / 0x4000).max(1)
}
fn ram_bank_count(&self) -> usize {
(self.ram.len() / 0x2000).max(1)
}
fn effective_rom_bank(&self) -> usize {
(self.rom_bank as usize) & (self.rom_bank_count() - 1)
}
fn effective_ram_bank(&self) -> usize {
(self.ram_bank as usize) & (self.ram_bank_count() - 1)
}
fn read_rom(&self, addr: u16) -> u8 {
let (bank, offset) = if addr < 0x4000 {
(0, addr as usize)
} else {
(self.effective_rom_bank(), addr as usize - 0x4000)
};
let idx = bank * 0x4000 + offset;
self.rom.get(idx).copied().unwrap_or(0xFF)
}
fn read_ram(&self, addr: u16) -> u8 {
if !self.ram_enabled || self.ram.is_empty() {
return 0xFF;
}
let offset = addr as usize - 0xA000;
let idx = self.effective_ram_bank() * 0x2000 + offset;
self.ram.get(idx).copied().unwrap_or(0xFF)
}
fn write_registers(&mut self, addr: u16, val: u8) {
match addr {
0x0000..=0x1FFF => {
self.ram_enabled = (val & 0x0F) == 0x0A;
}
0x2000..=0x2FFF => {
self.rom_bank = (self.rom_bank & 0x0100) | (val as u16);
}
0x3000..=0x3FFF => {
self.rom_bank = (self.rom_bank & 0x00FF) | (((val & 0x01) as u16) << 8);
}
0x4000..=0x5FFF => {
let mask = if self.has_rumble { 0x07 } else { 0x0F };
self.ram_bank = val & mask;
}
_ => {}
}
}
fn write_ram(&mut self, addr: u16, val: u8) {
if !self.ram_enabled || self.ram.is_empty() {
return;
}
let offset = addr as usize - 0xA000;
let idx = self.effective_ram_bank() * 0x2000 + offset;
if let Some(byte) = self.ram.get_mut(idx) {
*byte = val;
}
}
}
impl GbCartridge for Mbc5 {
fn read(&self, addr: u16) -> u8 {
match addr {
0x0000..=0x7FFF => self.read_rom(addr),
0xA000..=0xBFFF => self.read_ram(addr),
_ => 0xFF,
}
}
fn write(&mut self, addr: u16, val: u8) {
match addr {
0x0000..=0x5FFF => self.write_registers(addr, val),
0xA000..=0xBFFF => self.write_ram(addr, val),
_ => {}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_rom(bank_count: usize) -> Vec<u8> {
let mut rom = vec![0u8; bank_count * 0x4000];
for bank in 0..bank_count {
let start = bank * 0x4000;
rom[start..start + 0x4000].fill(bank as u8);
}
rom
}
fn make_ram(bank_count: usize) -> Vec<u8> {
vec![0u8; bank_count * 0x2000]
}
#[test]
fn test_mbc5_fixed_window_always_reads_bank0() {
let cart = Mbc5::new(make_rom(4), make_ram(0), false);
assert_eq!(cart.read(0x0000), 0x00);
assert_eq!(cart.read(0x3FFF), 0x00);
}
#[test]
fn test_mbc5_fixed_window_unchanged_after_bank_select() {
let mut cart = Mbc5::new(make_rom(4), make_ram(0), false);
cart.write(0x2000, 0x02);
assert_eq!(cart.read(0x0000), 0x00);
}
#[test]
fn test_mbc5_switchable_window_initially_maps_bank1() {
let cart = Mbc5::new(make_rom(4), make_ram(0), false);
assert_eq!(cart.read(0x4000), 0x01);
assert_eq!(cart.read(0x7FFF), 0x01);
}
#[test]
fn test_mbc5_writing_zero_to_bank_reg_selects_bank0() {
let mut cart = Mbc5::new(make_rom(4), make_ram(0), false);
cart.write(0x2000, 0x00);
assert_eq!(cart.read(0x4000), 0x00);
}
#[test]
fn test_mbc5_rom_bank_switch_low_byte_selects_correct_bank() {
let mut cart = Mbc5::new(make_rom(4), make_ram(0), false);
cart.write(0x2000, 0x02);
assert_eq!(cart.read(0x4000), 0x02);
assert_eq!(cart.read(0x7FFF), 0x02);
}
#[test]
fn test_mbc5_rom_bank_switch_to_bank3() {
let mut cart = Mbc5::new(make_rom(4), make_ram(0), false);
cart.write(0x2000, 0x03);
assert_eq!(cart.read(0x4000), 0x03);
}
#[test]
fn test_mbc5_9th_bit_selects_upper_bank() {
let mut cart = Mbc5::new(make_rom(512), make_ram(0), false);
cart.write(0x2000, 0xFF); cart.write(0x3000, 0x01); assert_eq!(cart.read(0x4000), 0xFF);
}
#[test]
fn test_mbc5_9th_bit_only_lowest_bit_matters() {
let mut cart = Mbc5::new(make_rom(4), make_ram(0), false);
cart.write(0x2000, 0x02); cart.write(0x3000, 0xFE); assert_eq!(cart.read(0x4000), 0x02); }
#[test]
fn test_mbc5_9th_bit_can_be_cleared() {
let mut cart = Mbc5::new(make_rom(512), make_ram(0), false);
cart.write(0x2000, 0x01); cart.write(0x3000, 0x01); cart.write(0x3000, 0x00); assert_eq!(cart.read(0x4000), 0x01); }
#[test]
fn test_mbc5_low_byte_write_preserves_high_bit() {
let mut cart = Mbc5::new(make_rom(512), make_ram(0), false);
cart.write(0x3000, 0x01); cart.write(0x2000, 0x05); assert_eq!(cart.read(0x4000), 5u8);
}
#[test]
fn test_mbc5_bank_masking_wraps_to_available_banks() {
let mut cart = Mbc5::new(make_rom(4), make_ram(0), false);
cart.write(0x2000, 0x05);
assert_eq!(cart.read(0x4000), 0x01);
}
#[test]
fn test_mbc5_ram_disabled_by_default_returns_0xff() {
let cart = Mbc5::new(make_rom(2), make_ram(1), false);
assert_eq!(cart.read(0xA000), 0xFF);
}
#[test]
fn test_mbc5_ram_enable_with_0x0a() {
let mut cart = Mbc5::new(make_rom(2), make_ram(1), false);
cart.write(0x0000, 0x0A); cart.write(0xA000, 0x42);
assert_eq!(cart.read(0xA000), 0x42);
}
#[test]
fn test_mbc5_ram_enable_with_any_lower_nibble_a() {
let mut cart = Mbc5::new(make_rom(2), make_ram(1), false);
cart.write(0x0000, 0x1A);
cart.write(0xA000, 0x55);
assert_eq!(cart.read(0xA000), 0x55);
}
#[test]
fn test_mbc5_ram_disable_with_0x00() {
let mut cart = Mbc5::new(make_rom(2), make_ram(1), false);
cart.write(0x0000, 0x0A); cart.write(0xA000, 0x42);
cart.write(0x0000, 0x00); assert_eq!(cart.read(0xA000), 0xFF);
}
#[test]
fn test_mbc5_ram_disable_non_0xa_lower_nibble() {
let mut cart = Mbc5::new(make_rom(2), make_ram(1), false);
cart.write(0x0000, 0x0A); cart.write(0xA000, 0x42);
cart.write(0x0000, 0x0B); assert_eq!(cart.read(0xA000), 0xFF);
}
#[test]
fn test_mbc5_ram_write_ignored_when_disabled() {
let mut cart = Mbc5::new(make_rom(2), make_ram(1), false);
cart.write(0xA000, 0x42);
cart.write(0x0000, 0x0A);
assert_eq!(cart.read(0xA000), 0x00); }
#[test]
fn test_mbc5_empty_ram_returns_0xff_even_when_enabled() {
let mut cart = Mbc5::new(make_rom(2), make_ram(0), false);
cart.write(0x0000, 0x0A); assert_eq!(cart.read(0xA000), 0xFF);
}
#[test]
fn test_mbc5_ram_bank_switching() {
let mut cart = Mbc5::new(make_rom(2), make_ram(2), false);
cart.write(0x0000, 0x0A);
cart.write(0x4000, 0x00); cart.write(0xA000, 0xAA);
cart.write(0x4000, 0x01); cart.write(0xA000, 0xBB);
assert_eq!(cart.read(0xA000), 0xBB);
cart.write(0x4000, 0x00);
assert_eq!(cart.read(0xA000), 0xAA);
}
#[test]
fn test_mbc5_ram_bank_register_masked_to_4_bits() {
let mut cart = Mbc5::new(make_rom(2), make_ram(1), false);
cart.write(0x0000, 0x0A);
cart.write(0x4000, 0xFF); cart.write(0xA000, 0x77);
assert_eq!(cart.read(0xA000), 0x77);
}
#[test]
fn test_mbc5_ram_bank_masking_wraps_to_available_banks() {
let mut cart = Mbc5::new(make_rom(2), make_ram(2), false);
cart.write(0x0000, 0x0A);
cart.write(0x4000, 0x01); cart.write(0xA000, 0xCC);
cart.write(0x4000, 0x03); assert_eq!(cart.read(0xA000), 0xCC);
}
#[test]
fn test_mbc5_rumble_bit3_does_not_affect_ram_bank() {
let mut cart = Mbc5::new(make_rom(2), make_ram(2), true);
cart.write(0x0000, 0x0A);
cart.write(0x4000, 0x00); cart.write(0xA000, 0xAA);
cart.write(0x4000, 0x08); assert_eq!(cart.read(0xA000), 0xAA);
}
#[test]
fn test_mbc5_rumble_ram_bank_masked_to_3_bits() {
let mut cart = Mbc5::new(make_rom(2), make_ram(8), true);
cart.write(0x0000, 0x0A);
cart.write(0x4000, 0x07); cart.write(0xA000, 0xBB);
cart.write(0x4000, 0xFF); assert_eq!(cart.read(0xA000), 0xBB);
}
#[test]
fn test_mbc5_non_rumble_ram_bank_uses_full_4_bits() {
let mut cart = Mbc5::new(make_rom(2), make_ram(16), false);
cart.write(0x0000, 0x0A);
cart.write(0x4000, 0x08); cart.write(0xA000, 0xCC);
cart.write(0x4000, 0x08);
assert_eq!(cart.read(0xA000), 0xCC);
}
}