refactor(core/cartridge): Refactor cartridge module to make it easier to configure savetypes
Former-commit-id: 2e8dd8c3f60c7de8c55bd4c38eaa0630af73c1cc
This commit is contained in:
parent
3fb75079a2
commit
5fc38546ce
119
src/core/cartridge/backup/backup_file.rs
Normal file
119
src/core/cartridge/backup/backup_file.rs
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
use std::fmt;
|
||||||
|
use std::fs::{File, OpenOptions};
|
||||||
|
use std::io::prelude::*;
|
||||||
|
use std::io::SeekFrom;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use serde::de::{self, Deserialize, Deserializer, SeqAccess, Visitor};
|
||||||
|
use serde::ser::{Serialize, SerializeStruct, Serializer};
|
||||||
|
|
||||||
|
use super::BackupMemoryInterface;
|
||||||
|
use crate::util::write_bin_file;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct BackupFile {
|
||||||
|
size: usize,
|
||||||
|
path: Option<PathBuf>,
|
||||||
|
file: Option<File>,
|
||||||
|
buffer: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clone for BackupFile {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
BackupFile::new(self.size, self.path.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for BackupFile {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
let mut state = serializer.serialize_struct("BackupFile", 2)?;
|
||||||
|
state.serialize_field("size", &self.size)?;
|
||||||
|
state.serialize_field("path", &self.path)?;
|
||||||
|
state.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for BackupFile {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
struct BackupFileVisitor;
|
||||||
|
|
||||||
|
impl<'de> Visitor<'de> for BackupFileVisitor {
|
||||||
|
type Value = BackupFile;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
formatter.write_str("struct BackupFile")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_seq<V>(self, mut seq: V) -> Result<BackupFile, V::Error>
|
||||||
|
where
|
||||||
|
V: SeqAccess<'de>,
|
||||||
|
{
|
||||||
|
let size = seq
|
||||||
|
.next_element()?
|
||||||
|
.ok_or_else(|| de::Error::invalid_length(0, &self))?;
|
||||||
|
let path: Option<PathBuf> = seq
|
||||||
|
.next_element()?
|
||||||
|
.ok_or_else(|| de::Error::invalid_length(1, &self))?;
|
||||||
|
Ok(BackupFile::new(size, path))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const FIELDS: &'static [&'static str] = &["size", "path"];
|
||||||
|
deserializer.deserialize_struct("BackupFile", FIELDS, BackupFileVisitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BackupFile {
|
||||||
|
pub fn new(size: usize, path: Option<PathBuf>) -> BackupFile {
|
||||||
|
// TODO handle errors without unwrap
|
||||||
|
let mut file: Option<File> = None;
|
||||||
|
let buffer = if let Some(path) = &path {
|
||||||
|
if !path.is_file() {
|
||||||
|
write_bin_file(&path, &vec![0xff; size]).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut _file = OpenOptions::new()
|
||||||
|
.read(true)
|
||||||
|
.write(true)
|
||||||
|
.open(&path)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
_file.read_to_end(&mut buffer).unwrap();
|
||||||
|
buffer.resize(size, 0xff);
|
||||||
|
|
||||||
|
file = Some(_file);
|
||||||
|
|
||||||
|
buffer
|
||||||
|
} else {
|
||||||
|
vec![0xff; size]
|
||||||
|
};
|
||||||
|
|
||||||
|
BackupFile {
|
||||||
|
size,
|
||||||
|
path,
|
||||||
|
file: file,
|
||||||
|
buffer: buffer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BackupMemoryInterface for BackupFile {
|
||||||
|
fn write(&mut self, offset: usize, value: u8) {
|
||||||
|
self.buffer[offset] = value;
|
||||||
|
if let Some(file) = &mut self.file {
|
||||||
|
file.seek(SeekFrom::Start(offset as u64)).unwrap();
|
||||||
|
file.write_all(&[value]).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read(&self, offset: usize) -> u8 {
|
||||||
|
self.buffer[offset]
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
use super::{BackupMemory, BackupMemoryInterface};
|
use super::{BackupFile, BackupMemoryInterface};
|
||||||
|
|
||||||
use num::FromPrimitive;
|
use num::FromPrimitive;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
@ -56,7 +56,7 @@ pub struct Flash {
|
||||||
mode: FlashMode,
|
mode: FlashMode,
|
||||||
bank: usize,
|
bank: usize,
|
||||||
|
|
||||||
memory: BackupMemory,
|
memory: BackupFile,
|
||||||
}
|
}
|
||||||
|
|
||||||
const MACRONIX_64K_CHIP_ID: u16 = 0x1CC2;
|
const MACRONIX_64K_CHIP_ID: u16 = 0x1CC2;
|
||||||
|
@ -66,14 +66,14 @@ const SECTOR_SIZE: usize = 0x1000;
|
||||||
const BANK_SIZE: usize = 0x10000;
|
const BANK_SIZE: usize = 0x10000;
|
||||||
|
|
||||||
impl Flash {
|
impl Flash {
|
||||||
pub fn new(flash_path: PathBuf, flash_size: FlashSize) -> Flash {
|
pub fn new(flash_path: Option<PathBuf>, flash_size: FlashSize) -> Flash {
|
||||||
let chip_id = match flash_size {
|
let chip_id = match flash_size {
|
||||||
FlashSize::Flash64k => MACRONIX_64K_CHIP_ID,
|
FlashSize::Flash64k => MACRONIX_64K_CHIP_ID,
|
||||||
FlashSize::Flash128k => MACRONIX_128K_CHIP_ID,
|
FlashSize::Flash128k => MACRONIX_128K_CHIP_ID,
|
||||||
};
|
};
|
||||||
|
|
||||||
let size: usize = flash_size.into();
|
let size: usize = flash_size.into();
|
||||||
let memory = BackupMemory::new(size, flash_path);
|
let memory = BackupFile::new(size, flash_path);
|
||||||
|
|
||||||
Flash {
|
Flash {
|
||||||
chip_id: chip_id,
|
chip_id: chip_id,
|
||||||
|
|
|
@ -1,126 +1,21 @@
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::fs::{File, OpenOptions};
|
|
||||||
use std::io::prelude::*;
|
|
||||||
use std::io::SeekFrom;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use serde::de::{self, Deserialize, Deserializer, SeqAccess, Visitor};
|
|
||||||
use serde::ser::{Serialize, SerializeStruct, Serializer};
|
|
||||||
|
|
||||||
use crate::util::write_bin_file;
|
|
||||||
|
|
||||||
|
mod backup_file;
|
||||||
|
pub use backup_file::BackupFile;
|
||||||
pub mod eeprom;
|
pub mod eeprom;
|
||||||
pub mod flash;
|
pub mod flash;
|
||||||
|
|
||||||
pub const BACKUP_FILE_EXT: &'static str = "sav";
|
#[derive(Debug, Primitive, Serialize, Deserialize, Copy, Clone, PartialEq)]
|
||||||
|
|
||||||
#[derive(Debug, Primitive, Serialize, Deserialize, Clone)]
|
|
||||||
pub enum BackupType {
|
pub enum BackupType {
|
||||||
Eeprom = 0,
|
Eeprom = 0,
|
||||||
Sram = 1,
|
Sram = 1,
|
||||||
Flash = 2,
|
Flash = 2,
|
||||||
Flash512 = 3,
|
Flash512 = 3,
|
||||||
Flash1M = 4,
|
Flash1M = 4,
|
||||||
|
AutoDetect = 5,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait BackupMemoryInterface: Sized + fmt::Debug {
|
pub trait BackupMemoryInterface: Sized + fmt::Debug {
|
||||||
fn write(&mut self, offset: usize, value: u8);
|
fn write(&mut self, offset: usize, value: u8);
|
||||||
fn read(&self, offset: usize) -> u8;
|
fn read(&self, offset: usize) -> u8;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct BackupMemory {
|
|
||||||
size: usize,
|
|
||||||
path: PathBuf,
|
|
||||||
file: File,
|
|
||||||
buffer: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Clone for BackupMemory {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
BackupMemory::new(self.size, self.path.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Serialize for BackupMemory {
|
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: Serializer,
|
|
||||||
{
|
|
||||||
let mut state = serializer.serialize_struct("BackupMemory", 2)?;
|
|
||||||
state.serialize_field("size", &self.size)?;
|
|
||||||
state.serialize_field("path", &self.path)?;
|
|
||||||
state.end()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for BackupMemory {
|
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
struct BackupMemoryVisitor;
|
|
||||||
|
|
||||||
impl<'de> Visitor<'de> for BackupMemoryVisitor {
|
|
||||||
type Value = BackupMemory;
|
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
formatter.write_str("struct BackupMemory")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_seq<V>(self, mut seq: V) -> Result<BackupMemory, V::Error>
|
|
||||||
where
|
|
||||||
V: SeqAccess<'de>,
|
|
||||||
{
|
|
||||||
let size = seq
|
|
||||||
.next_element()?
|
|
||||||
.ok_or_else(|| de::Error::invalid_length(0, &self))?;
|
|
||||||
let path: String = seq
|
|
||||||
.next_element()?
|
|
||||||
.ok_or_else(|| de::Error::invalid_length(1, &self))?;
|
|
||||||
Ok(BackupMemory::new(size, PathBuf::from(path)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const FIELDS: &'static [&'static str] = &["size", "path"];
|
|
||||||
deserializer.deserialize_struct("BackupMemory", FIELDS, BackupMemoryVisitor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BackupMemory {
|
|
||||||
pub fn new(size: usize, path: PathBuf) -> BackupMemory {
|
|
||||||
// TODO handle errors without unwrap
|
|
||||||
if !path.is_file() {
|
|
||||||
write_bin_file(&path, &vec![0xff; size]).unwrap();
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut file = OpenOptions::new()
|
|
||||||
.read(true)
|
|
||||||
.write(true)
|
|
||||||
.open(&path)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let mut buffer = Vec::new();
|
|
||||||
file.read_to_end(&mut buffer).unwrap();
|
|
||||||
buffer.resize(size, 0xff);
|
|
||||||
|
|
||||||
BackupMemory {
|
|
||||||
size,
|
|
||||||
path,
|
|
||||||
file: file,
|
|
||||||
buffer: buffer,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BackupMemoryInterface for BackupMemory {
|
|
||||||
fn write(&mut self, offset: usize, value: u8) {
|
|
||||||
self.buffer[offset] = value;
|
|
||||||
self.file.seek(SeekFrom::Start(offset as u64)).unwrap();
|
|
||||||
self.file.write_all(&[value]).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read(&self, offset: usize) -> u8 {
|
|
||||||
self.buffer[offset]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
178
src/core/cartridge/builder.rs
Normal file
178
src/core/cartridge/builder.rs
Normal file
|
@ -0,0 +1,178 @@
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::prelude::*;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use memmem::{Searcher, TwoWaySearcher};
|
||||||
|
use num::FromPrimitive;
|
||||||
|
use zip::ZipArchive;
|
||||||
|
|
||||||
|
use super::super::{GBAError, GBAResult};
|
||||||
|
use super::backup::eeprom::*;
|
||||||
|
use super::backup::flash::*;
|
||||||
|
use super::backup::{BackupFile, BackupType};
|
||||||
|
use super::header;
|
||||||
|
use super::BackupMedia;
|
||||||
|
use super::Cartridge;
|
||||||
|
|
||||||
|
use crate::util::read_bin_file;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct GamepakBuilder {
|
||||||
|
path: Option<PathBuf>,
|
||||||
|
bytes: Option<Box<[u8]>>,
|
||||||
|
save_type: BackupType,
|
||||||
|
create_backup_file: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GamepakBuilder {
|
||||||
|
pub fn new() -> GamepakBuilder {
|
||||||
|
GamepakBuilder {
|
||||||
|
save_type: BackupType::AutoDetect,
|
||||||
|
path: None,
|
||||||
|
bytes: None,
|
||||||
|
create_backup_file: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn buffer(mut self, bytes: &[u8]) -> Self {
|
||||||
|
self.bytes = Some(bytes.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn file(mut self, path: &Path) -> Self {
|
||||||
|
self.path = Some(path.to_path_buf());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_type(mut self, save_type: BackupType) -> Self {
|
||||||
|
self.save_type = save_type;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_sram(mut self) -> Self {
|
||||||
|
self.save_type = BackupType::Sram;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_flash128k(mut self) -> Self {
|
||||||
|
self.save_type = BackupType::Flash1M;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_flash64k(mut self) -> Self {
|
||||||
|
self.save_type = BackupType::Flash512;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_eeprom(mut self) -> Self {
|
||||||
|
self.save_type = BackupType::Eeprom;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn without_backup_to_file(mut self) -> Self {
|
||||||
|
self.create_backup_file = false;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build(mut self) -> GBAResult<Cartridge> {
|
||||||
|
let bytes = if let Some(bytes) = self.bytes {
|
||||||
|
Ok(bytes)
|
||||||
|
} else if let Some(path) = &self.path {
|
||||||
|
let loaded_rom = load_rom(&path)?;
|
||||||
|
Ok(loaded_rom.into())
|
||||||
|
} else {
|
||||||
|
Err(GBAError::CartridgeLoadError(
|
||||||
|
"either provide file() or buffer()".to_string(),
|
||||||
|
))
|
||||||
|
}?;
|
||||||
|
|
||||||
|
let header = header::parse(&bytes);
|
||||||
|
info!("Loaded ROM: {:?}", header);
|
||||||
|
|
||||||
|
if !self.create_backup_file {
|
||||||
|
self.path = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.save_type == BackupType::AutoDetect {
|
||||||
|
let detected = detect_backup_type(&bytes)?;
|
||||||
|
info!("Detected Backup: {:?}", detected);
|
||||||
|
|
||||||
|
self.save_type = detected;
|
||||||
|
}
|
||||||
|
|
||||||
|
let backup = create_backup(self.save_type, self.path);
|
||||||
|
|
||||||
|
let size = bytes.len();
|
||||||
|
Ok(Cartridge {
|
||||||
|
header: header,
|
||||||
|
bytes: bytes,
|
||||||
|
size: size,
|
||||||
|
backup: backup,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const BACKUP_FILE_EXT: &'static str = "sav";
|
||||||
|
fn create_backup(backup_type: BackupType, rom_path: Option<PathBuf>) -> BackupMedia {
|
||||||
|
let backup_path = if let Some(rom_path) = rom_path {
|
||||||
|
Some(rom_path.with_extension(BACKUP_FILE_EXT))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
match backup_type {
|
||||||
|
BackupType::Flash | BackupType::Flash512 => {
|
||||||
|
BackupMedia::Flash(Flash::new(backup_path, FlashSize::Flash64k))
|
||||||
|
}
|
||||||
|
BackupType::Flash1M => BackupMedia::Flash(Flash::new(backup_path, FlashSize::Flash128k)),
|
||||||
|
BackupType::Sram => BackupMedia::Sram(BackupFile::new(0x8000, backup_path)),
|
||||||
|
BackupType::Eeprom => {
|
||||||
|
BackupMedia::Eeprom(SpiController::new(BackupFile::new(0x200, backup_path)))
|
||||||
|
}
|
||||||
|
BackupType::AutoDetect => panic!("called create_backup with backup_type==AutoDetect"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn detect_backup_type(bytes: &[u8]) -> GBAResult<BackupType> {
|
||||||
|
const ID_STRINGS: &'static [&'static str] =
|
||||||
|
&["EEPROM", "SRAM", "FLASH_", "FLASH512_", "FLASH1M_"];
|
||||||
|
|
||||||
|
for i in 0..5 {
|
||||||
|
let search = TwoWaySearcher::new(ID_STRINGS[i].as_bytes());
|
||||||
|
match search.search_in(bytes) {
|
||||||
|
Some(_) => return Ok(BackupType::from_u8(i as u8).unwrap()),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
warn!("could not detect backup save type");
|
||||||
|
return Err(GBAError::CartridgeLoadError(
|
||||||
|
"could not detect backup save type".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_rom(path: &Path) -> GBAResult<Vec<u8>> {
|
||||||
|
match path.extension() {
|
||||||
|
Some(extension) => match extension.to_str() {
|
||||||
|
Some("zip") => {
|
||||||
|
let zipfile = File::open(path)?;
|
||||||
|
let mut archive = ZipArchive::new(zipfile)?;
|
||||||
|
for i in 0..archive.len() {
|
||||||
|
let mut file = archive.by_index(i)?;
|
||||||
|
if file.name().ends_with(".gba") {
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
file.read_to_end(&mut buf)?;
|
||||||
|
return Ok(buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
panic!("no .gba file contained in the zip file");
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let buf = read_bin_file(path)?;
|
||||||
|
return Ok(buf);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
let buf = read_bin_file(path)?;
|
||||||
|
return Ok(buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
59
src/core/cartridge/header.rs
Normal file
59
src/core/cartridge/header.rs
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::str::from_utf8;
|
||||||
|
|
||||||
|
/// From GBATEK
|
||||||
|
///
|
||||||
|
/// The first 192 bytes at 8000000h-80000BFh in ROM are used as cartridge header. The same header is also used for Multiboot images at 2000000h-20000BFh (plus some additional multiboot entries at 20000C0h and up).
|
||||||
|
///
|
||||||
|
/// Header Overview
|
||||||
|
/// Address Bytes Expl.
|
||||||
|
/// 000h 4 ROM Entry Point (32bit ARM branch opcode, eg. "B rom_start")
|
||||||
|
/// 004h 156 Nintendo Logo (compressed bitmap, required!)
|
||||||
|
/// 0A0h 12 Game Title (uppercase ascii, max 12 characters)
|
||||||
|
/// 0ACh 4 Game Code (uppercase ascii, 4 characters)
|
||||||
|
/// 0B0h 2 Maker Code (uppercase ascii, 2 characters)
|
||||||
|
/// 0B2h 1 Fixed value (must be 96h, required!)
|
||||||
|
/// 0B3h 1 Main unit code (00h for current GBA models)
|
||||||
|
/// 0B4h 1 Device type (usually 00h) (bit7=DACS/debug related)
|
||||||
|
/// 0B5h 7 Reserved Area (should be zero filled)
|
||||||
|
/// 0BCh 1 Software version (usually 00h)
|
||||||
|
/// 0BDh 1 Complement check (header checksum, required!)
|
||||||
|
/// 0BEh 2 Reserved Area (should be zero filled)
|
||||||
|
/// --- Additional Multiboot Header Entries ---
|
||||||
|
/// 0C0h 4 RAM Entry Point (32bit ARM branch opcode, eg. "B ram_start")
|
||||||
|
/// 0C4h 1 Boot mode (init as 00h - BIOS overwrites this value!)
|
||||||
|
/// 0C5h 1 Slave ID Number (init as 00h - BIOS overwrites this value!)
|
||||||
|
/// 0C6h 26 Not used (seems to be unused)
|
||||||
|
/// 0E0h 4 JOYBUS Entry Pt. (32bit ARM branch opcode, eg. "B joy_start")
|
||||||
|
///
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub struct CartridgeHeader {
|
||||||
|
// rom_entry_point: Addr,
|
||||||
|
game_title: String,
|
||||||
|
game_code: String,
|
||||||
|
maker_code: String,
|
||||||
|
software_version: u8,
|
||||||
|
checksum: u8,
|
||||||
|
// ram_entry_point: Addr,
|
||||||
|
// joybus_entry_point: Addr,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(bytes: &[u8]) -> CartridgeHeader {
|
||||||
|
// let (_, rom_entry_point) = le_u32(bytes).unwrap();
|
||||||
|
let game_title = from_utf8(&bytes[0xa0..0xac]).unwrap();
|
||||||
|
let game_code = from_utf8(&bytes[0xac..0xb0]).unwrap();
|
||||||
|
let maker_code = from_utf8(&bytes[0xb0..0xb2]).unwrap();
|
||||||
|
// let (_, ram_entry_point) = le_u32(&bytes[0xc0..]).unwrap();
|
||||||
|
// let (_, joybus_entry_point) = le_u32(&bytes[0xc0..]).unwrap();
|
||||||
|
|
||||||
|
CartridgeHeader {
|
||||||
|
// rom_entry_point: rom_entry_point,
|
||||||
|
game_title: String::from(game_title),
|
||||||
|
game_code: String::from(game_code),
|
||||||
|
maker_code: String::from(maker_code),
|
||||||
|
software_version: bytes[0xbc],
|
||||||
|
checksum: bytes[0xbd],
|
||||||
|
// ram_entry_point: ram_entry_point,
|
||||||
|
// joybus_entry_point: joybus_entry_point,
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,100 +1,26 @@
|
||||||
use std::fs::File;
|
|
||||||
use std::io::prelude::*;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use std::str::from_utf8;
|
|
||||||
|
|
||||||
use memmem::{Searcher, TwoWaySearcher};
|
|
||||||
use num::FromPrimitive;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use zip::ZipArchive;
|
|
||||||
|
|
||||||
use super::super::util::read_bin_file;
|
use super::{Addr, Bus};
|
||||||
use super::{Addr, Bus, GBAResult};
|
|
||||||
|
mod header;
|
||||||
|
use header::CartridgeHeader;
|
||||||
|
|
||||||
mod backup;
|
mod backup;
|
||||||
use backup::eeprom::*;
|
use backup::eeprom::SpiController;
|
||||||
use backup::flash::*;
|
use backup::flash::Flash;
|
||||||
use backup::{BackupMemory, BackupMemoryInterface, BackupType, BACKUP_FILE_EXT};
|
use backup::{BackupFile, BackupMemoryInterface};
|
||||||
|
|
||||||
/// From GBATEK
|
mod builder;
|
||||||
///
|
pub use builder::GamepakBuilder;
|
||||||
/// The first 192 bytes at 8000000h-80000BFh in ROM are used as cartridge header. The same header is also used for Multiboot images at 2000000h-20000BFh (plus some additional multiboot entries at 20000C0h and up).
|
|
||||||
///
|
|
||||||
/// Header Overview
|
|
||||||
/// Address Bytes Expl.
|
|
||||||
/// 000h 4 ROM Entry Point (32bit ARM branch opcode, eg. "B rom_start")
|
|
||||||
/// 004h 156 Nintendo Logo (compressed bitmap, required!)
|
|
||||||
/// 0A0h 12 Game Title (uppercase ascii, max 12 characters)
|
|
||||||
/// 0ACh 4 Game Code (uppercase ascii, 4 characters)
|
|
||||||
/// 0B0h 2 Maker Code (uppercase ascii, 2 characters)
|
|
||||||
/// 0B2h 1 Fixed value (must be 96h, required!)
|
|
||||||
/// 0B3h 1 Main unit code (00h for current GBA models)
|
|
||||||
/// 0B4h 1 Device type (usually 00h) (bit7=DACS/debug related)
|
|
||||||
/// 0B5h 7 Reserved Area (should be zero filled)
|
|
||||||
/// 0BCh 1 Software version (usually 00h)
|
|
||||||
/// 0BDh 1 Complement check (header checksum, required!)
|
|
||||||
/// 0BEh 2 Reserved Area (should be zero filled)
|
|
||||||
/// --- Additional Multiboot Header Entries ---
|
|
||||||
/// 0C0h 4 RAM Entry Point (32bit ARM branch opcode, eg. "B ram_start")
|
|
||||||
/// 0C4h 1 Boot mode (init as 00h - BIOS overwrites this value!)
|
|
||||||
/// 0C5h 1 Slave ID Number (init as 00h - BIOS overwrites this value!)
|
|
||||||
/// 0C6h 26 Not used (seems to be unused)
|
|
||||||
/// 0E0h 4 JOYBUS Entry Pt. (32bit ARM branch opcode, eg. "B joy_start")
|
|
||||||
///
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
|
||||||
pub struct CartridgeHeader {
|
|
||||||
// rom_entry_point: Addr,
|
|
||||||
game_title: String,
|
|
||||||
game_code: String,
|
|
||||||
maker_code: String,
|
|
||||||
software_version: u8,
|
|
||||||
checksum: u8,
|
|
||||||
// ram_entry_point: Addr,
|
|
||||||
// joybus_entry_point: Addr,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CartridgeHeader {
|
|
||||||
fn parse(bytes: &[u8]) -> CartridgeHeader {
|
|
||||||
// let (_, rom_entry_point) = le_u32(bytes).unwrap();
|
|
||||||
let game_title = from_utf8(&bytes[0xa0..0xac]).unwrap();
|
|
||||||
let game_code = from_utf8(&bytes[0xac..0xb0]).unwrap();
|
|
||||||
let maker_code = from_utf8(&bytes[0xb0..0xb2]).unwrap();
|
|
||||||
// let (_, ram_entry_point) = le_u32(&bytes[0xc0..]).unwrap();
|
|
||||||
// let (_, joybus_entry_point) = le_u32(&bytes[0xc0..]).unwrap();
|
|
||||||
|
|
||||||
CartridgeHeader {
|
|
||||||
// rom_entry_point: rom_entry_point,
|
|
||||||
game_title: String::from(game_title),
|
|
||||||
game_code: String::from(game_code),
|
|
||||||
maker_code: String::from(maker_code),
|
|
||||||
software_version: bytes[0xbc],
|
|
||||||
checksum: bytes[0xbd],
|
|
||||||
// ram_entry_point: ram_entry_point,
|
|
||||||
// joybus_entry_point: joybus_entry_point,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
pub enum BackupMedia {
|
pub enum BackupMedia {
|
||||||
Sram(BackupMemory),
|
Sram(BackupFile),
|
||||||
Flash(Flash),
|
Flash(Flash),
|
||||||
Eeprom(SpiController<BackupMemory>),
|
Eeprom(SpiController<BackupFile>),
|
||||||
Undetected,
|
Undetected,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BackupMedia {
|
|
||||||
pub fn type_string(&self) -> &'static str {
|
|
||||||
use BackupMedia::*;
|
|
||||||
match self {
|
|
||||||
Sram(..) => "SRAM",
|
|
||||||
Flash(..) => "FLASH",
|
|
||||||
Eeprom(..) => "EEPROM",
|
|
||||||
Undetected => "Undetected",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
pub struct Cartridge {
|
pub struct Cartridge {
|
||||||
pub header: CartridgeHeader,
|
pub header: CartridgeHeader,
|
||||||
|
@ -103,100 +29,6 @@ pub struct Cartridge {
|
||||||
pub(in crate) backup: BackupMedia,
|
pub(in crate) backup: BackupMedia,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_rom(path: &Path) -> GBAResult<Vec<u8>> {
|
|
||||||
match path.extension() {
|
|
||||||
Some(extension) => match extension.to_str() {
|
|
||||||
Some("zip") => {
|
|
||||||
let zipfile = File::open(path)?;
|
|
||||||
let mut archive = ZipArchive::new(zipfile)?;
|
|
||||||
for i in 0..archive.len() {
|
|
||||||
let mut file = archive.by_index(i)?;
|
|
||||||
if file.name().ends_with(".gba") {
|
|
||||||
let mut buf = Vec::new();
|
|
||||||
file.read_to_end(&mut buf)?;
|
|
||||||
return Ok(buf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
panic!("no .gba file contained in the zip file");
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
let buf = read_bin_file(path)?;
|
|
||||||
return Ok(buf);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
_ => {
|
|
||||||
let buf = read_bin_file(path)?;
|
|
||||||
return Ok(buf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Cartridge {
|
|
||||||
pub fn from_path(rom_path: &Path) -> GBAResult<Cartridge> {
|
|
||||||
let rom_bin = load_rom(rom_path)?;
|
|
||||||
Ok(Cartridge::from_bytes(
|
|
||||||
&rom_bin,
|
|
||||||
Some(rom_path.to_path_buf()),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_bytes(bytes: &[u8], rom_path: Option<PathBuf>) -> Cartridge {
|
|
||||||
let size = bytes.len();
|
|
||||||
let header = CartridgeHeader::parse(&bytes);
|
|
||||||
|
|
||||||
let backup = if let Some(path) = rom_path {
|
|
||||||
create_backup(bytes, &path)
|
|
||||||
} else {
|
|
||||||
BackupMedia::Undetected
|
|
||||||
};
|
|
||||||
|
|
||||||
info!("Header: {:?}", header);
|
|
||||||
info!("Backup: {}", backup.type_string());
|
|
||||||
|
|
||||||
Cartridge {
|
|
||||||
header: header,
|
|
||||||
bytes: bytes.into(),
|
|
||||||
size: size,
|
|
||||||
backup: backup,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_backup(bytes: &[u8], rom_path: &Path) -> BackupMedia {
|
|
||||||
let backup_path = rom_path.with_extension(BACKUP_FILE_EXT);
|
|
||||||
if let Some(backup_type) = detect_backup_type(bytes) {
|
|
||||||
match backup_type {
|
|
||||||
BackupType::Flash | BackupType::Flash512 => {
|
|
||||||
BackupMedia::Flash(Flash::new(backup_path, FlashSize::Flash64k))
|
|
||||||
}
|
|
||||||
BackupType::Flash1M => {
|
|
||||||
BackupMedia::Flash(Flash::new(backup_path, FlashSize::Flash128k))
|
|
||||||
}
|
|
||||||
BackupType::Sram => BackupMedia::Sram(BackupMemory::new(0x8000, backup_path)),
|
|
||||||
BackupType::Eeprom => {
|
|
||||||
BackupMedia::Eeprom(SpiController::new(BackupMemory::new(0x200, backup_path)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
BackupMedia::Undetected
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn detect_backup_type(bytes: &[u8]) -> Option<BackupType> {
|
|
||||||
const ID_STRINGS: &'static [&'static str] =
|
|
||||||
&["EEPROM", "SRAM", "FLASH_", "FLASH512_", "FLASH1M_"];
|
|
||||||
|
|
||||||
for i in 0..5 {
|
|
||||||
let search = TwoWaySearcher::new(ID_STRINGS[i].as_bytes());
|
|
||||||
match search.search_in(bytes) {
|
|
||||||
Some(_) => return Some(BackupType::from_u8(i as u8).unwrap()),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
warn!("could not detect backup type");
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
use super::sysbus::consts::*;
|
use super::sysbus::consts::*;
|
||||||
|
|
||||||
pub const EEPROM_BASE_ADDR: u32 = 0x0DFF_FF00;
|
pub const EEPROM_BASE_ADDR: u32 = 0x0DFF_FF00;
|
||||||
|
|
|
@ -191,7 +191,7 @@ mod tests {
|
||||||
|
|
||||||
use super::super::arm7tdmi;
|
use super::super::arm7tdmi;
|
||||||
use super::super::bus::Bus;
|
use super::super::bus::Bus;
|
||||||
use super::super::cartridge::Cartridge;
|
use super::super::cartridge::GamepakBuilder;
|
||||||
|
|
||||||
struct DummyInterface {}
|
struct DummyInterface {}
|
||||||
|
|
||||||
|
@ -208,7 +208,12 @@ mod tests {
|
||||||
fn make_mock_gba(rom: &[u8]) -> GameBoyAdvance {
|
fn make_mock_gba(rom: &[u8]) -> GameBoyAdvance {
|
||||||
let bios = vec![0; 0x4000];
|
let bios = vec![0; 0x4000];
|
||||||
let cpu = arm7tdmi::Core::new();
|
let cpu = arm7tdmi::Core::new();
|
||||||
let cartridge = Cartridge::from_bytes(rom, None);
|
let cartridge = GamepakBuilder::new()
|
||||||
|
.buffer(rom)
|
||||||
|
.with_sram()
|
||||||
|
.without_backup_to_file()
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
let dummy = Rc::new(RefCell::new(DummyInterface::new()));
|
let dummy = Rc::new(RefCell::new(DummyInterface::new()));
|
||||||
let mut gba = GameBoyAdvance::new(
|
let mut gba = GameBoyAdvance::new(
|
||||||
cpu,
|
cpu,
|
||||||
|
|
|
@ -23,14 +23,30 @@ use crate::debugger;
|
||||||
|
|
||||||
use zip;
|
use zip;
|
||||||
|
|
||||||
|
use std::error::Error;
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum GBAError {
|
pub enum GBAError {
|
||||||
IO(::std::io::Error),
|
IO(::std::io::Error),
|
||||||
CpuError(arm7tdmi::CpuError),
|
CpuError(arm7tdmi::CpuError),
|
||||||
|
CartridgeLoadError(String),
|
||||||
#[cfg(feature = "debugger")]
|
#[cfg(feature = "debugger")]
|
||||||
DebuggerError(debugger::DebuggerError),
|
DebuggerError(debugger::DebuggerError),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for GBAError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "error: {:?}", self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error for GBAError {
|
||||||
|
fn description(&self) -> &str {
|
||||||
|
"emulator error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub type GBAResult<T> = Result<T, GBAError>;
|
pub type GBAResult<T> = Result<T, GBAError>;
|
||||||
|
|
||||||
impl From<::std::io::Error> for GBAError {
|
impl From<::std::io::Error> for GBAError {
|
||||||
|
|
|
@ -74,7 +74,7 @@ pub trait InputInterface {
|
||||||
|
|
||||||
pub mod prelude {
|
pub mod prelude {
|
||||||
pub use super::core::arm7tdmi;
|
pub use super::core::arm7tdmi;
|
||||||
pub use super::core::cartridge::Cartridge;
|
pub use super::core::cartridge::GamepakBuilder;
|
||||||
pub use super::core::{GBAError, GBAResult, GameBoyAdvance};
|
pub use super::core::{GBAError, GBAResult, GameBoyAdvance};
|
||||||
#[cfg(feature = "debugger")]
|
#[cfg(feature = "debugger")]
|
||||||
pub use super::debugger::Debugger;
|
pub use super::debugger::Debugger;
|
||||||
|
|
|
@ -90,7 +90,7 @@ fn main() {
|
||||||
let rom_name = rom_path.file_name().unwrap().to_str().unwrap();
|
let rom_name = rom_path.file_name().unwrap().to_str().unwrap();
|
||||||
|
|
||||||
let bios_bin = read_bin_file(bios_path).unwrap();
|
let bios_bin = read_bin_file(bios_path).unwrap();
|
||||||
let cart = Cartridge::from_path(rom_path).unwrap();
|
let cart = GamepakBuilder::new().file(rom_path).build().unwrap();
|
||||||
let mut cpu = arm7tdmi::Core::new();
|
let mut cpu = arm7tdmi::Core::new();
|
||||||
if skip_bios {
|
if skip_bios {
|
||||||
cpu.skip_bios();
|
cpu.skip_bios();
|
||||||
|
|
|
@ -123,12 +123,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let mut savestate_path = get_savestate_path(&Path::new(&rom_path));
|
let mut savestate_path = get_savestate_path(&Path::new(&rom_path));
|
||||||
|
|
||||||
let mut rom_name = Path::new(&rom_path).file_name().unwrap().to_str().unwrap();
|
let mut rom_name = Path::new(&rom_path).file_name().unwrap().to_str().unwrap();
|
||||||
let cart = Cartridge::from_path(Path::new(&rom_path)).unwrap();
|
let gamepak = GamepakBuilder::new().file(Path::new(&rom_path)).build()?;
|
||||||
|
|
||||||
let mut gba = GameBoyAdvance::new(
|
let mut gba = GameBoyAdvance::new(
|
||||||
arm7tdmi::Core::new(),
|
arm7tdmi::Core::new(),
|
||||||
bios_bin,
|
bios_bin,
|
||||||
cart,
|
gamepak,
|
||||||
video.clone(),
|
video.clone(),
|
||||||
audio.clone(),
|
audio.clone(),
|
||||||
input.clone(),
|
input.clone(),
|
||||||
|
@ -229,14 +229,14 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
rom_path = filename;
|
rom_path = filename;
|
||||||
savestate_path = get_savestate_path(&Path::new(&rom_path));
|
savestate_path = get_savestate_path(&Path::new(&rom_path));
|
||||||
rom_name = Path::new(&rom_path).file_name().unwrap().to_str().unwrap();
|
rom_name = Path::new(&rom_path).file_name().unwrap().to_str().unwrap();
|
||||||
let cart = Cartridge::from_path(Path::new(&rom_path)).unwrap();
|
let gamepak = GamepakBuilder::new().file(Path::new(&rom_path)).build()?;
|
||||||
let bios_bin = read_bin_file(bios_path).unwrap();
|
let bios_bin = read_bin_file(bios_path).unwrap();
|
||||||
|
|
||||||
// create a new emulator - TODO, export to a function
|
// create a new emulator - TODO, export to a function
|
||||||
gba = GameBoyAdvance::new(
|
gba = GameBoyAdvance::new(
|
||||||
arm7tdmi::Core::new(),
|
arm7tdmi::Core::new(),
|
||||||
bios_bin,
|
bios_bin,
|
||||||
cart,
|
gamepak,
|
||||||
video.clone(),
|
video.clone(),
|
||||||
audio.clone(),
|
audio.clone(),
|
||||||
input.clone(),
|
input.clone(),
|
||||||
|
|
Reference in a new issue