use serde::Serialize;
use crate::error::RomAnalyzerError;
use crate::region::{Region, check_region_mismatch};
const GB_TITLE_START: usize = 0x134;
const GB_TITLE_END: usize = 0x143;
const GB_DESTINATION: usize = 0x14A;
const GBC_SYSTEM_TYPE: usize = 0x143;
const GBC_TITLE_END: usize = 0x13F;
#[derive(Debug, PartialEq, Clone, Serialize)]
pub struct GbAnalysis {
pub source_name: String,
pub region: Region,
pub region_string: String,
pub region_mismatch: bool,
pub system_type: String,
pub game_title: String,
pub destination_code: u8,
}
impl GbAnalysis {
pub fn print(&self) -> String {
format!(
"{}\n\
System: {}\n\
Game Title: {}\n\
Region Code: 0x{:02X}\n\
Region: {}",
self.source_name, self.system_type, self.game_title, self.destination_code, self.region
)
}
}
pub fn map_region(region_byte: u8) -> (&'static str, Region) {
match region_byte {
0x00 => ("Japan", Region::JAPAN),
0x01 => ("Non-Japan (International)", Region::USA | Region::EUROPE),
_ => ("Unknown", Region::UNKNOWN),
}
}
pub fn analyze_gb_data(data: &[u8], source_name: &str) -> Result<GbAnalysis, RomAnalyzerError> {
const HEADER_SIZE: usize = 0x150;
if data.len() < HEADER_SIZE {
return Err(RomAnalyzerError::DataTooSmall {
file_size: data.len(),
required_size: HEADER_SIZE,
details: "Game Boy header".to_string(),
});
}
let system_type = if data[GBC_SYSTEM_TYPE] == 0x80 || data[GBC_SYSTEM_TYPE] == 0xC0 {
"Game Boy Color (GBC)"
} else {
"Game Boy (GB)"
};
let title_end = if system_type == "Game Boy Color (GBC)" {
GBC_TITLE_END
} else {
GB_TITLE_END
};
let game_title = String::from_utf8_lossy(&data[GB_TITLE_START..title_end])
.trim_matches(char::from(0))
.to_string();
let destination_code = data[GB_DESTINATION];
let (region_name, region) = map_region(destination_code);
let region_mismatch = check_region_mismatch(source_name, region);
Ok(GbAnalysis {
source_name: source_name.to_string(),
region,
region_string: region_name.to_string(),
region_mismatch,
system_type: system_type.to_string(),
game_title,
destination_code,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn generate_gb_header(destination_code: u8, system_byte: u8, title: &str) -> Vec<u8> {
let mut data = vec![0; 0x150];
data[0x100..0x104].copy_from_slice(b"LOGO");
let mut title_bytes = title.as_bytes().to_vec();
let mut title_length = 11;
if system_byte & 0x80 == 0x00 {
title_length = 15;
}
title_bytes.resize(title_length, 0);
data[GB_TITLE_START..(GB_TITLE_START + title_length)].copy_from_slice(&title_bytes);
data[GB_DESTINATION] = destination_code;
data[GBC_SYSTEM_TYPE] = system_byte;
data
}
#[test]
fn test_analyze_gb_data_japan() -> Result<(), RomAnalyzerError> {
let data = generate_gb_header(0x00, 0x00, "GAMETITLE"); let analysis = analyze_gb_data(&data, "test_rom_jp.gb")?;
assert_eq!(analysis.source_name, "test_rom_jp.gb");
assert_eq!(analysis.system_type, "Game Boy (GB)");
assert_eq!(analysis.game_title, "GAMETITLE");
assert_eq!(analysis.destination_code, 0x00);
assert_eq!(analysis.region, Region::JAPAN);
assert_eq!(analysis.region_string, "Japan");
assert_eq!(
analysis.print(),
"test_rom_jp.gb\n\
System: Game Boy (GB)\n\
Game Title: GAMETITLE\n\
Region Code: 0x00\n\
Region: Japan"
);
Ok(())
}
#[test]
fn test_analyze_gb_data_non_japan() -> Result<(), RomAnalyzerError> {
let data = generate_gb_header(0x01, 0x00, "GAMETITLE"); let analysis = analyze_gb_data(&data, "test_rom_us.gb")?;
assert_eq!(analysis.source_name, "test_rom_us.gb");
assert_eq!(analysis.system_type, "Game Boy (GB)");
assert_eq!(analysis.game_title, "GAMETITLE");
assert_eq!(analysis.destination_code, 0x01);
assert_eq!(analysis.region, Region::USA | Region::EUROPE);
assert_eq!(analysis.region_string, "Non-Japan (International)");
assert_eq!(
analysis.print(),
"test_rom_us.gb\n\
System: Game Boy (GB)\n\
Game Title: GAMETITLE\n\
Region Code: 0x01\n\
Region: USA/Europe"
);
Ok(())
}
#[test]
fn test_analyze_gbc_data_japan() -> Result<(), RomAnalyzerError> {
let data = generate_gb_header(0x00, 0x80, "GBC TITLE"); let analysis = analyze_gb_data(&data, "test_rom_jp.gbc")?;
assert_eq!(analysis.source_name, "test_rom_jp.gbc");
assert_eq!(analysis.system_type, "Game Boy Color (GBC)");
assert_eq!(analysis.game_title, "GBC TITLE");
assert_eq!(analysis.destination_code, 0x00);
assert_eq!(analysis.region, Region::JAPAN);
assert_eq!(analysis.region_string, "Japan");
Ok(())
}
#[test]
fn test_analyze_gbc_data_non_japan() -> Result<(), RomAnalyzerError> {
let data = generate_gb_header(0x01, 0xC0, "GBC TITLE"); let analysis = analyze_gb_data(&data, "test_rom_eur.gbc")?;
assert_eq!(analysis.source_name, "test_rom_eur.gbc");
assert_eq!(analysis.system_type, "Game Boy Color (GBC)");
assert_eq!(analysis.game_title, "GBC TITLE");
assert_eq!(analysis.destination_code, 0x01);
assert_eq!(analysis.region, Region::USA | Region::EUROPE);
assert_eq!(analysis.region_string, "Non-Japan (International)");
Ok(())
}
#[test]
fn test_analyze_gb_long_title() -> Result<(), RomAnalyzerError> {
let data = generate_gb_header(0x00, 0x00, "LOOOOOONG TITLE"); let analysis = analyze_gb_data(&data, "test_rom_jp.gbc")?;
assert_eq!(analysis.source_name, "test_rom_jp.gbc");
assert_eq!(analysis.system_type, "Game Boy (GB)");
assert_eq!(analysis.game_title, "LOOOOOONG TITLE");
assert_eq!(analysis.destination_code, 0x00);
assert_eq!(analysis.region, Region::JAPAN);
assert_eq!(analysis.region_string, "Japan");
Ok(())
}
#[test]
fn test_analyze_gbc_long_title() -> Result<(), RomAnalyzerError> {
let data = generate_gb_header(0x00, 0x80, "LOONG TITLE"); let analysis = analyze_gb_data(&data, "test_rom_jp.gbc")?;
assert_eq!(analysis.source_name, "test_rom_jp.gbc");
assert_eq!(analysis.system_type, "Game Boy Color (GBC)");
assert_eq!(analysis.game_title, "LOONG TITLE");
assert_eq!(analysis.destination_code, 0x00);
assert_eq!(analysis.region, Region::JAPAN);
assert_eq!(analysis.region_string, "Japan");
Ok(())
}
#[test]
fn test_analyze_gb_unknown_code() -> Result<(), RomAnalyzerError> {
let data = generate_gb_header(0x02, 0x00, "UNKNOWN REG"); let analysis = analyze_gb_data(&data, "test_rom_unknown.gb")?;
assert_eq!(analysis.source_name, "test_rom_unknown.gb");
assert_eq!(analysis.region, Region::UNKNOWN);
assert_eq!(analysis.region_string, "Unknown");
Ok(())
}
#[test]
fn test_analyze_gb_data_too_small() {
let data = vec![0; 100]; let result = analyze_gb_data(&data, "too_small.gb");
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("too small"));
}
}