1use crate::cartridge::external::ExternalMemory;
2use crate::svp::Svp;
3use bincode::{Decode, Encode};
4use crc::Crc;
5use genesis_config::{GenesisEmulatorConfig, GenesisRegion};
6use jgenesis_common::cheats::CheatWordOverrides;
7use jgenesis_common::num::{GetBit, U16Ext};
8use jgenesis_proc_macros::{FakeDecode, FakeEncode, PartialClone};
9use regex::Regex;
10use std::sync::LazyLock;
11use std::{array, iter, mem};
12
13pub mod eeprom;
14pub mod external;
15
16const CRC: Crc<u32> = Crc::<u32>::new(&crc::CRC_32_ISO_HDLC);
17
18#[derive(Debug, Clone, Default, FakeEncode, FakeDecode)]
19pub struct Rom(pub Box<[u16]>);
20
21impl Rom {
22    #[must_use]
23    pub fn new(bytes: Vec<u8>) -> Self {
24        Self(bytes_to_words(bytes))
25    }
26
27    #[must_use]
28    pub fn read_byte(&self, address: u32) -> Option<u8> {
29        let word_addr = (address >> 1) as usize;
30
31        if word_addr < self.0.len() {
32            let word = self.0[word_addr];
33            let byte = word >> (8 * ((address & 1) ^ 1));
34            Some(byte as u8)
35        } else {
36            log::debug!("Out of bounds ROM byte read {address:06X}");
37            None
38        }
39    }
40
41    #[must_use]
42    pub fn read_word(&self, address: u32) -> Option<u16> {
43        let word_addr = (address >> 1) as usize;
44
45        if word_addr < self.0.len() {
46            Some(self.0[word_addr])
47        } else {
48            log::debug!("Out of bounds ROM word read {address:06X}");
49            None
50        }
51    }
52}
53
54fn bytes_to_words(bytes: Vec<u8>) -> Box<[u16]> {
55    let words: Vec<_> = bytes
56        .chunks(2)
57        .map(|chunk| {
58            let msb = chunk[0];
59            let lsb = chunk.get(1).copied().unwrap_or(0);
60            u16::from_be_bytes([msb, lsb])
61        })
62        .collect();
63    words.into_boxed_slice()
64}
65
66fn words_to_bytes(words: Box<[u16]>) -> Vec<u8> {
67    words.into_iter().flat_map(u16::to_be_bytes).collect()
68}
69
70#[derive(Debug, Clone, Copy, Encode, Decode)]
71pub struct BasicMapper {
72    ram_mapped: bool,
73}
74
75impl BasicMapper {
76    #[must_use]
77    pub fn new(initial_ram_mapped: bool) -> Self {
78        Self { ram_mapped: initial_ram_mapped }
79    }
80
81    #[must_use]
82    pub fn read_byte(self, address: u32, rom: &Rom, external: &ExternalMemory) -> Option<u8> {
83        if self.ram_mapped
84            && let Some(byte) = external.read_byte(address)
85        {
86            Some(byte)
87        } else {
88            rom.read_byte(address)
89        }
90    }
91
92    #[must_use]
93    pub fn read_word(self, address: u32, rom: &Rom, external: &ExternalMemory) -> Option<u16> {
94        if self.ram_mapped
95            && let Some(word) = external.read_word(address)
96        {
97            Some(word)
98        } else {
99            rom.read_word(address)
100        }
101    }
102
103    pub fn write_byte(self, address: u32, value: u8, external: &mut ExternalMemory) {
104        if self.ram_mapped {
105            external.write_byte(address, value);
106        }
107    }
108
109    pub fn write_word(self, address: u32, value: u16, external: &mut ExternalMemory) {
110        if self.ram_mapped {
111            external.write_word(address, value);
112        }
113    }
114
115    pub fn write_register(&mut self, address: u32, value: u8) {
116        if address == 0xA130F1 {
117            write_ram_mapped_register(&mut self.ram_mapped, value);
118        }
119    }
120}
121
122fn write_ram_mapped_register(ram_mapped: &mut bool, value: u8) {
123    *ram_mapped = value.bit(0);
124
125    log::debug!("RAM map register write: {value:02X} (mapped = {ram_mapped})");
126}
127
128#[derive(Debug, Clone, Copy, Encode, Decode)]
129pub struct SsfMapper {
130    rom_banks: [u8; 8],
131    ram_mapped: bool,
132}
133
134impl SsfMapper {
135    #[must_use]
136    pub fn new(initial_ram_mapped: bool) -> Self {
137        Self { rom_banks: array::from_fn(|i| i as u8), ram_mapped: initial_ram_mapped }
138    }
139
140    #[must_use]
141    pub fn map_address(self, address: u32) -> u32 {
142        let idx = (address >> 19) & 0x07;
143        let bank_number: u32 = self.rom_banks[idx as usize].into();
144        (bank_number << 19) | (address & 0x07FFFF)
145    }
146
147    #[must_use]
148    pub fn read_byte(self, address: u32, rom: &Rom, external: &ExternalMemory) -> Option<u8> {
149        if self.ram_mapped
150            && let Some(byte) = external.read_byte(address)
151        {
152            Some(byte)
153        } else {
154            let rom_addr = self.map_address(address);
155            rom.read_byte(rom_addr)
156        }
157    }
158
159    #[must_use]
160    pub fn read_word(self, address: u32, rom: &Rom, external: &ExternalMemory) -> Option<u16> {
161        if self.ram_mapped
162            && let Some(word) = external.read_word(address)
163        {
164            Some(word)
165        } else {
166            let rom_addr = self.map_address(address);
167            rom.read_word(rom_addr)
168        }
169    }
170
171    pub fn write_byte(self, address: u32, value: u8, external: &mut ExternalMemory) {
172        if self.ram_mapped {
173            external.write_byte(address, value);
174        }
175    }
176
177    pub fn write_word(self, address: u32, value: u16, external: &mut ExternalMemory) {
178        if self.ram_mapped {
179            external.write_word(address, value);
180        }
181    }
182
183    pub fn write_register(&mut self, address: u32, value: u8) {
184        match address {
185            0xA130F1 => write_ram_mapped_register(&mut self.ram_mapped, value),
186            0xA130F3..=0xA130FF if address.bit(0) => {
187                // ROM bank registers
188                let idx = (address >> 1) & 7;
189                self.rom_banks[idx as usize] = value;
190
191                log::trace!("ROM bank {idx} set to {value:02X}");
192            }
193            _ => {}
194        }
195    }
196
197    #[must_use]
198    #[allow(clippy::missing_panics_doc)]
199    pub fn should_use(rom: &[u8]) -> bool {
200        // Enable the bank switching mapper for any cartridge that declares its system type as "SEGA SSF"
201        if &rom[0x100..0x110] == b"SEGA SSF        " {
202            return true;
203        }
204
205        // If ROM is larger than 4 MB but address range is specified as $000000-$3FFFFF, assume it
206        // needs the SSF mapper (Super Street Fighter II, Demons of Asteborg, various homebrew)
207        if rom.len() > 0x400000 {
208            let rom_range_start = u32::from_be_bytes(rom[0x1A0..0x1A4].try_into().unwrap());
209            let rom_range_end = u32::from_be_bytes(rom[0x1A4..0x1A8].try_into().unwrap());
210            if rom_range_start == 0x000000 && rom_range_end == 0x3FFFFF {
211                return true;
212            }
213        }
214
215        false
216    }
217}
218
219#[derive(Debug, Clone, Encode, Decode)]
220enum Mapper {
221    Basic(BasicMapper),
222    Ssf(SsfMapper),
223    Svp(Box<Svp>),
224    // TODO this mapper is probably not implemented correctly - seems to have ROM banking
225    UnlRockmanX3,
226}
227
228impl Mapper {
229    fn read_byte(&mut self, address: u32, rom: &Rom, external: &ExternalMemory) -> Option<u8> {
230        match self {
231            Self::Basic(mapper) => mapper.read_byte(address, rom, external),
232            Self::Ssf(mapper) => mapper.read_byte(address, rom, external),
233            Self::Svp(svp) => {
234                let word = svp.m68k_read(address, &rom.0);
235                let byte = if !address.bit(0) { word.msb() } else { word.lsb() };
236                Some(byte)
237            }
238            Self::UnlRockmanX3 => rom.read_byte(address),
239        }
240    }
241
242    fn read_word(&mut self, address: u32, rom: &Rom, external: &ExternalMemory) -> Option<u16> {
243        match self {
244            Self::Basic(mapper) => mapper.read_word(address, rom, external),
245            Self::Ssf(mapper) => mapper.read_word(address, rom, external),
246            Self::Svp(svp) => Some(svp.m68k_read(address, &rom.0)),
247            Self::UnlRockmanX3 => {
248                match address {
249                    // The unlicensed Rockman X3 port depends on $A13000 reads returning a value where the lower
250                    // 4 bits are $C or else it will immediately crash and display "decode error"
251                    0xA13000 => Some(0xC),
252                    _ => rom.read_word(address),
253                }
254            }
255        }
256    }
257
258    fn peek_word(&self, address: u32, rom: &Rom, external: &ExternalMemory) -> Option<u16> {
259        match self {
260            Self::Basic(mapper) => mapper.read_word(address, rom, external),
261            Self::Ssf(mapper) => mapper.read_word(address, rom, external),
262            Self::Svp(svp) => Some(svp.m68k_peek(address, &rom.0)),
263            Self::UnlRockmanX3 => rom.read_word(address),
264        }
265    }
266
267    fn write_byte(&mut self, address: u32, value: u8, external: &mut ExternalMemory) {
268        match self {
269            Self::Basic(mapper) => mapper.write_byte(address, value, external),
270            Self::Ssf(mapper) => mapper.write_byte(address, value, external),
271            Self::Svp(svp) => svp.m68k_write_byte(address, value),
272            Self::UnlRockmanX3 => {}
273        }
274    }
275
276    fn write_word(&mut self, address: u32, value: u16, external: &mut ExternalMemory) {
277        match self {
278            Self::Basic(mapper) => mapper.write_word(address, value, external),
279            Self::Ssf(mapper) => mapper.write_word(address, value, external),
280            Self::Svp(svp) => svp.m68k_write_word(address, value),
281            Self::UnlRockmanX3 => {}
282        }
283    }
284
285    fn write_register_byte(&mut self, address: u32, value: u8) {
286        match self {
287            Self::Basic(mapper) => mapper.write_register(address, value),
288            Self::Ssf(mapper) => mapper.write_register(address, value),
289            Self::Svp(svp) => svp.m68k_write_byte(address, value),
290            Self::UnlRockmanX3 => {}
291        }
292    }
293
294    fn write_register_word(&mut self, address: u32, value: u16) {
295        match self {
296            Self::Basic(mapper) => mapper.write_register(address | 1, value as u8),
297            Self::Ssf(mapper) => mapper.write_register(address | 1, value as u8),
298            Self::Svp(svp) => svp.m68k_write_word(address, value),
299            Self::UnlRockmanX3 => {}
300        }
301    }
302
303    fn name(&self) -> &'static str {
304        match self {
305            Self::Basic(..) => "Basic",
306            Self::Ssf(..) => "SSF",
307            Self::Svp(..) => "SVP",
308            Self::UnlRockmanX3 => "Unlicensed Rockman X3",
309        }
310    }
311}
312
313pub trait GenesisRegionExt: Sized + Copy {
314    #[must_use]
315    fn from_rom(rom: &[u8]) -> Option<Self>;
316
317    #[must_use]
318    fn version_bit(self) -> bool;
319}
320
321impl GenesisRegionExt for GenesisRegion {
322    fn from_rom(rom: &[u8]) -> Option<Self> {
323        // European games with incorrect region headers that indicate US or JP support
324        const DEFAULT_EUROPE_CHECKSUMS: &[u32] = &[
325            0x28165BD1, // Alisia Dragoon (Europe)
326            0x224256C7, // Andre Agassi Tennis (Europe)
327            0x90F5C2B7, // Brian Lara Cricket (Europe)
328            0xEB8F4374, // Indiana Jones and the Last Crusade (Europe)
329            0xD97D1699, // International Rugby (Europe)
330            0xC0DCE0E5, // Midway Presents Arcade's Greatest Hits (Europe)
331            0x924E57D3, // Olympic Gold (Europe)
332            0xFA537A45, // Winter Olympics (Europe)
333            0xDACA01C3, // World Class Leader Board (Europe)
334            0x4C926BF6, // Nuance Xmas-Intro 2024
335            0x0F51DD6A, // Chaekopon by Limp Ninja
336        ];
337
338        if DEFAULT_EUROPE_CHECKSUMS.contains(&CRC.checksum(rom)) {
339            return Some(GenesisRegion::Europe);
340        }
341
342        // $100 contains the system name
343        // If it's "SEGA GENESIS" instead of "SEGA MEGA DRIVE", ignore region string and assume US
344        if &rom[0x100..0x100 + b"SEGA GENESIS".len()] == b"SEGA GENESIS" {
345            return Some(GenesisRegion::Americas);
346        }
347
348        // Shadow Squadron / Stellar Assault (UE) reports its region as E in the header,
349        // but it's NTSC-compatible; prefer Americas if region is not forced so it will run at
350        // 60Hz instead of 50Hz
351        if &rom[0x180..0x18E] == "GM MK-84509-00".as_bytes() {
352            return Some(GenesisRegion::Americas);
353        }
354
355        if &rom[0x1F0..0x1F6] == b"EUROPE" {
356            // Another World (E) has the string "EUROPE" in the region section; special case this
357            // so that it's not detected as U (this game does not work with NTSC timings)
358            return Some(GenesisRegion::Europe);
359        }
360
361        let region_bytes = &rom[0x1F0..0x1F3];
362
363        // Prefer Americas if region code contains a 'U'
364        if region_bytes.contains(&b'U') {
365            return Some(GenesisRegion::Americas);
366        }
367
368        // Otherwise, prefer Japan if it contains a 'J'
369        if region_bytes.contains(&b'J') {
370            return Some(GenesisRegion::Japan);
371        }
372
373        // Finally, prefer Europe if it contains an 'E'
374        if region_bytes.contains(&b'E') {
375            return Some(GenesisRegion::Europe);
376        }
377
378        // If region code contains neither a 'U' nor a 'J', treat it as a hex char
379        let c = region_bytes[0] as char;
380        let value = u8::from_str_radix(&c.to_string(), 16).ok()?;
381        if value.bit(2) {
382            // Bit 2 = Americas
383            Some(GenesisRegion::Americas)
384        } else if value.bit(0) {
385            // Bit 0 = Asia
386            Some(GenesisRegion::Japan)
387        } else if value.bit(3) {
388            // Bit 3 = Europe
389            Some(GenesisRegion::Europe)
390        } else {
391            // Invalid
392            None
393        }
394    }
395
396    #[inline]
397    fn version_bit(self) -> bool {
398        self != Self::Japan
399    }
400}
401
402#[derive(Debug, Clone, Encode, Decode)]
403pub struct CartridgeMetadata {
404    pub program_title: String,
405    pub region: GenesisRegion,
406    // If the game has known issues when sprite limits are removed (e.g. Sonic 1)
407    pub sprite_limit_compatibility_issues: bool,
408    pub six_button_incompatible: bool,
409}
410
411type CartridgeCheatOverrides = CheatWordOverrides<0x000000, 0x3FFFFF>;
412
413#[derive(Debug, Clone, Encode, Decode, PartialClone)]
414pub struct Cartridge {
415    #[partial_clone(default)]
416    rom: Rom,
417    external: ExternalMemory,
418    mapper: Mapper,
419    forced_region: Option<GenesisRegion>,
420    metadata: CartridgeMetadata,
421    cheat_overrides: CartridgeCheatOverrides,
422}
423
424const TRIPLE_PLAY_GOLD_SERIAL: &[u8] = b"T-172116";
425const TRIPLE_PLAY_96_SERIAL: &[u8] = b"T-172026";
426
427const QUACKSHOT_REV_A_SERIAL: &[u8] = b"GM 00004054-01";
428
429const ROCKMAN_X3_CHECKSUM: u32 = 0x3EE639F0;
430
431impl Cartridge {
432    #[must_use]
433    pub fn new(
434        rom_bytes: Vec<u8>,
435        initial_ram_bytes: Option<Vec<u8>>,
436        forced_region: Option<GenesisRegion>,
437        cheat_codes: &[(u32, u16)],
438    ) -> Self {
439        // Take checksum before potentially byteswapping the ROM
440        let checksum = CRC.checksum(&rom_bytes);
441        log::info!("ROM CRC32: {checksum:08X}");
442
443        let mut rom_bytes = ensure_rom_in_expected_format(rom_bytes);
444
445        let region = GenesisRegion::from_rom(&rom_bytes).unwrap_or_else(|| {
446            log::warn!("Unable to determine cartridge region from ROM header; using Americas");
447            GenesisRegion::Americas
448        });
449
450        log::info!("Cartridge region: {region:?}");
451
452        let external_memory = ExternalMemory::from_rom(&rom_bytes, checksum, initial_ram_bytes);
453
454        let serial_number = &rom_bytes[0x183..0x18B];
455        let is_triple_play = rom_bytes.len() >= 0x300000
456            && (serial_number == TRIPLE_PLAY_GOLD_SERIAL || serial_number == TRIPLE_PLAY_96_SERIAL);
457        if is_triple_play {
458            fix_triple_play_rom(&mut rom_bytes);
459        }
460
461        // Initialize ram_mapped to true if external memory is present and the address range is past
462        // the end of ROM, _or_ if the cartridge has EEPROM (some >2MB EEPROM games depend on this).
463        // Some games with cartridge RAM will never write to $A130F1, assuming RAM is always mapped.
464        //
465        // Special case Triple Play because its RAM is mapped to $200000-$20FFFF while ROM is mapped
466        // to $000000-$1FFFFF + $300000-$3FFFFF, and it never writes to $A130F1
467        let external_present = !matches!(external_memory, ExternalMemory::None);
468        let is_eeprom = matches!(external_memory, ExternalMemory::Eeprom { .. });
469        let ram_start_past_rom = external_memory.address_range().start >= rom_bytes.len() as u32;
470        let initial_ram_mapped =
471            external_present && (ram_start_past_rom || is_eeprom || is_triple_play);
472
473        let serial_number = &rom_bytes[0x183..0x18B];
474        let mapper = if is_virtua_racing(serial_number) {
475            // Only one game uses the SVP, Virtua Racing
476            Mapper::Svp(Box::new(Svp::new()))
477        } else if SsfMapper::should_use(&rom_bytes) {
478            Mapper::Ssf(SsfMapper::new(initial_ram_mapped))
479        } else if checksum == ROCKMAN_X3_CHECKSUM {
480            Mapper::UnlRockmanX3
481        } else {
482            Mapper::Basic(BasicMapper::new(initial_ram_mapped))
483        };
484
485        log::info!("Using mapper {}", mapper.name());
486
487        if rom_bytes.len() == 0x80000 && &rom_bytes[0x180..0x18E] == QUACKSHOT_REV_A_SERIAL {
488            rom_bytes = fix_quackshot_rev_a_rom(rom_bytes);
489        }
490
491        let program_title = parse_title_from_header(&rom_bytes, region);
492        let sprite_limit_compatibility_issues = has_sprite_limit_compatibility_issues(&rom_bytes);
493        let six_button_incompatible_game = is_six_button_incompatible(&rom_bytes);
494
495        let metadata = CartridgeMetadata {
496            program_title,
497            region,
498            sprite_limit_compatibility_issues,
499            six_button_incompatible: six_button_incompatible_game,
500        };
501
502        let cheat_overrides = CartridgeCheatOverrides::new(cheat_codes);
503
504        Self {
505            rom: Rom::new(rom_bytes),
506            external: external_memory,
507            mapper,
508            forced_region,
509            metadata,
510            cheat_overrides,
511        }
512    }
513
514    #[inline]
515    pub fn read<const WORD: bool>(&mut self, address: u32, open_bus: u16) -> u16 {
516        if WORD {
517            self.read_word(address, open_bus)
518        } else {
519            self.read_byte(address, open_bus).into()
520        }
521    }
522
523    #[inline]
524    pub fn read_byte(&mut self, address: u32, open_bus: u16) -> u8 {
525        if let Some(cheat) = self.cheat_overrides.get(address) {
526            return cheat.to_be_bytes()[(address & 1) as usize];
527        }
528
529        self.mapper
530            .read_byte(address, &self.rom, &self.external)
531            .unwrap_or_else(|| open_bus.to_be_bytes()[(address & 1) as usize])
532    }
533
534    #[inline]
535    pub fn read_word(&mut self, address: u32, open_bus: u16) -> u16 {
536        if let Some(cheat) = self.cheat_overrides.get(address) {
537            return cheat;
538        }
539
540        self.mapper.read_word(address, &self.rom, &self.external).unwrap_or(open_bus)
541    }
542
543    #[inline]
544    pub fn read_word_for_dma(&mut self, address: u32, open_bus: &mut u16) -> u16 {
545        // SVP cartridge memory has the same delay issue as Sega CD word RAM; Virtua Racing sets
546        // DMA source address 2 higher than the "correct" address
547        match &mut self.mapper {
548            Mapper::Svp(svp) => mem::replace(open_bus, svp.m68k_read(address, &self.rom.0)),
549            _ => {
550                *open_bus = self.read_word(address, *open_bus);
551                *open_bus
552            }
553        }
554    }
555
556    #[inline]
557    pub fn write<const WORD: bool>(&mut self, address: u32, value: u16) {
558        if WORD {
559            self.write_word(address, value);
560        } else {
561            self.write_byte(address, value as u8);
562        }
563    }
564
565    #[inline]
566    pub fn write_byte(&mut self, address: u32, value: u8) {
567        match address {
568            0x000000..=0x7FFFFF => {
569                self.mapper.write_byte(address, value, &mut self.external);
570            }
571            0xA13000..=0xA15FFF => {
572                self.mapper.write_register_byte(address, value);
573            }
574            _ => {
575                log::debug!("Write to invalid cartridge address: {address:06X} {value:02X}");
576            }
577        }
578    }
579
580    #[inline]
581    pub fn write_word(&mut self, address: u32, value: u16) {
582        match address {
583            0x000000..=0x7FFFFF => {
584                self.mapper.write_word(address, value, &mut self.external);
585            }
586            0xA13000..=0xA15FFF => {
587                self.mapper.write_register_word(address, value);
588            }
589            _ => {
590                log::debug!("Write to invalid cartridge address: {address:06X} {value:04X}");
591            }
592        }
593    }
594
595    #[must_use]
596    #[inline]
597    pub fn region(&self) -> GenesisRegion {
598        self.forced_region.unwrap_or(self.metadata.region)
599    }
600
601    #[inline]
602    pub fn tick(&mut self, m68k_cycles: u32) {
603        if let Mapper::Svp(svp) = &mut self.mapper {
604            svp.tick(&self.rom.0, m68k_cycles);
605        }
606    }
607
608    #[must_use]
609    pub fn take_rom(&mut self) -> Vec<u8> {
610        let words = mem::take(&mut self.rom.0);
611        words_to_bytes(words)
612    }
613
614    pub fn take_rom_from(&mut self, other: &mut Self) {
615        self.rom = mem::take(&mut other.rom);
616    }
617
618    #[inline]
619    #[must_use]
620    pub fn external_ram(&self) -> &[u8] {
621        self.external.get_memory()
622    }
623
624    #[inline]
625    #[must_use]
626    pub fn is_ram_persistent(&self) -> bool {
627        self.external.is_persistent()
628    }
629
630    #[inline]
631    #[must_use]
632    pub fn get_and_clear_ram_dirty(&mut self) -> bool {
633        self.external.get_and_clear_dirty_bit()
634    }
635
636    #[inline]
637    #[must_use]
638    pub fn metadata(&self) -> &CartridgeMetadata {
639        &self.metadata
640    }
641
642    #[inline]
643    #[must_use]
644    pub fn program_title(&self) -> &str {
645        &self.metadata.program_title
646    }
647
648    pub fn reload_config(&mut self, config: &GenesisEmulatorConfig) {
649        self.forced_region = config.forced_region;
650        self.cheat_overrides.update_cheat_codes(&config.cheat_codes);
651    }
652
653    #[must_use]
654    pub fn peek_word(&self, address: u32) -> u16 {
655        self.mapper.peek_word(address, &self.rom, &self.external).unwrap_or(0xFFFF)
656    }
657
658    #[must_use]
659    pub fn debug_rom_view(&mut self) -> &mut [u16] {
660        self.rom.0.as_mut()
661    }
662
663    #[must_use]
664    pub fn debug_rom_view_shared(&self) -> &[u16] {
665        self.rom.0.as_ref()
666    }
667}
668
669fn ensure_rom_in_expected_format(mut rom: Vec<u8>) -> Vec<u8> {
670    // For very tiny ROMs, pad to 1KB before doing anything else
671    // e.g. "Mona in 344 bytes" demo
672    const MIN_ROM_LEN: usize = 1024;
673
674    if rom.len() < MIN_ROM_LEN {
675        jgenesis_common::rom::mirror_to_next_power_of_two(&mut rom);
676
677        while rom.len() < MIN_ROM_LEN {
678            for i in 0..rom.len() {
679                rom.push(rom[i]);
680            }
681        }
682    }
683
684    rom = remove_copier_header(rom);
685    rom = deinterleave_rom(rom);
686    ensure_big_endian(rom)
687}
688
689fn remove_copier_header(rom: Vec<u8>) -> Vec<u8> {
690    // Some older ROMs contain a useless 512-byte copier header; remove it if present
691    if rom.len() & 0x3FF != 0x200 {
692        // ROM length is not off by 512 from a reasonable number
693        return rom;
694    }
695
696    // TMSS header is normally at $100-$103, would be at $303-$304 with the 512-byte header
697    let tmss_header = &rom[0x300..0x304];
698
699    // Interleaved header bytes are normally at $80-$81 for even and $2080-$2081 for odd
700    let interleaved_tmss_even = &rom[0x0280..0x0282];
701    let interleaved_tmss_odd = &rom[0x2280..0x2282];
702
703    if tmss_header != b"SEGA"
704        && tmss_header != b"ESAG"
705        && !(interleaved_tmss_even == b"EA" && interleaved_tmss_odd == b"SG")
706    {
707        // Removing the copier header would not produce a valid TMSS header
708        return rom;
709    }
710
711    log::info!("ROM image appears to have a 512-byte copier header; removing it");
712
713    rom.into_iter().skip(512).collect()
714}
715
716fn ensure_big_endian(mut rom: Vec<u8>) -> Vec<u8> {
717    // Every licensed game contains the ASCII string "SEGA" at $100-$104 in ROM
718    // If the string "ESAG" is detected there, byteswap the ROM
719    if &rom[0x100..0x104] == "ESAG".as_bytes() {
720        log::info!("Byteswapping ROM because it appears to be little-endian");
721
722        for chunk in rom.as_chunks_mut::<2>().0 {
723            chunk.swap(0, 1);
724        }
725    }
726
727    rom
728}
729
730fn deinterleave_rom(rom: Vec<u8>) -> Vec<u8> {
731    // Some older ROM images, usually with the .smd file extension, are interleaved.
732    // This format consists of 16KB blocks where each block contains 8KB of even bytes followed by
733    // 8KB of odd bytes.
734    if !rom.len().is_multiple_of(16 * 1024) {
735        // Interleaved ROM sizes should always be a multiple of 16KB
736        return rom;
737    }
738
739    if &rom[0x100..0x104] == b"SEGA" || &rom[0x100..0x104] == b"ESAG" {
740        // ROM image already contains valid TMSS text; don't try to deinterleave
741        return rom;
742    }
743
744    if &rom[0x0080..0x0082] != b"EA" || &rom[0x2080..0x2082] != b"SG" {
745        // Deinterleaving would not produce valid TMSS text; don't try to deinterleave
746        return rom;
747    }
748
749    log::info!("ROM image appears to be interleaved; deinterleaving it");
750
751    let mut deinterleaved = vec![0; rom.len()];
752    for block_addr in (0..rom.len()).step_by(0x4000) {
753        for i in 0..0x2000 {
754            deinterleaved[block_addr + 2 * i] = rom[block_addr + 0x2000 + i];
755            deinterleaved[block_addr + 2 * i + 1] = rom[block_addr + i];
756        }
757    }
758
759    deinterleaved
760}
761
762fn fix_triple_play_rom(rom: &mut Vec<u8>) {
763    // Triple Play expects the third MB of the ROM to be mapped to $300000-$3FFFFF instead
764    // of $200000-$2FFFFF; accomplish this by duplicating the data
765    if rom.len() < 0x400000 {
766        rom.extend(iter::repeat_n(0xFF, 0x400000 - rom.len()));
767    }
768
769    let (first, second) = rom.split_at_mut(0x300000);
770    second[..0x100000].copy_from_slice(&first[0x200000..0x300000]);
771}
772
773fn fix_quackshot_rev_a_rom(rom: Vec<u8>) -> Vec<u8> {
774    // QuackShot (Rev A) is a 512KB ROM with an unusual ROM address mapping:
775    //   $000000-$0FFFFF: First 256KB of ROM mirrored 4x
776    //   $100000-$1FFFFF: Second 256KB of ROM mirrored 4x
777    // Rather than implement custom mapping logic, just remap the ROM while loading it
778    let mut remapped_rom = vec![0; 0x200000];
779    for i in (0x000000..0x100000).step_by(0x40000) {
780        remapped_rom[i..i + 0x40000].copy_from_slice(&rom[..0x40000]);
781        remapped_rom[i + 0x100000..i + 0x140000].copy_from_slice(&rom[0x40000..]);
782    }
783
784    remapped_rom
785}
786
787#[must_use]
788#[allow(clippy::missing_panics_doc, clippy::items_after_statements)]
789pub fn parse_title_from_header(rom: &[u8], region: GenesisRegion) -> String {
790    let addr = match region {
791        GenesisRegion::Americas | GenesisRegion::Europe => 0x0150,
792        GenesisRegion::Japan => 0x0120,
793    };
794    let bytes = &rom[addr..addr + 48];
795    let title = bytes.iter().copied().map(|b| b as char).collect::<String>();
796
797    static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r" +").unwrap());
798    RE.replace_all(title.trim(), " ").into()
799}
800
801fn is_virtua_racing(serial_number: &[u8]) -> bool {
802    serial_number == b"MK-1229 " || serial_number == b"G-7001  "
803}
804
805fn has_sprite_limit_compatibility_issues(rom: &[u8]) -> bool {
806    const SERIAL_NUMBERS: &[&[u8]] = &[
807        b"GM 00001009", // Sonic the Hedgehog (USA, Europe)
808        b"GM 00004049", // Sonic the Hedgehog (Japan, Europe, Korea)
809    ];
810
811    let serial_number = &rom[0x180..0x18B];
812    SERIAL_NUMBERS.contains(&serial_number)
813}
814
815#[must_use]
816pub fn is_six_button_incompatible(rom: &[u8]) -> bool {
817    // List of games from https://segaretro.org/Six_Button_Control_Pad_(Mega_Drive)
818    const SERIAL_NUMBERS: &[&[u8]] = &[
819        b"GM_T-081056", // Arch Rivals - The Arcade Game (USA, Europe)
820        b"GM T-49116 ", // Beast Wrestler (USA)
821        b"GM T-74023-", // Double Dragon II - The Revenge (Japan)
822        b"GM T-50156 ", // The Faery Tale Adventure (USA, Europe)
823        b"GM 00004016", // Forgotten Worlds (World)
824        b"GM 00001122", // Golden Axe II (World)
825        b"T-88056-50 ", // International Rugby (Europe)
826        b"GM T-50016 ", // John Madden Football (USA, Europe)
827        b"GM T-103026", // King of the Monsters (USA)
828        b"GM G-4104 0", // King of the Monsters (Japan)
829        b"GM MK-1086-", // King of the Monsters (Europe)
830        b"GM MK-1210 ", // Mario Lemieux Hockey (USA, Europe)
831        b"GM T-48036 ", // Ms. Pac-Man (USA, Europe)
832        b"GM  T-79016", // Olympic Gold (Japan, USA, Europe)
833        b"GM T-113106", // Second Samurai (Europe)
834        b"GM T-50216 ", // Starflight (USA, Europe)
835        b"GM T-95026-", // Sunset Riders (USA, Europe)
836        b"GM T-70015-", // The Terminator (USA, Europe) (Sega CD)
837    ];
838
839    let serial_number = &rom[0x180..0x18B];
840    SERIAL_NUMBERS.contains(&serial_number)
841}