use std::io::Read;
use std::path::Path;
pub const BANK_SIZE: usize = 0x2000;
const SF2_PAGE_SIZE: usize = 0x80_000;
#[derive(Clone, Debug)]
pub struct Cartridge {
rom: Vec<u8>,
sf2: bool,
sf2_page: u8,
}
impl Cartridge {
#[must_use]
pub fn from_bytes(mut data: Vec<u8>) -> Self {
if data.len() % BANK_SIZE == 512 {
data.drain(0..512);
}
let sf2 = data.len() > 0x10_0000;
Self {
rom: data,
sf2,
sf2_page: 0,
}
}
#[must_use]
pub fn bank_count(&self) -> usize {
self.rom.len().div_ceil(BANK_SIZE)
}
pub fn from_path(path: impl AsRef<Path>) -> std::io::Result<Self> {
let path = path.as_ref();
let bytes = std::fs::read(path)?;
let is_zip = bytes.starts_with(b"PK\x03\x04")
|| path
.extension()
.is_some_and(|e| e.eq_ignore_ascii_case("zip"));
if is_zip {
Self::from_zip_bytes(&bytes)
} else {
Ok(Self::from_bytes(bytes))
}
}
pub fn from_zip_bytes(bytes: &[u8]) -> std::io::Result<Self> {
let reader = std::io::Cursor::new(bytes);
let mut archive = zip::ZipArchive::new(reader)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
let mut chosen: Option<usize> = None;
let mut fallback: Option<usize> = None;
for i in 0..archive.len() {
let file = archive
.by_index(i)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
if !file.is_file() {
continue;
}
let name = file.name().to_ascii_lowercase();
if name.ends_with(".pce") || name.ends_with(".bin") {
chosen = Some(i);
break;
}
fallback.get_or_insert(i);
}
let index = chosen.or(fallback).ok_or_else(|| {
std::io::Error::new(std::io::ErrorKind::NotFound, "no ROM entry in zip archive")
})?;
let mut file = archive
.by_index(index)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
let mut data = Vec::with_capacity(usize::try_from(file.size()).unwrap_or(0));
file.read_to_end(&mut data)?;
Ok(Self::from_bytes(data))
}
#[must_use]
pub fn read(&self, phys: u32) -> u8 {
if self.rom.is_empty() {
return 0xFF;
}
let mut index = phys as usize;
if self.sf2 {
let bank = (phys >> 13) & 0xFF;
if (0x40..=0x7F).contains(&bank) {
index += self.sf2_page as usize * SF2_PAGE_SIZE;
}
}
self.rom[index % self.rom.len()]
}
pub fn write(&mut self, phys: u32, _value: u8) {
if self.sf2 && (phys >> 13) == 0 && (phys & 0x1FF0) == 0x1FF0 {
self.sf2_page = (phys & 0x0F) as u8;
}
}
}