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