using System; using System.IO; using System.Linq; using System.Text; namespace DMGHeader { public class DmgHeader { public DmgEntryPoint EntryPoint { get; set; } public bool ValidLogo { get; set; } public string Title { get; set; } public CgbMode CgbMode { get; set; } public bool SgbSupport { get; set; } public string CartridgeType { get; set; } public MemorySize RomSize { get; set; } public MemorySize ExternalRamSize { get; set; } public DestinationCode DestinationCode { get; set; } public byte MaskRomVersion { get; set; } public byte HeaderChecksum { get; set; } public bool ValidHeaderChecksum { get; set; } public UInt16 CartridgeChecksum { get; set; } private byte[] _validLogo = { 0xCE, 0xED, 0x66, 0x66, 0xCC, 0x0D, 0x00, 0x0B, 0x03, 0x73, 0x00, 0x83, 0x00, 0x0C, 0x00, 0x0D, 0x00, 0x08, 0x11, 0x1F, 0x88, 0x89, 0x00, 0x0E, 0xDC, 0xCC, 0x6E, 0xE6, 0xDD, 0xDD, 0xD9, 0x99, 0xBB, 0xBB, 0x67, 0x63, 0x6E, 0x0E, 0xEC, 0xCC, 0xDD, 0xDC, 0x99, 0x9F, 0xBB, 0xB9, 0x33, 0x3E }; public DmgHeader(string filename) { using var fs = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read); using var br = new BinaryReader(fs); fs.Seek(0x100, SeekOrigin.Begin); EntryPoint = ReadEntryPoint(br.ReadBytes(4)); ValidLogo = ValidateLogo(br.ReadBytes(_validLogo.Length)); // See if the upper bit of the CGB flag is set to see how much of the title field to read. fs.Seek(0x143, SeekOrigin.Begin); var cgb = (br.ReadByte() & 0b10000000) != 0; fs.Seek(0x134, SeekOrigin.Begin); var titleLen = cgb ? 15 : 16; Title = ReadTitle(br.ReadBytes(titleLen)); CgbMode = cgb ? ReadCgbMode(br.ReadByte()) : CgbMode.Disabled; fs.Seek(0x146, SeekOrigin.Begin); SgbSupport = br.ReadByte() == 0x03; CartridgeType = ReadCartridgeType(br.ReadByte()); RomSize = ReadRomSize(br.ReadByte()); ExternalRamSize = ReadExternalRamSize(br.ReadByte()); DestinationCode = ReadDestinationCode(br.ReadByte()); fs.Seek(0x14C, SeekOrigin.Begin); MaskRomVersion = br.ReadByte(); HeaderChecksum = br.ReadByte(); fs.Seek(0x134, SeekOrigin.Begin); ValidHeaderChecksum = HeaderChecksum == CalculateHeaderChecksum(br.ReadBytes(0x19)); fs.Seek(0x14E, SeekOrigin.Begin); CartridgeChecksum = br.ReadUInt16(); } private byte CalculateHeaderChecksum(byte[] data) { byte x = 0; unchecked { x = data.Aggregate(x, (current, b) => (byte)(current - b - 1)); } return x; } private string ReadCartridgeType(byte type) { switch(type) { case 0x00: return "ROM ONLY"; case 0x01: return "MBC1"; case 0x02: return "MBC1+RAM"; case 0x03: return "MBC1+RAM+BATTERY"; case 0x05: return "MBC2"; case 0x06: return "MBC2+BATTERY"; case 0x08: return "ROM+RAM"; case 0x09: return "ROM+RAM+BATTERY"; case 0x0B: return "MMM01"; case 0x0C: return "MMM01+RAM"; case 0x0D: return "MMM01+RAM+BATTERY"; case 0x0F: return "MBC3+TIMER+BATTERY"; case 0x10: return "MBC3+TIMER+RAM+BATTERY"; case 0x11: return "MBC3"; case 0x12: return "MBC3+RAM"; case 0x13: return "MBC3+RAM+BATTERY"; case 0x19: return "MBC5"; case 0x1A: return "MBC5+RAM"; case 0x1B: return "MBC5+RAM+BATTERY"; case 0x1C: return "MBC5+RUMBLE"; case 0x1D: return "MBC5+RUMBLE+RAM"; case 0x1E: return "MBC5+RUMBLE+RAM+BATTERY"; case 0x20: return "MBC6"; case 0x22: return "MBC7+SENSOR+RUMBLE+RAM+BATTERY"; case 0xFC: return "POCKET CAMERA"; case 0xFD: return "BANDAI TAMA5"; case 0xFE: return "HuC3"; case 0xFF: return "HuC1+RAM+BATTERY"; default: return "Unknown"; } } private CgbMode ReadCgbMode(byte mode) { if((mode & 0b10000000) == 0) return CgbMode.Disabled; if((mode & 0b01000000) != 0) return CgbMode.Required; if((mode & 0b00001100) != 0) return CgbMode.PaletteOnly; return CgbMode.Enhanced; } private DestinationCode ReadDestinationCode(byte data) { switch(data) { case 0x00: return DestinationCode.Japanese; case 0x01: return DestinationCode.NonJapanese; default: return DestinationCode.Unknown; } } private DmgEntryPoint ReadEntryPoint(byte[] data) { var entry = new DmgEntryPoint(); var i = 0; for(; i < data.Length; i++) { if(data[i] != 0xC3 && data[i] != 0x18) continue; entry.Jump = data[i] == 0xC3 ? JumpType.Absolute : JumpType.Relative; break; } if(entry.Jump == JumpType.None) return entry; var addrLen = entry.Jump == JumpType.Absolute ? 2 : 1; var addr = data.Skip(++i).Take(addrLen).ToArray(); if(addr.Length != addrLen) return entry; if(entry.Jump == JumpType.Absolute) { // Stitch the values into a 16-bit value. entry.JumpAddress = addr[1] << 8 | addr[0]; } else { entry.JumpAddress = addr[0] + 0x100 + i; } return entry; } private MemorySize ReadExternalRamSize(byte data) { var size = new MemorySize(8); switch(data) { case 0x00: size.Size = 0; return size; case 0x01: size.Size = 2; return size; case 0x02: size.Size = 8; return size; case 0x03: size.Size = 32; return size; case 0x04: size.Size = 128; return size; case 0x05: size.Size = 64; return size; default: return null; } } private MemorySize ReadRomSize(byte data) { var size = new MemorySize(16); switch(data) { case 0x00: size.Size = 32; return size; case 0x01: size.Size = 64; return size; case 0x02: size.Size = 128; return size; case 0x03: size.Size = 256; return size; case 0x04: size.Size = 512; return size; case 0x05: size.Size = 1024; return size; case 0x06: size.Size = 2048; return size; case 0x07: size.Size = 4096; return size; case 0x08: size.Size = 8192; return size; case 0x52: size.Size = 1152; return size; case 0x53: size.Size = 1280; return size; case 0x54: size.Size = 1536; return size; default: return null; } } private string ReadTitle(byte[] data) { // Convert nulls to spaces. for(var i = 0; i < data.Length; i++) { if(data[i] == 0) data[i] = 32; } return Encoding.ASCII.GetString(data).Trim(); } private bool ValidateLogo(byte[] data) => data != null && data.Length == _validLogo.Length && data.SequenceEqual(_validLogo); } }