snes-disassembler
Version:
A Super Nintendo (SNES) ROM disassembler for 65816 assembly
452 lines • 19 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.RomParser = void 0;
const fs = __importStar(require("fs"));
const cartridge_types_1 = require("./cartridge-types");
class RomParser {
static parse(filePath) {
const data = fs.readFileSync(filePath);
// Check for SMC header (512 bytes)
const hasHeader = data.length % 1024 === 512;
const romData = hasHeader ? data.slice(512) : data;
// Determine ROM mapping mode
const hiRomHeaderOffset = 0x7FC0;
const loRomHeaderOffset = 0xFFC0;
// Check both LoROM and HiROM headers to determine which is valid
let headerOffset = loRomHeaderOffset;
let isHiRom = false;
if (romData.length > Math.max(loRomHeaderOffset, hiRomHeaderOffset) + 0x40) {
const loRomScore = this.scoreHeader(romData, loRomHeaderOffset, false);
const hiRomScore = this.scoreHeader(romData, hiRomHeaderOffset, true);
if (hiRomScore > loRomScore) {
headerOffset = hiRomHeaderOffset;
isHiRom = true;
}
else {
headerOffset = loRomHeaderOffset;
isHiRom = false;
}
}
const header = this.parseHeader(romData, headerOffset);
// Detect cartridge type and create enhanced cartridge info
const cartridgeType = (0, cartridge_types_1.detectCartridgeType)(header.mapMode, header.cartridgeType);
const memorySpeed = (0, cartridge_types_1.getMemorySpeed)(header.mapMode);
const hasBattery = (0, cartridge_types_1.hasBatteryBackup)(header.cartridgeType);
const sramSize = (0, cartridge_types_1.getSRAMSize)(header.ramSize);
const cartridgeInfo = {
type: cartridgeType,
mapMode: header.mapMode,
romSize: this.getROMSize(header.romSize),
ramSize: sramSize,
hasBattery,
hasRTC: cartridgeType === cartridge_types_1.CartridgeType.SRTC,
speed: memorySpeed,
regions: [],
specialChip: this.getSpecialChipName(cartridgeType)
};
// Create memory layout
const memoryRegions = (0, cartridge_types_1.createMemoryLayout)(cartridgeInfo);
cartridgeInfo.regions = memoryRegions;
return {
header,
data: romData,
isHiRom,
hasHeader,
cartridgeInfo,
memoryRegions
};
}
static scoreHeader(data, offset, isHiRom) {
let score = 0;
// Enhanced title validity check (max 35 points)
const title = data.slice(offset, offset + 21).toString('ascii').trim();
const printableChars = title.split('').filter(c => c.charCodeAt(0) >= 32 && c.charCodeAt(0) <= 126).length;
const titleRatio = title.length > 0 ? printableChars / title.length : 0;
if (titleRatio >= 0.9)
score += 35;
else if (titleRatio >= 0.8)
score += 25;
else if (titleRatio >= 0.6)
score += 15;
else if (titleRatio >= 0.4)
score += 5;
// Enhanced map mode check (max 25 points)
const mapMode = data[offset + 0x15];
const mapType = mapMode & 0x0F;
const speed = (mapMode & 0x30) >> 4;
if (isHiRom) {
if ((mapMode & 0x01) === 1)
score += 20; // HiROM should have bit 0 set
if (mapType === 0x01 || mapType === 0x05)
score += 5; // Valid HiROM map types
}
else {
if ((mapMode & 0x01) === 0)
score += 20; // LoROM should have bit 0 clear
if (mapType === 0x00 || mapType === 0x02 || mapType === 0x03)
score += 5; // Valid LoROM map types
}
// ROM size validation (max 15 points)
const romSize = data[offset + 0x17];
if (romSize >= 0x07 && romSize <= 0x0D) {
score += 15;
}
else if (romSize >= 0x05 && romSize <= 0x0F) {
score += 8; // Extended range with lower score
}
// Cartridge type validation (max 10 points)
const cartridgeType = data[offset + 0x16];
const validCartridgeTypes = [
0x00, 0x01, 0x02, 0x03, 0x05, 0x06, 0x09, 0x0A, 0x13, 0x14, 0x15, 0x1A,
0x34, 0x35, 0x43, 0x45, 0x55, 0xF3, 0xF5, 0xF6, 0xF9
];
if (validCartridgeTypes.includes(cartridgeType))
score += 10;
// Country code validation (max 8 points)
const countryCode = data[offset + 0x19];
if (countryCode <= 0x0D)
score += 8;
// Complement checksum validation (max 15 points)
const checksum = data.readUInt16LE(offset + 0x1C);
const complement = data.readUInt16LE(offset + 0x1E);
if ((checksum ^ complement) === 0xFFFF)
score += 15;
// Reset vector validation (max 12 points)
const resetVector = data.readUInt16LE(offset + 0x2C);
if (resetVector >= 0x8000 && resetVector <= 0xFFFF) {
score += 12;
}
else if (resetVector >= 0x4000) {
score += 6; // Partial credit for reasonable vectors
}
// Additional vector validation (max 10 points)
const nmiVector = data.readUInt16LE(offset + 0x2A);
const irqVector = data.readUInt16LE(offset + 0x2E);
let vectorScore = 0;
if (nmiVector >= 0x8000 && nmiVector <= 0xFFFF)
vectorScore += 5;
if (irqVector >= 0x8000 && irqVector <= 0xFFFF)
vectorScore += 5;
score += vectorScore;
return score;
}
static parseHeader(data, offset) {
// Read title (21 bytes)
const titleBytes = data.slice(offset, offset + 21);
const title = titleBytes.toString('ascii').replace(/\0+$/, '');
return {
title,
mapMode: data[offset + 0x15],
cartridgeType: data[offset + 0x16],
romSize: data[offset + 0x17],
ramSize: data[offset + 0x18],
countryCode: data[offset + 0x19],
licenseCode: data[offset + 0x1A],
versionNumber: data[offset + 0x1B],
checksum: data.readUInt16LE(offset + 0x1C),
complement: data.readUInt16LE(offset + 0x1E),
nativeVectors: {
cop: data.readUInt16LE(offset + 0x24),
brk: data.readUInt16LE(offset + 0x26),
abort: data.readUInt16LE(offset + 0x28),
nmi: data.readUInt16LE(offset + 0x2A),
reset: data.readUInt16LE(offset + 0x2C),
irq: data.readUInt16LE(offset + 0x2E)
},
emulationVectors: {
cop: data.readUInt16LE(offset + 0x34),
brk: data.readUInt16LE(offset + 0x36),
abort: data.readUInt16LE(offset + 0x38),
nmi: data.readUInt16LE(offset + 0x3A),
reset: data.readUInt16LE(offset + 0x3C),
irq: data.readUInt16LE(offset + 0x3E)
}
};
}
static getROMSize(romSizeByte) {
// ROM size is 2^romSizeByte KB
return (1 << romSizeByte) * 1024;
}
static getSpecialChipName(cartridgeType) {
const chipNames = {
[cartridge_types_1.CartridgeType.SA1]: 'SA-1 Super Accelerator',
[cartridge_types_1.CartridgeType.SuperFX]: 'SuperFX Graphics Support Unit',
[cartridge_types_1.CartridgeType.DSP1]: 'DSP-1 Digital Signal Processor',
[cartridge_types_1.CartridgeType.DSP2]: 'DSP-2 Digital Signal Processor',
[cartridge_types_1.CartridgeType.DSP3]: 'DSP-3 Digital Signal Processor',
[cartridge_types_1.CartridgeType.DSP4]: 'DSP-4 Digital Signal Processor',
[cartridge_types_1.CartridgeType.CX4]: 'Capcom CX4 Math Coprocessor',
[cartridge_types_1.CartridgeType.ST010]: 'Seta ST010 Graphics Processor',
[cartridge_types_1.CartridgeType.ST011]: 'Seta ST011 Graphics Processor',
[cartridge_types_1.CartridgeType.SPC7110]: 'SPC7110 Data Decompression',
[cartridge_types_1.CartridgeType.SDD1]: 'S-DD1 Data Decompression',
[cartridge_types_1.CartridgeType.SRTC]: 'S-RTC Real-Time Clock',
[cartridge_types_1.CartridgeType.OBC1]: 'OBC-1 Metal Combat Support',
[cartridge_types_1.CartridgeType.MSU1]: 'MSU-1 Audio Enhancement',
[cartridge_types_1.CartridgeType.BSX]: 'BSX Satellaview',
// Standard types with potential special chip support
[cartridge_types_1.CartridgeType.LoROM]: 'Possible enhancement chips',
[cartridge_types_1.CartridgeType.HiROM]: 'Possible enhancement chips',
[cartridge_types_1.CartridgeType.ExLoROM]: 'Possible enhancement chips',
[cartridge_types_1.CartridgeType.ExHiROM]: 'Possible enhancement chips',
[cartridge_types_1.CartridgeType.Unknown]: undefined
};
return chipNames[cartridgeType];
}
static getRomOffset(address, cartridgeInfo, romSize) {
const isHiRom = cartridgeInfo.type === cartridge_types_1.CartridgeType.HiROM ||
cartridgeInfo.type === cartridge_types_1.CartridgeType.ExHiROM;
return this.getRomOffsetWithWrapping(address, isHiRom, romSize || cartridgeInfo.romSize);
}
static getRomOffsetWithWrapping(address, isHiRom, romSize) {
let romOffset;
if (isHiRom) {
romOffset = this.calculateHiROMOffset(address);
}
else {
romOffset = this.calculateLoROMOffset(address);
}
// Apply bank wrapping for ROM size
if (romOffset >= 0) {
return romOffset % romSize;
}
throw new Error(`Invalid address for ROM mapping: $${address.toString(16).toUpperCase()}`);
}
static calculateHiROMOffset(address) {
const bank = (address >> 16) & 0xFF;
const offset = address & 0xFFFF;
// Banks C0-FF: Direct ROM mapping (64KB per bank)
if (bank >= 0xC0) {
return ((bank - 0xC0) * 0x10000) + offset;
}
// Banks 40-7F: Direct ROM mapping (64KB per bank)
if (bank >= 0x40 && bank <= 0x7F) {
return ((bank - 0x40) * 0x10000) + offset;
}
// Banks 80-BF: Mirror of 00-3F at $8000-$FFFF
if (bank >= 0x80 && bank <= 0xBF && offset >= 0x8000) {
return ((bank - 0x80) * 0x8000) + (offset - 0x8000);
}
// Banks 00-3F: ROM at $8000-$FFFF
if (bank <= 0x3F && offset >= 0x8000) {
return (bank * 0x8000) + (offset - 0x8000);
}
// Bank 00: Direct mapping for low addresses
if (bank === 0x00 && offset < 0x8000) {
return offset;
}
return -1; // Invalid mapping
}
static calculateLoROMOffset(address) {
const bank = (address >> 16) & 0xFF;
const offset = address & 0xFFFF;
// Banks 80-FF: FastROM mirror at $8000-$FFFF
if (bank >= 0x80 && offset >= 0x8000) {
return ((bank - 0x80) * 0x8000) + (offset - 0x8000);
}
// Banks 00-7F: ROM at $8000-$FFFF
if (bank <= 0x7F && offset >= 0x8000) {
return (bank * 0x8000) + (offset - 0x8000);
}
// Bank 00: Direct mapping for low addresses
if (bank === 0x00 && offset < 0x8000) {
return offset;
}
return -1; // Invalid mapping
}
// Legacy method for backward compatibility
static getRomOffsetLegacy(address, isHiRom) {
return this.getRomOffsetWithWrapping(address, isHiRom, 0x400000); // Default 4MB
}
static getPhysicalAddress(romOffset, isHiRom) {
if (isHiRom) {
return 0xC00000 + romOffset;
}
else {
const bank = Math.floor(romOffset / 0x8000) + 0x80;
const offset = (romOffset % 0x8000) + 0x8000;
return (bank << 16) | offset;
}
}
/**
* Detect and handle split ROM files (multi-part dumps)
* Based on file naming conventions and size analysis
*/
static detectSplitRom(filePath) {
const splitParts = [filePath];
// Common split ROM naming patterns
const patterns = [
/(.+)\.part(\d+)\.smc$/i,
/(.+)\.(\d+)\.smc$/i,
/(.+)_(\d+)\.smc$/i,
/(.+)-(\d+)\.smc$/i
];
for (const pattern of patterns) {
const match = filePath.match(pattern);
if (match) {
const baseName = match[1];
const partNum = parseInt(match[2]);
// Look for other parts
for (let i = 1; i <= 8; i++) { // Reasonable limit
if (i === partNum)
continue;
const testPath = `${baseName}.part${i}.smc`;
if (fs.existsSync(testPath)) {
splitParts.push(testPath);
}
}
break;
}
}
return splitParts.sort();
}
/**
* Combine split ROM parts into single buffer
*/
static combineSplitRom(splitParts) {
const buffers = [];
for (const part of splitParts) {
const data = fs.readFileSync(part);
buffers.push(data);
}
return Buffer.concat(buffers);
}
/**
* Detect interleaved ROM format
* Common in older ROM dumps where odd/even bytes are swapped
*/
static detectInterleavedFormat(data) {
// Check for interleaving by analyzing header patterns
// Interleaved ROMs often have garbled headers at standard locations
// Try standard header locations
const possibleOffsets = [0x7FC0, 0xFFC0, 0x81C0, 0x101C0];
for (const offset of possibleOffsets) {
if (offset + 0x20 >= data.length)
continue;
// Check if header makes sense when de-interleaved
const deInterleavedData = this.deInterleaveRom(data);
const score = this.scoreHeader(deInterleavedData, offset, false);
const originalScore = this.scoreHeader(data, offset, false);
if (score > originalScore + 2) { // Significant improvement
return true;
}
}
return false;
}
/**
* De-interleave ROM data (swap odd/even bytes)
*/
static deInterleaveRom(data) {
const deInterleaved = Buffer.alloc(data.length);
for (let i = 0; i < data.length - 1; i += 2) {
deInterleaved[i] = data[i + 1]; // Even positions get odd bytes
deInterleaved[i + 1] = data[i]; // Odd positions get even bytes
}
return deInterleaved;
}
/**
* Detect overdumped ROMs (ROMs with extra data beyond the actual ROM size)
* Common in older dumps where ROMs were padded to standard sizes
*/
static detectOverdump(data, expectedSize) {
if (data.length <= expectedSize) {
return { isOverdumped: false, originalSize: data.length };
}
// Check if extra data is just padding (zeros or repeated pattern)
const extraData = data.slice(expectedSize);
// Check for zero padding
const isZeroPadded = extraData.every(byte => byte === 0x00);
// Check for FF padding
const isFFPadded = extraData.every(byte => byte === 0xFF);
// Check for repeated pattern
let isRepeatedPattern = false;
if (extraData.length >= 4) {
const pattern = extraData.slice(0, 4);
isRepeatedPattern = true;
for (let i = 4; i < extraData.length; i += 4) {
const chunk = extraData.slice(i, i + 4);
if (!chunk.equals(pattern.slice(0, chunk.length))) {
isRepeatedPattern = false;
break;
}
}
}
const isOverdumped = isZeroPadded || isFFPadded || isRepeatedPattern;
return {
isOverdumped,
originalSize: isOverdumped ? expectedSize : data.length
};
}
/**
* Remove overdump padding from ROM data
*/
static removeOverdump(data, originalSize) {
return data.slice(0, originalSize);
}
/**
* Enhanced ROM parsing with support for special formats
*/
static parseAdvanced(filePath) {
// Check for split ROM files
const splitParts = this.detectSplitRom(filePath);
const isSplitRom = splitParts.length > 1;
// Load ROM data (combine if split)
let data = isSplitRom ? this.combineSplitRom(splitParts) : fs.readFileSync(filePath);
// Check for interleaved format
const isInterleaved = this.detectInterleavedFormat(data);
if (isInterleaved) {
data = this.deInterleaveRom(data);
}
// Parse basic ROM structure
const basicRom = this.parse(filePath);
// Check for overdump
const expectedSize = this.getROMSize(basicRom.header.romSize);
const overdumpInfo = this.detectOverdump(data, expectedSize);
if (overdumpInfo.isOverdumped) {
data = this.removeOverdump(data, overdumpInfo.originalSize);
}
return {
...basicRom,
data,
isInterleaved,
isSplitRom,
isOverdumped: overdumpInfo.isOverdumped,
originalSize: overdumpInfo.originalSize
};
}
}
exports.RomParser = RomParser;
//# sourceMappingURL=rom-parser.js.map