mod mapper;
pub use mapper::Mapper;
pub mod mappers;
use crate::core::cartridge::mapper::get_mapper;
use log::*;
use serde::{Deserialize, Serialize};
use std::{
cmp::max,
fmt::{Debug, Display},
};
#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)]
pub enum NametableArrangement {
OneScreen,
Horizontal,
Vertical,
Custom,
}
#[derive(Clone, Serialize, Deserialize)]
pub struct CartridgeMemory {
pub prg_ram: Vec<u8>,
pub prg_rom: Vec<u8>,
pub chr_ram: Vec<u8>,
pub chr_rom: Vec<u8>,
pub nametable_arrangement: NametableArrangement,
}
impl CartridgeMemory {
pub fn read_chr(&self, addr: usize) -> u8 {
if self.chr_rom.is_empty() {
self.chr_ram[addr % self.chr_ram.len()]
} else {
self.chr_rom[addr % self.chr_rom.len()]
}
}
pub fn write_chr(&mut self, addr: usize, value: u8) {
if !self.chr_ram.is_empty() {
let i = addr % self.chr_ram.len();
self.chr_ram[i] = value;
}
}
pub fn read_prg_ram(&self, addr: usize) -> u8 {
if self.prg_ram.is_empty() {
0
} else {
self.prg_ram[addr % self.prg_ram.len()]
}
}
pub fn write_prg_ram(&mut self, addr: usize, value: u8) {
if !self.prg_ram.is_empty() {
let i = addr % self.prg_ram.len();
self.prg_ram[i] = value;
}
}
pub fn read_prg_rom(&self, addr: usize) -> u8 {
if self.prg_rom.is_empty() {
0
} else {
self.prg_rom[addr % self.prg_rom.len()]
}
}
}
#[derive(Serialize, Deserialize)]
pub struct Cartridge {
pub memory: CartridgeMemory,
pub mapper: Box<dyn Mapper>,
has_battery_ram: bool,
}
impl Cartridge {
pub fn from_ines(
bytes: &[u8],
savedata: Option<Vec<u8>>,
) -> Result<Cartridge, std::string::String> {
if cfg!(debug_assertions) {
assert_eq!(bytes[0], b'N');
assert_eq!(bytes[1], b'E');
assert_eq!(bytes[2], b'S');
assert_eq!(bytes[3], 0x1A);
}
let prg_rom_size = 0x4000 * bytes[4] as usize;
let chr_rom_size = 0x2000 * bytes[5] as usize;
let prg_ram_size = max(bytes[8] as usize * 0x2000, 0x2000);
let mut chr_ram_size = if chr_rom_size == 0 { 0x2000 } else { 0x0 };
debug!("Cartridge header: {:X?}", &bytes[0..16]);
let has_battery_ram = (bytes[6] & 0x02) != 0;
let has_trainer = (bytes[6] & 0x04) != 0;
let alt_nametable_layout = (bytes[6] & 0x08) != 0;
debug!(
"Trainer: {}, alternate nametable: {}, battery backed ram: {}",
has_trainer, alt_nametable_layout, has_battery_ram
);
let total_file_size = 16 + if has_trainer { 512 } else { 0 } + prg_rom_size + chr_rom_size;
debug!(
"Total data size: {:X} bytes. File size: {:X}",
total_file_size,
bytes.len()
);
let file_type = if bytes[7] & 0x0C == 0x08 && bytes.len() >= total_file_size {
debug!("iNES 2.0 detected");
0
} else if bytes[7] & 0x0C == 0x04 {
debug!("Archaic iNES detected");
chr_ram_size = 0;
1
} else if bytes[7] & 0x0C == 0x00 {
debug!("iNES detected");
2
} else {
debug!("Archaic iNES probably detected");
1
};
debug!(
"Detected as {}, ignoring.",
if bytes[9] & 0x01 != 0 { "PAL" } else { "NTSC" }
);
debug!(
"{:X} bytes PRG ROM, {:X} bytes CHR ROM, {:X} bytes PRG RAM, {:X} bytes CHR RAM",
prg_rom_size, chr_rom_size, prg_ram_size, chr_ram_size
);
let mapper_id = (bytes[6] >> 4) + if file_type != 1 { bytes[7] & 0xF0 } else { 0 };
let nametable_arrangement = if (bytes[6] & 0x01) == 0 {
NametableArrangement::Vertical
} else {
NametableArrangement::Horizontal
};
debug!(
"Cartridge is using a {:?} nametable arrangment",
nametable_arrangement
);
debug!(
"Cartridge {} using an alternative nametable arrangement",
if (bytes[6] & 0x08) == 0 {
"isn't"
} else {
"is"
}
);
debug!(
"Cartridge is using {} mapper (0x{:X})",
mapper_id, mapper_id
);
let mapper = match get_mapper(mapper_id as usize) {
Some(s) => s,
None => return Err(format!("Unsupported mapper number: {}", mapper_id)),
};
let mut start = 16 + if has_trainer { 512 } else { 0 };
let mut end = start + prg_rom_size;
let prg_rom = bytes[start..end].to_vec();
start = end;
end += chr_rom_size;
debug!("Reading CHR ROM at {:#X}", start);
let chr_rom = bytes[start..end].to_vec();
let prg_ram = match savedata {
Some(data) => {
assert_eq!(data.len(), prg_ram_size);
data
}
None => vec![0; prg_ram_size],
};
Ok(Cartridge {
memory: CartridgeMemory {
prg_rom,
chr_rom,
prg_ram,
chr_ram: vec![0; chr_ram_size],
nametable_arrangement,
},
mapper,
has_battery_ram,
})
}
pub fn read_cpu(&self, addr: usize) -> u8 {
self.mapper.read_cpu(addr, &self.memory)
}
pub fn write_cpu(&mut self, addr: usize, value: u8) {
self.mapper.write_cpu(addr, &mut self.memory, value);
}
pub fn read_ppu(&mut self, addr: usize) -> u8 {
self.mapper.read_ppu(addr, &self.memory)
}
pub fn write_ppu(&mut self, addr: usize, value: u8) {
self.mapper.write_ppu(addr, &mut self.memory, value);
}
pub fn get_pattern_table(&self) -> &[u8] {
if self.memory.chr_ram.is_empty() {
return self.memory.chr_rom.as_slice();
}
&self.memory.chr_ram
}
pub fn transform_nametable_addr(&self, addr: usize) -> usize {
let nametable = self.mapper.nametable_arrangement(&self.memory);
match nametable {
NametableArrangement::OneScreen => addr % 0x400,
NametableArrangement::Horizontal => {
(addr - 0x2000) % 0x800
}
NametableArrangement::Vertical => {
if addr < 0x2800 {
addr % 0x400
} else {
(addr % 0x400) + 0x400
}
}
NametableArrangement::Custom => self.mapper.transform_nametable_addr(addr),
}
}
pub fn has_battery_backed_ram(&self) -> bool {
self.has_battery_ram
}
pub fn nametable_arrangement(&self) -> NametableArrangement {
self.mapper.nametable_arrangement(&self.memory)
}
pub fn advance_cpu_cycles(&mut self, cycles: u32) {
self.mapper.advance_cpu_cycles(cycles);
}
}
impl Display for Cartridge {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Display::fmt(&self.mapper, f)
}
}
impl Debug for Cartridge {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Debug::fmt(&self.mapper, f)
}
}