#[allow(clippy::module_inception)]
mod cartridge;
mod mbc0;
mod mbc1;
mod mbc2;
mod mbc5;
pub use cartridge::GbCartridge;
use mbc0::Mbc0;
use mbc1::Mbc1;
use mbc2::Mbc2;
use mbc5::Mbc5;
#[derive(Debug, PartialEq)]
pub enum RomError {
TooShort,
BadHeaderChecksum { expected: u8, actual: u8 },
UnsupportedMbc(u8),
}
fn compute_header_checksum(bytes: &[u8]) -> u8 {
bytes[0x0134..=0x014C]
.iter()
.fold(0u8, |acc, &b| acc.wrapping_sub(b).wrapping_sub(1))
}
fn ram_size_from_byte(byte: u8) -> usize {
match byte {
0x01 => 2 * 1024,
0x02 => 8 * 1024,
0x03 => 32 * 1024,
0x04 => 128 * 1024,
0x05 => 64 * 1024,
_ => 0,
}
}
pub fn load_cartridge(bytes: &[u8]) -> Result<Box<dyn GbCartridge>, RomError> {
if bytes.len() < 0x8000 {
return Err(RomError::TooShort);
}
let expected = compute_header_checksum(bytes);
let actual = bytes[0x014D];
if expected != actual {
return Err(RomError::BadHeaderChecksum { expected, actual });
}
let mbc_type = bytes[0x0147];
match mbc_type {
0x00 => Ok(Box::new(Mbc0::new(bytes.to_vec()))),
0x01..=0x03 => {
let ram_size = ram_size_from_byte(bytes[0x0149]);
Ok(Box::new(Mbc1::new(bytes.to_vec(), vec![0u8; ram_size])))
}
0x05..=0x06 => Ok(Box::new(Mbc2::new(bytes.to_vec()))),
0x19..=0x1E => {
let ram_size = ram_size_from_byte(bytes[0x0149]);
let has_rumble = mbc_type >= 0x1C;
Ok(Box::new(Mbc5::new(
bytes.to_vec(),
vec![0u8; ram_size],
has_rumble,
)))
}
n => Err(RomError::UnsupportedMbc(n)),
}
}
#[cfg(test)]
mod tests {
use super::{RomError, compute_header_checksum, load_cartridge};
fn make_valid_rom(mbc_type: u8, rom_size_byte: u8) -> Vec<u8> {
let bank_count: usize = 2 << (rom_size_byte as usize);
let mut rom = vec![0u8; bank_count * 0x4000];
rom[0x0147] = mbc_type;
rom[0x0148] = rom_size_byte;
rom[0x0149] = 0x00; let checksum = compute_header_checksum(&rom);
rom[0x014D] = checksum;
rom
}
#[test]
fn test_load_returns_error_for_too_short_input() {
let short = vec![0u8; 0x100];
assert!(matches!(load_cartridge(&short), Err(RomError::TooShort)));
}
#[test]
fn test_load_returns_error_for_bad_header_checksum() {
let mut rom = make_valid_rom(0x00, 0x00);
rom[0x014D] = rom[0x014D].wrapping_add(1); let result = load_cartridge(&rom);
assert!(matches!(result, Err(RomError::BadHeaderChecksum { .. })));
}
#[test]
fn test_load_returns_ok_for_valid_mbc0_rom() {
let rom = make_valid_rom(0x00, 0x00);
assert!(load_cartridge(&rom).is_ok());
}
#[test]
fn test_load_returns_ok_for_valid_mbc1_rom() {
let rom = make_valid_rom(0x01, 0x01);
assert!(load_cartridge(&rom).is_ok());
}
#[test]
fn test_load_returns_ok_for_mbc2_rom() {
let rom = make_valid_rom(0x05, 0x00);
assert!(load_cartridge(&rom).is_ok());
}
#[test]
fn test_load_returns_ok_for_mbc2_battery_rom() {
let rom = make_valid_rom(0x06, 0x00);
assert!(load_cartridge(&rom).is_ok());
}
#[test]
fn test_load_returns_ok_for_mbc5_rom() {
let rom = make_valid_rom(0x19, 0x01);
assert!(load_cartridge(&rom).is_ok());
}
#[test]
fn test_load_returns_ok_for_mbc5_ram_battery_rom() {
let rom = make_valid_rom(0x1B, 0x01);
assert!(load_cartridge(&rom).is_ok());
}
#[test]
fn test_load_returns_ok_for_mbc5_rumble_rom() {
let rom = make_valid_rom(0x1C, 0x01);
assert!(load_cartridge(&rom).is_ok());
}
#[test]
fn test_load_returns_ok_for_mbc5_rumble_ram_battery_rom() {
let rom = make_valid_rom(0x1E, 0x01);
assert!(load_cartridge(&rom).is_ok());
}
#[test]
fn test_load_returns_error_for_unsupported_mbc_type() {
let rom = make_valid_rom(0xFF, 0x00);
assert!(matches!(
load_cartridge(&rom),
Err(RomError::UnsupportedMbc(0xFF))
));
}
}