use super::cartridge::GbCartridge;
#[derive(Debug, Clone, Copy, PartialEq)]
enum EepromState {
Idle,
ReadCommand,
WriteCommand,
WriteData,
}
pub struct Mbc7 {
rom: Vec<u8>,
eeprom: [u8; 256],
rom_bank: u8,
access_enabled: bool,
eeprom_state: EepromState,
eeprom_addr: u8,
}
impl Mbc7 {
pub fn new(rom: Vec<u8>) -> Self {
Self {
rom,
eeprom: [0xFF; 256],
rom_bank: 1,
access_enabled: false,
eeprom_state: EepromState::Idle,
eeprom_addr: 0,
}
}
fn rom_bank_count(&self) -> usize {
(self.rom.len() / 0x4000).max(1)
}
fn effective_rom_bank(&self) -> usize {
(self.rom_bank as usize) % self.rom_bank_count()
}
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_eeprom_or_accel(&self, _addr: u16) -> u8 {
match self.eeprom_state {
EepromState::Idle => {
0x80
}
EepromState::ReadCommand => {
self.eeprom[self.eeprom_addr as usize]
}
EepromState::WriteCommand | EepromState::WriteData => {
0xFF
}
}
}
fn write_eeprom_register(&mut self, val: u8) {
let cs = (val & 0x80) != 0;
if !cs {
self.eeprom_state = EepromState::Idle;
return;
}
match val {
0x80..=0xFF => {
if (val & 0x40) != 0 {
self.eeprom_addr = val & 0x7F;
self.eeprom_state = EepromState::ReadCommand;
} else {
self.eeprom_addr = val & 0x7F;
self.eeprom_state = EepromState::WriteCommand;
}
}
0x00..=0x7F => {
if self.eeprom_state == EepromState::WriteCommand {
self.eeprom_state = EepromState::WriteData;
} else if self.eeprom_state == EepromState::WriteData {
self.eeprom[self.eeprom_addr as usize] = val;
self.eeprom_state = EepromState::Idle;
}
}
}
}
fn write_registers(&mut self, addr: u16, val: u8) {
match addr {
0x0000..=0x1FFF => {
self.access_enabled = (val & 0x0F) == 0x0A;
}
0x2000..=0x3FFF => {
self.rom_bank = val & 0x7F;
}
0x4000..=0x5FFF => {
self.write_eeprom_register(val);
}
_ => {}
}
}
fn write_eeprom_data(&mut self, addr: u16, val: u8) {
if !self.access_enabled {
return;
}
let offset = addr as usize - 0xA000;
if offset < 256 {
self.eeprom[offset] = val;
}
}
}
impl GbCartridge for Mbc7 {
fn read(&self, addr: u16) -> u8 {
match addr {
0x0000..=0x7FFF => self.read_rom(addr),
0xA000..=0xBFFF => {
if self.access_enabled {
self.read_eeprom_or_accel(addr)
} else {
0xFF
}
}
_ => 0xFF,
}
}
fn write(&mut self, addr: u16, val: u8) {
match addr {
0x0000..=0x7FFF => self.write_registers(addr, val),
0xA000..=0xBFFF => self.write_eeprom_data(addr, val),
_ => {}
}
}
fn mbc_state_snapshot(&self) -> Vec<u8> {
let mut snapshot = vec![self.rom_bank, self.access_enabled as u8];
snapshot.extend_from_slice(&self.eeprom);
snapshot
}
fn restore_mbc_state(&mut self, data: &[u8]) {
if data.len() >= 2 {
self.rom_bank = data[0];
self.access_enabled = data[1] != 0;
}
if data.len() >= 2 + 256 {
self.eeprom.copy_from_slice(&data[2..258]);
}
}
fn has_battery(&self) -> bool {
false
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_rom(size_bytes: usize) -> Vec<u8> {
vec![0u8; size_bytes]
}
#[test]
fn test_rom_bank_selection() {
let rom = make_rom(128 * 0x4000); let mut cart = Mbc7::new(rom);
assert_eq!(cart.effective_rom_bank(), 1);
cart.write_registers(0x2000, 0x00);
assert_eq!(cart.effective_rom_bank(), 0);
cart.write_registers(0x2000, 0x7F);
assert_eq!(cart.effective_rom_bank(), 127);
cart.write_registers(0x2000, 0x80); assert_eq!(cart.effective_rom_bank(), 0);
}
#[test]
fn test_access_enable_disable() {
let rom = make_rom(2 * 0x4000);
let mut cart = Mbc7::new(rom);
assert!(!cart.access_enabled);
assert_eq!(cart.read(0xA000), 0xFF);
cart.write_registers(0x0000, 0x0A);
assert!(cart.access_enabled);
let val = cart.read(0xA000);
assert_ne!(val, 0xFF);
cart.write_registers(0x0000, 0x00);
assert!(!cart.access_enabled);
assert_eq!(cart.read(0xA000), 0xFF);
}
#[test]
fn test_eeprom_write_read() {
let rom = make_rom(2 * 0x4000);
let mut cart = Mbc7::new(rom);
cart.write_registers(0x0000, 0x0A);
cart.write_eeprom_data(0xA000, 0x42);
assert_eq!(cart.eeprom[0], 0x42);
cart.write_eeprom_data(0xA000 + 255, 0x99);
assert_eq!(cart.eeprom[255], 0x99);
}
#[test]
fn test_eeprom_data_persistence() {
let rom = make_rom(2 * 0x4000);
let mut cart = Mbc7::new(rom);
cart.write_registers(0x0000, 0x0A);
for i in 0..256 {
cart.write_eeprom_data(0xA000 + i as u16, (i as u8).wrapping_mul(3));
}
for i in 0..256 {
assert_eq!(cart.eeprom[i], (i as u8).wrapping_mul(3));
}
}
#[test]
fn test_accelerometer_neutral_values() {
let rom = make_rom(2 * 0x4000);
let cart = Mbc7::new(rom);
assert_eq!(cart.read_eeprom_or_accel(0xA000), 0x80);
assert_eq!(cart.read_eeprom_or_accel(0xA001), 0x80);
}
#[test]
fn test_state_snapshot_restore() {
let rom = make_rom(4 * 0x4000);
let mut cart = Mbc7::new(rom.clone());
cart.write_registers(0x0000, 0x0A);
cart.write_registers(0x2000, 0x10);
cart.write_eeprom_data(0xA000, 0x42);
cart.write_eeprom_data(0xA001, 0x99);
let snapshot = cart.mbc_state_snapshot();
let mut cart2 = Mbc7::new(rom);
cart2.restore_mbc_state(&snapshot);
assert_eq!(cart2.rom_bank, 0x10);
assert!(cart2.access_enabled);
assert_eq!(cart2.eeprom[0], 0x42);
assert_eq!(cart2.eeprom[1], 0x99);
}
#[test]
fn test_rom_read() {
let mut rom_data = make_rom(4 * 0x4000);
rom_data[0x0100] = 0xAA;
rom_data[0x4100] = 0xBB;
rom_data[0x8100] = 0xCC;
let mut cart = Mbc7::new(rom_data);
assert_eq!(cart.read(0x0100), 0xAA);
assert_eq!(cart.read(0x4100), 0xBB);
cart.write_registers(0x2000, 0x02);
assert_eq!(cart.read(0x4100), 0xCC);
}
#[test]
fn test_out_of_range_reads() {
let rom = make_rom(2 * 0x4000);
let cart = Mbc7::new(rom);
assert_eq!(cart.read(0x8000), 0xFF);
assert_eq!(cart.read(0x9000), 0xFF);
assert_eq!(cart.read(0xC000), 0xFF);
assert_eq!(cart.read(0xE000), 0xFF);
}
#[test]
fn test_disabled_writes() {
let rom = make_rom(2 * 0x4000);
let mut cart = Mbc7::new(rom);
cart.write_eeprom_data(0xA000, 0x42);
assert_eq!(cart.eeprom[0], 0xFF);
cart.write_registers(0x0000, 0x0A);
cart.write_eeprom_data(0xA000, 0x42);
assert_eq!(cart.eeprom[0], 0x42); }
}