using System; using System.Collections; using System.Collections.Generic; using System.Drawing; using System.IO; using System.Linq; using System.Text; 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"); 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 && fields.ContainsKey(key)) fields.Remove(key); else fields[key] = (value ?? new byte[0]).ToArray(); } } /// /// 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); fields[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 name in fields.Keys) { data.AddRange(Encoding.UTF8.GetBytes(name)); int len = fields[name].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(fields[name]); } 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 + offset < maxLength)) length++; return Encoding.UTF8.GetString(data, offset, length); } /// /// Mapper name /// public string Mapper { get { if (fields.ContainsKey("MAPR")) return UTF8NToString(fields["MAPR"]); else return null; } set { fields["MAPR"] = StringToUTF8N(value); } } /// /// The dumper name /// /// public string DumperName { get { if (!fields.ContainsKey("DINF")) return null; return UTF8NToString(fields["DINF"], 100); } set { if (!fields.ContainsKey("DINF")) fields["DINF"] = new byte[204]; for (int i = 0; i < 100; i++) fields["DINF"][i] = 0; var name = StringToUTF8N(value); Array.Copy(name, 0, fields["DINF"], 0, Math.Min(100, name.Length)); } } /// /// The name of the dumping software or mechanism /// public string DumpingSoftware { get { if (!fields.ContainsKey("DINF")) return null; return UTF8NToString(fields["DINF"], 100, 104); } set { if (!fields.ContainsKey("DINF")) fields["DINF"] = new byte[204]; for (int i = 104; i < 104 + 100; i++) fields["DINF"][i] = 0; var name = StringToUTF8N(value); Array.Copy(name, 0, fields["DINF"], 104, Math.Min(100, name.Length)); } } /// /// Date of the dump /// public DateTime DumpDate { get { if (!fields.ContainsKey("DINF")) return new DateTime(); return new DateTime( year: fields["DINF"][102] | (fields["DINF"][103] << 8), month: fields["DINF"][101], day: fields["DINF"][100] ); } set { if (!fields.ContainsKey("DINF")) fields["DINF"] = new byte[204]; fields["DINF"][100] = (byte)value.Day; fields["DINF"][101] = (byte)value.Month; fields["DINF"][102] = (byte)(value.Year & 0xFF); fields["DINF"][103] = (byte)(value.Year >> 8); } } /// /// Name of the game /// public string GameName { get { if (fields.ContainsKey("NAME")) return UTF8NToString(fields["NAME"]); else return null; } set { fields["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 (fields.ContainsKey("TVCI") && fields["TVCI"].Length > 0) return (NesFile.Timing)fields["TVCI"][0]; else return NesFile.Timing.Ntsc; } set { fields["TVCI"] = new byte[] { (byte)value }; } } /// /// Controllers usable by this game (bitmask) /// public Controller Controllers { get { if (fields.ContainsKey("CTRL") && fields["CTRL"].Length > 0) return (Controller)fields["CTRL"][0]; 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 (fields.ContainsKey("BATR") && fields["BATR"].Length > 0) return fields["BATR"][0] != 0; else return false; } set { fields["BATR"] = new byte[] { (byte)(value ? 1 : 0) }; } } /// /// Mirroring type /// public MirroringType Mirroring { get { if (fields.ContainsKey("MIRR") && fields["MIRR"].Length > 0) return (MirroringType)fields["MIRR"][0]; 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 key in fields.Keys.Where(k => k.StartsWith("PRG"))) { var num = key[3]; var crc32 = Crc32Calculator.CalculateCRC32(fields[key]); fields[$"PCK{num}"] = new byte[] { (byte)(crc32 & 0xFF), (byte)((crc32 >> 8) & 0xFF), (byte)((crc32 >> 16) & 0xFF), (byte)((crc32 >> 24) & 0xFF) }; } foreach (var key in fields.Keys.Where(k => k.StartsWith("CHR"))) { var num = key[3]; var crc32 = Crc32Calculator.CalculateCRC32(fields[key]); 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, } } }