using System; using System.Collections; using System.Collections.Generic; using System.Drawing; using System.IO; using System.Linq; using System.Text; using System.Xml.Linq; namespace com.clusterrr.Famicom.Containers { /// /// UNIF file container for NES/Famicom games /// public class UnifFile : IEnumerable>> { /// /// UNIF fields /// private Dictionary fields = new Dictionary(); /// /// UNIF version /// public uint Version { get; set; } = 5; /// /// Get/set UNIF field /// /// UNIF data block key /// public IEnumerable 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 Array.AsReadOnly(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 ?? new byte[0]).ToArray(); } } /// /// 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, Array.AsReadOnly(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("CTRL") && Version < 7) throw new InvalidDataException("CTRL (controllers) field requires UNIF version 7 or greater"); if (ContainsField("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 v = kv.Value.ToArray(); int len = v.Length; data.AddRange(BitConverter.GetBytes(len)); data.AddRange(v); } 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 /// public string? Mapper { get => ContainsField("MAPR") ? UTF8NToString(this["MAPR"].ToArray()) : null; set { if (value == null) RemoveField("MAPR"); else this["MAPR"] = StringToUTF8N(value); } } /// /// The dumper name /// /// public string? DumperName { get { if (!ContainsField("DINF")) return null; var data = this["DINF"].ToArray(); if (data.Length >= 204 && data[0] != 0) return UTF8NToString(data, 100); else return null; } set { if (value != null || DumpingSoftware != null || DumpDate != null) { if (!ContainsField("DINF")) this["DINF"] = new byte[204]; var data = this["DINF"].ToArray(); 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["DINF"] = data; } else RemoveField("DINF"); } } /// /// The name of the dumping software or mechanism /// public string? DumpingSoftware { get { if (!ContainsField("DINF")) return null; var data = this["DINF"].ToArray(); 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("DINF")) this["DINF"] = new byte[204]; var data = this["DINF"].ToArray(); 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["DINF"] = data; } else RemoveField("DINF"); } } /// /// Date of the dump /// public DateTime? DumpDate { get { if (!ContainsField("DINF")) return null; var data = this["DINF"].ToArray(); 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("DINF")) this["DINF"] = new byte[204]; var data = this["DINF"].ToArray(); 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] = 0; data[101] = 0; data[102] = 0; data[103] = 0; } this["DINF"] = data; } else RemoveField("DINF"); } } /// /// Name of the game /// public string? GameName { get => ContainsField("NAME") ? UTF8NToString(this["NAME"].ToArray()) : null; set { if (value == null) RemoveField("NAME"); else this["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 /// public Timing? Region { get { if (ContainsField("TVCI") && this["TVCI"].Any()) return (Timing)this["TVCI"].First(); else return null; } set { if (value != null) this["TVCI"] = new byte[] { (byte)value }; else RemoveField("TVCI"); } } /// /// Controllers usable by this game (bitmask) /// public Controller? Controllers { get { if (ContainsField("CTRL") && this["CTRL"].Any()) return (Controller)this["CTRL"].First(); else return Controller.None; } set { if (value != null) fields["CTRL"] = new byte[] { (byte)value }; else RemoveField("CTRL"); } } /// /// Battery-backed (or other non-volatile memory) memory is present /// public bool? Battery { get { if (ContainsField("BATR") && this["BATR"].Any()) return this["BATR"].First() != 0; else return false; } set { if (value != null) fields["BATR"] = new byte[] { (byte)((bool)value ? 1 : 0) }; else RemoveField("BATR"); } } /// /// Mirroring type /// public MirroringType? Mirroring { get { if (ContainsField("MIRR") && this["MIRR"].Any()) return (MirroringType)this["MIRR"].First(); else return null; } set { if (value != null && value != MirroringType.Unknown) this["MIRR"] = new byte[] { (byte)value }; else this.RemoveField("MIRR"); } } /// /// PRG0 field /// public IEnumerable? PRG0 { get { if (ContainsField("PRG0")) return this["PRG0"]; else return null; } set { if (value != null) this["PRG0"] = value; else RemoveField("PRG0"); } } /// /// PRG1 field /// public IEnumerable? PRG1 { get { if (ContainsField("PRG1")) return this["PRG1"]; else return null; } set { if (value != null) this["PRG1"] = value; else RemoveField("PRG1"); } } /// /// PRG2 field /// public IEnumerable? PRG2 { get { if (ContainsField("PRG2")) return this["PRG2"]; else return null; } set { if (value != null) this["PRG2"] = value; else RemoveField("PRG2"); } } /// /// PRG3 field /// public IEnumerable? PRG3 { get { if (ContainsField("PRG3")) return this["PRG3"]; else return null; } set { if (value != null) this["PRG3"] = value; else RemoveField("PRG3"); } } /// /// CHR0 field /// public IEnumerable? CHR0 { get { if (ContainsField("CHR0")) return this["CHR0"]; else return null; } set { if (value != null) this["CHR0"] = value; else RemoveField("CHR0"); } } /// /// CHR1 field /// public IEnumerable? CHR1 { get { if (ContainsField("CHR1")) return this["CHR1"]; else return null; } set { if (value != null) this["CHR1"] = value; else RemoveField("CHR1"); } } /// /// CHR2 field /// public IEnumerable? CHR2 { get { if (ContainsField("CHR2")) return this["CHR2"]; else return null; } set { if (value != null) this["CHR2"] = value; else RemoveField("CHR2"); } } /// /// CHR3 field /// public IEnumerable? CHR3 { get { if (ContainsField("CHR3")) return this["CHR3"]; else return null; } set { if (value != null) this["CHR3"] = value; else RemoveField("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("PRG"))) { var num = kv.Key[3]; var crc32 = Crc32Calculator.CalculateCRC32(kv.Value.ToArray()); this[$"PCK{num}"] = BitConverter.GetBytes(crc32); } foreach (var kv in this.Where(kv => kv.Key.StartsWith("CHR"))) { var num = kv.Key[3]; var crc32 = Crc32Calculator.CalculateCRC32(kv.Value.ToArray()); this[$"CCK{num}"] = BitConverter.GetBytes(crc32); } } /// /// Calculate overall CRC32 /// /// public uint CalculateCRC32() => Crc32Calculator.CalculateCRC32( Enumerable.Concat(fields.Where(k => k.Key.StartsWith("PRG")).OrderBy(k => k.Key).SelectMany(i => i.Value), fields.Where(k => k.Key.StartsWith("CHR")).OrderBy(k => k.Key).SelectMany(i => i.Value)).ToArray() ); /// /// 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, } } }