using System; using System.IO; using System.Text; using NESDecompiler.Core.Exceptions; namespace NESDecompiler.Core.ROM { /// /// Handles loading and parsing NES ROM files /// public class ROMLoader { // iNES header constants private const int HEADER_SIZE = 16; private const int PRG_ROM_SIZE_OFFSET = 4; private const int CHR_ROM_SIZE_OFFSET = 5; private const int FLAGS_6_OFFSET = 6; private const int FLAGS_7_OFFSET = 7; private const int PRG_RAM_SIZE_OFFSET = 8; private const int FLAGS_9_OFFSET = 9; private const int FLAGS_10_OFFSET = 10; // ROM data private byte[]? romData; private ROMInfo? romInfo; /// /// Information about the loaded ROM /// public ROMInfo? ROMInfo => romInfo; /// /// Loads a NES ROM file from disk /// /// Path to the ROM file /// Information about the loaded ROM public ROMInfo LoadFromFile(string filePath) { if (!File.Exists(filePath)) { throw new FileNotFoundException("ROM file not found", filePath); } try { romData = File.ReadAllBytes(filePath); return ParseROMHeader(); } catch (Exception ex) when (ex is not ROMException) { throw new ROMException($"Failed to load ROM file: {ex.Message}", ex); } } /// /// Loads a NES ROM from a byte array /// /// The ROM data /// Information about the loaded ROM public ROMInfo LoadFromBytes(byte[] data) { if (data == null || data.Length < HEADER_SIZE) { throw new ROMException("Invalid ROM data: too short"); } romData = data; return ParseROMHeader(); } /// /// Parses the iNES ROM header /// /// Information about the ROM private ROMInfo ParseROMHeader() { // Verify iNES header signature "NES" followed by MS-DOS EOF if (romData!.Length < HEADER_SIZE || romData[0] != 0x4E || romData[1] != 0x45 || romData[2] != 0x53 || romData[3] != 0x1A) { throw new InvalidROMFormatException("Invalid iNES ROM header"); } romInfo = new ROMInfo { PRGROMSize = romData[PRG_ROM_SIZE_OFFSET] * 16384, // 16KB units CHRROMSize = romData[CHR_ROM_SIZE_OFFSET] * 8192, // 8KB units MapperNumber = (byte)((romData[FLAGS_7_OFFSET] & 0xF0) | ((romData[FLAGS_6_OFFSET] & 0xF0) >> 4)), MirroringType = (romData[FLAGS_6_OFFSET] & 0x01) == 0 ? MirroringType.Horizontal : MirroringType.Vertical, HasBatteryBackedRAM = (romData[FLAGS_6_OFFSET] & 0x02) != 0, HasTrainer = (romData[FLAGS_6_OFFSET] & 0x04) != 0, HasFourScreenVRAM = (romData[FLAGS_6_OFFSET] & 0x08) != 0, IsVSSystemCart = (romData[FLAGS_7_OFFSET] & 0x01) != 0, PRGROMOffset = HEADER_SIZE + (((romData[FLAGS_6_OFFSET] & 0x04) != 0) ? 512 : 0), RawData = romData }; romInfo.CHRROMOffset = romInfo.PRGROMOffset + romInfo.PRGROMSize; var end = romInfo.PRGROMOffset + romInfo.PRGROMSize; var nmiOffset = end - 6; var resetOffset = end - 4; var irqOffset = end - 2; ushort GetVectorAddress(int offset) { if (offset >= 0 && offset < romData.Length - 1) { return (ushort)(romData[offset] | (romData[offset + 1] << 8)); } return 0; } // Identify entry points (reset vector), NMI handler, and irq handler if (romInfo.PRGROMSize > 0) { // In 6502, reset vector is at 0xFFFC-0xFFFD // For NES, this is mapped to the end of the first PRG ROM bank romInfo.ResetVector = GetVectorAddress(resetOffset); romInfo.NmiVector = GetVectorAddress(nmiOffset); romInfo.IrqVector = GetVectorAddress(irqOffset); if (romInfo.NmiVector > 0) { romInfo.EntryPoints.Add(romInfo.NmiVector); } if (romInfo.IrqVector > 0) { romInfo.EntryPoints.Add(romInfo.IrqVector); } } return romInfo; } /// /// Gets a segment of the ROM data /// /// Starting offset /// Number of bytes to read /// The requested data segment public byte[] GetROMSegment(int offset, int length) { if (romData == null) throw new ROMException("No ROM data loaded"); if (offset < 0 || offset + length > romData.Length) throw new ROMException("Requested segment is out of bounds"); byte[] segment = new byte[length]; Array.Copy(romData, offset, segment, 0, length); return segment; } /// /// Gets the PRG ROM data /// /// The PRG ROM data public byte[] GetPRGROMData() { if (romInfo == null) throw new ROMException("No ROM data loaded"); return GetROMSegment(romInfo.PRGROMOffset, romInfo.PRGROMSize); } /// /// Gets the CHR ROM data /// /// The CHR ROM data public byte[] GetCHRROMData() { if (romInfo == null) throw new ROMException("No ROM data loaded"); if (romInfo.CHRROMSize == 0) return Array.Empty(); return GetROMSegment(romInfo.CHRROMOffset, romInfo.CHRROMSize); } } }