using System; using System.Collections; using System.Collections.Generic; using System.Drawing; using System.IO; using System.Linq; using System.Runtime.ConstrainedExecution; using System.Security.Cryptography; using System.Text; using System.Xml.Linq; namespace com.clusterrr.Famicom.Containers { /// /// UNIF file container for NES/Famicom games /// public class UnifFile : IEnumerable> { private const string FIELD_CTRL = "CTRL"; private const string FIELD_TVCI = "TVCI"; private const string FIELD_DINF = "DINF"; private const string FIELD_NAME = "NAME"; private const string FIELD_BATR = "BATR"; private const string FIELD_MAPR = "MAPR"; private const string FIELD_MIRR = "MIRR"; private const string FIELD_PRG0 = "PRG0"; private const string FIELD_PRG1 = "PRG1"; private const string FIELD_PRG2 = "PRG2"; private const string FIELD_PRG3 = "PRG3"; private const string FIELD_CHR0 = "CHR0"; private const string FIELD_CHR1 = "CHR1"; private const string FIELD_CHR2 = "CHR2"; private const string FIELD_CHR3 = "CHR3"; private const string PREFIX_PRG = "PRG"; private const string PREFIX_CHR = "CHR"; /// /// UNIF fields /// private Dictionary fields = new Dictionary(); /// /// UNIF version /// public uint Version { get; set; } = 5; /// /// Get/set UNIF field /// /// UNIF data block key /// public byte[] this[string key] { get { if (key.Length != 4) throw new ArgumentException("UNIF data block key must be 4 characters long"); if (!ContainsField(key)) throw new IndexOutOfRangeException($"There is no {key} field"); return fields[key]; } set { if (key.Length != 4) throw new ArgumentException("UNIF data block key must be 4 characters long"); if (value == null) this.RemoveField(key); else fields[key] = value; } } /// /// Returns true if field exists in the UNIF /// /// Field code /// True if field exists in the UNIF public bool ContainsField(string fieldName) => fields.ContainsKey(fieldName); /// /// Remove field from the UNIF /// /// public void RemoveField(string fieldName) => fields.Remove(fieldName); /// /// Returns enumerator that iterates throught fields /// /// IEnumerable object public IEnumerator> GetEnumerator() => fields.Select(kv => new KeyValuePair(kv.Key, kv.Value)).GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); /// /// Constructor to create empty UnifFile object /// public UnifFile() { DumpDate = DateTime.Now; } /// /// Create UnifFile object from raw .unf file contents /// /// Raw UNIF data public UnifFile(byte[] data) { var header = new byte[32]; Array.Copy(data, header, 32); if (header[0] != 'U' || header[1] != 'N' || header[2] != 'I' || header[3] != 'F') throw new InvalidDataException("Invalid UNIF header"); Version = BitConverter.ToUInt32(header, 4); int pos = 32; while (pos < data.Length) { var type = Encoding.UTF8.GetString(data, pos, 4); pos += 4; int length = BitConverter.ToInt32(data, pos); pos += 4; var fieldData = new byte[length]; Array.Copy(data, pos, fieldData, 0, length); this[type] = fieldData; pos += length; } } /// /// Create UnifFile object from specified file /// /// Path to the .unf file public UnifFile(string fileName) : this(File.ReadAllBytes(fileName)) { } /// /// Create UnifFile object from raw .unf file contents /// /// /// UnifFile object public static UnifFile FromBytes(byte[] data) => new UnifFile(data); /// /// Create UnifFile object from specified file /// /// Path to the .unf file /// UnifFile object public static UnifFile FromFile(string filename) => new UnifFile(filename); /// /// Returns .unf file contents /// /// public byte[] ToBytes() { // Some checks if (ContainsField(FIELD_CTRL) && Version < 7) throw new InvalidDataException("CTRL (controllers) field requires UNIF version 7 or greater"); if (ContainsField(FIELD_TVCI) && Version < 6) throw new InvalidDataException("TVCI (controllers) field requires UNIF version 6 or greater"); var data = new List(); // Header data.AddRange(Encoding.UTF8.GetBytes("UNIF")); data.AddRange(BitConverter.GetBytes(Version)); data.AddRange(Enumerable.Repeat(0, 24).ToArray()); // Fields foreach (var kv in this) { data.AddRange(Encoding.UTF8.GetBytes(kv.Key)); var value = kv.Value; int len = value.Length; data.AddRange(BitConverter.GetBytes(len)); data.AddRange(value); } return data.ToArray(); } /// /// Save as .unf file /// /// Target filename public void Save(string filename) => File.WriteAllBytes(filename, ToBytes()); /// /// Convert string to null-terminated UTF string /// /// Input text /// Output byte[] array private static byte[] StringToUTF8N(string text) { var str = Encoding.UTF8.GetBytes(text); var result = new byte[str.Length + 1]; Array.Copy(str, result, str.Length); return result; } /// /// Convert null-terminated UTF string to string /// /// Input array of bytes /// Maximum number of bytes to parse /// Start offset /// private static string UTF8NToString(byte[] data, int maxLength = int.MaxValue, int offset = 0) { int length = 0; while ((data[length + offset] != 0) && (length + offset < data.Length) && (length < maxLength)) length++; return Encoding.UTF8.GetString(data, offset, length); } /// /// Mapper name (null if none) /// public string? Mapper { get => ContainsField(FIELD_MAPR) ? UTF8NToString(this[FIELD_MAPR]) : null; set { if (value == null) RemoveField(FIELD_MAPR); else this[FIELD_MAPR] = StringToUTF8N(value); } } /// /// The dumper name (null if none) /// /// public string? DumperName { get { if (!ContainsField(FIELD_DINF)) return null; var data = this[FIELD_DINF]; if (data.Length >= 204 && data[0] != 0) return UTF8NToString(data, 100); else return null; } set { if (value != null || DumpingSoftware != null || DumpDate != null) { if (!ContainsField(FIELD_DINF)) this[FIELD_DINF] = new byte[204]; var data = this[FIELD_DINF]; for (int i = 0; i < 100; i++) data[i] = 0; if (value != null) { var name = StringToUTF8N(value); Array.Copy(name, 0, data, 0, Math.Min(100, name!.Length)); } this[FIELD_DINF] = data; } else RemoveField(FIELD_DINF); } } /// /// The name of the dumping software or mechanism (null if none) /// public string? DumpingSoftware { get { if (!ContainsField(FIELD_DINF)) return null; var data = this[FIELD_DINF]; if (data.Length >= 204 && data[0] != 0) return UTF8NToString(data, 100, 104); else return null; } set { if (value != null || DumperName != null || DumpDate != null) { if (!ContainsField(FIELD_DINF)) this[FIELD_DINF] = new byte[204]; var data = this[FIELD_DINF]; for (int i = 104; i < 104 + 100; i++) data[i] = 0; if (value != null) { var name = StringToUTF8N(value); Array.Copy(name, 0, data, 104, Math.Min(100, name!.Length)); } this[FIELD_DINF] = data; } else RemoveField(FIELD_DINF); } } /// /// Date of the dump (null if none) /// public DateTime? DumpDate { get { if (!ContainsField(FIELD_DINF)) return null; var data = this[FIELD_DINF]; if (data[0] == 0 && data[1] == 0 && data[2] == 0 && data[3] == 0) return null; return new DateTime( year: data[102] | (data[103] << 8), month: data[101], day: data[100] ); } set { if (value != null || DumperName != null || DumpingSoftware != null) { if (!ContainsField(FIELD_DINF)) this[FIELD_DINF] = new byte[204]; var data = this[FIELD_DINF]; if (value != null) { data[100] = (byte)value.Value.Day; data[101] = (byte)value.Value.Month; data[102] = (byte)(value.Value.Year & 0xFF); data[103] = (byte)(value.Value.Year >> 8); } else { // Is it valid? data[100] = data[101] = data[102] = data[103] = 0; } this[FIELD_DINF] = data; } else RemoveField(FIELD_DINF); } } /// /// Name of the game (null if none) /// public string? GameName { get => ContainsField(FIELD_NAME) ? UTF8NToString(this[FIELD_NAME]) : null; set { if (value == null) RemoveField(FIELD_NAME); else this[FIELD_NAME] = StringToUTF8N(value!); } } /// /// For non-homebrew NES/Famicom games, this field's value is always a function of the region in which a game was released (null if none) /// public Timing? Region { get { if (ContainsField(FIELD_TVCI) && this[FIELD_TVCI].Any()) return (Timing)this[FIELD_TVCI].First(); else return null; } set { if (value != null) this[FIELD_TVCI] = new byte[] { (byte)value }; else RemoveField(FIELD_TVCI); } } /// /// Controllers usable by this game, bitmask (null if none) /// public Controller? Controllers { get { if (ContainsField(FIELD_CTRL) && this[FIELD_CTRL].Any()) return (Controller)this[FIELD_CTRL].First(); else return Controller.None; } set { if (value != null) fields[FIELD_CTRL] = new byte[] { (byte)value }; else RemoveField(FIELD_CTRL); } } /// /// Battery-backed (or other non-volatile memory) memory is present (null if none) /// public bool? Battery { get { if (ContainsField(FIELD_BATR) && this[FIELD_BATR].Any()) return this[FIELD_BATR].First() != 0; else return false; } set { if (value != null) fields[FIELD_BATR] = new byte[] { (byte)((bool)value ? 1 : 0) }; else RemoveField(FIELD_BATR); } } /// /// Mirroring type (null if none) /// public MirroringType? Mirroring { get { if (ContainsField(FIELD_MIRR) && this[FIELD_MIRR].Any()) return (MirroringType)this[FIELD_MIRR].First(); else return null; } set { if (value != null && value != MirroringType.Unknown) this[FIELD_MIRR] = new byte[] { (byte)value }; else this.RemoveField(FIELD_MIRR); } } /// /// PRG0 field (null if none) /// public byte[]? PRG0 { get { if (ContainsField(FIELD_PRG0)) return this[FIELD_PRG0]; else return null; } set { if (value != null) this[FIELD_PRG0] = value; else RemoveField(FIELD_PRG0); } } /// /// PRG1 field (null if none) /// public byte[]? PRG1 { get { if (ContainsField(FIELD_PRG1)) return this[FIELD_PRG1]; else return null; } set { if (value != null) this[FIELD_PRG1] = value; else RemoveField(FIELD_PRG1); } } /// /// PRG2 field (null if none) /// public byte[]? PRG2 { get { if (ContainsField(FIELD_PRG2)) return this[FIELD_PRG2]; else return null; } set { if (value != null) this[FIELD_PRG2] = value; else RemoveField(FIELD_PRG2); } } /// /// PRG3 field (null if none) /// public byte[]? PRG3 { get { if (ContainsField(FIELD_PRG3)) return this[FIELD_PRG3]; else return null; } set { if (value != null) this[FIELD_PRG3] = value; else RemoveField(FIELD_PRG3); } } /// /// CHR0 field (null if none) /// public byte[]? CHR0 { get { if (ContainsField(FIELD_CHR0)) return this[FIELD_CHR0]; else return null; } set { if (value != null) this[FIELD_CHR0] = value; else RemoveField(FIELD_CHR0); } } /// /// CHR1 field (null if none) /// public byte[]? CHR1 { get { if (ContainsField(FIELD_CHR1)) return this[FIELD_CHR1]; else return null; } set { if (value != null) this[FIELD_CHR1] = value; else RemoveField(FIELD_CHR1); } } /// /// CHR2 field (null if none) /// public byte[]? CHR2 { get { if (ContainsField(FIELD_CHR2)) return this[FIELD_CHR2]; else return null; } set { if (value != null) this[FIELD_CHR2] = value; else RemoveField(FIELD_CHR2); } } /// /// CHR3 field (null if none) /// public byte[]? CHR3 { get { if (ContainsField(FIELD_CHR3)) return this[FIELD_CHR3]; else return null; } set { if (value != null) this[FIELD_CHR3] = value; else RemoveField(FIELD_CHR3); } } /// /// Calculate CRC32 for PRG and CHR fields and store it into PCKx and CCKx fields /// public void CalculateAndStoreCRCs() { foreach (var kv in this.Where(kv => kv.Key.StartsWith(PREFIX_PRG))) { var num = kv.Key[3]; using var crc32 = new Crc32(); var crc32sum = crc32.ComputeHash(kv.Value); this[$"PCK{num}"] = crc32sum; } foreach (var kv in this.Where(kv => kv.Key.StartsWith(PREFIX_CHR))) { var num = kv.Key[3]; using var crc32 = new Crc32(); var crc32sum = crc32.ComputeHash(kv.Value); this[$"CCK{num}"] = crc32sum; } } /// /// Calculate MD5 checksum of ROM (all PRG fields + all CHR fields) /// /// MD5 checksum for all PRG and CHR data public byte[] CalculateMD5() { using var md5 = MD5.Create(); foreach(var kv in fields.Where(k => k.Key.StartsWith(PREFIX_PRG)).OrderBy(k => k.Key) .Concat(fields.Where(k => k.Key.StartsWith(PREFIX_CHR)).OrderBy(k => k.Key))) { var v = kv.Value; int sizeUpPow2 = 0; if (v.Length > 0) { sizeUpPow2 = 1; while (sizeUpPow2 < v.Length) sizeUpPow2 <<= 1; } md5.TransformBlock(v, 0, v.Length, null, 0); md5.TransformBlock(Enumerable.Repeat(byte.MaxValue, sizeUpPow2 - v.Length).ToArray(), 0, sizeUpPow2 - v.Length, null, 0); } md5.TransformFinalBlock(new byte[0], 0, 0); return md5.Hash; } /// /// Calculate CRC32 checksum of ROM (all PRG fields + all CHR fields) /// /// CRC32 checksum for all PRG and CHR data public uint CalculateCRC32() { using var crc32 = new Crc32(); foreach (var kv in fields.Where(k => k.Key.StartsWith(PREFIX_PRG)).OrderBy(k => k.Key) .Concat(fields.Where(k => k.Key.StartsWith(PREFIX_CHR)).OrderBy(k => k.Key))) { var v = kv.Value; int sizeUpPow2 = 0; if (v.Length > 0) { sizeUpPow2 = 1; while (sizeUpPow2 < v.Length) sizeUpPow2 <<= 1; } crc32.TransformBlock(v, 0, v.Length, null, 0); crc32.TransformBlock(Enumerable.Repeat(byte.MaxValue, sizeUpPow2 - v.Length).ToArray(), 0, sizeUpPow2 - v.Length, null, 0); } crc32.TransformFinalBlock(new byte[0], 0, 0); return BitConverter.ToUInt32(crc32.Hash.Reverse().ToArray(), 0); } /// /// Default game controller(s) /// [Flags] public enum Controller { /// /// None /// None = 0, /// /// Standatd Controller /// StandardController = 1, /// /// Zapper /// Zapper = 2, /// /// R.O.B. /// ROB = 4, /// /// Arkanoid Controller /// ArkanoidController = 8, /// /// Power Pad /// PowerPad = 16, /// /// Four Score /// FourScore = 32, } } }