tetanes_core/
cart.rs

1//! NES cartridge implementation.
2
3use crate::{
4    common::{NesRegion, Regional},
5    fs,
6    mapper::{
7        self, Axrom, BandaiFCG, Bf909x, Bnrom, Cnrom, ColorDreams, Dxrom76, Dxrom88, Dxrom95,
8        Dxrom154, Dxrom206, Exrom, Fxrom, Gxrom, JalecoSs88006, Mapper, Mmc1Revision, Namco163,
9        Nina003006, Nrom, Pxrom, SunsoftFme7, Sxrom, Txrom, Uxrom, Vrc6,
10        m024_m026_vrc6::Revision as Vrc6Revision, m034_nina001::Nina001,
11    },
12    mem::{Memory, RamState},
13    ppu::Mirroring,
14};
15use serde::{Deserialize, Serialize};
16use std::{
17    fs::File,
18    io::{BufReader, Read},
19    path::Path,
20};
21use thiserror::Error;
22use tracing::{debug, error, info};
23
24const PRG_ROM_BANK_SIZE: usize = 0x4000;
25const CHR_ROM_BANK_SIZE: usize = 0x2000;
26
27pub type Result<T> = std::result::Result<T, Error>;
28
29#[derive(Error, Debug)]
30#[must_use]
31pub enum Error {
32    #[error("invalid nes header (found: ${value:04X} at byte: {byte}). {message}")]
33    InvalidHeader {
34        byte: u8,
35        value: u8,
36        message: String,
37    },
38    #[error("mapper: {0}")]
39    InvalidMapper(#[from] mapper::Error),
40    #[error("{context}: {source:?}")]
41    Io {
42        context: String,
43        source: std::io::Error,
44    },
45}
46
47impl Error {
48    pub fn io(source: std::io::Error, context: impl Into<String>) -> Self {
49        Self::Io {
50            context: context.into(),
51            source,
52        }
53    }
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
57#[must_use]
58pub struct GameInfo {
59    pub crc32: u32,
60    pub region: NesRegion,
61    pub mapper_num: u16,
62    pub submapper_num: u8,
63}
64
65/// An NES cartridge.
66#[must_use]
67pub struct Cart {
68    name: String,
69    header: NesHeader,
70    region: NesRegion,
71    ram_state: RamState,
72    pub(crate) mapper: Mapper,
73    pub(crate) chr_rom: Memory<Vec<u8>>, // Character ROM
74    pub(crate) chr_ram: Memory<Vec<u8>>, // Character RAM
75    pub(crate) prg_rom: Memory<Vec<u8>>, // Program ROM
76    pub(crate) prg_ram: Memory<Vec<u8>>, // Program RAM
77    pub(crate) ex_ram: Memory<Vec<u8>>,  // Internal Extra RAM
78    pub(crate) game_info: Option<GameInfo>,
79}
80
81impl Default for Cart {
82    fn default() -> Self {
83        Self::empty()
84    }
85}
86
87impl Cart {
88    pub fn empty() -> Self {
89        let ram_state = RamState::default();
90        let mut empty = Self {
91            name: "Empty Cart".to_string(),
92            header: NesHeader::default(),
93            region: NesRegion::Ntsc,
94            ram_state: RamState::default(),
95            mapper: Mapper::none(),
96            chr_rom: Memory::rom().with_size(CHR_ROM_BANK_SIZE),
97            chr_ram: Memory::ram(ram_state),
98            prg_rom: Memory::rom().with_size(PRG_ROM_BANK_SIZE),
99            prg_ram: Memory::ram(ram_state),
100            ex_ram: Memory::ram(ram_state),
101            game_info: None,
102        };
103        empty.mapper = Nrom::load(&mut empty).expect("valid empty mapper");
104        empty
105    }
106
107    /// Load `Cart` from a ROM path.
108    ///
109    /// # Errors
110    ///
111    /// If the NES header is corrupted, the ROM file cannot be read, or the data does not match
112    /// the header, then an error is returned.
113    pub fn from_path<P: AsRef<Path>>(path: P, ram_state: RamState) -> Result<Self> {
114        let path = path.as_ref();
115        let mut rom = BufReader::new(
116            File::open(path)
117                .map_err(|err| Error::io(err, format!("failed to open rom {path:?}")))?,
118        );
119        Self::from_rom(path.to_string_lossy(), &mut rom, ram_state)
120    }
121
122    /// Load `Cart` from ROM data.
123    ///
124    /// # Errors
125    ///
126    /// If the NES header is invalid, or the ROM data does not match the header, then an error is
127    /// returned.
128    pub fn from_rom<S, F>(name: S, mut rom_data: &mut F, ram_state: RamState) -> Result<Self>
129    where
130        S: ToString,
131        F: Read,
132    {
133        let name = name.to_string();
134        let header = NesHeader::load(&mut rom_data)?;
135        debug!("{header:?}");
136
137        let prg_rom_len = (header.prg_rom_banks as usize) * PRG_ROM_BANK_SIZE;
138        let mut prg_rom = Memory::rom().with_size(prg_rom_len);
139        rom_data.read_exact(&mut prg_rom).map_err(|err| {
140            if let std::io::ErrorKind::UnexpectedEof = err.kind() {
141                Error::InvalidHeader {
142                    byte: 4,
143                    value: header.prg_rom_banks as u8,
144                    message: format!(
145                        "expected `{}` prg-rom banks ({prg_rom_len} total bytes)",
146                        header.prg_rom_banks
147                    ),
148                }
149            } else {
150                Error::io(err, "failed to read prg-rom")
151            }
152        })?;
153
154        let prg_ram_size = Self::calculate_ram_size(header.prg_ram_shift)?;
155        let prg_ram = Memory::ram(ram_state).with_size(prg_ram_size);
156
157        let mut chr_rom =
158            Memory::rom().with_size((header.chr_rom_banks as usize) * CHR_ROM_BANK_SIZE);
159        let chr_ram = if header.chr_rom_banks > 0 {
160            rom_data.read_exact(&mut chr_rom).map_err(|err| {
161                if let std::io::ErrorKind::UnexpectedEof = err.kind() {
162                    Error::InvalidHeader {
163                        byte: 5,
164                        value: header.chr_rom_banks as u8,
165                        message: format!(
166                            "expected `{}` chr-rom banks ({prg_rom_len} total bytes)",
167                            header.chr_rom_banks
168                        ),
169                    }
170                } else {
171                    Error::io(err, "failed to read chr-rom")
172                }
173            })?;
174            Memory::ram(ram_state)
175        } else {
176            let chr_ram_size = Self::calculate_ram_size(header.chr_ram_shift)?;
177            Memory::ram(ram_state).with_size(chr_ram_size)
178        };
179
180        let game_info = Self::lookup_info(&prg_rom, &chr_rom);
181        let region = if matches!(header.variant, NesVariant::INes | NesVariant::Nes2) {
182            match header.tv_mode {
183                1 => NesRegion::Pal,
184                3 => NesRegion::Dendy,
185                _ => game_info
186                    .as_ref()
187                    .map(|info| info.region)
188                    .unwrap_or_default(),
189            }
190        } else {
191            game_info
192                .as_ref()
193                .map(|info| info.region)
194                .unwrap_or_default()
195        };
196
197        let mut cart = Self {
198            name,
199            header,
200            region,
201            ram_state,
202            mapper: Mapper::none(),
203            chr_rom,
204            chr_ram,
205            prg_rom,
206            prg_ram,
207            ex_ram: Memory::ram(ram_state),
208            game_info,
209        };
210        cart.mapper = match cart.header.mapper_num {
211            0 => Nrom::load(&mut cart)?,
212            1 => Sxrom::load(&mut cart, Mmc1Revision::BC)?,
213            2 => Uxrom::load(&mut cart)?,
214            3 => Cnrom::load(&mut cart)?,
215            4 => Txrom::load(&mut cart)?,
216            5 => Exrom::load(&mut cart)?,
217            7 => Axrom::load(&mut cart)?,
218            9 => Pxrom::load(&mut cart)?,
219            10 => Fxrom::load(&mut cart)?,
220            11 => ColorDreams::load(&mut cart)?,
221            16 | 153 | 157 | 159 => BandaiFCG::load(&mut cart)?,
222            18 => JalecoSs88006::load(&mut cart)?,
223            19 | 210 => Namco163::load(&mut cart)?,
224            24 => Vrc6::load(&mut cart, Vrc6Revision::A)?,
225            26 => Vrc6::load(&mut cart, Vrc6Revision::B)?,
226            34 => {
227                // ≥ 16K implies NINA-001; ≤ 8K implies BNROM
228                if cart.has_chr_rom() && cart.chr_rom.len() >= 0x4000 {
229                    Nina001::load(&mut cart)?
230                } else {
231                    Bnrom::load(&mut cart)?
232                }
233            }
234            66 => Gxrom::load(&mut cart)?,
235            69 => SunsoftFme7::load(&mut cart)?,
236            71 => Bf909x::load(&mut cart)?,
237            76 => Dxrom76::load(&mut cart)?,
238            79 | 113 | 146 => Nina003006::load(&mut cart)?,
239            88 => Dxrom88::load(&mut cart)?,
240            95 => Dxrom95::load(&mut cart)?,
241            154 => Dxrom154::load(&mut cart)?,
242            155 => Sxrom::load(&mut cart, Mmc1Revision::A)?,
243            206 => Dxrom206::load(&mut cart)?,
244            _ => Mapper::none(),
245        };
246
247        info!("loaded ROM `{cart}`");
248        debug!("{cart:?}");
249
250        Ok(cart)
251    }
252
253    #[must_use]
254    #[allow(clippy::missing_const_for_fn)] // false positive on non-const deref coercion
255    pub fn name(&self) -> &str {
256        &self.name
257    }
258
259    #[must_use]
260    #[allow(clippy::missing_const_for_fn)] // false positive on non-const deref coercion
261    pub fn chr_rom(&self) -> &[u8] {
262        &self.chr_rom
263    }
264
265    #[must_use]
266    #[allow(clippy::missing_const_for_fn)] // false positive on non-const deref coercion
267    pub fn chr_ram(&self) -> &[u8] {
268        &self.chr_ram
269    }
270
271    #[must_use]
272    #[allow(clippy::missing_const_for_fn)] // false positive on non-const deref coercion
273    pub fn prg_rom(&self) -> &[u8] {
274        &self.prg_rom
275    }
276
277    #[must_use]
278    #[allow(clippy::missing_const_for_fn)] // false positive on non-const deref coercion
279    pub fn prg_ram(&self) -> &[u8] {
280        &self.prg_ram
281    }
282
283    #[must_use]
284    #[allow(clippy::missing_const_for_fn)] // false positive on non-const deref coercion
285    pub fn has_chr_rom(&self) -> bool {
286        !self.chr_rom.is_empty()
287    }
288
289    #[must_use]
290    #[allow(clippy::missing_const_for_fn)] // false positive on non-const deref coercion
291    pub fn has_chr_ram(&self) -> bool {
292        !self.chr_ram.is_empty()
293    }
294
295    #[must_use]
296    #[allow(clippy::missing_const_for_fn)] // false positive on non-const deref coercion
297    pub fn has_prg_ram(&self) -> bool {
298        !self.prg_ram.is_empty()
299    }
300
301    #[must_use]
302    pub const fn is_ines(&self) -> bool {
303        matches!(
304            self.header.variant,
305            NesVariant::ArchaicINes | NesVariant::INes07 | NesVariant::INes
306        )
307    }
308
309    #[must_use]
310    pub const fn is_nes2(&self) -> bool {
311        matches!(self.header.variant, NesVariant::Nes2)
312    }
313
314    /// Returns whether this cartridge has battery-backed Save RAM.
315    #[must_use]
316    pub const fn battery_backed(&self) -> bool {
317        self.header.flags & 0x02 == 0x02
318    }
319
320    /// Returns `RamState`.
321    pub const fn ram_state(&self) -> RamState {
322        self.ram_state
323    }
324
325    /// Returns hardware configured `Mirroring`.
326    pub fn mirroring(&self) -> Mirroring {
327        if self.header.flags & 0x08 == 0x08 {
328            Mirroring::FourScreen
329        } else {
330            match self.header.flags & 0x01 {
331                0 => Mirroring::Horizontal,
332                1 => Mirroring::Vertical,
333                _ => unreachable!("impossible mirroring"),
334            }
335        }
336    }
337
338    /// Returns the Mapper number for this Cart.
339    #[must_use]
340    pub fn mapper_num(&self) -> u16 {
341        self.game_info
342            .as_ref()
343            .map(|info| info.mapper_num)
344            .unwrap_or(self.header.mapper_num)
345    }
346
347    /// Returns the Sub-Mapper number for this Cart.
348    #[must_use]
349    pub fn submapper_num(&self) -> u8 {
350        self.game_info
351            .as_ref()
352            .map(|info| info.submapper_num)
353            .unwrap_or(self.header.submapper_num)
354    }
355
356    /// Returns the Mapper and Board name for this Cart.
357    #[must_use]
358    pub fn mapper_board(&self) -> &'static str {
359        NesHeader::mapper_board(self.mapper_num())
360    }
361
362    /// Allows mappers to add PRG-RAM.
363    pub(crate) fn add_prg_ram(&mut self, capacity: usize) {
364        self.prg_ram.resize(capacity);
365    }
366
367    /// Allows mappers to add CHR-RAM.
368    pub(crate) fn add_chr_ram(&mut self, capacity: usize) {
369        self.chr_ram.resize(capacity);
370    }
371
372    /// Allows mappers to add EX-RAM.
373    pub(crate) fn add_exram(&mut self, capacity: usize) {
374        self.ex_ram.resize(capacity);
375    }
376
377    fn calculate_ram_size(value: u8) -> Result<usize> {
378        if value > 0 {
379            64usize
380                .checked_shl(value.into())
381                .ok_or_else(|| Error::InvalidHeader {
382                    byte: 11,
383                    value,
384                    message: "header ram size larger than 64".to_string(),
385                })
386        } else {
387            Ok(0)
388        }
389    }
390
391    fn lookup_info(prg_rom: &[u8], chr: &[u8]) -> Option<GameInfo> {
392        const GAME_DB: &[u8] = include_bytes!("../game_db.dat");
393
394        let Ok(games) = fs::load_bytes::<Vec<GameInfo>>(GAME_DB) else {
395            error!("failed to load `game_regions.dat`");
396            return None;
397        };
398
399        let mut crc32 = fs::compute_crc32(prg_rom);
400        if !chr.is_empty() {
401            crc32 = fs::compute_combine_crc32(crc32, chr);
402        }
403
404        match games.binary_search_by(|game| game.crc32.cmp(&crc32)) {
405            Ok(index) => {
406                info!(
407                    "found game matching crc: {crc32:#010X}. info: {:?}",
408                    games[index]
409                );
410                Some(games[index].clone())
411            }
412            Err(_) => {
413                info!("no game found matching crc: {crc32:#010X}");
414                None
415            }
416        }
417    }
418}
419
420impl Regional for Cart {
421    fn region(&self) -> NesRegion {
422        self.region
423    }
424
425    fn set_region(&mut self, region: NesRegion) {
426        self.region = region;
427    }
428}
429
430impl std::fmt::Display for Cart {
431    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
432        write!(
433            f,
434            "{} - {}, CHR-ROM: {}K, CHR-RAM: {}K, PRG-ROM: {}K, PRG-RAM: {}K, EX-RAM: {}K, Mirroring: {:?}, Battery: {}",
435            self.name,
436            self.mapper_board(),
437            self.chr_rom.len() / 0x0400,
438            self.chr_ram.len() / 0x0400,
439            self.prg_rom.len() / 0x0400,
440            self.prg_ram.len() / 0x0400,
441            self.ex_ram.len() / 0x0400,
442            self.mirroring(),
443            self.battery_backed(),
444        )
445    }
446}
447
448impl std::fmt::Debug for Cart {
449    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
450        f.debug_struct("Cart")
451            .field("name", &self.name)
452            .field("header", &self.header)
453            .field("region", &self.region)
454            .field("ram_state", &self.ram_state)
455            .field("mapper", &self.mapper)
456            .field("mirroring", &self.mirroring())
457            .field("battery_backed", &self.battery_backed())
458            .field("chr_rom", &self.chr_rom)
459            .field("chr_ram", &self.chr_ram)
460            .field("prg_rom", &self.prg_rom)
461            .field("prg_ram", &self.prg_ram)
462            .field("ex_ram", &self.ex_ram)
463            .finish()
464    }
465}
466
467#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)]
468#[must_use]
469pub enum NesVariant {
470    #[default]
471    ArchaicINes,
472    INes07,
473    INes,
474    Nes2,
475}
476
477/// An `iNES` or `NES 2.0` formatted header representing hardware specs of a given NES cartridge.
478///
479/// <https://wiki.nesdev.org/w/index.php/INES>
480/// <https://wiki.nesdev.org/w/index.php/NES_2.0>
481/// <https://nesdev.org/NESDoc.pdf> (page 28)
482#[derive(Default, Copy, Clone, PartialEq, Eq)]
483#[must_use]
484pub struct NesHeader {
485    pub variant: NesVariant,
486    pub mapper_num: u16,    // The primary mapper number
487    pub submapper_num: u8,  // NES 2.0 https://wiki.nesdev.org/w/index.php/NES_2.0_submappers
488    pub flags: u8,          // Mirroring, Battery, Trainer, VS Unisystem, Playchoice-10, NES 2.0
489    pub prg_rom_banks: u16, // Number of 16KB PRG-ROM banks (Program ROM)
490    pub chr_rom_banks: u16, // Number of 8KB CHR-ROM banks (Character ROM)
491    pub prg_ram_shift: u8,  // NES 2.0 PRG-RAM
492    pub chr_ram_shift: u8,  // NES 2.0 CHR-RAM
493    pub tv_mode: u8,        // NES 2.0 NTSC/PAL indicator
494    pub vs_data: u8,        // NES 2.0 VS System data
495}
496
497impl NesHeader {
498    /// Load `NesHeader` from a ROM path.
499    ///
500    /// # Errors
501    ///
502    /// If the NES header is corrupted, the ROM file cannot be read, or the data does not match
503    /// the header, then an error is returned.
504    pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self> {
505        let path = path.as_ref();
506        let mut rom = BufReader::new(
507            File::open(path)
508                .map_err(|err| Error::io(err, format!("failed to open rom {path:?}")))?,
509        );
510        Self::load(&mut rom)
511    }
512
513    /// Load `NesHeader` from ROM data.
514    ///
515    /// # Errors
516    ///
517    /// If the NES header is invalid, then an error is returned.
518    pub fn load<F: Read>(rom_data: &mut F) -> Result<Self> {
519        let mut header = [0u8; 16];
520        rom_data.read_exact(&mut header).map_err(|err| {
521            if let std::io::ErrorKind::UnexpectedEof = err.kind() {
522                Error::InvalidHeader {
523                    byte: 0,
524                    value: 0,
525                    message: "expected 16-byte header".to_string(),
526                }
527            } else {
528                Error::io(err, "failed to read nes header")
529            }
530        })?;
531
532        // Header checks
533        if header[0..4] != *b"NES\x1a" {
534            return Err(Error::InvalidHeader {
535                byte: 0,
536                value: header[0],
537                message: "nes header signature not found".to_string(),
538            });
539        }
540        if (header[7] & 0x0C) == 0x04 {
541            return Err(Error::InvalidHeader {
542                byte: 7,
543                value: header[7],
544                message: "header is corrupted by `DiskDude!`. repair and try again".to_string(),
545            });
546        }
547        if (header[7] & 0x0C) == 0x0C {
548            return Err(Error::InvalidHeader {
549                byte: 7,
550                value: header[7],
551                message: "unrecognized header format. repair and try again".to_string(),
552            });
553        }
554
555        let mut prg_rom_banks = u16::from(header[4]);
556        let mut chr_rom_banks = u16::from(header[5]);
557        // Upper 4 bits of flags 6 = D0..D3 and 7 = D4..D7
558        let mut mapper_num = u16::from(((header[6] & 0xF0) >> 4) | (header[7] & 0xF0));
559        // Lower 4 bits of flag 6 = D0..D3, upper 4 bits of flag 7 = D4..D7
560        let flags = (header[6] & 0x0F) | ((header[7] & 0x0F) << 4);
561
562        // NES 2.0 Format
563        let mut submapper_num = 0;
564        let mut prg_ram_shift = 0;
565        let mut chr_ram_shift = 0;
566        let mut tv_mode = 0;
567        let mut vs_data = 0;
568        // If D2..D3 of flag 7 == 2, then NES 2.0 (supports bytes 0-15)
569        let variant = if header[7] & 0x0C == 0x08 {
570            // lower 4 bits of flag 8 = D8..D11 of mapper num
571            mapper_num |= u16::from(header[8] & 0x0F) << 8;
572            // upper 4 bits of flag 8 = D0..D3 of submapper
573            submapper_num = (header[8] & 0xF0) >> 4;
574            // lower 4 bits of flag 9 = D8..D11 of prg_rom_size
575            prg_rom_banks |= u16::from(header[9] & 0x0F) << 8;
576            // upper 4 bits of flag 9 = D8..D11 of chr_rom_size
577            chr_rom_banks |= u16::from(header[9] & 0xF0) << 4;
578            prg_ram_shift = header[10];
579            chr_ram_shift = header[11];
580            tv_mode = header[12];
581            vs_data = header[13];
582
583            if prg_ram_shift & 0x0F == 0x0F || prg_ram_shift & 0xF0 == 0xF0 {
584                return Err(Error::InvalidHeader {
585                    byte: 10,
586                    value: prg_ram_shift,
587                    message: "invalid prg-ram size in header".to_string(),
588                });
589            }
590            if chr_ram_shift & 0x0F == 0x0F || chr_ram_shift & 0xF0 == 0xF0 {
591                return Err(Error::InvalidHeader {
592                    byte: 11,
593                    value: chr_ram_shift,
594                    message: "invalid chr-ram size in header".to_string(),
595                });
596            }
597            if chr_ram_shift & 0xF0 == 0xF0 {
598                return Err(Error::InvalidHeader {
599                    byte: 11,
600                    value: chr_ram_shift,
601                    message: "battery-backed chr-ram is currently not supported".to_string(),
602                });
603            }
604            if header[14] > 0 || header[15] > 0 {
605                return Err(Error::InvalidHeader {
606                    byte: 14,
607                    value: header[14],
608                    message: "unrecognized data found at header offsets 14-15".to_string(),
609                });
610            }
611            NesVariant::Nes2
612        } else if header[7] & 0x0C == 0x04 {
613            // If D2..D3 of flag 7 == 1, then archaic iNES (supports bytes 0-7)
614            for (i, value) in header.iter().enumerate().take(16).skip(8) {
615                if *value > 0 {
616                    return Err(Error::InvalidHeader {
617                        byte: i as u8,
618                        value: *value,
619                        message: format!(
620                            "unrecogonized data found at header byte {i}. repair and try again"
621                        ),
622                    });
623                }
624            }
625            NesVariant::ArchaicINes
626        } else if header[7] & 0x0C == 00 && header[12..=15].iter().all(|v| *v == 0) {
627            // If D2..D3 of flag 7 == 0 and bytes 12-15 are all 0, then iNES (supports bytes 0-9)
628            NesVariant::INes
629        } else {
630            // Else iNES 0.7 or archaic iNES (supports mapper high nibble)
631            NesVariant::INes07
632        };
633
634        // Trainer
635        if flags & 0x04 == 0x04 {
636            return Err(Error::InvalidHeader {
637                byte: 6,
638                value: header[6],
639                message: "trained roms are currently not supported.".to_string(),
640            });
641        }
642
643        Ok(Self {
644            variant,
645            mapper_num,
646            submapper_num,
647            flags,
648            prg_rom_banks,
649            chr_rom_banks,
650            prg_ram_shift,
651            chr_ram_shift,
652            tv_mode,
653            vs_data,
654        })
655    }
656
657    #[must_use]
658    pub const fn mapper_board(mapper_num: u16) -> &'static str {
659        match mapper_num {
660            0 => "Mapper 000 - NROM",
661            1 => "Mapper 001 - SxROM/MMC1B/C",
662            2 => "Mapper 002 - UxROM",
663            3 => "Mapper 003 - CNROM",
664            4 => "Mapper 004 - TxROM/MMC3/MMC6",
665            5 => "Mapper 005 - ExROM/MMC5",
666            6 => "Mapper 006 - FFE 1M/2M",
667            7 => "Mapper 007 - AxROM",
668            8 => "Mapper 008 - FFE 1M/2M", // Also Mapper 006 Submapper 4
669            9 => "Mapper 009 - PxROM/MMC2",
670            10 => "Mapper 010 - FxROM/MMC4",
671            11 => "Mapper 011 - Color Dreams",
672            12 => "Mapper 012 - Gouder/FFE 4M/MMC3",
673            13 => "Mapper 013 - CPROM",
674            14 => "Mapper 014 - UNL SL1632",
675            15 => "Mapper 015 - K1029/30",
676            16 => "Mapper 016 - Bandai FCG",
677            17 => "Mapper 017 - FFE",
678            18 => "Mapper 018 - Jaleco SS 88006",
679            19 => "Mapper 019 - Namco 129/163",
680            20 => "Mapper 020 - FDS",
681            21 => "Mapper 021 - Vrc4a/Vrc4c",
682            22 => "Mapper 022 - Vrc2a",
683            23 => "Mapper 023 - Vrc4e",
684            24 => "Mapper 024 - Vrc6a",
685            25 => "Mapper 025 - Vrc4b",
686            26 => "Mapper 026 - Vrc6b",
687            27 => "Mapper 027 - Vrc4x",
688            28 => "Mapper 028 - Action 53",
689            29 => "Mapper 029 - Sealie Computing",
690            30 => "Mapper 030 - UNROM 512",
691            31 => "Mapper 031 - NSF",
692            32 => "Mapper 032 - Irem G101",
693            33 => "Mapper 033 - Taito TC0190",
694            34 => "Mapper 034 - BNROM/NINA-001",
695            35 => "Mapper 035 - JY Company",
696            36 => "Mapper 036 - TXC 22000",
697            37 => "Mapper 037 - MMC3 Multicart",
698            38 => "Mapper 038 - UNL PCI556",
699            39 => "Mapper 039 - Subor",
700            40 => "Mapper 040 - NTDEC 2722",
701            41 => "Mapper 041 - Caltron 6-in-1",
702            42 => "Mapper 042",
703            43 => "Mapper 043 - TONY-I/YS-612",
704            44 => "Mapper 044 - MMC3 Multicart",
705            45 => "Mapper 045 - MMC3 Multicart",
706            46 => "Mapper 046 - Color Dreams",
707            47 => "Mapper 047 - MMC3 Multicart",
708            48 => "Mapper 048 - Taito TC0690",
709            49 => "Mapper 049 - MMC Multicart",
710            50 => "Mapper 050",
711            51 => "Mapper 051",
712            52 => "Mapper 052 - Realtec 8213/MMC Multicaart",
713            53 => "Mapper 053 - Supervision",
714            54 => "Mapper 054 - Novel Diamond",
715            55 => "Mapper 055 - UNIF BTL-MARIO1-MALEE2",
716            56 => "Mapper 056",
717            57 => "Mapper 057",
718            58 => "Mapper 058",
719            59 => "Mapper 059 - BMC T3H53/D1038",
720            60 => "Mapper 060",
721            61 => "Mapper 061",
722            62 => "Mapper 062",
723            63 => "Mapper 063",
724            64 => "Mapper 064 - RAMBO-1",
725            65 => "Mapper 065 - Irem H3001",
726            66 => "Mapper 066 - GxROM/MxROM",
727            67 => "Mapper 067 - Sunsoft-3",
728            68 => "Mapper 068 - Sunsoft-4",
729            69 => "Mapper 069 - Sunsoft FME-7",
730            70 => "Mapper 070 - Bandai",
731            71 => "Mapper 071 - BF909x",
732            72 => "Mapper 072 - Jaleco JF-17",
733            73 => "Mapper 073 - Vrc3",
734            74 => "Mapper 074",
735            75 => "Mapper 075 - Vrc1",
736            76 => "Mapper 076 - NAMCOT-108",
737            77 => "Mapper 077",
738            78 => "Mapper 078",
739            79 => "Mapper 079 - NINA-03/06",
740            80 => "Mapper 080 - Taito X1005",
741            81 => "Mapper 081 - NTDEC 715021",
742            82 => "Mapper 082 - Taito X1017",
743            83 => "Mapper 083",
744            84 => "Mapper 084",
745            85 => "Mapper 085 - Vrc7",
746            86 => "Mapper 086 - Jaleco JF-13",
747            87 => "Mapper 087 - Jaleco JF-xx",
748            88 => "Mapper 088",
749            89 => "Mapper 089 - Sunsoft",
750            90 => "Mapper 090 - JY Company",
751            91 => "Mapper 091",
752            92 => "Mapper 092",
753            93 => "Mapper 093 - Sunsoft",
754            94 => "Mapper 094 - UxROM",
755            95 => "Mapper 095 - NAMCOT-3425",
756            96 => "Mapper 096 - Oeka Kids",
757            97 => "Mapper 097 - Irem TAM-S1",
758            98 => "Mapper 098",
759            99 => "Mapper 099 - Vs. System",
760            100 => "Mapper 100",
761            101 => "Mapper 101 - Jaleco JF-10",
762            102 => "Mapper 102",
763            103 => "Mapper 103",
764            104 => "Mapper 104 - Golden Five",
765            105 => "Mapper 105 - MMC1",
766            106 => "Mapper 106",
767            107 => "Mapper 107",
768            108 => "Mapper 108",
769            109 => "Mapper 109",
770            110 => "Mapper 110",
771            111 => "Mapper 111 - GTROM",
772            112 => "Mapper 112",
773            113 => "Mapper 113 - NINA-03/06",
774            114 => "Mapper 114 - MMC3",
775            115 => "Mapper 115 - MMC3",
776            116 => "Mapper 116 - SOMARI-P",
777            117 => "Mapper 117",
778            118 => "Mapper 118 - TxSROM",
779            119 => "Mapper 119 - TQROM",
780            120 => "Mapper 120",
781            121 => "Mapper 121 - MMC3",
782            122 => "Mapper 122",
783            123 => "Mapper 123 - MMC3",
784            124 => "Mapper 124",
785            125 => "Mapper 125 - UNL-LH32",
786            126 => "Mapper 126 - MMC36",
787            127 => "Mapper 127",
788            128 => "Mapper 128",
789            129 => "Mapper 129",
790            130 => "Mapper 130",
791            131 => "Mapper 131",
792            132 => "Mapper 132 - TXC",
793            133 => "Mapper 133 - Sachen 3009",
794            134 => "Mapper 134 - MMC3",
795            135 => "Mapper 135 - Sachen 8259A",
796            136 => "Mapper 136 - Sachen 3011",
797            137 => "Mapper 137 - Sachen 8259D",
798            138 => "Mapper 138 - Sachen 8259B",
799            139 => "Mapper 139 - Sachen 8259C",
800            140 => "Mapper 140 - Jaleco JF-11/14",
801            141 => "Mapper 141 - Sachen 8259A",
802            142 => "Mapper 142 - Kaiser KS-7032",
803            143 => "Mapper 143 - NROM",
804            144 => "Mapper 144 - Color Dreams",
805            145 => "Mapper 145 - Sachen SA-72007",
806            146 => "Mapper 146 - NINA-03/06",
807            147 => "Mapper 147 - Sachen 3018",
808            148 => "Mapper 148 - Sachen SA-008-A/Tengen 800008",
809            149 => "Mapper 149 - Sachen SA-0036",
810            150 => "Mapper 150 - Sach SA-015/630",
811            151 => "Mapper 151 - Vrc1",
812            152 => "Mapper 152",
813            153 => "Mapper 153 - Bandai FCG",
814            154 => "Mapper 154 - NAMCOT-3453",
815            155 => "Mapper 155 - SxROM/MMC1A",
816            156 => "Mapper 156 - Daou",
817            157 => "Mapper 157 - Bandai FCG",
818            158 => "Mapper 158 - Tengen 800037",
819            159 => "Mapper 159 - Bandai FCG",
820            160 => "Mapper 160",
821            161 => "Mapper 161",
822            162 => "Mapper 162 - Wàixīng",
823            163 => "Mapper 163 - Nánjīng",
824            164 => "Mapper 164 - Dōngdá/Yànchéng",
825            165 => "Mapper 165 - MMC3",
826            166 => "Mapper 166 - Subor",
827            167 => "Mapper 167 - Subor",
828            168 => "Mapper 168 - Racermate",
829            169 => "Mapper 169 - Yuxing",
830            170 => "Mapper 170",
831            171 => "Mapper 171 - Kaiser KS-7058",
832            172 => "Mapper 172",
833            173 => "Mapper 173",
834            174 => "Mapper 174",
835            175 => "Mapper 175 - Kaiser KS-7022",
836            176 => "Mapper 176 - MMC3",
837            177 => "Mapper 177 - Hénggé Diànzǐ",
838            178 => "Mapper 178",
839            179 => "Mapper 179",
840            180 => "Mapper 180 - UNROM",
841            181 => "Mapper 181",
842            182 => "Mapper 182 - MMC3",
843            183 => "Mapper 183",
844            184 => "Mapper 184 - Sunsoft",
845            185 => "Mapper 185 - CNROM",
846            186 => "Mapper 186",
847            187 => "Mapper 187 - Kǎshèng/MMC3",
848            188 => "Mapper 188 - Bandai Karaoke",
849            189 => "Mapper 189 - MMC3",
850            190 => "Mapper 190 -",
851            191 => "Mapper 191 - MMC3",
852            192 => "Mapper 192 - Wàixīng",
853            193 => "Mapper 193 - NTDEC TC-112",
854            194 => "Mapper 194 - MMC3",
855            195 => "Mapper 195 - Wàixīng/MMC3",
856            196 => "Mapper 196 - MMC3",
857            197 => "Mapper 197 - MMC3",
858            198 => "Mapper 198 - MMC3",
859            199 => "Mapper 199 - Wàixīng/MMC3",
860            200 => "Mapper 200",
861            201 => "Mapper 201 - NROM",
862            202 => "Mapper 202",
863            203 => "Mapper 203",
864            204 => "Mapper 204",
865            205 => "Mapper 205 - MMC3",
866            206 => "Mapper 206 - DxROM",
867            207 => "Mapper 207 - Taito X1-005",
868            208 => "Mapper 208 - MMC3",
869            209 => "Mapper 209 - JY Company",
870            210 => "Mapper 210 - Namco",
871            211 => "Mapper 211 - JyCompany",
872            212 => "Mapper 212",
873            213 => "Mapper 213",
874            214 => "Mapper 214",
875            215 => "Mapper 215 - MMC3",
876            216 => "Mapper 216",
877            217 => "Mapper 217 - MMC3",
878            218 => "Mapper 218",
879            219 => "Mapper 219 - Kǎshèng/MMC3",
880            220 => "Mapper 220",
881            221 => "Mapper 221 - NTDEC N625092",
882            222 => "Mapper 222",
883            223 => "Mapper 223",
884            224 => "Mapper 224 - Jncota/MMC3",
885            225 => "Mapper 225",
886            226 => "Mapper 226",
887            227 => "Mapper 227",
888            228 => "Mapper 228- Active Enterprises",
889            229 => "Mapper 229",
890            230 => "Mapper 230",
891            231 => "Mapper 231",
892            232 => "Mapper 232 - BF909x",
893            233 => "Mapper 233",
894            234 => "Mapper 234 - Maxi 15 Multicart",
895            235 => "Mapper 235",
896            236 => "Mapper 236 - Realtec",
897            237 => "Mapper 237",
898            238 => "Mapper 238 - MMC3",
899            239 => "Mapper 239",
900            240 => "Mapper 240",
901            241 => "Mapper 241 - BxROM",
902            242 => "Mapper 242",
903            243 => "Mapper 243 - Sachen SA-020A",
904            244 => "Mapper 244",
905            245 => "Mapper 245 - Wàixīng/MMC3",
906            246 => "Mapper 246",
907            247 => "Mapper 247",
908            248 => "Mapper 248",
909            249 => "Mapper 249 - MMC3",
910            250 => "Mapper 250 - Nitra/MMC3",
911            251 => "Mapper 251",
912            252 => "Mapper 252 - Wàixīng",
913            253 => "Mapper 253 - Wàixīng",
914            254 => "Mapper 254 - MMC3",
915            255 => "Mapper 255",
916            _ => "Invalid Mapper",
917        }
918    }
919}
920
921impl std::fmt::Debug for NesHeader {
922    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
923        f.debug_struct("NesHeader")
924            .field("version", &self.variant)
925            .field("mapper_num", &format_args!("{:03}", &self.mapper_num))
926            .field("submapper_num", &self.submapper_num)
927            .field("flags", &format_args!("0b{:08b}", &self.flags))
928            .field("prg_rom_banks", &self.prg_rom_banks)
929            .field("chr_rom_banks", &self.chr_rom_banks)
930            .field("prg_ram_shift", &self.prg_ram_shift)
931            .field("chr_ram_shift", &self.chr_ram_shift)
932            .field("tv_mode", &self.tv_mode)
933            .field("vs_data", &self.vs_data)
934            .finish()
935    }
936}
937
938#[cfg(test)]
939mod tests {
940    use super::*;
941
942    macro_rules! test_headers {
943        ($(($test:ident, $data:expr, $header:expr$(,)?)),*$(,)?) => {$(
944            #[test]
945            fn $test() {
946                let header = NesHeader::load(&mut $data.as_slice()).expect("valid header");
947                assert_eq!(header, $header);
948            }
949        )*};
950    }
951
952    #[rustfmt::skip]
953    test_headers!(
954        (
955            mapper000_horizontal,
956            [0x4E, 0x45, 0x53, 0x1A,
957             0x02, 0x01, 0x01, 0x00,
958             0x00, 0x00, 0x00, 0x00,
959             0x00, 0x00, 0x00, 0x00],
960            NesHeader {
961                variant: NesVariant::INes,
962                mapper_num: 0,
963                flags: 0b0000_0001,
964                prg_rom_banks: 2,
965                chr_rom_banks: 1,
966                ..NesHeader::default()
967            },
968        ),
969        (
970            mapper001_vertical,
971            [0x4E, 0x45, 0x53, 0x1A,
972             0x08, 0x00, 0x10, 0x00,
973             0x00, 0x00, 0x00, 0x00,
974             0x00, 0x00, 0x00, 0x00],
975            NesHeader {
976                variant: NesVariant::INes,
977                mapper_num: 1,
978                flags: 0b0000_0000,
979                prg_rom_banks: 8,
980                chr_rom_banks: 0,
981                ..NesHeader::default()
982            },
983        ),
984    );
985}