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