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 int Version { get; set; } = 7; /// /// 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 = header[4] | (header[5] << 8) | (header[6] << 16) | (header[7] << 24); int pos = 32; while (pos < data.Length) { var type = Encoding.UTF8.GetString(data, pos, 4); pos += 4; int length = data[pos] | (data[pos + 1] << 8) | (data[pos + 2] << 16) | (data[pos + 3] << 24); 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() { var data = new List(); var header = new byte[32]; Array.Copy(Encoding.UTF8.GetBytes("UNIF"), header, 4); header[4] = (byte)(Version & 0xFF); header[5] = (byte)((Version >> 8) & 0xFF); header[6] = (byte)((Version >> 16) & 0xFF); header[7] = (byte)((Version >> 24) & 0xFF); data.AddRange(header); foreach (var kv in this) { data.AddRange(Encoding.UTF8.GetBytes(kv.Key)); var v = kv.Value.ToArray(); int len = v.Length; data.Add((byte)(len & 0xFF)); data.Add((byte)((len >> 8) & 0xFF)); data.Add((byte)((len >> 16) & 0xFF)); data.Add((byte)((len >> 24) & 0xFF)); 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) { if (data == null || data.Length == 0) return null; int length = 0; while ((data[length + offset] != 0) && (length + offset < data.Length) && (length + offset < maxLength)) length++; return Encoding.UTF8.GetString(data, offset, length); } /// /// Mapper name /// public string? Mapper { get => UTF8NToString(this["MAPR"]?.ToArray()); set { if (value == null) RemoveField("MAPR"); else this["MAPR"] = StringToUTF8N(value); } } /// /// The dumper name /// /// public string? DumperName { get => UTF8NToString(fields["DINF"], 100); set { 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; } } /// /// The name of the dumping software or mechanism /// public string? DumpingSoftware { get => UTF8NToString(fields["DINF"], 100, 104); set { 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, fields["DINF"], 104, Math.Min(100, name!.Length)); } this["DINF"] = data; } } /// /// Date of the dump /// public DateTime? DumpDate { get { if (!ContainsField("DINF")) return null; var data = this["DINF"].ToArray(); return new DateTime( year: data[102] | (data[103] << 8), month: data[101], day: data[100] ); } set { if (!ContainsField("DINF")) this["DINF"] = new byte[204]; if (value != null) { var data = this["DINF"].ToArray(); 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); this["DINF"] = data; } } } /// /// Name of the game /// public string? GameName { get => UTF8NToString(this["NAME"]?.ToArray()); 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 NesFile.Timing Region { get { if (ContainsField("TVCI") && this["TVCI"].Any()) return (NesFile.Timing)this["TVCI"].First(); else return NesFile.Timing.Ntsc; } set { this["TVCI"] = new byte[] { (byte)value }; } } /// /// 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 { fields["CTRL"] = new byte[] { (byte)value }; } } /// /// 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 { fields["BATR"] = new byte[] { (byte)(value ? 1 : 0) }; } } /// /// Mirroring type /// public MirroringType Mirroring { get { if (ContainsField("MIRR") && this["MIRR"].Any()) return (MirroringType)this["MIRR"].First(); else return MirroringType.Unknown; } set { fields["MIRR"] = new byte[] { (byte)value }; } } /// /// 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()); fields[$"PCK{num}"] = new byte[] { (byte)(crc32 & 0xFF), (byte)((crc32 >> 8) & 0xFF), (byte)((crc32 >> 16) & 0xFF), (byte)((crc32 >> 24) & 0xFF) }; } foreach (var kv in this.Where(kv => kv.Key.StartsWith("CHR"))) { var num = kv.Key[3]; var crc32 = Crc32Calculator.CalculateCRC32(kv.Value.ToArray()); fields[$"CCK{num}"] = new byte[] { (byte)(crc32 & 0xFF), (byte)((crc32 >> 8) & 0xFF), (byte)((crc32 >> 16) & 0xFF), (byte)((crc32 >> 24) & 0xFF) }; } } /// /// 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, } } }