1use log::debug;
10use serde::Serialize;
11
12use crate::RomAnalyzerError;
13use crate::region::{Region, check_region_mismatch, infer_region_from_filename};
14
15const POSSIBLE_HEADER_STARTS: &[usize] = &[0x7ff0, 0x3ff0, 0x1ff0];
16const REGION_CODE_OFFSET: usize = 0xf;
17const SEGA_HEADER_SIGNATURE: &[u8] = b"TMR SEGA";
18
19#[derive(Debug, PartialEq, Clone, Serialize)]
21pub struct GameGearAnalysis {
22 pub source_name: String,
24 pub region: Region,
26 pub region_string: String,
28 pub region_mismatch: bool,
30 pub region_found: bool,
32}
33
34impl GameGearAnalysis {
35 pub fn print(&self) -> String {
37 let region_not_in_rom_header = if !self.region_found {
38 "\nNote: Region information not in ROM header, inferred from filename."
39 } else {
40 ""
41 };
42 format!(
43 "{}\n\
44 System: Sega Game Gear\n\
45 Region: {}\
46 {}",
47 self.source_name, self.region, region_not_in_rom_header
48 )
49 }
50}
51
52pub fn map_region(region_byte: u8) -> (&'static str, Region) {
87 let region_code_value: u8 = region_byte >> 4;
88 match region_code_value {
89 0x3 => ("SMS Japan", Region::JAPAN),
90 0x4 => ("SMS Export", Region::USA | Region::EUROPE),
91 0x5 => ("GameGear Japan", Region::JAPAN),
92 0x6 => ("GameGear Export", Region::USA | Region::EUROPE),
93 0x7 => ("GameGear International", Region::USA | Region::EUROPE),
94 _ => ("Unknown", Region::UNKNOWN),
95 }
96}
97
98pub fn analyze_gamegear_data(
119 data: &[u8],
120 source_name: &str,
121) -> Result<GameGearAnalysis, RomAnalyzerError> {
122 let header_start_opt = POSSIBLE_HEADER_STARTS.iter().copied().find(|&offset| {
125 data.get(offset..offset + SEGA_HEADER_SIGNATURE.len()) == Some(SEGA_HEADER_SIGNATURE)
126 });
127
128 let mut region = Region::UNKNOWN;
129 let mut region_name = "Unknown".to_string();
130 let mut region_found = false;
131
132 if let Some(header_start) = header_start_opt {
133 debug!("Found signature at 0x{:x}", header_start);
134 if let Some(®ion_byte) = data.get(header_start + REGION_CODE_OFFSET) {
135 let (name, region_val) = map_region(region_byte);
136 region_name = name.to_string();
137 region = region_val;
138 if region != Region::UNKNOWN {
139 region_found = true;
140 }
141 } else {
142 debug!(
143 "ROM too small to read region code from header at 0x{:x}",
144 header_start
145 );
146 }
147 }
148
149 if !region_found {
150 region = infer_region_from_filename(source_name);
151 region_name = region.to_string();
152 }
153
154 let region_mismatch = check_region_mismatch(source_name, region);
155
156 Ok(GameGearAnalysis {
157 source_name: source_name.to_string(),
158 region,
159 region_string: region_name.to_string(),
160 region_mismatch,
161 region_found,
162 })
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 fn create_rom_data_with_header(header_start: usize, region_code: u8) -> Vec<u8> {
171 let mut data = vec![0; 0x8000]; if data.len() > header_start + REGION_CODE_OFFSET {
173 data[header_start..header_start + SEGA_HEADER_SIGNATURE.len()]
175 .copy_from_slice(SEGA_HEADER_SIGNATURE);
176 data[header_start + REGION_CODE_OFFSET] = region_code;
178 }
179 data
180 }
181
182 #[test]
183 fn test_analyze_gamegear_data_header_signature_present_region_byte_missing()
184 -> Result<(), RomAnalyzerError> {
185 let header_start = 0x7ff0;
186 let signature_len = SEGA_HEADER_SIGNATURE.len();
187 let mut data = vec![0; header_start + signature_len];
189 data[header_start..].copy_from_slice(SEGA_HEADER_SIGNATURE);
190
191 let analysis = analyze_gamegear_data(&data, "my_game_usa.gg")?;
192 assert_eq!(analysis.source_name, "my_game_usa.gg");
193 assert_eq!(analysis.region, Region::USA);
194 assert_eq!(analysis.region_string, "USA");
195 assert!(!analysis.region_found); Ok(())
197 }
198
199 #[test]
200 fn test_analyze_gamegear_data_header_japan_0x7ff0() -> Result<(), RomAnalyzerError> {
201 let data = create_rom_data_with_header(0x7ff0, 0x50);
203 let analysis = analyze_gamegear_data(&data, "test_rom.gg")?;
204 assert_eq!(analysis.source_name, "test_rom.gg");
205 assert_eq!(analysis.region, Region::JAPAN);
206 assert_eq!(analysis.region_string, "GameGear Japan");
207 assert!(analysis.region_found);
208 assert_eq!(
209 analysis.print(),
210 "test_rom.gg\n\
211 System: Sega Game Gear\n\
212 Region: Japan"
213 );
214 Ok(())
215 }
216
217 #[test]
218 fn test_analyze_gamegear_data_header_export_0x3ff0() -> Result<(), RomAnalyzerError> {
219 let data = create_rom_data_with_header(0x3ff0, 0x60);
221 let analysis = analyze_gamegear_data(&data, "test_rom.gg")?;
222 assert_eq!(analysis.source_name, "test_rom.gg");
223 assert_eq!(analysis.region, Region::USA | Region::EUROPE);
224 assert_eq!(analysis.region_string, "GameGear Export");
225 assert!(analysis.region_found);
226 assert_eq!(
227 analysis.print(),
228 "test_rom.gg\n\
229 System: Sega Game Gear\n\
230 Region: USA/Europe"
231 );
232 Ok(())
233 }
234
235 #[test]
236 fn test_analyze_gamegear_data_header_international_0x1ff0() -> Result<(), RomAnalyzerError> {
237 let data = create_rom_data_with_header(0x1ff0, 0x70);
239 let analysis = analyze_gamegear_data(&data, "test_rom.gg")?;
240 assert_eq!(analysis.source_name, "test_rom.gg");
241 assert_eq!(analysis.region, Region::USA | Region::EUROPE);
242 assert_eq!(analysis.region_string, "GameGear International");
243 assert!(analysis.region_found);
244 Ok(())
245 }
246
247 #[test]
248 fn test_analyze_gamegear_data_no_header_infer_from_filename() -> Result<(), RomAnalyzerError> {
249 let data = vec![0; 0x8000]; let analysis = analyze_gamegear_data(&data, "my_game_usa.gg")?;
251 assert_eq!(analysis.source_name, "my_game_usa.gg");
252 assert_eq!(analysis.region, Region::USA);
253 assert_eq!(analysis.region_string, "USA");
254 assert!(!analysis.region_found);
255 assert_eq!(
256 analysis.print(),
257 "my_game_usa.gg\n\
258 System: Sega Game Gear\n\
259 Region: USA\n\
260 Note: Region information not in ROM header, inferred from filename."
261 );
262 Ok(())
263 }
264
265 #[test]
266 fn test_analyze_gamegear_data_header_unknown_region_infer_from_filename()
267 -> Result<(), RomAnalyzerError> {
268 let data = create_rom_data_with_header(0x7ff0, 0xF0);
270 let analysis = analyze_gamegear_data(&data, "my_game_japan.gg")?;
271 assert_eq!(analysis.source_name, "my_game_japan.gg");
272 assert_eq!(analysis.region, Region::JAPAN);
273 assert_eq!(analysis.region_string, "Japan");
274 assert!(!analysis.region_found); Ok(())
276 }
277
278 #[test]
279 fn test_analyze_gamegear_data_get_region_name() {
280 assert_eq!(map_region(0x30), ("SMS Japan", Region::JAPAN));
281 assert_eq!(
282 map_region(0x40),
283 ("SMS Export", Region::USA | Region::EUROPE)
284 );
285 assert_eq!(map_region(0x50), ("GameGear Japan", Region::JAPAN));
286 assert_eq!(
287 map_region(0x60),
288 ("GameGear Export", Region::USA | Region::EUROPE)
289 );
290 assert_eq!(
291 map_region(0x70),
292 ("GameGear International", Region::USA | Region::EUROPE)
293 );
294 assert_eq!(map_region(0x00), ("Unknown", Region::UNKNOWN));
295 assert_eq!(map_region(0xF0), ("Unknown", Region::UNKNOWN));
296 }
297
298 #[test]
299 fn test_analyze_gamegear_data_usa() -> Result<(), RomAnalyzerError> {
300 let data = vec![0; 0x100]; let analysis = analyze_gamegear_data(&data, "test_rom_usa.gg")?;
302 assert_eq!(analysis.source_name, "test_rom_usa.gg");
303 assert_eq!(analysis.region, Region::USA);
304 assert_eq!(analysis.region_string, "USA");
305 Ok(())
306 }
307
308 #[test]
309 fn test_analyze_gamegear_data_japan() -> Result<(), RomAnalyzerError> {
310 let data = vec![0; 0x100]; let analysis = analyze_gamegear_data(&data, "test_rom_jp.gg")?;
312 assert_eq!(analysis.source_name, "test_rom_jp.gg");
313 assert_eq!(analysis.region, Region::JAPAN);
314 assert_eq!(analysis.region_string, "Japan");
315 Ok(())
316 }
317
318 #[test]
319 fn test_analyze_gamegear_data_europe() -> Result<(), RomAnalyzerError> {
320 let data = vec![0; 0x100]; let analysis = analyze_gamegear_data(&data, "test_rom_eur.gg")?;
322 assert_eq!(analysis.source_name, "test_rom_eur.gg");
323 assert_eq!(analysis.region, Region::EUROPE);
324 assert_eq!(analysis.region_string, "Europe");
325 Ok(())
326 }
327
328 #[test]
329 fn test_analyze_gamegear_data_unknown() -> Result<(), RomAnalyzerError> {
330 let data = vec![0; 0x100]; let analysis = analyze_gamegear_data(&data, "test_rom.gg")?;
332 assert_eq!(analysis.source_name, "test_rom.gg");
333 assert_eq!(analysis.region, Region::UNKNOWN);
334 assert_eq!(analysis.region_string, "Unknown");
335 Ok(())
336 }
337}