Skip to content

Commit

Permalink
core: add cli option for overriding mbc type
Browse files Browse the repository at this point in the history
You can know pass, for example, `--mbc='MBC1,0x8000,0x2000'` to the cli
to override the mbc type, rom size and ram size.
  • Loading branch information
Rodrigodd committed Nov 29, 2024
1 parent 625334e commit 5bda6a1
Show file tree
Hide file tree
Showing 5 changed files with 352 additions and 98 deletions.
299 changes: 208 additions & 91 deletions core/src/gameboy/cartridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self, String> {
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<Self> {
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,
Expand Down Expand Up @@ -229,11 +397,32 @@ impl SaveState for Cartridge {
Ok(())
}
}

#[allow(clippy::result_large_err)]
impl Cartridge {
pub fn new(rom: Vec<u8>) -> Result<Self, (String, Option<Self>)> {
Self::new_maybe_with_spec(rom, None)
}

pub fn new_with_spec_str(
rom: Vec<u8>,
spec: Option<&str>,
) -> Result<Self, (String, Option<Self>)> {
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<u8>) -> Result<Self, (String, Option<Self>)> {
fn new_maybe_with_spec(
mut rom: Vec<u8>,
spec: Option<MbcSpecification>,
) -> Result<Self, (String, Option<Self>)> {
let mut error = String::new();

let header = match CartridgeHeader::from_bytes(&rom) {
Expand All @@ -245,106 +434,34 @@ 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 {
header,
lower_bank: 0,
upper_bank: 1,
rom,
ram: vec![0; ram_size],
ram: vec![0; spec.ram_size],
mbc,
};

Expand Down
4 changes: 4 additions & 0 deletions core/src/parser/mod.rs
Original file line number Diff line number Diff line change
@@ -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<u32, std::io::Error> {
let mut value = [0; 4];
file.read_exact(&mut value)?;
Expand Down
Loading

0 comments on commit 5bda6a1

Please sign in to comment.