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); } } } }