Cartridge 模块完整代码

在这一节, 主要为 Cartridge 模块做一个收尾工作. 从工程角度来说, Game Boy 仿真器并不关心具体的 MBC 类型, 因此它会期望 Cartridge 模块提供一个如下的公开函数:

pub fn power_up(path: impl AsRef<Path>) -> Box<dyn Cartridge>;

此函数可以解析指定目录下的 ROM 文件, 并根据 Cartridge Header 中的信息自动选择合适的 MBC 实现来包装 ROM 数据, 最后返回一个 Cartridge trait object. Cartridge trait 定义如下:

pub trait Cartridge: Memory + Stable + Send {
    fn title(&self) -> String {
        let mut buf = String::new();
        let ic = 0x0134;
        let oc = if self.get(0x0143) == 0x80 { 0x013e } else { 0x0143 };
        for i in ic..oc {
            match self.get(i) {
                0 => break,
                v => buf.push(v as char),
            }
        }
        buf
    }
}

当读取到 ROM 的数据时, 正如本章节开头所讲的, Game Boy 会验证数据的合法性, 如果数据非法则不会启动游戏. 有两步验证过程, 分别是任天堂的 LOGO 验证和标题校验和验证, 代码实现如下.

const NINTENDO_LOGO: [u8; 48] = [
    0xCE, 0xED, 0x66, 0x66, 0xCC, 0x0D, 0x00, 0x0B, 0x03, 0x73, 0x00, 0x83,
    0x00, 0x0C, 0x00, 0x0D, 0x00, 0x08, 0x11, 0x1F, 0x88, 0x89, 0x00, 0x0E,
    0xDC, 0xCC, 0x6E, 0xE6, 0xDD, 0xDD, 0xD9, 0x99, 0xBB, 0xBB, 0x67, 0x63,
    0x6E, 0x0E, 0xEC, 0xCC, 0xDD, 0xDC, 0x99, 0x9F, 0xBB, 0xB9, 0x33, 0x3E,
];

fn ensure_logo(cart: &dyn Cartridge) {
    for i in 0..48 {
        if cart.get(0x0104 + i as u16) != NINTENDO_LOGO[i as usize] {
            panic!("Nintendo logo is incorrect")
        }
    }
}

fn ensure_header_checksum(cart: &dyn Cartridge) {
    let mut v: u8 = 0;
    for i in 0x0134..0x014d {
        v = v.wrapping_sub(cart.get(i)).wrapping_sub(1);
    }
    if cart.get(0x014d) != v {
        panic!("Cartridge's header checksum is incorrect")
    }
}

同时为了给 ROM 和 RAM 预分配空间, 需要从 Cartridge Header 中获取 ROM Size 和 RAM Size. 它们在 Header 中均只占用一个字节, 具体映射关系如代码展示.

// Specifies the ROM Size of the cartridge. Typically calculated as "32KB shl N".
fn rom_size(b: u8) -> usize {
    let bank = 16384;
    match b {
        0x00 => bank * 2,
        0x01 => bank * 4,
        0x02 => bank * 8,
        0x03 => bank * 16,
        0x04 => bank * 32,
        0x05 => bank * 64,
        0x06 => bank * 128,
        0x07 => bank * 256,
        0x08 => bank * 512,
        0x52 => bank * 72,
        0x53 => bank * 80,
        0x54 => bank * 96,
        n => panic!("Unsupported rom size: 0x{:02x}", n),
    }
}

// Specifies the size of the external RAM in the cartridge (if any).
fn ram_size(b: u8) -> usize {
    match b {
        0x00 => 0,
        0x01 => 1024 * 2,
        0x02 => 1024 * 8,
        0x03 => 1024 * 32,
        0x04 => 1024 * 128,
        0x05 => 1024 * 64,
        n => panic!("Unsupported ram size: 0x{:02x}", n),
    }
}

最终, 完善 power_up 函数如下所示.

