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 serde::{Deserialize, Serialize};
|
||||
|
@ -56,7 +56,7 @@ pub struct Flash {
|
|||
mode: FlashMode,
|
||||
bank: usize,
|
||||
|
||||
memory: BackupMemory,
|
||||
memory: BackupFile,
|
||||
}
|
||||
|
||||
const MACRONIX_64K_CHIP_ID: u16 = 0x1CC2;
|
||||
|
@ -66,14 +66,14 @@ const SECTOR_SIZE: usize = 0x1000;
|
|||
const BANK_SIZE: usize = 0x10000;
|
||||
|
||||
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 {
|
||||
FlashSize::Flash64k => MACRONIX_64K_CHIP_ID,
|
||||
FlashSize::Flash128k => MACRONIX_128K_CHIP_ID,
|
||||
};
|
||||
|
||||
let size: usize = flash_size.into();
|
||||
let memory = BackupMemory::new(size, flash_path);
|
||||
let memory = BackupFile::new(size, flash_path);
|
||||
|
||||
Flash {
|
||||
chip_id: chip_id,
|
||||
|
|
|
@ -1,126 +1,21 @@
|
|||
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 flash;
|
||||
|
||||
pub const BACKUP_FILE_EXT: &'static str = "sav";
|
||||
|
||||
#[derive(Debug, Primitive, Serialize, Deserialize, Clone)]
|
||||
#[derive(Debug, Primitive, Serialize, Deserialize, Copy, Clone, PartialEq)]
|
||||
pub enum BackupType {
|
||||
Eeprom = 0,
|
||||
Sram = 1,
|
||||
Flash = 2,
|
||||
Flash512 = 3,
|
||||
Flash1M = 4,
|
||||
AutoDetect = 5,
|
||||
}
|
||||
|
||||
pub trait BackupMemoryInterface: Sized + fmt::Debug {
|
||||
fn write(&mut self, offset: usize, value: 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 zip::ZipArchive;
|
||||
|
||||
use super::super::util::read_bin_file;
|
||||
use super::{Addr, Bus, GBAResult};
|
||||
use super::{Addr, Bus};
|
||||
|
||||
mod header;
|
||||
use header::CartridgeHeader;
|
||||
|
||||
mod backup;
|
||||
use backup::eeprom::*;
|
||||
use backup::flash::*;
|
||||
use backup::{BackupMemory, BackupMemoryInterface, BackupType, BACKUP_FILE_EXT};
|
||||
use backup::eeprom::SpiController;
|
||||
use backup::flash::Flash;
|
||||
use backup::{BackupFile, BackupMemoryInterface};
|
||||
|
||||
/// 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,
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
mod builder;
|
||||
pub use builder::GamepakBuilder;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub enum BackupMedia {
|
||||
Sram(BackupMemory),
|
||||
Sram(BackupFile),
|
||||
Flash(Flash),
|
||||
Eeprom(SpiController<BackupMemory>),
|
||||
Eeprom(SpiController<BackupFile>),
|
||||
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)]
|
||||
pub struct Cartridge {
|
||||
pub header: CartridgeHeader,
|
||||
|
@ -103,100 +29,6 @@ pub struct Cartridge {
|
|||
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::*;
|
||||
|
||||
pub const EEPROM_BASE_ADDR: u32 = 0x0DFF_FF00;
|
||||
|
|
|
@ -191,7 +191,7 @@ mod tests {
|
|||
|
||||
use super::super::arm7tdmi;
|
||||
use super::super::bus::Bus;
|
||||
use super::super::cartridge::Cartridge;
|
||||
use super::super::cartridge::GamepakBuilder;
|
||||
|
||||
struct DummyInterface {}
|
||||
|
||||
|
@ -208,7 +208,12 @@ mod tests {
|
|||
fn make_mock_gba(rom: &[u8]) -> GameBoyAdvance {
|
||||
let bios = vec![0; 0x4000];
|
||||
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 mut gba = GameBoyAdvance::new(
|
||||
cpu,
|
||||
|
|
|
@ -23,14 +23,30 @@ use crate::debugger;
|
|||
|
||||
use zip;
|
||||
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum GBAError {
|
||||
IO(::std::io::Error),
|
||||
CpuError(arm7tdmi::CpuError),
|
||||
CartridgeLoadError(String),
|
||||
#[cfg(feature = "debugger")]
|
||||
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>;
|
||||
|
||||
impl From<::std::io::Error> for GBAError {
|
||||
|
|
|
@ -74,7 +74,7 @@ pub trait InputInterface {
|
|||
|
||||
pub mod prelude {
|
||||
pub use super::core::arm7tdmi;
|
||||
pub use super::core::cartridge::Cartridge;
|
||||
pub use super::core::cartridge::GamepakBuilder;
|
||||
pub use super::core::{GBAError, GBAResult, GameBoyAdvance};
|
||||
#[cfg(feature = "debugger")]
|
||||
pub use super::debugger::Debugger;
|
||||
|
|
|
@ -90,7 +90,7 @@ fn main() {
|
|||
let rom_name = rom_path.file_name().unwrap().to_str().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();
|
||||
if 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 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(
|
||||
arm7tdmi::Core::new(),
|
||||
bios_bin,
|
||||
cart,
|
||||
gamepak,
|
||||
video.clone(),
|
||||
audio.clone(),
|
||||
input.clone(),
|
||||
|
@ -229,14 +229,14 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
rom_path = filename;
|
||||
savestate_path = get_savestate_path(&Path::new(&rom_path));
|
||||
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();
|
||||
|
||||
// create a new emulator - TODO, export to a function
|
||||
gba = GameBoyAdvance::new(
|
||||
arm7tdmi::Core::new(),
|
||||
bios_bin,
|
||||
cart,
|
||||
gamepak,
|
||||
video.clone(),
|
||||
audio.clone(),
|
||||
input.clone(),
|
||||
|
|
Reference in a new issue