From ea2a1daadf67638bee2265da8625efa49ee17f06 Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Tue, 14 Apr 2026 10:55:09 -0400 Subject: [PATCH] Add NES image writing --- SabreTools.Wrappers/NESCart.Extraction.cs | 168 +----------- SabreTools.Wrappers/NESCart.Writing.cs | 312 ++++++++++++++++++++++ 2 files changed, 319 insertions(+), 161 deletions(-) create mode 100644 SabreTools.Wrappers/NESCart.Writing.cs diff --git a/SabreTools.Wrappers/NESCart.Extraction.cs b/SabreTools.Wrappers/NESCart.Extraction.cs index ad76939c..fa043c37 100644 --- a/SabreTools.Wrappers/NESCart.Extraction.cs +++ b/SabreTools.Wrappers/NESCart.Extraction.cs @@ -1,7 +1,5 @@ using System; using System.IO; -using SabreTools.Data.Models.NES; -using SabreTools.Numerics.Extensions; namespace SabreTools.Wrappers { @@ -28,141 +26,8 @@ namespace SabreTools.Wrappers // Try to write the data try { - // Open the output file for writing using var fs = File.Open(headerPath, FileMode.Create, FileAccess.Write, FileShare.None); - - // Bytes 0-3 - fs.Write(Header.IdentificationString); - fs.Flush(); - - // Byte 4 - fs.Write(Header.PrgRomSize); - fs.Flush(); - - // Byte 5 - fs.Write(Header.ChrRomSize); - fs.Flush(); - - // Byte 6 - byte byte6 = 0; - byte6 |= (byte)NametableArrangement; - if (BatteryBackedPrgRam) - byte6 |= 0b00000010; - if (TrainerPresent) - byte6 |= 0b00000100; - if (AlternativeNametableLayout) - byte6 |= 0b00001000; - byte6 |= (byte)(Header.MapperLowerNibble << 4); - fs.Write(byte6); - fs.Flush(); - - // Byte 7 - byte byte7 = 0; - byte7 |= (byte)ConsoleType; - if (NES20) - byte7 |= 0b00001000; - byte7 |= (byte)(Header.MapperUpperNibble << 4); - fs.Write(byte7); - fs.Flush(); - - if (Header is CartHeader1 header1) - { - // Byte 8 - fs.Write(header1.PrgRamSize); - fs.Flush(); - - // Byte 9 - fs.Write((byte)header1.TVSystem); - fs.Flush(); - - // Byte 10 - byte byte10 = 0; - byte10 |= (byte)TVSystemExtended; - byte10 |= (byte)(header1.Byte10ReservedBits23 << 2); - if (PrgRamPresent) - byte10 |= 0b00010000; - if (HasBusConflicts) - byte10 |= 0b00100000; - byte10 |= (byte)(header1.Byte10ReservedBits67 << 6); - fs.Write(byte10); - fs.Flush(); - - // Bytes 11-15 - fs.Write(header1.Padding); - fs.Flush(); - } - else if (Header is CartHeader2 header2) - { - // Byte 8 - byte byte8 = 0; - byte8 |= header2.MapperMSB; - byte8 |= (byte)(header2.Submapper << 4); - fs.Write(byte8); - fs.Flush(); - - // Byte 9 - byte byte9 = 0; - byte9 |= header2.PrgRomSizeMSB; - byte9 |= (byte)(header2.ChrRomSizeMSB << 4); - fs.Write(byte9); - fs.Flush(); - - // Byte 10 - byte byte10 = 0; - byte10 |= header2.PrgRamShiftCount; - byte10 |= (byte)(header2.PrgNvramEepromShiftCount << 4); - fs.Write(byte10); - fs.Flush(); - - // Byte 11 - byte byte11 = 0; - byte11 |= header2.ChrRamShiftCount; - byte11 |= (byte)(header2.ChrNvramShiftCount << 4); - fs.Write(byte11); - fs.Flush(); - - // Byte 12 - fs.Write((byte)header2.CPUPPUTiming); - fs.Flush(); - - // Byte 13 - if (ConsoleType == ConsoleType.VSUnisystem) - { - byte byte13 = 0; - byte13 |= (byte)header2.VsSystemType; - byte13 |= (byte)((byte)header2.VsHardwareType << 4); - fs.Write(byte13); - fs.Flush(); - } - else if (ConsoleType == ConsoleType.ExtendedConsoleType) - { - byte byte13 = 0; - byte13 |= (byte)header2.ExtendedConsoleType; - byte13 |= (byte)(header2.Byte13ReservedBits47 << 4); - fs.Write(byte13); - fs.Flush(); - } - else - { - fs.Write(header2.Reserved13); - fs.Flush(); - } - - // Byte 14 - fs.Write(header2.MiscellaneousROMs); - fs.Flush(); - - // Byte 15 - fs.Write((byte)DefaultExpansionDevice); - fs.Flush(); - } - else - { - if (includeDebug) Console.Error.WriteLine("Unknown header type, incomplete header data output"); - } - - // Header extracted - success = true; + success |= WriteHeader(fs, includeDebug); } catch (Exception ex) { @@ -179,13 +44,8 @@ namespace SabreTools.Wrappers // Try to write the data try { - // Open the output file for writing using var fs = File.Open(trainerPath, FileMode.Create, FileAccess.Write, FileShare.None); - fs.Write(Trainer, 0, Trainer.Length); - fs.Flush(); - - // Trainer extracted - success = true; + success |= WriteTrainer(fs, includeDebug); } catch (Exception ex) { @@ -204,11 +64,7 @@ namespace SabreTools.Wrappers { // Open the output file for writing using var fs = File.Open(prgRomPath, FileMode.Create, FileAccess.Write, FileShare.None); - fs.Write(PrgRomData, 0, PrgRomData.Length); - fs.Flush(); - - // PRG-ROM extracted - success = true; + success |= WritePrgRom(fs, includeDebug); } catch (Exception ex) { @@ -227,11 +83,7 @@ namespace SabreTools.Wrappers { // Open the output file for writing using var fs = File.Open(chrRomPath, FileMode.Create, FileAccess.Write, FileShare.None); - fs.Write(ChrRomData, 0, ChrRomData.Length); - fs.Flush(); - - // CHR-ROM extracted - success = true; + success |= WriteChrRom(fs, includeDebug); } catch (Exception ex) { @@ -250,10 +102,8 @@ namespace SabreTools.Wrappers { // Open the output file for writing using var fs = File.Open(unheaderedPath, FileMode.Create, FileAccess.Write, FileShare.None); - fs.Write(PrgRomData, 0, PrgRomData.Length); - fs.Flush(); - fs.Write(ChrRomData, 0, ChrRomData.Length); - fs.Flush(); + success |= WritePrgRom(fs, includeDebug); + success |= WriteChrRom(fs, includeDebug); // Unheadered ROM extracted success = true; @@ -275,11 +125,7 @@ namespace SabreTools.Wrappers { // Open the output file for writing using var fs = File.Open(instRomPath, FileMode.Create, FileAccess.Write, FileShare.None); - fs.Write(PlayChoiceInstRom, 0, PlayChoiceInstRom.Length); - fs.Flush(); - - // PC10 INST-ROM extracted - success = true; + success |= WritePlayChoiceInstRom(fs, includeDebug); } catch (Exception ex) { diff --git a/SabreTools.Wrappers/NESCart.Writing.cs b/SabreTools.Wrappers/NESCart.Writing.cs new file mode 100644 index 00000000..65fbed57 --- /dev/null +++ b/SabreTools.Wrappers/NESCart.Writing.cs @@ -0,0 +1,312 @@ +using System; +using System.IO; +using SabreTools.Data.Models.NES; +using SabreTools.Numerics.Extensions; + +namespace SabreTools.Wrappers +{ + public partial class NESCart : IWritable + { + /// + public bool Write(string outputPath, bool includeDebug) + { + // Ensure an output path + if (string.IsNullOrEmpty(outputPath)) + { + string outputFilename = Filename is null + ? (Guid.NewGuid().ToString() + ".nes") + : (Filename + ".new"); + outputPath = Path.GetFullPath(outputFilename); + } + + // Check for invalid data + if (Header is null || PrgRomData is null || PrgRomData.Length == 0) + { + if (includeDebug) Console.WriteLine("Model was invalid, cannot write!"); + return false; + } + + // Open the output file for writing + using var fs = File.Open(outputPath, FileMode.Create, FileAccess.Write, FileShare.None); + + // Header data + if (!WriteHeader(fs, includeDebug)) + return false; + + // Trainer data + if (Trainer is not null && Trainer.Length > 0) + WriteTrainer(fs, includeDebug); + + // PRG-ROM data + if (!WritePrgRom(fs, includeDebug)) + return false; + + // CHR-ROM data + if (ChrRomData is not null && ChrRomData.Length > 0) + WriteChrRom(fs, includeDebug); + + // INST-ROM data + if (PlayChoiceInstRom is not null && PlayChoiceInstRom.Length > 0) + WritePlayChoiceInstRom(fs, includeDebug); + + // TODO: Add PlayChoice-10 PROM data writing + + return true; + } + + /// + /// Write CHR-ROM data to the stream + /// + /// Stream to write to + /// True to include debug data, false otherwise + /// True if the writing was successful, false otherwise + private bool WriteChrRom(Stream stream, bool includeDebug) + { + if (includeDebug) Console.WriteLine("Attempting to write CHR-ROM data"); + + // Try to write the data + try + { + stream.Write(ChrRomData, 0, ChrRomData.Length); + stream.Flush(); + return true; + } + catch (Exception ex) + { + if (includeDebug) Console.Error.WriteLine(ex); + return false; + } + } + + /// + /// Write header data to the stream + /// + /// Stream to write to + /// True to include debug data, false otherwise + /// True if the writing was successful, false otherwise + private bool WriteHeader(Stream stream, bool includeDebug) + { + if (includeDebug) Console.WriteLine("Attempting to write header data"); + + if (Header is null) + { + if (includeDebug) Console.WriteLine("Header was invalid!"); + return false; + } + + // Try to write the data + try + { + // Bytes 0-3 + stream.Write(Header.IdentificationString); + stream.Flush(); + + // Byte 4 + stream.Write(Header.PrgRomSize); + stream.Flush(); + + // Byte 5 + stream.Write(Header.ChrRomSize); + stream.Flush(); + + // Byte 6 + byte byte6 = 0; + byte6 |= (byte)NametableArrangement; + if (BatteryBackedPrgRam) + byte6 |= 0b00000010; + if (TrainerPresent) + byte6 |= 0b00000100; + if (AlternativeNametableLayout) + byte6 |= 0b00001000; + byte6 |= (byte)(Header.MapperLowerNibble << 4); + stream.Write(byte6); + stream.Flush(); + + // Byte 7 + byte byte7 = 0; + byte7 |= (byte)ConsoleType; + if (NES20) + byte7 |= 0b00001000; + byte7 |= (byte)(Header.MapperUpperNibble << 4); + stream.Write(byte7); + stream.Flush(); + + if (Header is CartHeader1 header1) + { + // Byte 8 + stream.Write(header1.PrgRamSize); + stream.Flush(); + + // Byte 9 + stream.Write((byte)header1.TVSystem); + stream.Flush(); + + // Byte 10 + byte byte10 = 0; + byte10 |= (byte)TVSystemExtended; + byte10 |= (byte)(header1.Byte10ReservedBits23 << 2); + if (PrgRamPresent) + byte10 |= 0b00010000; + if (HasBusConflicts) + byte10 |= 0b00100000; + byte10 |= (byte)(header1.Byte10ReservedBits67 << 6); + stream.Write(byte10); + stream.Flush(); + + // Bytes 11-15 + stream.Write(header1.Padding); + stream.Flush(); + } + else if (Header is CartHeader2 header2) + { + // Byte 8 + byte byte8 = 0; + byte8 |= header2.MapperMSB; + byte8 |= (byte)(header2.Submapper << 4); + stream.Write(byte8); + stream.Flush(); + + // Byte 9 + byte byte9 = 0; + byte9 |= header2.PrgRomSizeMSB; + byte9 |= (byte)(header2.ChrRomSizeMSB << 4); + stream.Write(byte9); + stream.Flush(); + + // Byte 10 + byte byte10 = 0; + byte10 |= header2.PrgRamShiftCount; + byte10 |= (byte)(header2.PrgNvramEepromShiftCount << 4); + stream.Write(byte10); + stream.Flush(); + + // Byte 11 + byte byte11 = 0; + byte11 |= header2.ChrRamShiftCount; + byte11 |= (byte)(header2.ChrNvramShiftCount << 4); + stream.Write(byte11); + stream.Flush(); + + // Byte 12 + stream.Write((byte)header2.CPUPPUTiming); + stream.Flush(); + + // Byte 13 + if (ConsoleType == ConsoleType.VSUnisystem) + { + byte byte13 = 0; + byte13 |= (byte)header2.VsSystemType; + byte13 |= (byte)((byte)header2.VsHardwareType << 4); + stream.Write(byte13); + stream.Flush(); + } + else if (ConsoleType == ConsoleType.ExtendedConsoleType) + { + byte byte13 = 0; + byte13 |= (byte)header2.ExtendedConsoleType; + byte13 |= (byte)(header2.Byte13ReservedBits47 << 4); + stream.Write(byte13); + stream.Flush(); + } + else + { + stream.Write(header2.Reserved13); + stream.Flush(); + } + + // Byte 14 + stream.Write(header2.MiscellaneousROMs); + stream.Flush(); + + // Byte 15 + stream.Write((byte)DefaultExpansionDevice); + stream.Flush(); + } + else + { + if (includeDebug) Console.Error.WriteLine("Unknown header type, incomplete header data output"); + } + + // Header extracted + return true; + } + catch (Exception ex) + { + if (includeDebug) Console.Error.WriteLine(ex); + return false; + } + } + + /// + /// Write INST-ROM data to the stream + /// + /// Stream to write to + /// True to include debug data, false otherwise + /// True if the writing was successful, false otherwise + private bool WritePlayChoiceInstRom(Stream stream, bool includeDebug) + { + if (includeDebug) Console.WriteLine("Attempting to write INST-ROM data"); + + // Try to write the data + try + { + stream.Write(PlayChoiceInstRom, 0, PlayChoiceInstRom.Length); + stream.Flush(); + return true; + } + catch (Exception ex) + { + if (includeDebug) Console.Error.WriteLine(ex); + return false; + } + } + + /// + /// Write PRG-ROM data to the stream + /// + /// Stream to write to + /// True to include debug data, false otherwise + /// True if the writing was successful, false otherwise + private bool WritePrgRom(Stream stream, bool includeDebug) + { + if (includeDebug) Console.WriteLine("Attempting to write PRG-ROM data"); + + // Try to write the data + try + { + stream.Write(PrgRomData, 0, PrgRomData.Length); + stream.Flush(); + return true; + } + catch (Exception ex) + { + if (includeDebug) Console.Error.WriteLine(ex); + return false; + } + } + + /// + /// Write trainer data to the stream + /// + /// Stream to write to + /// True to include debug data, false otherwise + /// True if the writing was successful, false otherwise + private bool WriteTrainer(Stream stream, bool includeDebug) + { + if (includeDebug) Console.WriteLine("Attempting to write trainer data"); + + // Try to write the data + try + { + stream.Write(Trainer, 0, Trainer.Length); + stream.Flush(); + return true; + } + catch (Exception ex) + { + if (includeDebug) Console.Error.WriteLine(ex); + return false; + } + } + } +}