use alloc::{format, vec};
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use anyhow::{anyhow, bail, Context};
use crate::{
common::{NesRegion, Regional},
mapper::{
m024_m026_vrc6::Vrc6Revision, Axrom, Bf909x, Cnrom, Exrom, Gxrom, Mapper, Mmc1Revision,
Nrom, Pxrom, Sxrom, Txrom, Uxrom, Vrc6,
},
mem::RamState,
ppu::Mirroring,
};
use anyhow::Result;
use core::convert::TryInto;
const PRG_ROM_BANK_SIZE: usize = 0x4000;
const CHR_ROM_BANK_SIZE: usize = 0x2000;
#[derive(Default, Clone)]
#[must_use]
pub struct Cart {
name: String,
header: NesHeader,
region: NesRegion,
ram_state: RamState,
pub(crate) mapper: Mapper,
pub(crate) chr_rom: Vec<u8>,
pub(crate) chr_ram: Vec<u8>,
pub(crate) ex_ram: Vec<u8>,
pub(crate) prg_rom: Vec<u8>,
pub(crate) prg_ram: Vec<u8>, }
impl Cart {
pub fn empty() -> Self {
let mut empty = Self {
name: "Empty Cart".to_string(),
header: NesHeader::default(),
region: NesRegion::default(),
ram_state: RamState::default(),
mapper: Mapper::none(),
chr_rom: vec![0x00; CHR_ROM_BANK_SIZE],
chr_ram: vec![],
ex_ram: vec![],
prg_rom: vec![0x00; PRG_ROM_BANK_SIZE],
prg_ram: vec![],
};
empty.mapper = Nrom::load(&mut empty);
empty
}
pub fn from_rom(name: String, rom_data: Vec<u8>, ram_state: RamState) -> Result<Self>
{
let header = NesHeader::load(&rom_data[0..16])?;
let prg_rom_len = (header.prg_rom_banks as usize) * PRG_ROM_BANK_SIZE;
let mut prg_rom = rom_data[16..16 + prg_rom_len].to_vec();
let prg_ram_size = Self::calculate_ram_size(header.prg_ram_shift).context("prg_ram")?;
let mut prg_ram = vec![0x00; prg_ram_size];
RamState::fill(&mut prg_ram, ram_state);
let chr_rom_len = (header.chr_rom_banks as usize) * CHR_ROM_BANK_SIZE;
let mut chr_rom = if header.chr_rom_banks > 0 {
rom_data[16 + prg_rom_len..16 + prg_rom_len + chr_rom_len].to_vec()
} else {
vec![0x00; chr_rom_len]
};
let mut chr_ram = vec![];
if chr_rom.is_empty() {
let chr_ram_size = Self::calculate_ram_size(header.chr_ram_shift).context("chr_ram")?;
chr_ram.resize(chr_ram_size, 0x00);
RamState::fill(&mut chr_ram, ram_state);
}
let mut cart = Self {
name,
header,
region: NesRegion::default(),
ram_state,
mapper: Mapper::none(),
chr_rom,
chr_ram,
ex_ram: vec![],
prg_rom,
prg_ram,
};
cart.mapper = match cart.header.mapper_num {
0 => Nrom::load(&mut cart),
1 => Sxrom::load(&mut cart, Mmc1Revision::BC),
2 => Uxrom::load(&mut cart),
3 => Cnrom::load(&mut cart),
4 => Txrom::load(&mut cart),
5 => Exrom::load(&mut cart),
7 => Axrom::load(&mut cart),
9 => Pxrom::load(&mut cart),
24 => Vrc6::load(&mut cart, Vrc6Revision::A),
26 => Vrc6::load(&mut cart, Vrc6Revision::B),
66 => Gxrom::load(&mut cart),
71 => Bf909x::load(&mut cart),
155 => Sxrom::load(&mut cart, Mmc1Revision::A),
_ => bail!("unimplemented mapper: {}", cart.header.mapper_num),
};
log::info!("Loaded `{}`", cart);
log::debug!("{:?}", cart);
Ok(cart)
}
#[inline]
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[inline]
#[must_use]
pub fn chr_rom(&self) -> &[u8] {
&self.chr_rom
}
#[inline]
#[must_use]
pub fn chr_ram(&self) -> &[u8] {
&self.chr_ram
}
#[inline]
#[must_use]
pub fn prg_rom(&self) -> &[u8] {
&self.prg_rom
}
#[inline]
#[must_use]
pub fn prg_ram(&self) -> &[u8] {
&self.prg_ram
}
#[inline]
#[must_use]
pub fn has_chr(&self) -> bool {
!self.chr_rom.is_empty() || !self.chr_ram.is_empty()
}
#[inline]
#[must_use]
pub fn chr_len(&self) -> usize {
if !self.chr_rom.is_empty() {
self.chr_rom.len()
} else if !self.chr_ram.is_empty() {
self.chr_ram.len()
} else {
0
}
}
#[inline]
#[must_use]
pub fn has_prg_ram(&self) -> bool {
!self.prg_ram.is_empty()
}
#[inline]
#[must_use]
pub const fn battery_backed(&self) -> bool {
self.header.flags & 0x02 == 0x02
}
#[inline]
pub const fn ram_state(&self) -> RamState {
self.ram_state
}
#[inline]
pub fn mirroring(&self) -> Mirroring {
if self.header.flags & 0x08 == 0x08 {
Mirroring::FourScreen
} else {
match self.header.flags & 0x01 {
0 => Mirroring::Horizontal,
1 => Mirroring::Vertical,
_ => unreachable!("impossible mirroring"),
}
}
}
#[inline]
#[must_use]
pub const fn mapper_num(&self) -> u16 {
self.header.mapper_num
}
#[inline]
#[must_use]
pub const fn submapper_num(&self) -> u8 {
self.header.submapper_num
}
#[inline]
#[must_use]
pub const fn mapper_board(&self) -> &'static str {
self.header.mapper_board()
}
pub(crate) fn add_prg_ram(&mut self, capacity: usize) {
self.prg_ram.resize(capacity, 0x00);
RamState::fill(&mut self.prg_ram, self.ram_state);
}
pub(crate) fn add_chr_ram(&mut self, capacity: usize) {
self.chr_ram.resize(capacity, 0x00);
RamState::fill(&mut self.chr_ram, self.ram_state);
}
pub(crate) fn add_ex_ram(&mut self, capacity: usize) {
self.ex_ram.resize(capacity, 0x00);
RamState::fill(&mut self.ex_ram, self.ram_state);
}
fn calculate_ram_size(value: u8) -> Result<usize> {
if value > 0 {
64usize
.checked_shl(value.into())
.ok_or_else(|| anyhow!("invalid header ram size: ${:02X}", value))
} else {
Ok(0)
}
}
}
impl Regional for Cart {
#[inline]
fn region(&self) -> NesRegion {
self.region
}
#[inline]
fn set_region(&mut self, region: NesRegion) {
self.region = region;
}
}
impl core::fmt::Display for Cart {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::result::Result<(), core::fmt::Error> {
write!(
f,
"{} - {}, CHR-ROM: {}K, CHR-RAM: {}K, PRG-ROM: {}K, PRG-RAM: {}K, Mirroring: {:?}, Battery: {}",
self.name,
self.mapper_board(),
self.chr_rom.len() / 0x0400,
self.chr_ram.len() / 0x0400,
self.prg_rom.len() / 0x0400,
self.prg_ram.len() / 0x0400,
self.mirroring(),
self.battery_backed(),
)
}
}
impl core::fmt::Debug for Cart {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::result::Result<(), core::fmt::Error> {
f.debug_struct("Cart")
.field("name", &self.name)
.field("header", &self.header)
.field("region", &self.region)
.field("ram_state", &self.ram_state)
.field("mapper", &self.mapper)
.field("mirroring", &self.mirroring())
.field("battery_backed", &self.battery_backed())
.field("chr_rom_len", &self.chr_rom.len())
.field("chr_ram_len", &self.chr_ram.len())
.field("ex_ram_len", &self.ex_ram.len())
.field("prg_rom_len", &self.prg_rom.len())
.field("prg_ram_len", &self.prg_ram.len())
.finish()
}
}
#[derive(Default, Copy, Clone, PartialEq, Eq)]
#[must_use]
pub struct NesHeader {
pub version: u8,
pub mapper_num: u16,
pub submapper_num: u8,
pub flags: u8,
pub prg_rom_banks: u16,
pub chr_rom_banks: u16,
pub prg_ram_shift: u8,
pub chr_ram_shift: u8,
pub tv_mode: u8,
pub vs_data: u8, }
impl NesHeader {
pub fn load(header: &[u8]) -> Result<Self> {
assert_eq!(header.len(), 16);
if header[0..4] != *b"NES\x1a" {
bail!("nes header signature not found");
} else if (header[7] & 0x0C) == 0x04 {
bail!("header is corrupted by `DiskDude!`. repair and try again");
} else if (header[7] & 0x0C) == 0x0C {
bail!("unrecognized header format. repair and try again");
}
let mut prg_rom_banks = u16::from(header[4]);
let mut chr_rom_banks = u16::from(header[5]);
let mut mapper_num = u16::from(((header[6] & 0xF0) >> 4) | (header[7] & 0xF0));
let flags = (header[6] & 0x0F) | ((header[7] & 0x0F) << 4);
let mut version = 1; let mut submapper_num = 0;
let mut prg_ram_shift = 0;
let mut chr_ram_shift = 0;
let mut tv_mode = 0;
let mut vs_data = 0;
if header[7] & 0x0C == 0x08 {
version = 2;
mapper_num |= u16::from(header[8] & 0x0F) << 8;
submapper_num = (header[8] & 0xF0) >> 4;
prg_rom_banks |= u16::from(header[9] & 0x0F) << 8;
chr_rom_banks |= u16::from(header[9] & 0xF0) << 4;
prg_ram_shift = header[10];
chr_ram_shift = header[11];
tv_mode = header[12];
vs_data = header[13];
if prg_ram_shift & 0x0F == 0x0F || prg_ram_shift & 0xF0 == 0xF0 {
bail!("invalid prg-ram size in header");
} else if chr_ram_shift & 0x0F == 0x0F || chr_ram_shift & 0xF0 == 0xF0 {
bail!("invalid chr-ram size in header");
} else if chr_ram_shift & 0xF0 == 0xF0 {
bail!("battery-backed chr-ram is currently not supported");
} else if header[14] > 0 || header[15] > 0 {
bail!("unrecognized data found at header offsets 14-15");
}
} else {
for (i, header) in header.iter().enumerate().take(16).skip(8) {
if *header > 0 {
bail!(
"unrecogonized data found at header offset {}. repair and try again",
i,
);
}
}
}
if flags & 0x04 == 0x04 {
bail!("trained roms are currently not supported.");
}
Ok(Self {
version,
mapper_num,
submapper_num,
flags,
prg_rom_banks,
chr_rom_banks,
prg_ram_shift,
chr_ram_shift,
tv_mode,
vs_data,
})
}
#[must_use]
pub const fn mapper_board(&self) -> &'static str {
match self.mapper_num {
0 => "Mapper 000 - NROM",
1 => "Mapper 001 - SxROM/MMC1B/C",
2 => "Mapper 002 - UxROM",
3 => "Mapper 003 - CNROM",
4 => "Mapper 004 - TxROM/MMC3/MMC6",
5 => "Mapper 005 - ExROM/MMC5",
7 => "Mapper 007 - AxROM",
9 => "Mapper 009 - PxROM",
24 => "Mapper 024 - Vrc6a",
26 => "Mapper 026 - Vrc6b",
66 => "Mapper 066 - GxROM/MxROM",
71 => "Mapper 071 - Camerica/Codemasters/BF909x",
155 => "Mapper 155 - SxROM/MMC1A",
_ => "Unimplemented Mapper",
}
}
}
impl core::fmt::Debug for NesHeader {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::result::Result<(), core::fmt::Error> {
f.debug_struct("NesHeader")
.field("version", &self.version)
.field("mapper_num", &format_args!("{:03}", &self.mapper_num))
.field("submapper_num", &self.submapper_num)
.field("flags", &format_args!("0b{:08b}", &self.flags))
.field("prg_rom_banks", &self.prg_rom_banks)
.field("chr_rom_banks", &self.chr_rom_banks)
.field("prg_ram_shift", &self.prg_ram_shift)
.field("chr_ram_shift", &self.chr_ram_shift)
.field("tv_mode", &self.tv_mode)
.field("vs_data", &self.vs_data)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
macro_rules! test_headers {
($(($test:ident, $data:expr, $header:expr$(,)?)),*$(,)?) => {$(
#[test]
fn $test() {
let header = NesHeader::load($data.as_slice()).expect("valid header");
assert_eq!(header, $header);
}
)*};
}
#[rustfmt::skip]
test_headers!(
(
mapper000_horizontal,
[0x4Eu8, 0x45, 0x53, 0x1A,
0x02, 0x01, 0x01, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00],
NesHeader {
version: 1,
mapper_num: 0,
flags: 0b0000_0001,
prg_rom_banks: 2,
chr_rom_banks: 1,
..NesHeader::default()
},
),
(
mapper001_vertical,
[0x4Eu8, 0x45, 0x53, 0x1A,
0x08, 0x00, 0x10, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00],
NesHeader {
version: 1,
mapper_num: 1,
flags: 0b0000_0000,
prg_rom_banks: 8,
chr_rom_banks: 0,
..NesHeader::default()
},
),
);
}