From 9e68cf7abe118140a994195b57ff62026e608479 Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Fri, 6 Mar 2026 11:03:54 -0500 Subject: [PATCH] Add NES cart reader --- SabreTools.Serialization/Models/NES/Cart.cs | 7 +- .../Models/NES/Constants.cs | 15 ++ .../Models/NES/Header2.cs | 2 +- SabreTools.Serialization/Readers/NESCart.cs | 143 ++++++++++++++++++ 4 files changed, 165 insertions(+), 2 deletions(-) create mode 100644 SabreTools.Serialization/Models/NES/Constants.cs create mode 100644 SabreTools.Serialization/Readers/NESCart.cs diff --git a/SabreTools.Serialization/Models/NES/Cart.cs b/SabreTools.Serialization/Models/NES/Cart.cs index 55b9f78a..eabf6ecd 100644 --- a/SabreTools.Serialization/Models/NES/Cart.cs +++ b/SabreTools.Serialization/Models/NES/Cart.cs @@ -35,7 +35,12 @@ namespace SabreTools.Data.Models.NES /// /// PlayChoice PROM, if present (16 bytes Data, 16 bytes CounterOut) /// - /// This is often missing; see PC10 ROM-Images for details + /// + /// This is often missing; see PC10 ROM-Images for details + /// 16 bytes RP5H01 PROM Data output (needed to decrypt the INST ROM) + /// 16 bytes RP5H01 PROM CounterOut output (needed to decrypt the INST ROM) (usually constant: 00,00,00,00,FF,FF,FF,FF,00,00,00,00,FF,FF,FF,FF) + /// + /// TODO: Split into 2 parts public byte[] PlayChoicePROM { get; set; } = []; /// diff --git a/SabreTools.Serialization/Models/NES/Constants.cs b/SabreTools.Serialization/Models/NES/Constants.cs new file mode 100644 index 00000000..4f3f4ad3 --- /dev/null +++ b/SabreTools.Serialization/Models/NES/Constants.cs @@ -0,0 +1,15 @@ +namespace SabreTools.Data.Models.NES +{ + public static class Constants + { + /// + /// NES + /// + public static readonly byte[] SignatureBytes = [0x4E, 0x45, 0x53, 0x1A]; + + /// + /// NES + /// + public static readonly string SignatureString = "NES" + (char)0x1A; + } +} diff --git a/SabreTools.Serialization/Models/NES/Header2.cs b/SabreTools.Serialization/Models/NES/Header2.cs index d4013d3c..2f31a166 100644 --- a/SabreTools.Serialization/Models/NES/Header2.cs +++ b/SabreTools.Serialization/Models/NES/Header2.cs @@ -70,7 +70,7 @@ namespace SabreTools.Data.Models.NES /// Number of miscellaneous ROMs present /// /// Only bits 0-1 are used - public bool MiscellaneousROMs { get; set; } + public byte MiscellaneousROMs { get; set; } /// /// Default Expansion Device diff --git a/SabreTools.Serialization/Readers/NESCart.cs b/SabreTools.Serialization/Readers/NESCart.cs new file mode 100644 index 00000000..addf780d --- /dev/null +++ b/SabreTools.Serialization/Readers/NESCart.cs @@ -0,0 +1,143 @@ +using System.IO; +using SabreTools.Data.Models.NES; +using SabreTools.IO.Extensions; +using static SabreTools.Data.Models.NES.Constants; + +namespace SabreTools.Serialization.Readers +{ + public class NESCart : BaseBinaryReader + { + /// + public override Cart? Deserialize(Stream? data) + { + // If the data is invalid + if (data is null || !data.CanRead) + return null; + + try + { + // Cache the current offset + long initialOffset = data.Position; + + // Create a new NES cart image to fill + var cart = new Cart(); + + #region Header + + // Try to parse the header + var header = ParseHeader(data); + if (header == null) + return null; + + // Set the header + cart.Header = header; + + #endregion + + // Read the trainer data, if necessary +#if NET20 || NET35 + if ((cart.Header.Flag6 & Flag6.TrainerPresent) != 0) +#else + if (cart.Header.Flag6.HasFlag(Flag6.TrainerPresent)) +#endif + cart.Trainer = data.ReadBytes(512); + + // Read the PRG-ROM data + // TODO: Make model for PRG-ROM data blocks + int prgRomSize = cart.Header.PRGROMSize * 16384; + if (prgRomSize > 0) + cart.PRGROMData = data.ReadBytes(prgRomSize); + + // Read the CHR-ROM data, if necessary + int chrRomSize = cart.Header.CHRROMSize * 8192; + if (chrRomSize > 0) + cart.CHRROMData = data.ReadBytes(chrRomSize); + + // Read the PlayChoice INST-ROM and PROM data, if necessary +#if NET20 || NET35 + if ((cart.Header.Flag7 & Flag7.PlayChoice10) != 0 + && (cart.Header.Flag7 & Flag7.VSUnisystem) == 0) +#else + if (cart.Header.Flag7.HasFlag(Flag7.PlayChoice10) + && !cart.Header.Flag7.HasFlag(Flag7.VSUnisystem)) +#endif + { + cart.PlayChoiceINSTROM = data.ReadBytes(8192); + cart.PlayChoicePROM = data.ReadBytes(32); + } + + // Read the cart title, if it exists + // TODO: Read cart title + + return cart; + } + catch + { + // Ignore the actual error + return null; + } + } + + /// + /// Parse a Stream into a Header + /// + /// Stream to parse + /// Filled Header on success, null on error + public static Header? ParseHeader(Stream data) + { + // Cache data until NES 2.0 flag determined + byte[] identificationString = data.ReadBytes(4); + if (identificationString.EqualsExactly(SignatureBytes)) + return null; + + byte prgRomSize = data.ReadByteValue(); + byte chrRomSize = data.ReadByteValue(); + Flag6 flag6 = (Flag6)data.ReadByteValue(); + Flag7 flag7 = (Flag7)data.ReadByteValue(); + + // NES 2.0 +#if NET20 || NET35 + if ((flag7 & Flag7.NES20) != 0) +#else + if (flag7.HasFlag(Flag7.NES20)) +#endif + { + var obj = new Header2(); + + obj.IdentificationString = identificationString; + obj.PRGROMSize = prgRomSize; + obj.CHRROMSize = chrRomSize; + obj.Flag6 = flag6; + obj.Flag7 = flag7; + obj.MapperMSBSubmapper = data.ReadByteValue(); + obj.PRGCHRMSB = data.ReadByteValue(); + obj.PRGRAMEEPROMSize = data.ReadByteValue(); + obj.CHRRAMSize = data.ReadByteValue(); + obj.CPUPPUTiming = (CPUPPUTiming)data.ReadByteValue(); + obj.ExtendedSystemType = (ExtendedSystemType)data.ReadByteValue(); + obj.MiscellaneousROMs = data.ReadByteValue(); + obj.DefaultExpansionDevice = (DefaultExpansionDevice)data.ReadByteValue(); + + return obj; + } + + // NES 1.0 + else + { + var obj = new Header1(); + + obj.IdentificationString = identificationString; + obj.PRGROMSize = prgRomSize; + obj.CHRROMSize = chrRomSize; + obj.Flag6 = flag6; + obj.Flag7 = flag7; + obj.PRGRAMSize = data.ReadByteValue(); + obj.TVSystem = (TVSystem)data.ReadByteValue(); + obj.Flag10 = (Flag10)data.ReadByteValue(); + obj.Padding = data.ReadBytes(5); + + return obj; + } + } + } +}