Add NES cart writer

This commit is contained in:
Matt Nadareski
2026-04-14 12:33:27 -04:00
parent 650402df39
commit 4e70a7665c
4 changed files with 359 additions and 33 deletions

View File

@@ -0,0 +1,24 @@
using System.IO;
using Xunit;
namespace SabreTools.Serialization.Writers.Test
{
public class NESCartTests
{
[Fact]
public void SerializeArray_Null_Null()
{
var serializer = new NESCart();
byte[]? actual = serializer.SerializeArray(null);
Assert.Null(actual);
}
[Fact]
public void SerializeStream_Null_Null()
{
var serializer = new NESCart();
Stream? actual = serializer.SerializeStream(null);
Assert.Null(actual);
}
}
}

View File

@@ -0,0 +1,322 @@
using System;
using System.IO;
using SabreTools.Data.Models.NES;
using SabreTools.IO.Extensions;
using SabreTools.Numerics.Extensions;
namespace SabreTools.Serialization.Writers
{
public class NESCart : BaseBinaryWriter<Cart>
{
/// <inheritdoc/>
public override Stream? SerializeStream(Cart? obj)
{
// If the metadata file is null
if (obj is null)
return null;
// Setup the writer and output
var stream = new MemoryStream();
// Write header
if (!WriteHeader(obj.Header, stream))
return null;
// Trainer data
if (obj.Trainer is not null && obj.Trainer.Length > 0 && !WriteTrainer(obj.Trainer, stream))
return null;
// PRG-ROM data
if (!WritePrgRom(obj.PrgRomData, stream))
return null;
// CHR-ROM data
if (obj.ChrRomData is not null && obj.ChrRomData.Length > 0 && !WriteChrRom(obj.ChrRomData, stream))
return null;
// INST-ROM data
if (obj.PlayChoiceInstRom is not null && obj.PlayChoiceInstRom.Length > 0 && !WritePlayChoiceInstRom(obj.PlayChoiceInstRom, stream))
return null;
// TODO: Add PlayChoice-10 PROM data writing
// Return the stream
stream.SeekIfPossible(0, SeekOrigin.Begin);
return stream;
}
/// <summary>
/// Write CHR-ROM data to the stream
/// </summary>
/// <param name="stream">Stream to write to</param>
/// <returns>True if the writing was successful, false otherwise</returns>
public bool WriteChrRom(byte[]? obj, Stream stream)
{
if (obj is null || obj.Length == 0)
{
if (Debug) Console.WriteLine("CHR-ROM data was invalid!");
return false;
}
// Try to write the data
try
{
if (Debug) Console.WriteLine("Attempting to write CHR-ROM data");
stream.Write(obj, 0, obj.Length);
stream.Flush();
return true;
}
catch (Exception ex)
{
if (Debug) Console.Error.WriteLine(ex);
return false;
}
}
/// <summary>
/// Write header data to the stream
/// </summary>
/// <param name="stream">Stream to write to</param>
/// <returns>True if the writing was successful, false otherwise</returns>
public bool WriteHeader(CartHeader? obj, Stream stream)
{
if (obj is null)
{
if (Debug) Console.WriteLine("Header was invalid!");
return false;
}
// Try to write the data
try
{
if (Debug) Console.WriteLine("Attempting to write header data");
// Bytes 0-3
stream.Write(obj.IdentificationString);
stream.Flush();
// Byte 4
stream.Write(obj.PrgRomSize);
stream.Flush();
// Byte 5
stream.Write(obj.ChrRomSize);
stream.Flush();
// Byte 6
byte byte6 = 0;
byte6 |= (byte)obj.NametableArrangement;
if (obj.BatteryBackedPrgRam)
byte6 |= 0b00000010;
if (obj.TrainerPresent)
byte6 |= 0b00000100;
if (obj.AlternativeNametableLayout)
byte6 |= 0b00001000;
byte6 |= (byte)(obj.MapperLowerNibble << 4);
stream.Write(byte6);
stream.Flush();
// Byte 7
byte byte7 = 0;
byte7 |= (byte)obj.ConsoleType;
if (obj.NES20)
byte7 |= 0b00001000;
byte7 |= (byte)(obj.MapperUpperNibble << 4);
stream.Write(byte7);
stream.Flush();
if (obj 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)header1.TVSystemExtended;
byte10 |= (byte)(header1.Byte10ReservedBits23 << 2);
if (header1.PrgRamPresent)
byte10 |= 0b00010000;
if (header1.HasBusConflicts)
byte10 |= 0b00100000;
byte10 |= (byte)(header1.Byte10ReservedBits67 << 6);
stream.Write(byte10);
stream.Flush();
// Bytes 11-15
stream.Write(header1.Padding);
stream.Flush();
}
else if (obj 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 (obj.ConsoleType == ConsoleType.VSUnisystem)
{
byte byte13 = 0;
byte13 |= (byte)header2.VsSystemType;
byte13 |= (byte)((byte)header2.VsHardwareType << 4);
stream.Write(byte13);
stream.Flush();
}
else if (obj.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)header2.DefaultExpansionDevice);
stream.Flush();
}
else
{
if (Debug) Console.Error.WriteLine("Unknown header type, incomplete header data output");
}
// Header extracted
return true;
}
catch (Exception ex)
{
if (Debug) Console.Error.WriteLine(ex);
return false;
}
}
/// <summary>
/// Write INST-ROM data to the stream
/// </summary>
/// <param name="stream">Stream to write to</param>
/// <returns>True if the writing was successful, false otherwise</returns>
public bool WritePlayChoiceInstRom(byte[]? obj, Stream stream)
{
if (obj is null || obj.Length == 0)
{
if (Debug) Console.WriteLine("INST-ROM data was invalid!");
return false;
}
// Try to write the data
try
{
if (Debug) Console.WriteLine("Attempting to write INST-ROM data");
stream.Write(obj, 0, obj.Length);
stream.Flush();
return true;
}
catch (Exception ex)
{
if (Debug) Console.Error.WriteLine(ex);
return false;
}
}
/// <summary>
/// Write PRG-ROM data to the stream
/// </summary>
/// <param name="stream">Stream to write to</param>
/// <returns>True if the writing was successful, false otherwise</returns>
public bool WritePrgRom(byte[]? obj, Stream stream)
{
if (obj is null || obj.Length == 0)
{
if (Debug) Console.WriteLine("PRG-ROM data was invalid!");
return false;
}
// Try to write the data
try
{
if (Debug) Console.WriteLine("Attempting to write PRG-ROM data");
stream.Write(obj, 0, obj.Length);
stream.Flush();
return true;
}
catch (Exception ex)
{
if (Debug) Console.Error.WriteLine(ex);
return false;
}
}
/// <summary>
/// Write trainer data to the stream
/// </summary>
/// <param name="stream">Stream to write to</param>
/// <returns>True if the writing was successful, false otherwise</returns>
public bool WriteTrainer(byte[]? obj, Stream stream)
{
if (obj is null || obj.Length == 0)
{
if (Debug) Console.WriteLine("Trainer data was invalid!");
return false;
}
// Try to write the data
try
{
if (Debug) Console.WriteLine("Attempting to write trainer data");
stream.Write(obj, 0, obj.Length);
stream.Flush();
return true;
}
catch (Exception ex)
{
if (Debug) Console.Error.WriteLine(ex);
return false;
}
}
}
}

