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,
}
}
}