From 668478b8a4afd6c0a5934a760d5b9b9e85e6633a Mon Sep 17 00:00:00 2001 From: Rodrigo Batista de Moraes Date: Thu, 28 Nov 2024 22:03:04 -0300 Subject: [PATCH] core: add cli option for overriding mbc type You can know pass, for example, `--mbc='MBC1,0x8000,0x2000'` to the cli to override the mbc type, rom size and ram size. --- core/src/gameboy/cartridge.rs | 299 +++++++++++++++++++++++----------- core/src/parser/mod.rs | 4 + core/src/parser/size.rs | 114 +++++++++++++ native/src/main.rs | 19 ++- src/rom_loading.rs | 14 +- 5 files changed, 352 insertions(+), 98 deletions(-) create mode 100644 core/src/parser/size.rs diff --git a/core/src/gameboy/cartridge.rs b/core/src/gameboy/cartridge.rs index 0d318cd..e553d2b 100644 --- a/core/src/gameboy/cartridge.rs +++ b/core/src/gameboy/cartridge.rs @@ -166,6 +166,174 @@ enum Mbc { Mbc5(Mbc5), } +enum MbcKind { + Mbc0, + Mbc1, + Mbc1M, + Mbc2, + Mbc3, + Mbc5, +} + +pub struct MbcSpecification { + // The mbc type, as indicated in the cartridge header. + kind: MbcKind, + // The rom size, in bytes. Should be a value in ROM_SIZES. + rom_size: usize, + // The ram size, in bytes. Should be a value in RAM_SIZES. + ram_size: usize, +} + +impl MbcSpecification { + fn from_str(string: &str) -> Result { + let mut parts = string.split(','); + + let error_message = "expected 3 comma separated values"; + + let kind = parts.next().ok_or(error_message)?; + let rom_size = parts.next().ok_or(error_message)?; + let ram_size = parts.next().ok_or(error_message)?; + + if parts.next().is_some() { + return Err(error_message.to_string()); + } + + let kind = match kind { + "MBC1" => MbcKind::Mbc1, + "MBC1M" => MbcKind::Mbc1M, + "MBC2" => MbcKind::Mbc2, + "MBC3" => MbcKind::Mbc3, + "MBC5" => MbcKind::Mbc5, + _ => return Err(format!("invalid mbc type '{}'", kind)), + }; + + let rom_size = crate::parser::parse_size(rom_size) + .ok() + .or_else(|| crate::parser::parse_number(rom_size).ok()) + .ok_or_else(|| format!("invalid rom size '{}'", rom_size))? + as usize; + + ROM_SIZES + .iter() + .find(|&&x| x == rom_size) + .ok_or_else(|| format!("invalid rom size '{}'", rom_size))?; + + let ram_size = crate::parser::parse_size(ram_size) + .ok() + .or_else(|| crate::parser::parse_number(ram_size).ok()) + .ok_or_else(|| format!("invalid ram size '{}'", ram_size))? + as usize; + + RAM_SIZES + .iter() + .find(|&&x| x == ram_size) + .ok_or_else(|| format!("invalid ram size '{}'", ram_size))?; + + Ok(Self { + kind, + rom_size, + ram_size, + }) + } + + fn from_header(header: &CartridgeHeader, error: &mut String, rom: &[u8]) -> Option { + let rom_size = header + .rom_size_in_bytes() + .ok_or_else(|| format!("Rom size type '{:02x}' is not supported", header.rom_size)); + + let rom_size = match rom_size { + Ok(rom_size) if rom_size != rom.len() => { + let size = *ROM_SIZES.iter().find(|&&x| x >= rom.len()).unwrap(); + writeln!( + error, + "The ROM header expected rom size as '{}' bytes, but the given rom has '{}' bytes. Deducing size from ROM size as {}.", + rom_size, + rom.len(), + size, + ).unwrap(); + size + } + Ok(size) => size, + Err(err) => { + let size = *ROM_SIZES.iter().find(|&&x| x >= rom.len()).unwrap(); + writeln!(error, "{}, deducing size from ROM size as {}", err, size,).unwrap(); + size + } + }; + + // Cartridge Type + let mbc_kind = header.cartridge_type; + let kind = match mbc_kind { + 0 | 8 | 9 => MbcKind::Mbc0, + 1..=3 => 'mbc1: { + // Detect if it is a MBC1M card + if header.rom_size == 5 { + let mut number_of_games = 0; + for i in 0..4 { + let header = match CartridgeHeader::from_bytes(&rom[i * 0x40000..]) { + Ok(x) | Err((Some(x), _)) => x, + Err((None, _)) => continue, + }; + if header.check_logo() { + number_of_games += 1; + } + } + // multicarts will have, at least, a game selecion screen, and two other games. + if number_of_games >= 3 { + break 'mbc1 MbcKind::Mbc1M; + } + } + MbcKind::Mbc1 + } + 5 | 6 => MbcKind::Mbc2, + 0x0F..=0x13 => MbcKind::Mbc3, + 0x19..=0x1E => MbcKind::Mbc5, + _ => { + writeln!( + error, + "MBC type '{}' ({:02x}) is not supported", + mbc_type_name(mbc_kind), + mbc_kind + ) + .unwrap(); + return None; + } + }; + + let ram_size_type = header.ram_size; + + let ram_size = if let MbcKind::Mbc2 = kind { + if ram_size_type != 0 { + writeln!( + error, + "Cartridge use MBC2, with a integrated ram (type '00'), but report the ram type '{:02x}'", + ram_size_type, + ).unwrap(); + } + 0x200 + } else { + match RAM_SIZES.get(ram_size_type as usize).copied() { + Some(x) => x, + None => { + writeln!( + error, + "Ram size type '{:02x}' is not supported, using RAM size as 0x2000", + ram_size_type, + ) + .unwrap(); + 0x2000 + } + } + }; + + Some(Self { + kind, + rom_size, + ram_size, + }) + } +} + #[derive(PartialEq, Eq, Clone)] pub struct Cartridge { pub header: CartridgeHeader, @@ -229,11 +397,32 @@ impl SaveState for Cartridge { Ok(()) } } + +#[allow(clippy::result_large_err)] impl Cartridge { + pub fn new(rom: Vec) -> Result)> { + Self::new_maybe_with_spec(rom, None) + } + + pub fn new_with_spec_str( + rom: Vec, + spec: Option<&str>, + ) -> Result)> { + Self::new_maybe_with_spec( + rom, + match spec { + None => None, + Some(spec) => Some(MbcSpecification::from_str(spec).map_err(|x| (x, None))?), + }, + ) + } + /// Create a new cartridge from the given ROM. If the ROM is invalid, return the a error /// message and a deduced cartridge, if possible. - #[allow(clippy::result_large_err)] - pub fn new(mut rom: Vec) -> Result)> { + fn new_maybe_with_spec( + mut rom: Vec, + spec: Option, + ) -> Result)> { let mut error = String::new(); let header = match CartridgeHeader::from_bytes(&rom) { @@ -245,98 +434,26 @@ impl Cartridge { Err((None, err)) => return Err((err, None)), }; - let rom_size = header - .rom_size_in_bytes() - .ok_or_else(|| format!("Rom size type '{:02x}' is not supported", header.rom_size)); - - match rom_size { - Ok(rom_size) if rom_size != rom.len() => { - let size = *ROM_SIZES.iter().find(|&&x| x >= rom.len()).unwrap(); - writeln!( - &mut error, - "The ROM header expected rom size as '{}' bytes, but the given rom has '{}' bytes. Deducing size from ROM size as {}.", - rom_size, - rom.len(), - size, - ).unwrap(); - rom.resize(size, 0); - } - Ok(_) => {} - Err(err) => { - let size = *ROM_SIZES.iter().find(|&&x| x >= rom.len()).unwrap(); - writeln!( - &mut error, - "{}, deducing size from ROM size as {}", - err, size, - ) - .unwrap(); - rom.resize(size, 0); - } + let spec = match spec { + Some(spec) => spec, + None => match MbcSpecification::from_header(&header, &mut error, &rom) { + Some(v) => v, + None => return Err((error, None)), + }, }; - // Cartridge Type - let mbc_kind = header.cartridge_type; - let mbc = match mbc_kind { - 0 | 8 | 9 => Mbc::None(Mbc0 {}), - 1..=3 => 'mbc1: { - // Detect if it is a MBC1M card - if header.rom_size == 5 { - let mut number_of_games = 0; - for i in 0..4 { - let header = match CartridgeHeader::from_bytes(&rom[i * 0x40000..]) { - Ok(x) | Err((Some(x), _)) => x, - Err((None, _)) => continue, - }; - if header.check_logo() { - number_of_games += 1; - } - } - // multicarts will have, at least, a game selecion screen, and two other games. - if number_of_games >= 3 { - break 'mbc1 Mbc::Mbc1M(Mbc1M::new()); - } - } - Mbc::Mbc1(Mbc1::new()) - } - 5 | 6 => Mbc::Mbc2(Mbc2::new()), - 0x0F..=0x13 => Mbc::Mbc3(Mbc3::new()), - 0x19..=0x1E => Mbc::Mbc5(Mbc5::new()), - _ => { - writeln!( - &mut error, - "MBC type '{}' ({:02x}) is not supported", - mbc_type_name(mbc_kind), - mbc_kind - ) - .unwrap(); - return Err((error, None)); - } - }; + let rom_size = spec.rom_size; - let ram_size_type = header.ram_size; + // resize the rom in case the header expected a different size + rom.resize(rom_size, 0); - let ram_size = if let Mbc::Mbc2(_) = mbc { - if ram_size_type != 0 { - writeln!( - &mut error, - "Cartridge use MBC2, with a integrated ram (type '00'), but report the ram type '{:02x}'", - ram_size_type, - ).unwrap(); - } - 0x200 - } else { - match RAM_SIZES.get(ram_size_type as usize).copied() { - Some(x) => x, - None => { - writeln!( - &mut error, - "Ram size type '{:02x}' is not supported, using RAM size as 0x2000", - ram_size_type, - ) - .unwrap(); - 0x2000 - } - } + let mbc = match spec.kind { + MbcKind::Mbc0 => Mbc::None(Mbc0 {}), + MbcKind::Mbc1 => Mbc::Mbc1(Mbc1::new()), + MbcKind::Mbc1M => Mbc::Mbc1M(Mbc1M::new()), + MbcKind::Mbc2 => Mbc::Mbc2(Mbc2::new()), + MbcKind::Mbc3 => Mbc::Mbc3(Mbc3::new()), + MbcKind::Mbc5 => Mbc::Mbc5(Mbc5::new()), }; let cartridge = Self { @@ -344,7 +461,7 @@ impl Cartridge { lower_bank: 0, upper_bank: 1, rom, - ram: vec![0; ram_size], + ram: vec![0; spec.ram_size], mbc, }; diff --git a/core/src/parser/mod.rs b/core/src/parser/mod.rs index 97eff52..8043184 100644 --- a/core/src/parser/mod.rs +++ b/core/src/parser/mod.rs @@ -1,5 +1,9 @@ use std::io::{Read, Seek, SeekFrom}; +mod size; + +pub use size::{parse_number, parse_size}; + fn read_u32(file: &mut impl Read) -> Result { let mut value = [0; 4]; file.read_exact(&mut value)?; diff --git a/core/src/parser/size.rs b/core/src/parser/size.rs new file mode 100644 index 0000000..392dcd4 --- /dev/null +++ b/core/src/parser/size.rs @@ -0,0 +1,114 @@ +use std::num::ParseIntError; + +#[derive(Debug, PartialEq)] +pub enum ParseSizeError { + InvalidFormat, + InvalidSuffix, + NumberTooLarge, + ParseError(ParseIntError), +} + +pub fn parse_number(input: &str) -> Result { + if let Some(stripped) = input.strip_prefix("0x") { + u64::from_str_radix(stripped, 16) + } else { + input.parse() + } +} + +pub fn parse_size(input: &str) -> Result { + if input.is_empty() { + return Err(ParseSizeError::InvalidFormat); + } + + let (num_part, suffix) = input.trim().split_at( + input + .find(|c: char| !c.is_ascii_digit() && c != 'x') + .unwrap_or(input.len()), + ); + + let num = parse_number(num_part).map_err(ParseSizeError::ParseError)?; + + let multiplier = match suffix.trim() { + "B" => 1, + "kB" => 1_024, + "MB" => 1_024 * 1_024, + "GB" => 1_024 * 1_024 * 1_024, + "" => return Err(ParseSizeError::InvalidFormat), + _ => return Err(ParseSizeError::InvalidSuffix), + }; + + let result = num + .checked_mul(multiplier as u64) + .ok_or(ParseSizeError::NumberTooLarge)?; + + if result > u32::MAX as u64 { + return Err(ParseSizeError::NumberTooLarge); + } + + Ok(result) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_number_decimal() { + assert_eq!(parse_number("123"), Ok(123)); + assert_eq!(parse_number("0"), Ok(0)); + } + + #[test] + fn test_parse_number_hexadecimal() { + assert_eq!(parse_number("0x10"), Ok(16)); + assert_eq!(parse_number("0x1F4"), Ok(500)); + } + + #[test] + fn test_parse_number_invalid() { + assert!(parse_number("not_a_number").is_err()); + assert!(parse_number("0xZZZ").is_err()); + } + + #[test] + fn test_parse_size_with_hexadecimal() { + assert_eq!(parse_size("0x100B"), Ok(256)); + assert_eq!(parse_size("0x1kB"), Ok(1_024)); + assert_eq!(parse_size("0x2MB"), Ok(2 * 1_024 * 1_024)); + } + + #[test] + fn test_valid_sizes() { + assert_eq!(parse_size("0B"), Ok(0)); + assert_eq!(parse_size("256B"), Ok(256)); + assert_eq!(parse_size("1kB"), Ok(1_024)); + assert_eq!(parse_size("2MB"), Ok(2 * 1_024 * 1_024)); + assert_eq!(parse_size("1GB"), Ok(1_024 * 1_024 * 1_024)); + } + + #[test] + fn test_invalid_formats() { + assert_eq!(parse_size(""), Err(ParseSizeError::InvalidFormat)); + assert_eq!(parse_size("256"), Err(ParseSizeError::InvalidFormat)); + assert_eq!(parse_size("256MBExtra"), Err(ParseSizeError::InvalidSuffix)); + } + + #[test] + fn test_invalid_suffix() { + assert_eq!(parse_size("256mB"), Err(ParseSizeError::InvalidSuffix)); + assert_eq!(parse_size("256TB"), Err(ParseSizeError::InvalidSuffix)); + } + + #[test] + fn test_parse_errors() { + assert!(matches!( + parse_size("not_a_numberMB"), + Err(ParseSizeError::ParseError(_)) + )); + assert!(matches!( + parse_size("0xnot_hexB"), + Err(ParseSizeError::ParseError(_)) + )); + } +} diff --git a/native/src/main.rs b/native/src/main.rs index 5d77978..268b30b 100644 --- a/native/src/main.rs +++ b/native/src/main.rs @@ -11,7 +11,7 @@ use std::path::PathBuf; use clap::{ArgAction, Args, Parser, Subcommand}; use gameroy_lib::config::parse_screen_size; -use gameroy_lib::{config, gameroy, rom_loading::load_gameboy, RomFile}; +use gameroy_lib::{config, gameroy, rom_loading::load_gameboy_with_spec, RomFile}; mod bench; @@ -35,7 +35,7 @@ pub struct Cli { // // The disassembly produced follows no particular synxtax, and don't show all instructions or // data. It only shows instructions that are statically reachable from the entry point. - #[arg(long)] + #[arg(long, requires("rom_path"))] disassembly: bool, /// Play the given .vbm file @@ -74,6 +74,17 @@ pub struct Cli { #[arg(long, value_name = "WIDTHxHEIGHT")] screen_size: Option, + /// The MBC type of the rom + /// + /// Overrides the MBC type of the rom, useful in case its is not correctly detected. Must be a + /// string in the format ",,", where is the MBC type (either + /// "MBC1", "MBC1M", "MBC2", "MBC3" or "MBC5"), is the size of the rom in bytes, and + /// is the size of the ram in bytes. The sizes must be a power of 2 multiple of + /// 0x4000. The size can be in decimal or hexadecimal format, and can have a suffix of "B", + /// + #[arg(long)] + mbc: Option, + #[command(subcommand)] command: Option, } @@ -202,7 +213,7 @@ pub fn main() { Err(e) => return eprintln!("failed to load '{}': {}", rom_path, e), }; - let gb = load_gameboy(rom, None); + let gb = load_gameboy_with_spec(rom, None, args.mbc.as_deref()); let mut gb = match gb { Ok(x) => x, Err(e) => return eprintln!("failed to load rom: {}", e), @@ -230,7 +241,7 @@ pub fn main() { let file = RomFile::from_path(PathBuf::from(rom_path)); - let gb = load_gameboy(rom, None); + let gb = load_gameboy_with_spec(rom, None, args.mbc.as_deref()); match gb { Ok(x) => Some((file, x)), Err(e) => return eprintln!("failed to load rom: {}", e), diff --git a/src/rom_loading.rs b/src/rom_loading.rs index e3f40f3..498037d 100644 --- a/src/rom_loading.rs +++ b/src/rom_loading.rs @@ -20,13 +20,21 @@ cfg_if::cfg_if! { } pub fn load_gameboy(rom: Vec, ram: Option>) -> Result, String> { + load_gameboy_with_spec(rom, ram, None) +} + +pub fn load_gameboy_with_spec( + rom: Vec, + ram: Option>, + spec: Option<&str>, +) -> Result, String> { let boot_rom = load_boot_rom(); - let mut cartridge = match Cartridge::new(rom) { + let mut cartridge = match Cartridge::new_with_spec_str(rom, spec) { Ok(rom) => Ok(rom), Err((warn, Some(rom))) => { - println!("Warning: {}", warn); - log::error!("{}", warn); + println!("Warning: {}", warn.strip_suffix('\n').unwrap_or(&warn)); + log::warn!("{}", warn); Ok(rom) } Err((err, None)) => Err(err),