pub mod gb;
pub mod gba;
pub mod n3ds;
pub mod nds;
use std::fmt;
use serde::{Serialize, Serializer};
use crate::io::ByteSource;
use crate::{Error, Result};
const HEAD_SIZE: u64 = 0x200;
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Identity {
pub platform: Platform,
pub title: String,
pub code: Option<GameCode>,
pub region: Region,
pub revision: u16,
}
impl fmt::Display for Identity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} ROM {:?}", self.platform, self.title)?;
match self.code {
Some(code) => write!(f, " ({code})"),
None => Ok(()),
}
}
}
pub fn identify<S: ByteSource + ?Sized>(source: &S) -> Result<Identity> {
let head = source.read_up_to(0, HEAD_SIZE)?;
if nds::is_nds(&head) {
Ok(nds::Header::parse(&head)?.identity())
} else if gba::is_gba(&head) {
gba::identify(&head)
} else if gb::is_gb(&head) {
gb::identify(&head)
} else if n3ds::is_n3ds(&head) {
n3ds::identify(source)
} else {
Err(Error::Unsupported(
"not a Game Boy, GBA, DS, or 3DS ROM".into(),
))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Platform {
GameBoy,
GameBoyColor,
GameBoyAdvance,
NintendoDs,
Nintendo3ds,
}
impl fmt::Display for Platform {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::GameBoy => "Game Boy",
Self::GameBoyColor => "Game Boy Color",
Self::GameBoyAdvance => "Game Boy Advance",
Self::NintendoDs => "Nintendo DS",
Self::Nintendo3ds => "Nintendo 3DS",
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct GameCode(pub [u8; 4]);
impl GameCode {
pub fn title(&self) -> &[u8; 3] {
self.0.first_chunk().expect("four bytes")
}
pub fn region(&self) -> Region {
Region::from_letter(self.0[3])
}
}
impl fmt::Display for GameCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0
.iter()
.try_for_each(|&byte| write!(f, "{}", byte as char))
}
}
impl Serialize for GameCode {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.collect_str(self)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Region {
Japan,
Usa,
Europe,
UsaEurope,
Germany,
France,
Italy,
Spain,
Korea,
China,
Australia,
World,
Overseas,
Unknown(char),
}
impl Region {
pub fn from_letter(letter: u8) -> Self {
match letter {
b'J' => Self::Japan,
b'E' => Self::Usa,
b'P' => Self::Europe,
b'O' => Self::UsaEurope,
b'D' => Self::Germany,
b'F' => Self::France,
b'I' => Self::Italy,
b'S' => Self::Spain,
b'K' => Self::Korea,
b'C' => Self::China,
b'U' => Self::Australia,
b'A' => Self::World,
other => Self::Unknown(other as char),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unknown_bytes_are_unsupported() {
let error = identify(&[0u8; 0x200][..]).unwrap_err();
assert!(matches!(error, Error::Unsupported(_)));
assert!(identify(&[][..]).is_err());
}
#[test]
fn game_codes_split_into_title_and_region() {
let code = GameCode(*b"CPUE");
assert_eq!(code.title(), b"CPU");
assert_eq!(code.region(), Region::Usa);
assert_eq!(code.to_string(), "CPUE");
}
}