using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
namespace com.clusterrr.Famicom.Containers
{
///
/// iNES / NES 2.0 file container for NES/Famicom games
///
public partial class NesFile
{
///
/// PRG data
///
public byte[] PRG
{
get => prg;
set => prg = (value ?? Array.Empty()).ToArray();
}
///
/// CHR data
///
public byte[] CHR
{
get => chr;
set => chr = (value ?? Array.Empty()).ToArray();
}
///
/// Trainer
///
public byte[] Trainer
{
get => trainer;
set
{
if (value != null && value.Count() != 0 && value.Count() > 512)
throw new ArgumentOutOfRangeException("Trainer size must be 512 bytes or less");
chr = value ?? Array.Empty();
}
}
///
/// Miscellaneous ROM (NES 2.0 only)
///
public byte[] MiscellaneousROM
{
get => miscellaneousROM;
set => miscellaneousROM = value ?? Array.Empty();
}
///
/// Mapper number
///
public ushort Mapper { get; set; } = 0;
///
/// Submapper number (NES 2.0 only)
///
public byte Submapper { get; set; } = 0;
///
/// Battery-backed (or other non-volatile memory) memory is present
///
public bool Battery { get; set; } = false;
private iNesVersion version = NesFile.iNesVersion.iNES;
///
/// Version of .nes file format: iNES or NES 2.0
///
public iNesVersion Version
{
get => version;
set
{
if (value != iNesVersion.iNES && value != iNesVersion.NES20)
throw new ArgumentException("Only version 1 and 2 allowed", nameof(Version));
version = value;
}
}
///
/// PRG RAM Size (NES 2.0 only)
///
public uint PrgRamSize { get; set; } = 0;
private uint prgNvRamSize = 0;
///
/// PRG NVRAM Size (NES 2.0 only)
///
public uint PrgNvRamSize
{
get => prgNvRamSize; set
{
prgNvRamSize = value;
if (prgNvRamSize > 0 || chrNvRamSize > 0)
Battery = true;
}
}
///
/// CHR RAM Size (NES 2.0 only)
///
public uint ChrRamSize { get; set; } = 0;
private uint chrNvRamSize = 0;
private byte[] prg = Array.Empty();
private byte[] chr = Array.Empty();
private byte[] trainer = Array.Empty();
private byte[] miscellaneousROM = Array.Empty();
///
/// CHR NVRAM Size (NES 2.0 only)
///
public uint ChrNvRamSize
{
get => chrNvRamSize; set
{
chrNvRamSize = value;
if (prgNvRamSize > 0 || chrNvRamSize > 0)
Battery = true;
}
}
///
/// Mirroring type
///
public MirroringType Mirroring { get; set; } = MirroringType.Horizontal;
///
/// For non-homebrew NES/Famicom games, this field's value is always a function of the region in which a game was released (NES 2.0 only)
///
public Timing Region { get; set; } = Timing.Ntsc;
///
/// Console type (NES 2.0 only)
///
public ConsoleType Console { get; set; } = ConsoleType.Normal;
///
/// Vs. System PPU type (used when Console is ConsoleType.VsSystem)
///
public VsPpuType VsPpu { get; set; } = VsPpuType.RP2C03B;
///
/// Vs. System hardware type (used when Console is ConsoleType.VsSystem)
///
public VsHardwareType VsHardware { get; set; } = VsHardwareType.VsUnisystemNormal;
///
/// Extended console type (used when Console is ConsoleType.Extended)
///
public ExtendedConsoleType ExtendedConsole { get; set; } = ExtendedConsoleType.RegularNES;
///
/// Default expansion device (NES 2.0 only)
///
public ExpansionDevice DefaultExpansionDevice { get; set; } = ExpansionDevice.Unspecified;
///
/// Miscellaneous ROMs сount (NES 2.0 only)
///
public byte MiscellaneousROMsCount { get; set; } = 0;
///
/// Version of iNES format
///
public enum iNesVersion
{
///
/// Classic iNES format
///
iNES = 1,
///
/// NES 2.0 format
///
NES20 = 2
}
///
/// Console type
///
public enum ConsoleType
{
///
/// Nintendo Entertainment System/Family Computer
///
Normal = 0,
///
/// Nintendo Vs. System
///
VsSystem = 1,
///
/// Nintendo Playchoice 10
///
Playchoice10 = 2,
///
/// Extended Console Type
///
Extended = 3
}
///
/// Vs. System PPU type
///
public enum VsPpuType
{
///
/// RP2C03B
///
RP2C03B = 0x00,
///
/// RP2C03G
///
RP2C03G = 0x01,
///
/// RP2C04-0001
///
RP2C04_0001 = 0x02,
///
/// RP2C04-0002
///
RP2C04_0002 = 0x03,
///
/// RP2C04-0003
///
RP2C04_0003 = 0x04,
///
/// RP2C04-0004
///
RP2C04_0004 = 0x05,
///
/// RC2C03B
///
RC2C03B = 0x06,
///
/// RC2C03C
///
RC2C03C = 0x07,
///
/// RC2C05-01 ($2002 AND $?? =$1B)
///
RC2C05_01 = 0x08,
///
/// RC2C05-02 ($2002 AND $3F =$3D)
///
RC2C05_02 = 0x09,
///
/// RC2C05-03 ($2002 AND $1F =$1C)
///
RC2C05_03 = 0x0A,
///
/// RC2C05-04 ($2002 AND $1F =$1B)
///
RC2C05_04 = 0x0B,
///
/// RC2C05-05 ($2002 AND $1F =unknown)
///
RC2C05_05 = 0x0C
};
///
/// Vs. System hardware type
///
public enum VsHardwareType
{
///
/// Vs. Unisystem (normal)
///
VsUnisystemNormal = 0x00,
///
/// Vs. Unisystem (RBI Baseball protection)
///
VsUnisystemRBIBaseballProtection = 0x01,
///
/// Vs. Unisystem (TKO Boxing protection)
///
VsUnisystemTKOBoxingProtection = 0x02,
///
/// Vs. Unisystem (Super Xevious protection)
///
VsUnisystemSuperXeviousProtection = 0x03,
///
/// Vs. Unisystem (Vs. Ice Climber Japan protection)
///
VsUnisystemVsIceClimberJapanProtection = 0x04,
///
/// Vs. Dual System (normal)
///
VsDualSystemNormal = 0x05,
///
/// Vs. Dual System (Raid on Bungeling Bay protection)
///
VsDualSystemRaidOnBungelingBayProtection = 0x06
}
///
/// Extended console type
///
public enum ExtendedConsoleType
{
///
/// Regular NES/Famicom/Dendy
///
RegularNES = 0x00,
///
/// Nintendo Vs. System
///
NintendoVsSystem = 0x01,
///
/// Playchoice 10
///
Playchoice10 = 0x02,
///
/// Regular Famiclone, but with CPU that supports Decimal Mode (e.g. Bit Corporation Creator)
///
FamicloneWithDecimalMode = 0x03,
///
/// V.R. Technology VT01 with monochrome palette
///
VRTechnologyVT01Monochrome = 0x04,
///
/// V.R. Technology VT01 with red/cyan STN palette
///
VRTechnologyVT01WithRedCyanSTNPalette = 0x05,
///
/// V.R. Technology VT02
///
VRTechnologyVT02 = 0x06,
///
/// V.R. Technology VT03
///
VRTechnologyVT03 = 0x07,
///
/// V.R. Technology VT09
///
VRTechnologyVT09 = 0x08,
///
/// V.R. Technology VT32
///
VRTechnologyVT32 = 0x09,
///
/// V.R. Technology VT369
///
VRTechnologyVT369 = 0x0A,
///
/// UMC UM6578
///
UMC_UM6578 = 0x0B
}
///
/// Type of expansion device connected to console, source: https://www.nesdev.org/wiki/NES_2.0#Default_Expansion_Device
///
public enum ExpansionDevice
{
///
/// Expansion device is not specified
///
Unspecified = 0x00,
///
/// Standard NES/Famicom controllers
///
Standard = 0x01,
///
/// NES Four Score/Satellite with two additional standard controllers
///
NesFourScore = 0x02,
///
/// Famicom Four Players Adapter with two additional standard controllers
///
FamicomFourPlayersAdapter = 0x03,
///
/// Vs. System
///
VsSystem = 0x04,
///
/// Vs. System with reversed inputs
///
VsSystemWithReversedInputs = 0x05,
///
/// Vs. Pinball (Japan)
///
VsPinball = 0x06,
///
/// Vs. Zapper
///
VsZapper = 0x07,
///
/// Zapper ($4017)
///
Zapper = 0x08,
///
/// Two Zappers
///
TwoZappers = 0x09,
///
/// Bandai Hyper Shot Lightgun
///
BandaiHyperShotLightgun = 0x0A,
///
/// Power Pad Side A
///
PowerPadSideA = 0x0B,
///
/// Power Pad Side B
///
PowerPadSideB = 0x0C,
///
/// Family Trainer Side A
///
FamilyTrainerSideA = 0x0D,
///
/// Family Trainer Side B
///
FamilyTrainerSideB = 0x0E,
///
/// Arkanoid Vaus Controller (NES)
///
ArkanoidVausControllerNES = 0x0F,
///
/// Arkanoid Vaus Controller (Famicom)
///
ArkanoidVausControllerFamicom = 0x10,
///
/// Two Vaus Controllers plus Famicom Data Recorder
///
TwoVausControllersPlusFamicomDataRecorder = 0x11,
///
/// Konami Hyper Shot Controller
///
KonamiHyperShotController = 0x12,
///
/// Coconuts Pachinko Controller
///
CoconutsPachinkoController = 0x13,
///
/// Exciting Boxing Punching Bag (Blowup Doll)
///
ExcitingBoxingPunchingBag = 0x14,
///
/// Jissen Mahjong Controller
///
JissenMahjongController = 0x15,
///
/// Party Tap
///
PartyTap = 0x16,
///
/// Oeka Kids Tablet
///
OekaKidsTablet = 0x17,
///
/// Sunsoft Barcode Battler
///
SunsoftBarcodeBattler = 0x18,
///
/// Miracle Piano Keyboard
///
MiraclePianoKeyboard = 0x19,
///
/// Pokkun Moguraa (Whack-a-Mole Mat and Mallet)
///
PokkunMoguraa = 0x1A,
///
/// Top Rider(Inflatable Bicycle)
///
TopRider = 0x1B,
///
/// Double-Fisted (Requires or allows use of two controllers by one player)
///
DoubleFisted = 0x1C,
///
/// Famicom 3D System
///
Famicom3DSystem = 0x1D,
///
/// Doremikko Keyboard
///
DoremikkoKeyboard = 0x1E,
///
/// R.O.B. Gyro Set
///
RobGyroSet = 0x1F,
///
/// Famicom Data Recorder (don't emulate keyboard)
///
FamicomDataRecorder = 0x20,
///
/// ASCII Turbo File
///
ASCIITurboFile = 0x21,
///
/// IGS Storage Battle Box
///
IGSStorageBattleBox = 0x22,
///
/// Family BASIC Keyboard plus Famicom Data Recorder
///
FamilyBasicKeyboardPlusFamicomDataRecorder = 0x23,
///
/// Dongda PEC-586 Keyboard
///
DongdaPEC586Keyboard = 0x24,
///
/// Bit Corp. Bit-79 Keyboard
///
BitCorpBit79Keyboard = 0x25,
///
/// Subor Keyboard
///
SuborKeyboard = 0x26,
///
/// Subor Keyboard plus mouse (3x8-bit protocol)
///
SuborKeyboardPlusMouse3x8 = 0x27,
///
/// Subor Keyboard plus mouse (24-bit protocol)
///
SuborKeyboardPlusMouse24 = 0x28,
///
/// SNES Mouse ($4017.d0)
///
SnesMouse4017 = 0x29,
///
/// Multicart
///
Multicart = 0x2A,
///
/// Two SNES controllers replacing the two standard NES controllers
///
TwoSnesControllers = 0x2B,
///
/// RacerMate Bicycle
///
RacerMateBicycle = 0x2C,
///
/// U-Force
///
UForce = 0x2D,
///
/// R.O.B. Stack-Up
///
RobStackUp = 0x2E,
///
/// City Patrolman Lightgun
///
CityPatrolmanLightgun = 0x2F,
///
/// Sharp C1 Cassette Interface
///
SharpC1CassetteInterface = 0x30,
///
/// Standard Controller with swapped Left-Right/Up-Down/B-A
///
StandardControllerWithSwapped = 0x31,
///
/// Excalibor Sudoku Pad
///
ExcaliborSudokuPad = 0x32,
///
/// ABL Pinball
///
AblPinball = 0x33,
///
/// Golden Nugget Casino extra buttons
///
GoldenNuggetCasinoExtraButtons = 0x34,
}
///
/// Constructor to create empty NesFile object
///
public NesFile()
{
}
///
/// Create NesFile object from raw .nes file contents
///
/// Raw .nes file data
public NesFile(byte[] data)
{
var header = new byte[16];
Array.Copy(data, header, header.Length);
if (header[0] != 'N' ||
header[1] != 'E' ||
header[2] != 'S' ||
header[3] != 0x1A) throw new InvalidDataException("Invalid iNES header");
if ((header[7] & 0x0C) == 0x08)
Version = iNesVersion.NES20;
else if (!(header[12] == 0 && header[13] == 0 && header[14] == 0 && header[15] == 0))
{
// archaic iNES
header[7] = header[8] = header[9] = header[10] = header[11] = header[12] = header[13] = header[14] = header[15] = 0;
}
uint prgSize = 0;
uint chrSize = 0;
Mirroring = (MirroringType)(header[6] & 1);
Battery = (header[6] & (1 << 1)) != 0;
if ((header[6] & (1 << 2)) != 0)
trainer = new byte[512];
else
trainer = Array.Empty();
if ((header[6] & (1 << 3)) != 0)
Mirroring = MirroringType.FourScreenVram;
if (Version == iNesVersion.iNES)
{
prgSize = (uint)(header[4] * 0x4000);
chrSize = (uint)(header[5] * 0x2000);
Mapper = (byte)((header[6] >> 4) | (header[7] & 0xF0));
Console = (ConsoleType)(header[7] & 3);
PrgRamSize = (uint)(header[8] == 0 ? 0x2000 : header[8] * 0x2000);
}
else if (Version == iNesVersion.NES20) // NES 2.0
{
if ((header[9] & 0x0F) != 0x0F)
prgSize = (uint)((((header[9] & 0x0F) << 8) | header[4]) * 0x4000);
else
prgSize = (uint)((1 << (header[4] >> 2)) * ((header[4] & 3) * 2 + 1)); // omg
if ((header[9] & 0xF0) != 0xF0)
chrSize = (uint)((((header[9] & 0xF0) << 4) | header[5]) * 0x2000);
else
chrSize = (uint)((1 << (header[5] >> 2)) * ((header[5] & 3) * 2 + 1));
Mapper = (ushort)((header[6] >> 4) | (header[7] & 0xF0) | ((header[8] & 0x0F) << 8));
Submapper = (byte)(header[8] >> 4);
Console = (ConsoleType)(header[7] & 3);
if ((header[10] & 0x0F) > 0)
PrgRamSize = (uint)(64 << (header[10] & 0x0F));
if ((header[10] & 0xF0) > 0)
PrgNvRamSize = (uint)(64 << ((header[10] & 0xF0) >> 4));
if ((header[11] & 0x0F) > 0)
ChrRamSize = (uint)(64 << (header[11] & 0x0F));
if ((header[11] & 0xF0) > 0)
ChrNvRamSize = (uint)(64 << ((header[11] & 0xF0) >> 4));
Region = (Timing)header[12];
switch (Console)
{
case ConsoleType.VsSystem:
VsPpu = (VsPpuType)(header[13] & 0x0F);
VsHardware = (VsHardwareType)(header[13] >> 4);
break;
case ConsoleType.Extended:
ExtendedConsole = (ExtendedConsoleType)(header[13] & 0x0F);
break;
}
MiscellaneousROMsCount = (byte)(header[14] & 3);
DefaultExpansionDevice = (ExpansionDevice)(header[15] & 0x3F);
}
uint offset = (uint)header.Length;
if (trainer.Length > 0)
{
if (offset < data.Length)
Array.Copy(data, offset, trainer, 0, Math.Max(0, Math.Min(trainer.Length, data.Length - offset)));
offset += (uint)trainer.Length;
}
prg = new byte[prgSize];
if (offset < data.Length)
Array.Copy(data, offset, prg, 0, Math.Max(0, Math.Min(prgSize, data.Length - offset))); // Ignore end for some bad ROMs
offset += prgSize;
chr = new byte[chrSize];
if (offset < data.Length)
Array.Copy(data, offset, chr, 0, Math.Max(0, Math.Min(chrSize, data.Length - offset)));
offset += chrSize;
if (MiscellaneousROMsCount > 0)
{
MiscellaneousROM = new byte[data.Length - offset];
Array.Copy(data, offset, miscellaneousROM, 0, miscellaneousROM.Length);
}
else
{
MiscellaneousROM = Array.Empty();
}
}
///
/// Create NesFile object from the specified .nes file
///
/// Path to the .nes file
public NesFile(string fileName)
: this(File.ReadAllBytes(fileName))
{
}
///
/// Create NesFile object from raw .nes file contents
///
/// Raw ROM data
/// NesFile object
public static NesFile FromBytes(byte[] data) => new NesFile(data);
///
/// Create NesFile object from the specified .nes file
///
/// Path to the .nes file
/// NesFile object
public static NesFile FromFile(string filename) => new NesFile(filename);
///
/// Returns .nes file contents (header + PRG + CHR)
///
/// .nes file contents
public byte[] ToBytes()
{
var data = new List();
var header = new byte[16];
header[0] = (byte)'N';
header[1] = (byte)'E';
header[2] = (byte)'S';
header[3] = 0x1A;
ulong prgSizePadded, chrSizePadded;
if (Version == iNesVersion.iNES)
{
if (Console == ConsoleType.Extended)
throw new InvalidDataException("Extended console type is supported by NES 2.0 only");
if (Mapper > 255)
throw new InvalidDataException("Mapper number > 255 is supported by NES 2.0 only");
if (Submapper != 0)
throw new InvalidDataException("Submapper number is supported by NES 2.0 only");
var length16k = prg.Length / 0x4000;
if (length16k > 0xFF) throw new ArgumentOutOfRangeException("PRG size is too big for iNES, use NES 2.0 instead");
header[4] = (byte)Math.Ceiling((double)prg.Length / 0x4000);
prgSizePadded = header[4] * 0x4000UL;
var length8k = chr.Length / 0x2000;
if (length8k > 0xFF) throw new ArgumentOutOfRangeException("CHR size is too big for iNES, use NES 2.0 instead");
header[5] = (byte)Math.Ceiling((double)chr.Length / 0x2000);
chrSizePadded = header[5] * 0x2000UL;
switch (Mirroring)
{
case MirroringType.Unknown: // mirroring field ignored
case MirroringType.Horizontal:
case MirroringType.Vertical:
case MirroringType.FourScreenVram:
case MirroringType.MapperControlled: // mirroring field ignored
break;
default:
throw new InvalidDataException($"{Mirroring} mirroring is not supported by iNES");
}
// Hard-wired nametable mirroring type
if (Mirroring == MirroringType.Vertical)
header[6] |= 1;
// "Battery" and other non-volatile memory
if (Battery)
header[6] |= (1 << 1);
// 512-byte Trainer
if (trainer.Length > 0)
header[6] |= (1 << 2);
// Hard-wired four-screen mode
if (Mirroring == MirroringType.FourScreenVram)
header[6] |= (1 << 3);
// Mapper Number D0..D3
header[6] |= (byte)(Mapper << 4);
// Console type
header[7] |= (byte)((byte)Console & 3);
// Mapper Number D4..D7
header[7] |= (byte)(Mapper & 0xF0);
data.AddRange(header);
if (trainer.Length > 0)
{
data.AddRange(trainer);
if (trainer.Length < 512) data.AddRange(Enumerable.Repeat(0xFF, (int)512 - trainer.Length));
}
data.AddRange(prg);
data.AddRange(Enumerable.Repeat(byte.MaxValue, (int)prgSizePadded - prg.Length));
data.AddRange(chr);
data.AddRange(Enumerable.Repeat(byte.MaxValue, (int)chrSizePadded - chr.Length));
}
else if (Version == iNesVersion.NES20)
{
var length16k = (uint)Math.Ceiling((double)prg.Length / 0x4000);
if (length16k <= 0xEFF)
{
header[4] = (byte)(length16k & 0xFF);
header[9] |= (byte)(length16k >> 8);
prgSizePadded = length16k * 0x4000;
}
else
{
byte exponent, multiplier;
(exponent, multiplier, prgSizePadded) = SizeToExponent((ulong)prg.Length);
header[4] = (byte)((exponent << 2) | (multiplier & 3));
header[9] |= 0x0F;
}
var length8k = (uint)Math.Ceiling((double)chr.Length / 0x2000);
if (length8k <= 0xEFF)
{
header[5] = (byte)(length8k & 0xFF);
header[9] |= (byte)((length8k >> 4) & 0xF0);
chrSizePadded = length8k * 0x2000;
}
else
{
byte exponent, multiplier;
(exponent, multiplier, chrSizePadded) = SizeToExponent((ulong)chr.Length);
header[5] = (byte)((exponent << 2) | (multiplier & 3));
header[9] |= 0xF0;
}
// Hard-wired nametable mirroring type
if (Mirroring == MirroringType.Vertical)
header[6] |= 1;
// "Battery" and other non-volatile memory
if (Battery)
header[6] |= (1 << 1);
// 512-byte Trainer
if (trainer.Length > 0)
header[6] |= (1 << 2);
// Hard-wired four-screen mode
if (Mirroring == MirroringType.FourScreenVram)
header[6] |= (1 << 3);
// Mapper Number D0..D3
header[6] |= (byte)(Mapper << 4);
// Console type
header[7] |= (byte)((byte)Console & 3);
// NES 2.0 identifier
header[7] |= 1 << 3;
// Mapper Number D4..D7
header[7] |= (byte)(Mapper & 0xF0);
// Mapper number D8..D11
header[8] |= (byte)((Mapper >> 8) & 0x0F);
// Submapper
header[8] |= (byte)(Submapper << 4);
// PRG RAM (volatile) shift count
var prgRamBitSize = PrgRamSize > 0 ? Math.Max(1, (int)Math.Ceiling(Math.Log(PrgRamSize, 2)) - 6) : 0;
header[10] |= (byte)(prgRamBitSize & 0x0F);
// PRG-NVRAM/EEPROM (non-volatile) shift count
var prgNvRamBitSize = PrgNvRamSize > 0 ? Math.Max(1, (int)Math.Ceiling(Math.Log(PrgNvRamSize, 2)) - 6) : 0;
header[10] |= (byte)((prgNvRamBitSize << 4) & 0xF0);
// CHR-RAM size (volatile) shift count
var chrRamBitSize = ChrRamSize > 0 ? Math.Max(1, (int)Math.Ceiling(Math.Log(ChrRamSize, 2)) - 6) : 0;
header[11] |= (byte)(chrRamBitSize & 0x0F);
// CHR-NVRAM size (non-volatile) shift count
var chrNvRamBitSize = ChrNvRamSize > 0 ? Math.Max(1, (int)Math.Ceiling(Math.Log(ChrNvRamSize, 2)) - 6) : 0;
header[11] |= (byte)((chrNvRamBitSize << 4) & 0xF0);
// CPU/PPU timing mode
header[12] |= (byte)((byte)Region & 3);
switch (Console)
{
// When Byte 7 AND 3 =1: Vs. System Type
case ConsoleType.VsSystem:
// Vs. PPU Type
header[13] |= (byte)((byte)VsPpu & 0x0F);
// Vs. Hardware Type
header[13] |= (byte)(((byte)VsHardware << 4) & 0xF0);
break;
// When Byte 7 AND 3 =3: Extended Console Type
case ConsoleType.Extended:
// Extended Console Type
header[13] = (byte)ExtendedConsole;
break;
}
// Miscellaneous ROMs
header[14] |= (byte)(MiscellaneousROMsCount & 3);
// Default Expansion Device
header[15] |= (byte)((byte)DefaultExpansionDevice & 0x3F);
data.AddRange(header);
if (trainer.Length > 0)
{
data.AddRange(trainer);
if (trainer.Length < 512) data.AddRange(Enumerable.Repeat(byte.MaxValue, (int)512 - trainer.Length));
}
data.AddRange(prg);
data.AddRange(Enumerable.Repeat(byte.MaxValue, (int)prgSizePadded - prg.Length));
data.AddRange(chr);
data.AddRange(Enumerable.Repeat(byte.MaxValue, (int)chrSizePadded - chr.Length));
if (MiscellaneousROMsCount > 0 || miscellaneousROM.Length > 0)
{
if (MiscellaneousROMsCount == 0)
throw new InvalidDataException("MiscellaneousROMsCount is zero while MiscellaneousROM is not empty");
if (MiscellaneousROM.Length == 0)
throw new InvalidDataException("MiscellaneousROM is empty while MiscellaneousROMsCount is not zero");
data.AddRange(miscellaneousROM);
}
}
return data.ToArray();
}
///
/// Save as .nes file
///
/// Target filename
public void Save(string filename) => File.WriteAllBytes(filename, ToBytes());
private static ulong ExponentToSize(byte exponent, byte multiplier)
=> (1UL << exponent) * (ulong)(multiplier * 2 + 1);
private static (byte Exponent, byte Multiplier, ulong Padded) SizeToExponent(ulong value)
{
if (value == 0) return (0, 0, 1);
if (value < 8)
{
var r = SizeToExponent(value << 3);
return ((byte)(r.Exponent - 3), r.Multiplier, r.Padded >> 3);
}
// Calculate bits required to store number
byte bitsize = 0;
while (value >> bitsize > 0) bitsize++;
// Split it into two parts
var major = value >> (bitsize - 3);
var minor = value & (ulong)~(0b111 << (bitsize - 3));
// Round up
if (minor != 0) major++;
byte e, m;
switch (major)
{
case 0b100:
e = (byte)(bitsize - 1);
m = 0; // 0*2+1=1
break;
case 0b101:
e = (byte)(bitsize - 3);
m = 2; // 2*2+1=5
break;
case 0b110:
e = (byte)(bitsize - 2);
m = 1; // 1*2+1=3
break;
case 0b111:
e = (byte)(bitsize - 3);
m = 3; // 3*2+1=7
break;
case 0b1000:
e = (byte)bitsize;
m = 0; // 0*2+1=1
break;
default:
throw new InvalidProgramException();
}
return (e, m, ExponentToSize(e, m));
}
///
/// Calculate MD5 checksum of ROM (CHR+PRG without header)
///
/// MD5 checksum for all PRG and CHR data
public byte[] CalculateMD5()
{
int prgSizeUpPow2 = 1;
int chrSizeUpPow2 = 1;
if (PRG.Length == 0)
prgSizeUpPow2 = 0; // Is it possible?
else
while (prgSizeUpPow2 < PRG.Length) prgSizeUpPow2 <<= 1;
if (CHR.Length == 0)
chrSizeUpPow2 = 0;
else
while (chrSizeUpPow2 < CHR.Length) chrSizeUpPow2 <<= 1;
using (var md5 = MD5.Create())
{
md5.TransformBlock(prg, 0, prg.Length, null, 0);
md5.TransformBlock(Enumerable.Repeat(byte.MaxValue, prgSizeUpPow2 - prg.Length).ToArray(), 0, prgSizeUpPow2 - prg.Length, null, 0);
md5.TransformBlock(chr, 0, chr.Length, null, 0);
md5.TransformBlock(Enumerable.Repeat(byte.MaxValue, chrSizeUpPow2 - chr.Length).ToArray(), 0, chrSizeUpPow2 - chr.Length, null, 0);
md5.TransformFinalBlock(new byte[0], 0, 0);
return md5.Hash;
}
}
///
/// Calculate CRC32 checksum of ROM (CHR+PRG without header)
///
/// CRC32 checksum for all PRG and CHR data
public uint CalculateCRC32()
{
int prgSizeUpPow2 = 1;
int chrSizeUpPow2 = 1;
if (PRG.Length == 0)
prgSizeUpPow2 = 0; // Is it possible?
else
while (prgSizeUpPow2 < PRG.Length) prgSizeUpPow2 <<= 1;
if (CHR.Length == 0)
chrSizeUpPow2 = 0;
else
while (chrSizeUpPow2 < CHR.Length) chrSizeUpPow2 <<= 1;
using (var crc32 = new Crc32())
{
crc32.TransformBlock(prg, 0, prg.Length, null, 0);
crc32.TransformBlock(Enumerable.Repeat(byte.MaxValue, prgSizeUpPow2 - prg.Length).ToArray(), 0, prgSizeUpPow2 - prg.Length, null, 0);
crc32.TransformBlock(chr, 0, chr.Length, null, 0);
crc32.TransformBlock(Enumerable.Repeat(byte.MaxValue, chrSizeUpPow2 - chr.Length).ToArray(), 0, chrSizeUpPow2 - chr.Length, null, 0);
crc32.TransformFinalBlock(new byte[0], 0, 0);
return BitConverter.ToUInt32(crc32.Hash.Reverse().ToArray(), 0);
}
}
}
}