aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMiquel Sabaté Solà <mssola@mssola.com>2026-10-06 17:15:16 +0200
committerMiquel Sabaté Solà <mssola@mssola.com>2026-10-06 17:27:50 +0200
commit4bf12db91feb5db98d65b518dbad0c1a8c6748df (patch)
treebb0d84fc589b83ef57c464c241b97314a54cdc66
parentffd5f8684cf3871250ab2f0007b0324d74c134bb (diff)
downloadtools.nes-4bf12db91feb5db98d65b518dbad0c1a8c6748df.tar.gz
tools.nes-4bf12db91feb5db98d65b518dbad0c1a8c6748df.zip
readrom: add support for default expansion device
The 15th byte from the NES 2.0 header was wilfully ignored, but it can be relevant to the end user. Hence, add support for it in the 'header' library, and add the frontend code into 'readrom'. The point is not to support every single thing out there, but I have added the stuff that I thought was most relevant, and we can add more items to it whenever the need appears. Signed-off-by: Miquel Sabaté Solà <mssola@mssola.com>
-rw-r--r--crates/readrom/src/main.rs29
-rw-r--r--lib/header/src/lib.rs68
-rw-r--r--tests/expected/readrom/header.txt20
3 files changed, 95 insertions, 22 deletions
diff --git a/crates/readrom/src/main.rs b/crates/readrom/src/main.rs
index f7411e3..d3701d0 100644
--- a/crates/readrom/src/main.rs
+++ b/crates/readrom/src/main.rs
@@ -175,47 +175,50 @@ fn print_header(header: &Header) {
println!("Header:");
match header.kind {
- Kind::INes => println!(" Kind:\t\t\tiNES"),
- Kind::Nes20 => println!(" Kind:\t\t\tNES 2.0"),
+ Kind::INes => println!(" Kind:\t\t\t\tiNES"),
+ Kind::Nes20 => println!(" Kind:\t\t\t\tNES 2.0"),
}
println!(
- " PRG ROM size:\t\t{} bytes ({}KB)",
+ " PRG ROM size:\t\t\t{} bytes ({}KB)",
header.prg_rom_size * 16 * 1024,
header.prg_rom_size * 16
);
println!(
- " CHR ROM size:\t\t{} bytes ({}KB)",
+ " CHR ROM size:\t\t\t{} bytes ({}KB)",
header.chr_rom_size * 8 * 1024,
header.chr_rom_size * 8
);
if let Some(def) = &header.prg_ram_definition {
println!(
- " PRG-RAM size:\t\t{} bytes ({}KB); {}",
+ " PRG-RAM size:\t\t\t{} bytes ({}KB); {}",
def.size,
def.size / 1024,
def.kind
);
} else if header.has_persistent_memory {
- println!(" PRG-RAM size:\t\t8192 bytes (8KB)");
+ println!(" PRG-RAM size:\t\t\t8192 bytes (8KB)");
}
if let Some(def) = &header.chr_ram_definition {
println!(
- " CHR-RAM size:\t\t{} bytes ({}KB); {}",
+ " CHR-RAM size:\t\t\t{} bytes ({}KB); {}",
def.size,
def.size / 1024,
def.kind
);
}
- println!(" Mapper:\t\t{}", header.mapper);
+ println!(" Mapper:\t\t\t{}", header.mapper);
println!(
- " Mirroring:\t\t{}",
+ " Mirroring:\t\t\t{}",
capitalize(header.nametable_arrangement.mirroring())
);
- println!(" CPU/PPU timing:\t{}", header.timing);
+ println!(" CPU/PPU timing:\t\t{}", header.timing);
+ if let Some(ded) = header.default_expansion_device.as_ref() {
+ println!(" Default Expansion Device:\t{}", ded);
+ }
}
fn print_vectors(addrs: &[u8]) {
@@ -226,15 +229,15 @@ fn print_vectors(addrs: &[u8]) {
}
println!(
- " NMI:\t\t\t{:#04x}",
+ " NMI:\t\t\t\t{:#04x}",
u16::from_le_bytes([addrs[0], addrs[1]])
);
println!(
- " Reset:\t\t{:#04x}",
+ " Reset:\t\t\t{:#04x}",
u16::from_le_bytes([addrs[2], addrs[3]])
);
println!(
- " IRQ:\t\t\t{:#04x}",
+ " IRQ:\t\t\t\t{:#04x}",
u16::from_le_bytes([addrs[4], addrs[5]])
);
}
diff --git a/lib/header/src/lib.rs b/lib/header/src/lib.rs
index cf0b736..3a62e35 100644
--- a/lib/header/src/lib.rs
+++ b/lib/header/src/lib.rs
@@ -146,6 +146,48 @@ impl std::fmt::Display for Timing {
}
}
+/// The default expansion device available on the NES 2.0 header.
+#[derive(Debug)]
+pub enum DefaultExpansionDevice {
+ Unknown,
+ StandardControllers,
+ FourScoreSatellite,
+ FamicomFourPlayersAdapter,
+ VsSystem4016,
+ VsSystem4017,
+ VsZapper,
+ Zapper,
+ TwoZappers,
+ PowerPadA,
+ PowerPadB,
+}
+
+impl std::fmt::Display for DefaultExpansionDevice {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ match self {
+ Self::Unknown => write!(f, "Unknown"),
+ Self::StandardControllers => write!(f, "Standard NES/Famicom controllers"),
+ Self::FourScoreSatellite => write!(
+ f,
+ "NES Four Score/Satellite with two additional standard controllers"
+ ),
+ Self::FamicomFourPlayersAdapter => {
+ write!(
+ f,
+ "Famicom Four Players Adapter with two additional standard controllers using the \"simple\" protocol"
+ )
+ }
+ Self::VsSystem4016 => write!(f, "Vs. System (1P via $4016)"),
+ Self::VsSystem4017 => write!(f, "Vs. System (1P via $4017)"),
+ Self::VsZapper => write!(f, "Vs. Zapper"),
+ Self::Zapper => write!(f, "Zapper ($4017)"),
+ Self::TwoZappers => write!(f, "Two Zappers"),
+ Self::PowerPadA => write!(f, "Power Pad Side A"),
+ Self::PowerPadB => write!(f, "Power Pad Side B"),
+ }
+ }
+}
+
/// Information that has been parsed from an NES/Famicom ROM header. The
/// `Header` struct implements the `TryFrom` trait. Hence, use
/// `Header::try_from` in order to parse a header.
@@ -190,6 +232,10 @@ pub struct Header {
/// The kind of the header.
pub kind: Kind,
+
+ /// The default expansion device that can be specified in the NES 2.0
+ /// header. Used to specify the default way to experience a game.
+ pub default_expansion_device: Option<DefaultExpansionDevice>,
}
impl TryFrom<&[u8]> for Header {
@@ -219,11 +265,33 @@ impl TryFrom<&[u8]> for Header {
prg_ram_definition: parse_ram_definition(&kind, bytes.get(10), has_persistent_memory),
chr_ram_definition: parse_ram_definition(&kind, bytes.get(11), has_persistent_memory),
timing: parse_timing(&kind, bytes.get(12)),
+ default_expansion_device: if matches!(kind, Kind::Nes20) {
+ Some(parse_default_expansion_device(bytes.get(15).unwrap_or(&0)))
+ } else {
+ None
+ },
kind,
})
}
}
+// Returns the DefaultExpansionDevice for the given byte value.
+fn parse_default_expansion_device(byte: &u8) -> DefaultExpansionDevice {
+ match byte {
+ 1 => DefaultExpansionDevice::StandardControllers,
+ 2 => DefaultExpansionDevice::FourScoreSatellite,
+ 3 => DefaultExpansionDevice::FamicomFourPlayersAdapter,
+ 4 => DefaultExpansionDevice::VsSystem4016,
+ 5 => DefaultExpansionDevice::VsSystem4017,
+ 7 => DefaultExpansionDevice::VsZapper,
+ 8 => DefaultExpansionDevice::Zapper,
+ 9 => DefaultExpansionDevice::TwoZappers,
+ 11 => DefaultExpansionDevice::PowerPadA,
+ 12 => DefaultExpansionDevice::PowerPadB,
+ _ => DefaultExpansionDevice::Unknown,
+ }
+}
+
// Parse the given `byte` by assuming it's a RAM definition. It will just return
// `None` if `has_persistent_memory` is false or the `kind` is not NES 2.0.
fn parse_ram_definition(
diff --git a/tests/expected/readrom/header.txt b/tests/expected/readrom/header.txt
index a2d4a81..fc963e0 100644
--- a/tests/expected/readrom/header.txt
+++ b/tests/expected/readrom/header.txt
@@ -1,11 +1,13 @@
Header:
- Kind: NES 2.0
- PRG ROM size: 32768 bytes (32KB)
- CHR ROM size: 8192 bytes (8KB)
- Mapper: NROM
- Mirroring: Horizontal
- CPU/PPU timing: NTSC
+ Kind: NES 2.0
+ PRG ROM size: 32768 bytes (32KB)
+ CHR ROM size: 8192 bytes (8KB)
+ Mapper: NROM
+ Mirroring: Horizontal
+ CPU/PPU timing: NTSC
+ Default Expansion Device: Unknown
+ Raw: [ 4E 45 53 1A 02 01 00 08 00 00 00 00 00 00 00 00 ]
Vectors:
- NMI: 0xa2f8
- Reset: 0xa3c2
- IRQ: 0xa3c1
+ NMI: 0xa2f8
+ Reset: 0xa3c2
+ IRQ: 0xa3c1