Skip to main content

monsoon_core/emulation/rom/
mod.rs

1//! ROM file parsing and metadata.
2//!
3//! This module provides types for loading and representing NES ROM files
4//! in the iNES, NES 2.0, and related formats. The main entry point is
5//! [`RomFile::load()`], which auto-detects the format and parses the ROM.
6//!
7//! For programmatic ROM construction (e.g., in tests), use [`RomBuilder`].
8
9mod formats;
10
11use std::error::Error;
12use std::fmt::{Debug, Display, Formatter};
13
14use num_enum::{FromPrimitive, IntoPrimitive};
15use serde::{Deserialize, Serialize};
16use sha2::{Digest, Sha256};
17
18use crate::emulation::mem::nametable_memory::{NametableArrangement, NametableMemory};
19use crate::emulation::mem::{Memory, MemoryDevice, Ram, Rom};
20use crate::emulation::rom::formats::archaic_ines::ArchaicInes;
21use crate::emulation::rom::formats::ines::Ines;
22use crate::emulation::rom::formats::ines_07::Ines07;
23use crate::emulation::rom::formats::ines2::Ines2;
24
25/// Errors that can occur while parsing a ROM file.
26#[derive(Debug, Clone)]
27pub enum ParseError {
28    /// The sizes declared in the ROM header exceed the actual file length.
29    SizeBiggerThanFile,
30    /// The ROM data is too short to contain a valid header (minimum 16 bytes).
31    InvalidHeader,
32    /// The ROM format is not recognized (missing `NES\x1A` magic bytes).
33    UnsupportedFormat,
34}
35
36impl Display for ParseError {
37    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
38        match self {
39            ParseError::SizeBiggerThanFile => {
40                write!(
41                    f,
42                    "Rom sizes specified in header are larger than total rom size"
43                )
44            }
45            ParseError::InvalidHeader => {
46                write!(f, "Rom data is too short to contain a valid header")
47            }
48            ParseError::UnsupportedFormat => {
49                write!(f, "Rom format is not recognized")
50            }
51        }
52    }
53}
54
55impl Error for ParseError {}
56
57/// Trait for ROM format parsers.
58///
59/// Each supported ROM format (iNES, NES 2.0, etc.) implements this trait.
60/// Users should not need to call parsers directly; use [`RomFile::load()`]
61/// instead, which auto-detects the format.
62#[doc(hidden)]
63pub trait RomParser: Debug {
64    fn parse(&self, rom: &[u8], name: Option<String>) -> Result<RomFile, ParseError>;
65}
66
67/// A parsed NES ROM file.
68///
69/// Contains all metadata extracted from the ROM header (mapper number,
70/// memory sizes, mirroring, etc.) as well as the raw ROM data used for
71/// loading into the emulator's memory map.
72///
73/// # Loading a ROM
74///
75/// ```rust,no_run
76/// use monsoon_core::emulation::rom::RomFile;
77///
78/// # let raw_bytes: &[u8] = &[];
79/// let rom = RomFile::load(raw_bytes, Some("my_game.nes".to_string())).expect("invalid ROM");
80/// println!("Mapper: {}", rom.mapper);
81/// ```
82///
83/// # Constructing a ROM programmatically
84///
85/// Use [`RomBuilder`] for test scenarios where you need custom ROM metadata
86/// without providing actual ROM data.
87#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
88pub struct RomFile {
89    /// Human-readable name of the ROM (typically the file name).
90    pub name: Option<String>,
91    /// PRG (program) memory sizes.
92    pub prg_memory: PrgMemory,
93    /// CHR (character/graphics) memory sizes.
94    pub chr_memory: ChrMemory,
95    /// iNES mapper number identifying the cartridge board hardware.
96    pub mapper: RomMapper,
97    /// Default expansion device identifier (NES 2.0).
98    pub default_expansion_device: ExpansionDevice,
99    /// Number of miscellaneous ROM areas (NES 2.0).
100    pub misc_rom_count: u8,
101    /// Extended console type (NES 2.0), if applicable.
102    pub extended_console_type: Option<ExtendedConsoleType>,
103    /// VS System hardware type, if applicable.
104    pub vs_system_hardware_type: Option<VsHardwareType>,
105    /// VS System PPU type, if applicable.
106    pub vs_system_ppu_type: Option<VsSystemPpuType>,
107    /// CPU/PPU timing mode (0 = NTSC, 1 = PAL, 2 = Multi-region, 3 = Dendy).
108    pub timing_region: RomTimingRegion,
109    /// Console type (0 = NES/Famicom, 1 = VS System, 2 = Playchoice-10, 3 =
110    /// Extended).
111    pub console_type: ConsoleType,
112    /// Nametable mirroring mode from header bit 0 (`true` = vertical, `false` =
113    /// horizontal).
114    pub hardwired_nametable_layout: bool,
115    /// Whether the cartridge contains battery-backed persistent memory.
116    pub is_battery_backed: bool,
117    /// Whether a 512-byte trainer is present before PRG data.
118    pub trainer_present: bool,
119    /// Whether the ROM uses alternative nametable layouts.
120    pub alternative_nametables: bool,
121    /// Submapper number (NES 2.0).
122    pub submapper_number: u8,
123    /// SHA-256 checksum of the raw ROM data.
124    pub data_checksum: [u8; 32],
125    /// Raw ROM file bytes. Skipped during serialization to reduce save state
126    /// size.
127    #[serde(skip)]
128    pub data: Vec<u8>,
129}
130
131#[derive(
132    Debug,
133    Copy,
134    Clone,
135    Eq,
136    PartialEq,
137    Ord,
138    PartialOrd,
139    Hash,
140    FromPrimitive,
141    IntoPrimitive,
142    Serialize,
143    Deserialize,
144)]
145#[repr(u8)]
146#[serde(into = "u8", from = "u8")]
147pub enum ExpansionDevice {
148    Unspecified = 0,
149    StandardController = 1,
150    FourScore = 2,
151    FourScoreSimple = 3,
152    VsSystem1P4016 = 4,
153    VsSystem1P4017 = 5,
154    VsZapper = 7,
155    Zapper4017 = 8,
156    TwoZappers = 9,
157    BandaiHyperShotLightgun = 10,
158    PowerPadSideA = 11,
159    PowerPadSideB = 12,
160    FamilyTrainerSideA = 13,
161    FamilyTrainerSideB = 14,
162    ArkanoidVausNes = 15,
163    ArkanoidVausFamicom = 16,
164    TwoVausPlusDataRecorder = 17,
165    KonamiHyperShot = 18,
166    CoconutsPachinko = 19,
167    ExcitingBoxingPunchingBag = 20,
168    JissenMahjong = 21,
169    PartyTap = 22,
170    OekaKidsTablet = 23,
171    SunsoftBarcodeBattler = 24,
172    MiraclePianoKeyboard = 25,
173    PokkunMoguraaTapTapMat = 26,
174    TopRider = 27,
175    DoubleFisted = 28,
176    Famicom3dSystem = 29,
177    DoremikkoKeyboard = 30,
178    RobGyromite = 31,
179    FamicomDataRecorder = 32,
180    AsciiTurboFile = 33,
181    IgsStorageBattleBox = 34,
182    FamilyBasicKeyboardPlusDataRecorder = 35,
183    PecKeyboard = 36,
184    Bit79Keyboard = 37,
185    SuborKeyboard = 38,
186    SuborKeyboardPlusMacroWinnersMouse = 39,
187    SuborKeyboardPlusSuborMouse4016 = 40,
188    SnesMouse4016 = 41,
189    Multicart = 42,
190    TwoSnesControllers = 43,
191    RacerMateBicycle = 44,
192    UForce = 45,
193    RobStackUp = 46,
194    CityPatrolmanLightgun = 47,
195    SharpC1CassetteInterface = 48,
196    StandardControllerSwappedLayout = 49,
197    ExcaliburSudokuPad = 50,
198    AblPinball = 51,
199    GoldenNuggetCasinoExtraButtons = 52,
200    KedaKeyboard = 53,
201    SuborKeyboardPlusSuborMouse4017 = 54,
202    PortTestController = 55,
203    BandaiMultiGamePlayerGamepadButtons = 56,
204    VenomTvDanceMat = 57,
205    LgTvRemoteControl = 58,
206    FamicomNetworkController = 59,
207    KingFishingController = 60,
208    CroakyKaraokeController = 61,
209    KingwonKeyboard = 62,
210    ZechengKeyboard = 63,
211    SuborKeyboardPlusL90RotatedPs2Mouse4017 = 64,
212    Ps2KeyboardUM6578PlusPs2Mouse4017 = 65,
213    Ps2MouseUM6578 = 66,
214    YuxingMouse4016 = 67,
215    SuborKeyboardPlusYuxingMouse4016 = 68,
216    GiggleTvPump = 69,
217    BBKKeyboardPlusR90RotatedPs2Mouse4017 = 70,
218    MagicalCooking = 71,
219    SnesMouse4017 = 72,
220    Zapper4016 = 73,
221    ArkanoidVausControllerPrototype = 74,
222    TvMahjongGameController = 75,
223    MahjongGekitouDensetsuController = 76,
224    SuborKeyboardPlusXInvertedPs2Mouse4017 = 77,
225    IbmPcXtKeyboard = 78,
226    SuborKeyboardPlusMegaBookMouse = 79,
227    #[num_enum(catch_all)]
228    Unknown(u8),
229}
230
231impl Display for ExpansionDevice {
232    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
233        let str: &str = match self {
234            ExpansionDevice::Unspecified => "Unspecified",
235            ExpansionDevice::StandardController => "Standard NES/Famicom Controllers",
236            ExpansionDevice::FourScore => {
237                "NES Four Score/Satellite w/ two additional standard controllers"
238            }
239            ExpansionDevice::FourScoreSimple => {
240                "Famicom Four Player Adapter w/ two additional standard controllers using the \
241                 \"simple\" protocol"
242            }
243            ExpansionDevice::VsSystem1P4016 => "Vs. System (One Player via Port 1)",
244            ExpansionDevice::VsSystem1P4017 => "Vs. System (One Player via Port 2)",
245            ExpansionDevice::VsZapper => "Vs. Zapper",
246            ExpansionDevice::Zapper4017 => "Zapper (via Port 2)",
247            ExpansionDevice::TwoZappers => "Two Zappers",
248            ExpansionDevice::BandaiHyperShotLightgun => "Bandai Hyper Shot Lightgun",
249            ExpansionDevice::PowerPadSideA => "Power Pad Side A",
250            ExpansionDevice::PowerPadSideB => "Power Pad Side B",
251            ExpansionDevice::FamilyTrainerSideA => "Family Trainer Side A",
252            ExpansionDevice::FamilyTrainerSideB => "Family Trainer Side B",
253            ExpansionDevice::ArkanoidVausNes => "Arkanoid Vaus Controller (NES)",
254            ExpansionDevice::ArkanoidVausFamicom => "Arkanoid Vaus Controller (Famicom)",
255            ExpansionDevice::TwoVausPlusDataRecorder => {
256                "Two Arkanoid Vaus Controllers + Famicom Data Recorder"
257            }
258            ExpansionDevice::KonamiHyperShot => "Konami Hyper Shot Controller",
259            ExpansionDevice::CoconutsPachinko => "Coconuts Pachinko Controller",
260            ExpansionDevice::ExcitingBoxingPunchingBag => "Exciting Boxing Punching Bag",
261            ExpansionDevice::JissenMahjong => "Jissen Mahjong Controller",
262            ExpansionDevice::PartyTap => "米澤 (Yonezawa) Party Tap",
263            ExpansionDevice::OekaKidsTablet => "Oeka Kids Tablet",
264            ExpansionDevice::SunsoftBarcodeBattler => "Sunsoft Barcode Battler",
265            ExpansionDevice::MiraclePianoKeyboard => "Miracle Piano Keyboard",
266            ExpansionDevice::PokkunMoguraaTapTapMat => "Pokkun Moguraa Tap-tap Mat1",
267            ExpansionDevice::TopRider => "Top Rider",
268            ExpansionDevice::DoubleFisted => "Double Fisted",
269            ExpansionDevice::Famicom3dSystem => "Famicom 3D System",
270            ExpansionDevice::DoremikkoKeyboard => "Doremikko Keyboard",
271            ExpansionDevice::RobGyromite => "R.O.B Gyromite",
272            ExpansionDevice::FamicomDataRecorder => "Famicom Data Recorder",
273            ExpansionDevice::AsciiTurboFile => "ASCII Turbo File",
274            ExpansionDevice::IgsStorageBattleBox => "IGS Storage Battle Box",
275            ExpansionDevice::FamilyBasicKeyboardPlusDataRecorder => {
276                "Family Basic Keyboard + Famicom Data Recorder"
277            }
278            ExpansionDevice::PecKeyboard => "东达 (Dōngdá) PEC Keyboard",
279            ExpansionDevice::Bit79Keyboard => "普澤 (Pǔzé, a.k.a. Bit Corp.) Bit-79 Keyboard",
280            ExpansionDevice::SuborKeyboard => "小霸王 (Xiǎobàwáng, a.k.a. Subor) Keyboard",
281            ExpansionDevice::SuborKeyboardPlusMacroWinnersMouse => {
282                "小霸王 (Xiǎobàwáng, a.k.a. Subor) Keyboard + Macro Winners Mouse"
283            }
284            ExpansionDevice::SuborKeyboardPlusSuborMouse4016 => {
285                "小霸王 (Xiǎobàwáng, a.k.a. Subor) Keyboard + Subor Mouse (via Port 1)"
286            }
287            ExpansionDevice::SnesMouse4016 => "SNES Mouse (via Port 1)",
288            ExpansionDevice::Multicart => "Multicart",
289            ExpansionDevice::TwoSnesControllers => "Two SNES Controllers",
290            ExpansionDevice::RacerMateBicycle => "RacerMate Bicycle",
291            ExpansionDevice::UForce => "U-Force",
292            ExpansionDevice::RobStackUp => "R.O.B Stack-Up",
293            ExpansionDevice::CityPatrolmanLightgun => "City Patrolman Lightgun",
294            ExpansionDevice::SharpC1CassetteInterface => "Sharp C1 Cassette Interface",
295            ExpansionDevice::StandardControllerSwappedLayout => {
296                "Standard Controller with swapped Left-Right/Up-Down/B-A"
297            }
298            ExpansionDevice::ExcaliburSudokuPad => "Excalibur Sudoku Pad",
299            ExpansionDevice::AblPinball => "ABL Pinball",
300            ExpansionDevice::GoldenNuggetCasinoExtraButtons => "Golden Nugget Casino Controller",
301            ExpansionDevice::KedaKeyboard => "科达 (Kēdá) Keyboard",
302            ExpansionDevice::SuborKeyboardPlusSuborMouse4017 => {
303                "小霸王 (Xiǎobàwáng, a.k.a. Subor) Keyboard + Subor Mouse (via Port 2)"
304            }
305            ExpansionDevice::PortTestController => "Port test controller",
306            ExpansionDevice::BandaiMultiGamePlayerGamepadButtons => {
307                "Bandai Multi Game Player Gamepad"
308            }
309            ExpansionDevice::VenomTvDanceMat => "Venom TV Dance Mat",
310            ExpansionDevice::LgTvRemoteControl => "LG TV Remote Control",
311            ExpansionDevice::FamicomNetworkController => "Famicom Network Controller",
312            ExpansionDevice::KingFishingController => "King Fishing Controller",
313            ExpansionDevice::CroakyKaraokeController => "Croaky Karaoke Controller",
314            ExpansionDevice::KingwonKeyboard => "科王 (Kēwáng, a.k.a. Kingwon) Keyboard",
315            ExpansionDevice::ZechengKeyboard => "泽诚 (Zéchéng) Keyboard",
316            ExpansionDevice::SuborKeyboardPlusL90RotatedPs2Mouse4017 => {
317                "小霸王 (Xiǎobàwáng, a.k.a. Subor) Keyboard + PS/2 mouse rotated left (via Port 2)"
318            }
319            ExpansionDevice::Ps2KeyboardUM6578PlusPs2Mouse4017 => {
320                "PS/2 Keyboard in UM6578 PS/2 port + PS/2 Mouse (via Port 2)"
321            }
322            ExpansionDevice::Ps2MouseUM6578 => "PS/2 Mouse in UM6578 PS/2 port",
323            ExpansionDevice::YuxingMouse4016 => "裕兴 (Yùxìng) Mouse (via Port 1)",
324            ExpansionDevice::SuborKeyboardPlusYuxingMouse4016 => {
325                "小霸王 (Xiǎobàwáng, a.k.a. Subor )Keyboard + 裕兴 (Yùxìng) Mouse (via Port 1)"
326            }
327            ExpansionDevice::GiggleTvPump => "Giggle TV Pump",
328            ExpansionDevice::BBKKeyboardPlusR90RotatedPs2Mouse4017 => {
329                "步步高 (Bùbùgāo, a.k.a. BBK) Keyboard + PS/2 mouse rotated right (via Port 2)"
330            }
331            ExpansionDevice::MagicalCooking => "Magical Cooking",
332            ExpansionDevice::SnesMouse4017 => "SNES Mouse (via Port 2)",
333            ExpansionDevice::Zapper4016 => "Zapper (via Port 1)",
334            ExpansionDevice::ArkanoidVausControllerPrototype => {
335                "Arkanoid Vaus Controller (Prototype)"
336            }
337            ExpansionDevice::TvMahjongGameController => "TV 麻雀 Game (TV Mahjong Game) Controller",
338            ExpansionDevice::MahjongGekitouDensetsuController => {
339                "麻雀激闘伝説 (Mahjong Gekitou Densetsu) Controller"
340            }
341            ExpansionDevice::SuborKeyboardPlusXInvertedPs2Mouse4017 => {
342                "小霸王 (Xiǎobàwáng, a.k.a. Subor) Keyboard + X-inverted PS/2 mouse via (Port 2)"
343            }
344            ExpansionDevice::IbmPcXtKeyboard => "IBM PC/XT Keyboard",
345            ExpansionDevice::SuborKeyboardPlusMegaBookMouse => {
346                "小霸王 (Xiǎobàwáng, a.k.a. Subor) Keyboard + Mega Book Mouse"
347            }
348            ExpansionDevice::Unknown(_) => "Unknown Expansion Device",
349        };
350
351        let id: u8 = (*self).into();
352
353        write!(f, "{str} (Header: {})", id)
354    }
355}
356
357#[derive(
358    Debug,
359    Copy,
360    Clone,
361    Eq,
362    PartialEq,
363    Ord,
364    PartialOrd,
365    Hash,
366    FromPrimitive,
367    IntoPrimitive,
368    Serialize,
369    Deserialize,
370)]
371#[repr(u8)]
372#[serde(into = "u8", from = "u8")]
373pub enum ExtendedConsoleType {
374    NesFamicom = 0,
375    VsSystem = 1,
376    Playchoice10 = 2,
377    DecimalModeFamiclone = 3,
378    EPSMFamicom = 4,
379    VT01 = 5,
380    VT02 = 6,
381    VT03 = 7,
382    VT09 = 8,
383    VT32 = 9,
384    VT369 = 0xA,
385    UM6578 = 0xB,
386    FamicomNetworkSystem = 0xC,
387    #[num_enum(catch_all)]
388    Unknown(u8),
389}
390
391impl Display for ExtendedConsoleType {
392    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
393        let str: &str = match self {
394            ExtendedConsoleType::NesFamicom => "Nes/Famicom/Dendy",
395            ExtendedConsoleType::VsSystem => "Nintendo Vs. System",
396            ExtendedConsoleType::Playchoice10 => "Nintendo Playchoice 10",
397            ExtendedConsoleType::DecimalModeFamiclone => "Famiclone w/ Decimal Mode CPU",
398            ExtendedConsoleType::EPSMFamicom => {
399                "Nes/Famicom/Dendy w/ EPSM module or plug-through Cartridge"
400            }
401            ExtendedConsoleType::VT01 => "V.R. Technology VT01 with red/cyan STN palette",
402            ExtendedConsoleType::VT02 => "V.R Technology VT02",
403            ExtendedConsoleType::VT03 => "V.R Technology VT03",
404            ExtendedConsoleType::VT09 => "V.R Technology VT09",
405            ExtendedConsoleType::VT32 => "V.R Technology VT32",
406            ExtendedConsoleType::VT369 => "V.R Technology VT369",
407            ExtendedConsoleType::UM6578 => "UMC UM6578",
408            ExtendedConsoleType::FamicomNetworkSystem => "Famicom Network System",
409            ExtendedConsoleType::Unknown(_) => "Unknown Extended Console Type",
410        };
411
412        let id: u8 = (*self).into();
413
414        write!(f, "{str} (Header: {})", id)
415    }
416}
417
418#[derive(
419    Debug,
420    Copy,
421    Clone,
422    Eq,
423    PartialEq,
424    Ord,
425    PartialOrd,
426    Hash,
427    FromPrimitive,
428    IntoPrimitive,
429    Serialize,
430    Deserialize,
431)]
432#[repr(u8)]
433#[serde(into = "u8", from = "u8")]
434pub enum VsHardwareType {
435    UniSystem = 0,
436    UnisystemRbiBaseball = 1,
437    UnisystemTkoBoxing = 2,
438    UnisystemSuperXevious = 3,
439    UnisystemVcIceClimberJapan = 4,
440    DualSystem = 5,
441    DualSystemRaidOnBungelingBay = 6,
442    #[num_enum(catch_all)]
443    Unknown(u8),
444}
445
446impl Display for VsHardwareType {
447    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
448        let str: &str = match self {
449            VsHardwareType::UniSystem => "Vs. Unisystem (normal)",
450            VsHardwareType::UnisystemRbiBaseball => "Vs. Unisystem (RBI Baseball protection)",
451            VsHardwareType::UnisystemTkoBoxing => "Vs. Unisystem (TKO Boxing protection)",
452            VsHardwareType::UnisystemSuperXevious => "Vs. Unisystem (Super Xevious protection)",
453            VsHardwareType::UnisystemVcIceClimberJapan => {
454                "Vs. Unisystem (Vs. Ice Climber Japan protection)"
455            }
456            VsHardwareType::DualSystem => "Vs. Dual System (normal)",
457            VsHardwareType::DualSystemRaidOnBungelingBay => {
458                "Vs. Dual System (Raid on Bungeling Bay protection)"
459            }
460            VsHardwareType::Unknown(_) => "Unknown Vs. System hardware type",
461        };
462
463        let id: u8 = (*self).into();
464
465        write!(f, "{str} (Header: {})", id)
466    }
467}
468
469#[derive(
470    Debug,
471    Copy,
472    Clone,
473    Eq,
474    PartialEq,
475    Ord,
476    PartialOrd,
477    Hash,
478    FromPrimitive,
479    IntoPrimitive,
480    Serialize,
481    Deserialize,
482)]
483#[repr(u8)]
484#[serde(into = "u8", from = "u8")]
485pub enum VsSystemPpuType {
486    RP2C03 = 0,
487    RP2C04_0001 = 2,
488    RP2C04_0002 = 3,
489    RP2C04_0003 = 4,
490    RP2C04_0004 = 5,
491    RC2C05_01 = 8,
492    RC2C05_02 = 9,
493    RC2C05_03 = 0xA,
494    RC2C05_04 = 0xB,
495    #[num_enum(catch_all)]
496    Unknown(u8),
497}
498
499impl Display for VsSystemPpuType {
500    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
501        let str: &str = match self {
502            VsSystemPpuType::RP2C03 => "Any RP2C03/RC2C03 Variant",
503            VsSystemPpuType::RP2C04_0001 => "RP2C04-0001",
504            VsSystemPpuType::RP2C04_0002 => "RP2C04-0002",
505            VsSystemPpuType::RP2C04_0003 => "RP2C04-0003",
506            VsSystemPpuType::RP2C04_0004 => "RP2C04-0004",
507            VsSystemPpuType::RC2C05_01 => "RC2C05-01",
508            VsSystemPpuType::RC2C05_02 => "RC2C05-02",
509            VsSystemPpuType::RC2C05_03 => "RC2C05-03",
510            VsSystemPpuType::RC2C05_04 => "RC2C05-04",
511            VsSystemPpuType::Unknown(_) => "Unknown Vs. System PPU",
512        };
513
514        let id: u8 = (*self).into();
515
516        write!(f, "{str} (Header: {})", id)
517    }
518}
519
520#[derive(
521    Debug,
522    Copy,
523    Clone,
524    Eq,
525    PartialEq,
526    Ord,
527    PartialOrd,
528    Hash,
529    FromPrimitive,
530    IntoPrimitive,
531    Serialize,
532    Deserialize,
533)]
534#[repr(u8)]
535#[serde(into = "u8", from = "u8")]
536pub enum ConsoleType {
537    NesFamicom = 0,
538    VsSystem = 1,
539    Playchoice10 = 2,
540    Extended = 3,
541    #[num_enum(catch_all)]
542    Unknown(u8),
543}
544
545impl Display for ConsoleType {
546    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
547        let str: &str = match self {
548            ConsoleType::NesFamicom => "Nes/Famicom/Dendy",
549            ConsoleType::VsSystem => "Nintendo Vs. System",
550            ConsoleType::Playchoice10 => "Nintendo Playchoice 10",
551            ConsoleType::Extended => "Extended Console Type",
552            ConsoleType::Unknown(_) => "Unknown Console Type",
553        };
554
555        let id: u8 = (*self).into();
556
557        write!(f, "{str} (Header: {})", id)
558    }
559}
560
561#[derive(
562    Debug,
563    Copy,
564    Clone,
565    Eq,
566    PartialEq,
567    Ord,
568    PartialOrd,
569    Hash,
570    FromPrimitive,
571    IntoPrimitive,
572    Serialize,
573    Deserialize,
574)]
575#[repr(u8)]
576#[serde(into = "u8", from = "u8")]
577pub enum RomTimingRegion {
578    RP2C02 = 0,
579    RP2C07 = 1,
580    Multi = 2,
581    UA6538 = 3,
582    #[num_enum(catch_all)]
583    Unknown(u8),
584}
585
586impl Display for RomTimingRegion {
587    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
588        let str: &str = match self {
589            RomTimingRegion::RP2C02 => "NTSC/RP2C02",
590            RomTimingRegion::RP2C07 => "Licensed PAL/RP2C07",
591            RomTimingRegion::Multi => "Multiple Regions",
592            RomTimingRegion::UA6538 => "Dendy/UA6538",
593            RomTimingRegion::Unknown(_) => "Unknown Region",
594        };
595
596        let id: u8 = (*self).into();
597
598        write!(f, "{str} (Header: {})", id)
599    }
600}
601
602#[derive(
603    Debug,
604    Copy,
605    Clone,
606    Eq,
607    PartialEq,
608    Ord,
609    PartialOrd,
610    Hash,
611    FromPrimitive,
612    IntoPrimitive,
613    Serialize,
614    Deserialize,
615)]
616#[repr(u16)]
617#[serde(into = "u16", from = "u16")]
618pub enum RomMapper {
619    NRom = 0,
620    MMC1 = 1,
621    #[num_enum(catch_all)]
622    Unknown(u16),
623}
624
625impl Display for RomMapper {
626    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
627        let str: &str = match self {
628            RomMapper::NRom => "NROM",
629            RomMapper::MMC1 => "MMC1",
630            RomMapper::Unknown(_) => "Unknown Mapper",
631        };
632
633        let mapper_num: u16 = (*self).into();
634
635        write!(f, "{str} (INes Mapper {})", mapper_num)
636    }
637}
638
639/// PRG (program) memory size information from the ROM header.
640#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
641pub struct PrgMemory {
642    /// Size of PRG ROM in bytes.
643    pub prg_rom_size: u32,
644    /// Size of PRG RAM (volatile) in bytes.
645    pub prg_ram_size: u32,
646    /// Size of PRG NVRAM (non-volatile / battery-backed) in bytes.
647    pub prg_nvram_size: u32,
648}
649
650impl PrgMemory {
651    fn new(prg_rom_size: u32, prg_ram_size: u32, prg_nvram_size: u32) -> PrgMemory {
652        Self {
653            prg_rom_size,
654            prg_nvram_size,
655            prg_ram_size,
656        }
657    }
658}
659
660/// CHR (character/graphics) memory size information from the ROM header.
661#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
662pub struct ChrMemory {
663    /// Size of CHR ROM in bytes.
664    pub chr_rom_size: u32,
665    /// Size of CHR RAM (volatile) in bytes.
666    pub chr_ram_size: u32,
667    /// Size of CHR NVRAM (non-volatile) in bytes.
668    pub chr_nvram_size: u32,
669}
670
671impl ChrMemory {
672    fn new(chr_rom_size: u32, chr_ram_size: u32, chr_nvram_size: u32) -> ChrMemory {
673        Self {
674            chr_rom_size,
675            chr_nvram_size,
676            chr_ram_size,
677        }
678    }
679}
680
681impl RomFile {
682    fn range_all_zeros(arr: &[u8], start: usize, end: usize) -> bool {
683        if start > end || end > arr.len() {
684            return false;
685        }
686        arr[start..end].iter().all(|&x| x == 0)
687    }
688
689    fn get_rom_type(rom: &[u8]) -> Result<Box<dyn RomParser>, ParseError> {
690        // iNES and NES 2.0 headers are 16 bytes minimum
691        if rom.len() < 16 {
692            return Err(ParseError::InvalidHeader);
693        }
694
695        if rom.starts_with(&[0x4E, 0x45, 0x53, 0x1A]) {
696            let prg_rom_size_lsb = rom[4] as u16;
697            let prg_rom_size_msb = (rom[9] & 0xF) as u16;
698
699            let prg_rom_size = Ines2::get_prg_rom_size(prg_rom_size_lsb, prg_rom_size_msb);
700
701            let chr_rom_size_lsb = rom[5] as u16;
702            let chr_rom_size_msb = (rom[9] & 0xF0) as u16;
703
704            let chr_rom_size = Ines2::get_chr_rom_size(chr_rom_size_lsb, chr_rom_size_msb);
705
706            if rom[7] & 0b00001100 == 8
707                && (prg_rom_size as usize + chr_rom_size as usize) < rom.len()
708            {
709                return Ok(Box::new(Ines2));
710            }
711
712            if rom[7] & 0b00001100 == 4 {
713                return Ok(Box::new(ArchaicInes));
714            }
715
716            if rom[7] & 0b00001100 == 0 && Self::range_all_zeros(rom, 12, 16) {
717                return Ok(Box::new(Ines));
718            }
719
720            return Ok(Box::new(Ines07));
721        }
722
723        Err(ParseError::UnsupportedFormat)
724    }
725
726    /// Parses a ROM file from raw bytes.
727    ///
728    /// Auto-detects the ROM format (iNES, NES 2.0, archaic iNES, etc.)
729    /// from the header and extracts all metadata. The raw data is stored
730    /// in [`data`](RomFile::data) and a SHA-256 checksum is computed.
731    ///
732    /// # Arguments
733    ///
734    /// * `data` — The complete ROM file as a byte slice.
735    /// * `name` — An optional human-readable name (e.g., the file name).
736    ///
737    /// # Errors
738    ///
739    /// Returns a [`ParseError`] if:
740    /// - The data is too short to contain a valid header
741    ///   ([`ParseError::InvalidHeader`]).
742    /// - The ROM format is not recognized ([`ParseError::UnsupportedFormat`]).
743    /// - The header declares sizes larger than the file
744    ///   ([`ParseError::SizeBiggerThanFile`]).
745    pub fn load(data: &[u8], name: Option<String>) -> Result<RomFile, ParseError> {
746        let mut hasher = Sha256::new();
747        hasher.update(data);
748        let hash: [u8; 32] = hasher.finalize().into();
749
750        let rom_type = RomFile::get_rom_type(data)?;
751        let mut rom_file = rom_type.parse(data, name)?;
752        rom_file.data = data.to_vec();
753        rom_file.data_checksum = hash;
754        Ok(rom_file)
755    }
756
757    /// Extracts the PRG ROM region as a read-only [`Memory`] device.
758    ///
759    /// This is used internally to populate the CPU memory map at addresses
760    /// `$8000`-`$FFFF`.
761    #[doc(hidden)]
762    pub fn get_prg_rom(&self) -> Memory {
763        let mut rom = Rom::new(self.prg_memory.prg_rom_size as usize);
764
765        let mut start = 16usize;
766
767        if self.trainer_present {
768            start += 512;
769        }
770
771        rom.load(
772            self.data[start..start + self.prg_memory.prg_rom_size as usize]
773                .to_vec()
774                .into_boxed_slice(),
775        );
776        Memory::Rom(rom)
777    }
778
779    /// Extracts the CHR ROM region as a read-only [`Memory`] device, if
780    /// present.
781    ///
782    /// Returns `None` when the ROM uses CHR RAM instead of CHR ROM
783    /// (i.e., `chr_rom_size == 0`).
784    #[doc(hidden)]
785    pub fn get_chr_rom(&self) -> Option<Memory> {
786        if self.chr_memory.chr_rom_size == 0 {
787            return None;
788        }
789
790        let mut rom = Rom::new(self.chr_memory.chr_rom_size as usize);
791
792        let mut start = 16usize;
793
794        if self.trainer_present {
795            start += 512;
796        }
797
798        rom.load(
799            self.data[start + self.prg_memory.prg_rom_size as usize
800                ..start
801                    + self.prg_memory.prg_rom_size as usize
802                    + self.chr_memory.chr_rom_size as usize]
803                .to_vec()
804                .into_boxed_slice(),
805        );
806        Some(Memory::Rom(rom))
807    }
808
809    /// Extracts the PRG RAM region as a writable [`Memory`] device.
810    ///
811    /// This is mapped at CPU addresses `$6000`-`$7FFF` and may be
812    /// battery-backed for save data.
813    #[doc(hidden)]
814    pub fn get_prg_ram(&self) -> Memory {
815        let mut ram = Ram::new(self.prg_memory.prg_ram_size as usize);
816
817        let mut start = 16usize;
818
819        if self.trainer_present {
820            start += 512;
821        }
822
823        ram.load(
824            self.data[start..start + self.prg_memory.prg_rom_size as usize]
825                .to_vec()
826                .into_boxed_slice(),
827        );
828
829        Memory::Ram(ram)
830    }
831
832    /// Creates the nametable memory for the PPU based on the ROM's mirroring
833    /// mode.
834    ///
835    /// Returns a [`Memory`] device configured for either horizontal or vertical
836    /// nametable mirroring, as specified by the ROM header.
837    #[doc(hidden)]
838    pub fn get_nametable_memory(&self) -> Memory {
839        let mirroring = match self.hardwired_nametable_layout {
840            true => NametableArrangement::Vertical,
841            false => NametableArrangement::Horizontal,
842        };
843        Memory::NametableMemory(NametableMemory::new(mirroring))
844    }
845}
846
847impl From<&RomFile> for RomFile {
848    fn from(rom: &RomFile) -> Self { rom.clone() }
849}
850
851impl From<&[u8]> for RomFile {
852    fn from(data: &[u8]) -> Self { RomFile::load(data, None).expect("Failed to parse ROM data") }
853}
854
855impl From<&(&[u8], String)> for RomFile {
856    fn from((data, name): &(&[u8], String)) -> Self {
857        RomFile::load(data, Some(name.clone())).expect("Failed to parse ROM data")
858    }
859}
860
861#[cfg(not(target_arch = "wasm32"))]
862impl From<&String> for RomFile {
863    fn from(path: &String) -> Self {
864        use std::fs::File;
865        use std::io::Read;
866
867        let mut data = Vec::new();
868        File::open(path)
869            .unwrap_or_else(|e| panic!("Failed to open ROM file '{}': {}", path, e))
870            .read_to_end(&mut data)
871            .unwrap_or_else(|e| panic!("Failed to read ROM file '{}': {}", path, e));
872
873        RomFile::load(&data, Some(path.clone())).expect("Failed to parse ROM data")
874    }
875}
876
877/// A builder for constructing [`RomFile`] instances programmatically.
878///
879/// This is primarily useful for testing when you need a ROM with specific
880/// metadata but no actual ROM data.
881///
882/// # Example
883///
884/// ```rust
885/// use monsoon_core::emulation::rom::{RomBuilder, RomMapper};
886///
887/// let rom = RomBuilder::new()
888///     .prg_rom_size(32 * 1024)
889///     .chr_rom_size(8 * 1024)
890///     .mapper_number(0)
891///     .hardwired_nametable_layout(true) // vertical mirroring
892///     .build();
893///
894/// assert_eq!(rom.mapper, RomMapper::NRom);
895/// ```
896pub struct RomBuilder {
897    name: Option<String>,
898    prg_rom_size: u32,
899    chr_rom_size: u32,
900    mapper_number: u16,
901    default_expansion_device: u8,
902    misc_rom_count: u8,
903    extended_console_type: Option<u8>,
904    vs_system_hardware_type: Option<u8>,
905    vs_system_ppu_type: Option<u8>,
906    rom_timing_region: u8,
907    chr_nvram_size: u32,
908    chr_ram_size: u32,
909    prg_nvram_size: u32,
910    prg_ram_size: u32,
911    console_type: u8,
912    hardwired_nametable_layout: bool,
913    is_battery_backed: bool,
914    trainer_present: bool,
915    alternative_nametables: bool,
916    submapper_number: u8,
917}
918
919impl Default for RomBuilder {
920    fn default() -> Self {
921        Self {
922            name: None,
923            prg_rom_size: 0,
924            chr_rom_size: 0,
925            mapper_number: 0,
926            default_expansion_device: 0,
927            misc_rom_count: 0,
928            extended_console_type: None,
929            vs_system_hardware_type: None,
930            vs_system_ppu_type: None,
931            rom_timing_region: 0,
932            chr_nvram_size: 0,
933            chr_ram_size: 0,
934            prg_nvram_size: 0,
935            prg_ram_size: 8 * 1024,
936            console_type: 0,
937            hardwired_nametable_layout: false,
938            is_battery_backed: false,
939            trainer_present: false,
940            alternative_nametables: false,
941            submapper_number: 0,
942        }
943    }
944}
945
946impl RomBuilder {
947    /// Creates a new builder with default values (mapper 0, 8 KB PRG RAM,
948    /// all other sizes zero).
949    pub fn new() -> Self { Self::default() }
950
951    /// Sets the PRG ROM size in bytes.
952    pub fn prg_rom_size(mut self, size: u32) -> Self {
953        self.prg_rom_size = size;
954        self
955    }
956
957    /// Sets the CHR ROM size in bytes.
958    pub fn chr_rom_size(mut self, size: u32) -> Self {
959        self.chr_rom_size = size;
960        self
961    }
962
963    /// Sets the iNES mapper number.
964    pub fn mapper_number(mut self, number: u16) -> Self {
965        self.mapper_number = number;
966        self
967    }
968
969    /// Sets the default expansion device (NES 2.0).
970    pub fn default_expansion_device(mut self, device: u8) -> Self {
971        self.default_expansion_device = device;
972        self
973    }
974
975    /// Sets the miscellaneous ROM count (NES 2.0).
976    pub fn misc_rom_count(mut self, count: u8) -> Self {
977        self.misc_rom_count = count;
978        self
979    }
980
981    /// Sets the extended console type (NES 2.0).
982    pub fn extended_console_type(mut self, console_type: Option<u8>) -> Self {
983        self.extended_console_type = console_type;
984        self
985    }
986
987    /// Sets the VS System hardware type.
988    pub fn vs_system_hardware_type(mut self, hardware_type: Option<u8>) -> Self {
989        self.vs_system_hardware_type = hardware_type;
990        self
991    }
992
993    /// Sets the VS System PPU type.
994    pub fn vs_system_ppu_type(mut self, ppu_type: Option<u8>) -> Self {
995        self.vs_system_ppu_type = ppu_type;
996        self
997    }
998
999    /// Sets the CPU/PPU timing mode (0 = NTSC, 1 = PAL, 2 = Multi, 3 = Dendy).
1000    pub fn cpu_ppu_timing(mut self, timing: u8) -> Self {
1001        self.rom_timing_region = timing;
1002        self
1003    }
1004
1005    /// Sets the CHR NVRAM (non-volatile) size in bytes.
1006    pub fn chr_nvram_size(mut self, size: u32) -> Self {
1007        self.chr_nvram_size = size;
1008        self
1009    }
1010
1011    /// Sets the CHR RAM (volatile) size in bytes.
1012    pub fn chr_ram_size(mut self, size: u32) -> Self {
1013        self.chr_ram_size = size;
1014        self
1015    }
1016
1017    /// Sets the PRG NVRAM (non-volatile / battery-backed) size in bytes.
1018    pub fn prg_nvram_size(mut self, size: u32) -> Self {
1019        self.prg_nvram_size = size;
1020        self
1021    }
1022
1023    /// Sets the PRG RAM (volatile) size in bytes.
1024    pub fn prg_ram_size(mut self, size: u32) -> Self {
1025        self.prg_ram_size = size;
1026        self
1027    }
1028
1029    /// Sets the console type (0 = NES, 1 = VS System, 2 = Playchoice-10, 3 =
1030    /// Extended).
1031    pub fn console_type(mut self, console_type: u8) -> Self {
1032        self.console_type = console_type;
1033        self
1034    }
1035
1036    /// Sets the nametable mirroring (`true` = vertical, `false` = horizontal).
1037    pub fn hardwired_nametable_layout(mut self, value: bool) -> Self {
1038        self.hardwired_nametable_layout = value;
1039        self
1040    }
1041
1042    /// Sets whether the cartridge has battery-backed memory.
1043    pub fn battery_backed(mut self, value: bool) -> Self {
1044        self.is_battery_backed = value;
1045        self
1046    }
1047
1048    /// Sets whether a 512-byte trainer is present in the ROM.
1049    pub fn trainer_present(mut self, value: bool) -> Self {
1050        self.trainer_present = value;
1051        self
1052    }
1053
1054    /// Sets whether the ROM uses alternative nametable layouts.
1055    pub fn alternative_nametables(mut self, value: bool) -> Self {
1056        self.alternative_nametables = value;
1057        self
1058    }
1059
1060    /// Sets the submapper number (NES 2.0).
1061    pub fn submapper_number(mut self, number: u8) -> Self {
1062        self.submapper_number = number;
1063        self
1064    }
1065
1066    /// Sets the ROM name.
1067    pub fn name(mut self, value: Option<String>) -> Self {
1068        self.name = value;
1069        self
1070    }
1071
1072    /// Consumes the builder and produces a [`RomFile`].
1073    ///
1074    /// The resulting ROM will have an empty `data` field and a zeroed checksum.
1075    pub fn build(self) -> RomFile {
1076        RomFile {
1077            name: self.name,
1078            prg_memory: PrgMemory::new(self.prg_rom_size, self.prg_ram_size, self.prg_nvram_size),
1079            chr_memory: ChrMemory::new(self.chr_rom_size, self.chr_ram_size, self.chr_nvram_size),
1080            mapper: RomMapper::from(self.mapper_number),
1081            default_expansion_device: ExpansionDevice::from(self.default_expansion_device),
1082            misc_rom_count: self.misc_rom_count,
1083            extended_console_type: self.extended_console_type.map(ExtendedConsoleType::from),
1084            vs_system_hardware_type: self.vs_system_hardware_type.map(VsHardwareType::from),
1085            vs_system_ppu_type: self.vs_system_ppu_type.map(VsSystemPpuType::from),
1086            timing_region: RomTimingRegion::from(self.rom_timing_region),
1087            console_type: ConsoleType::from(self.console_type),
1088            hardwired_nametable_layout: self.hardwired_nametable_layout,
1089            is_battery_backed: self.is_battery_backed,
1090            trainer_present: self.trainer_present,
1091            alternative_nametables: self.alternative_nametables,
1092            submapper_number: self.submapper_number,
1093            data_checksum: [0; 32],
1094            data: Vec::new(),
1095        }
1096    }
1097}
1098
1099#[cfg(test)]
1100mod tests {
1101    use super::*;
1102
1103    #[test]
1104    fn repr_enums_serialize_as_numbers() {
1105        let serialized = serde_json::to_string(&ExpansionDevice::StandardController)
1106            .expect("failed to serialize expansion device");
1107        assert_eq!(serialized, "1");
1108
1109        let serialized =
1110            serde_json::to_string(&RomMapper::MMC1).expect("failed to serialize rom mapper");
1111        assert_eq!(serialized, "1");
1112    }
1113
1114    #[test]
1115    fn repr_enums_deserialize_unknown_values() {
1116        let expansion: ExpansionDevice =
1117            serde_json::from_str("200").expect("failed to deserialize expansion device");
1118        assert_eq!(expansion, ExpansionDevice::Unknown(200));
1119
1120        let mapper: RomMapper = serde_json::from_str("9999").expect("failed to deserialize mapper");
1121        assert_eq!(mapper, RomMapper::Unknown(9999));
1122    }
1123
1124    #[test]
1125    fn rom_file_enum_fields_use_repr_values_in_json() {
1126        let rom = RomBuilder::new()
1127            .mapper_number(1)
1128            .default_expansion_device(1)
1129            .console_type(1)
1130            .cpu_ppu_timing(2)
1131            .vs_system_hardware_type(Some(5))
1132            .vs_system_ppu_type(Some(3))
1133            .extended_console_type(Some(0xC))
1134            .build();
1135
1136        let json = serde_json::to_string(&rom).expect("failed to serialize rom");
1137        assert!(json.contains("\"mapper\":1"));
1138        assert!(json.contains("\"default_expansion_device\":1"));
1139        assert!(json.contains("\"console_type\":1"));
1140        assert!(json.contains("\"timing_region\":2"));
1141        assert!(json.contains("\"vs_system_hardware_type\":5"));
1142        assert!(json.contains("\"vs_system_ppu_type\":3"));
1143        assert!(json.contains("\"extended_console_type\":12"));
1144    }
1145}