View File

@@ -14,6 +14,9 @@ namespace SabreTools.Wrappers
: Path.GetFileNameWithoutExtension(Filename);
string basePath = Path.Combine(outputDirectory, baseFilename);
// Create the writer
var writer = new Serialization.Writers.NESCart { Debug = includeDebug };
// Check if any data was extracted successfully
bool success = false;
@@ -27,7 +30,7 @@ namespace SabreTools.Wrappers
try
{
using var fs = File.Open(headerPath, FileMode.Create, FileAccess.Write, FileShare.None);
success |= WriteHeader(fs, includeDebug);
success |= writer.WriteHeader(Header, fs);
}
catch (Exception ex)
{
@@ -45,7 +48,7 @@ namespace SabreTools.Wrappers
try
{
using var fs = File.Open(trainerPath, FileMode.Create, FileAccess.Write, FileShare.None);
success |= WriteTrainer(fs, includeDebug);
success |= writer.WriteTrainer(Trainer, fs);
}
catch (Exception ex)
{
@@ -64,7 +67,7 @@ namespace SabreTools.Wrappers
{
// Open the output file for writing
using var fs = File.Open(prgRomPath, FileMode.Create, FileAccess.Write, FileShare.None);
success |= WritePrgRom(fs, includeDebug);
success |= writer.WritePrgRom(PrgRomData, fs);
}
catch (Exception ex)
{
@@ -83,7 +86,7 @@ namespace SabreTools.Wrappers
{
// Open the output file for writing
using var fs = File.Open(chrRomPath, FileMode.Create, FileAccess.Write, FileShare.None);
success |= WriteChrRom(fs, includeDebug);
success |= writer.WriteChrRom(ChrRomData, fs);
}
catch (Exception ex)
{
@@ -102,8 +105,8 @@ namespace SabreTools.Wrappers
{
// Open the output file for writing
using var fs = File.Open(unheaderedPath, FileMode.Create, FileAccess.Write, FileShare.None);
success |= WritePrgRom(fs, includeDebug);
success |= WriteChrRom(fs, includeDebug);
success |= writer.WritePrgRom(PrgRomData, fs);
success |= writer.WriteChrRom(ChrRomData, fs);
// Unheadered ROM extracted
success = true;
@@ -125,7 +128,7 @@ namespace SabreTools.Wrappers
{
// Open the output file for writing
using var fs = File.Open(instRomPath, FileMode.Create, FileAccess.Write, FileShare.None);
success |= WritePlayChoiceInstRom(fs, includeDebug);
success |= writer.WritePlayChoiceInstRom(PlayChoiceInstRom, fs);
}
catch (Exception ex)
{

View File

@@ -26,32 +26,9 @@ namespace SabreTools.Wrappers
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;
// Create and use the writer
var writer = new Serialization.Writers.NESCart { Debug = includeDebug };
return writer.SerializeFile(Model, outputPath);
}
/// <summary>