pub fn power_up(path: impl AsRef<Path>) -> Box<dyn Cartridge> {
    let mut f = File::open(path.as_ref()).unwrap();
    let mut rom = Vec::new();
    f.read_to_end(&mut rom).unwrap();
    if rom.len() < 0x150 {
        panic!("Missing required information area which located at 0100-014F")
    }
    let rom_max = rom_size(rom[0x0148]);
    if rom.len() > rom_max {
        panic!("Rom size more than {}", rom_max);
    }
    let cart: Box<dyn Cartridge> = match rom[0x0147] {
        0x00 => Box::new(RomOnly::power_up(rom)),
        0x01 => Box::new(Mbc1::power_up(rom, vec![], "")),
        0x02 => {
            let ram_max = ram_size(rom[0x0149]);
            Box::new(Mbc1::power_up(rom, vec![0; ram_max], ""))
        }
        0x03 => {
            let ram_max = ram_size(rom[0x0149]);
            let sav_path = path.as_ref().to_path_buf().with_extension("sav");
            let ram = ram_read(sav_path.clone(), ram_max);
            Box::new(Mbc1::power_up(rom, ram, sav_path))
        }
        0x05 => {
            let ram_max = 512;
            Box::new(Mbc2::power_up(rom, vec![0; ram_max], ""))
        }
        0x06 => {
            let ram_max = 512;
            let sav_path = path.as_ref().to_path_buf().with_extension("sav");
            let ram = ram_read(sav_path.clone(), ram_max);
            Box::new(Mbc2::power_up(rom, ram, sav_path))
        }
        0x0f => {
            let sav_path = path.as_ref().to_path_buf().with_extension("sav");
            let rtc_path = path.as_ref().to_path_buf().with_extension("rtc");
            Box::new(Mbc3::power_up(rom, vec![], sav_path, rtc_path))
        }
        0x10 => {
            let ram_max = ram_size(rom[0x0149]);
            let sav_path = path.as_ref().to_path_buf().with_extension("sav");
            let ram = ram_read(sav_path.clone(), ram_max);
            let rtc_path = path.as_ref().to_path_buf().with_extension("rtc");
            Box::new(Mbc3::power_up(rom, ram, sav_path, rtc_path))
        }
        0x11 => Box::new(Mbc3::power_up(rom, vec![], "", "")),
        0x12 => {
            let ram_max = ram_size(rom[0x0149]);
            Box::new(Mbc3::power_up(rom, vec![0; ram_max], "", ""))
        }
        0x13 => {
            let ram_max = ram_size(rom[0x0149]);
            let sav_path = path.as_ref().to_path_buf().with_extension("sav");
            let ram = ram_read(sav_path.clone(), ram_max);
            Box::new(Mbc3::power_up(rom, ram, sav_path, ""))
        }
        0x19 => Box::new(Mbc5::power_up(rom, vec![], "")),
        0x1a => {
            let ram_max = ram_size(rom[0x0149]);
            Box::new(Mbc5::power_up(rom, vec![0; ram_max], ""))
        }
        0x1b => {
            let ram_max = ram_size(rom[0x0149]);
            let sav_path = path.as_ref().to_path_buf().with_extension("sav");
            let ram = ram_read(sav_path.clone(), ram_max);
            Box::new(Mbc5::power_up(rom, ram, sav_path))
        }
        0xff => {
            let ram_max = ram_size(rom[0x0149]);
            let sav_path = path.as_ref().to_path_buf().with_extension("sav");
            let ram = ram_read(sav_path.clone(), ram_max);
            Box::new(HuC1::power_up(rom, ram, sav_path))
        }
        n => panic!("Unsupported cartridge type: 0x{:02x}", n),
    };
    ensure_logo(cart.as_ref());
    ensure_header_checksum(cart.as_ref());
    cart
}

在 Game Boy 平台上曾诞生了无数的游戏, 虽然不敢保证, 但绝大多数游戏卡带的信息都能在 https://ladecadence.net/trastero/listado%20juegos%20gameboy.html 网站上找到. 该网站上总共拥有 6000 余个 Game Boy 游戏卡带的详细信息, 这些信息包括: 游戏名称, ROM 大小, RAM 大小, MBC 类型等.

img

读者可以根据这些数据对自己的 Cartridge 模块代码进行简单测试.