snes-disassembler
Version:
A Super Nintendo (SNES) ROM disassembler for 65816 assembly
419 lines • 15.6 kB
JavaScript
"use strict";
/**
* Phase 4: Output & Integration - Multiple Output Formats
*
* Implements various assembler formats and output types for SNES disassembly:
* - ca65-compatible assembly source files
* - WLA-DX assembler format support
* - bass assembler output format
* - HTML output with hyperlinked cross-references
* - JSON/XML export for external tools
* - Symbol table management with external file support
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.BassFormatter = exports.WLADXFormatter = exports.CA65Formatter = exports.OutputFormatter = void 0;
class OutputFormatter {
constructor(rom, symbols = new Map(), crossRefs = [], options = {}) {
this.rom = rom;
this.symbols = symbols;
this.crossRefs = crossRefs;
this.options = {
includeHeader: true,
includeComments: true,
includeBytes: false,
includeTiming: false,
includeSymbols: true,
includeCrossReferences: false,
lineNumbers: false,
uppercase: true,
tabWidth: 4,
...options
};
}
formatAddress(address) {
const hex = address.toString(16).toUpperCase();
return this.options.uppercase ? `$${hex.padStart(6, '0')}` : `$${hex.padStart(6, '0').toLowerCase()}`;
}
formatBytes(bytes) {
return bytes.map(b => {
const hex = b.toString(16);
return this.options.uppercase ? hex.toUpperCase().padStart(2, '0') : hex.padStart(2, '0');
}).join(' ');
}
getSymbolName(address) {
return this.symbols.get(address)?.name;
}
generateHeader() {
const lines = [];
if (this.options.includeHeader) {
lines.push('; SNES ROM Disassembly');
lines.push(`; Generated by SNES Disassembler - ${this.getName()} Format`);
lines.push(`; Title: ${this.rom.header.title}`);
lines.push(`; Map Mode: ${this.rom.cartridgeInfo.type}`);
lines.push(`; ROM Size: ${(this.rom.cartridgeInfo.romSize / 1024).toFixed(0)} KB`);
lines.push(`; Reset Vector: ${this.formatAddress(this.rom.header.nativeVectors.reset)}`);
if (this.rom.cartridgeInfo.specialChip) {
lines.push(`; Special Chip: ${this.rom.cartridgeInfo.specialChip}`);
}
lines.push(`; Generated: ${new Date().toISOString()}`);
lines.push('');
}
return lines;
}
formatOperand(line) {
if (line.operand === undefined) {
return '';
}
// Check for symbol first
const symbolName = this.getSymbolName(line.operand);
if (symbolName) {
return symbolName;
}
// Format based on addressing mode
const operand = line.operand;
switch (line.instruction.addressingMode) {
case '#': // Immediate
if (operand <= 0xFF) {
return `#${this.formatAddress(operand).slice(1, 3)}`;
}
else {
return `#${this.formatAddress(operand).slice(1, 5)}`;
}
case 'abs': // Absolute
case 'long': // Absolute Long
return this.formatAddress(operand);
case 'zp': // Zero Page
case 'dp': // Direct Page
return this.formatAddress(operand).slice(-2);
case 'rel': // Relative
case 'rell': // Relative Long
return this.formatAddress(operand);
default:
return this.formatAddress(operand);
}
}
addSymbol(address, entry) {
this.symbols.set(address, entry);
}
addCrossReference(ref) {
this.crossRefs.push(ref);
}
getSymbols() {
return new Map(this.symbols);
}
getCrossReferences() {
return [...this.crossRefs];
}
}
exports.OutputFormatter = OutputFormatter;
class CA65Formatter extends OutputFormatter {
getName() {
return 'CA65';
}
getFileExtension() {
return '.s';
}
format(lines) {
const output = [];
// Add assembler-specific directives
output.push(...this.generateHeader());
output.push('; CA65 Assembler Configuration');
output.push('.p816 ; Enable 65816 mode');
output.push('.smart ; Enable smart mode');
output.push('');
// Add memory mapping directives based on cartridge type
this.addMemoryMapping(output);
// Add symbols table
if (this.options.includeSymbols && this.symbols.size > 0) {
output.push('; Symbol Definitions');
for (const [address, symbol] of this.symbols) {
if (symbol.scope === 'GLOBAL' || symbol.scope === 'EXPORT') {
output.push(`${symbol.name} = ${this.formatAddress(address)}`);
}
}
output.push('');
}
// Format disassembly lines
for (const line of lines) {
const formattedLine = this.formatCA65Line(line);
if (formattedLine) {
output.push(formattedLine);
}
}
return output.join('\n');
}
addMemoryMapping(output) {
output.push('; Memory Mapping Configuration');
switch (this.rom.cartridgeInfo.type) {
case 'LoROM':
output.push('.segment "CODE" ; LoROM Code Segment');
output.push('.org $8000 ; LoROM Bank 0 Start');
break;
case 'HiROM':
output.push('.segment "CODE" ; HiROM Code Segment');
output.push('.org $C000 ; HiROM Bank 0 Start');
break;
case 'ExLoROM':
output.push('.segment "CODE" ; ExLoROM Code Segment');
output.push('.org $8000 ; ExLoROM Bank 0 Start');
break;
case 'ExHiROM':
output.push('.segment "CODE" ; ExHiROM Code Segment');
output.push('.org $C000 ; ExHiROM Bank 0 Start');
break;
}
output.push('');
}
formatCA65Line(line) {
let output = '';
// Add label if present
if (line.label) {
output += `${line.label}:\n`;
}
// Indent instruction
output += ' ';
// Add line number if requested
if (this.options.lineNumbers) {
output += `${line.address.toString(16).toUpperCase().padStart(6, '0')}: `;
}
// Add bytes if requested
if (this.options.includeBytes) {
output += `; ${this.formatBytes(line.bytes).padEnd(12)} `;
}
// Format instruction
const mnemonic = this.options.uppercase ?
line.instruction.mnemonic.toUpperCase() :
line.instruction.mnemonic.toLowerCase();
output += mnemonic.padEnd(4);
// Format operand
const operandStr = this.formatOperand(line);
if (operandStr) {
output += ` ${operandStr}`;
}
// Add comment
if (this.options.includeComments && line.comment) {
output = output.padEnd(40) + ` ; ${line.comment}`;
}
// Add timing information if requested
if (this.options.includeTiming) {
const cycles = typeof line.instruction.cycles === 'number' ?
line.instruction.cycles : line.instruction.cycles.base;
output = output.padEnd(60) + ` ; ${cycles} cycles`;
}
return output;
}
}
exports.CA65Formatter = CA65Formatter;
class WLADXFormatter extends OutputFormatter {
getName() {
return 'WLA-DX';
}
getFileExtension() {
return '.asm';
}
format(lines) {
const output = [];
// Add WLA-DX specific directives
output.push(...this.generateHeader());
output.push('; WLA-DX Assembler Configuration');
output.push('.MEMORYMAP');
this.addWLAMemoryMapping(output);
output.push('.ENDME');
output.push('');
output.push('.ROMBANKSIZE $8000 ; 32KB ROM banks');
output.push('.ROMBANKS 32 ; Number of ROM banks');
output.push('');
// Add ROM header
output.push('.SNESHEADER');
output.push(`NAME "${this.rom.header.title}"`);
output.push('LOROM ; LoROM mapping');
output.push('SLOWROM ; SlowROM timing');
output.push('CARTRIDGETYPE $00 ; ROM only');
output.push('ROMSIZE $08 ; 2 Mbit');
output.push('SRAMSIZE $00 ; No SRAM');
output.push('COUNTRY $01 ; NTSC');
output.push('LICENSEECODE $00');
output.push('VERSION $00');
output.push('.ENDSNES');
output.push('');
// Format disassembly lines
output.push('.BANK 0 SLOT 0');
output.push('.ORG $0000');
output.push('');
for (const line of lines) {
const formattedLine = this.formatWLALine(line);
if (formattedLine) {
output.push(formattedLine);
}
}
return output.join('\n');
}
addWLAMemoryMapping(output) {
switch (this.rom.cartridgeInfo.type) {
case 'LoROM':
output.push('SLOTSIZE $8000 ; 32KB slots');
output.push('DEFAULTSLOT 0');
output.push('SLOT 0 $8000 ; Bank 0 at $8000-$FFFF');
break;
case 'HiROM':
output.push('SLOTSIZE $10000 ; 64KB slots');
output.push('DEFAULTSLOT 0');
output.push('SLOT 0 $0000 ; Bank 0 at $0000-$FFFF');
break;
}
}
formatWLALine(line) {
let output = '';
// Add label if present
if (line.label) {
output += `${line.label}:\n`;
}
// Format instruction with WLA-DX syntax
const mnemonic = this.options.uppercase ?
line.instruction.mnemonic.toUpperCase() :
line.instruction.mnemonic.toLowerCase();
output += ` ${mnemonic}`;
// Format operand with WLA-DX specific syntax
const operandStr = this.formatWLAOperand(line);
if (operandStr) {
output += ` ${operandStr}`;
}
// Add comment
if (this.options.includeComments && line.comment) {
output = output.padEnd(40) + ` ; ${line.comment}`;
}
return output;
}
formatWLAOperand(line) {
if (line.operand === undefined) {
return '';
}
// WLA-DX uses $ for hex values
const operand = line.operand;
const symbolName = this.getSymbolName(operand);
if (symbolName) {
return symbolName;
}
// WLA-DX specific formatting
switch (line.instruction.addressingMode) {
case '#': // Immediate
return `#$${operand.toString(16).toUpperCase().padStart(2, '0')}`;
case 'abs': // Absolute
return `$${operand.toString(16).toUpperCase().padStart(4, '0')}`;
case 'long': // Absolute Long
return `$${operand.toString(16).toUpperCase().padStart(6, '0')}`;
case 'zp': // Zero Page
case 'dp': // Direct Page
return `$${operand.toString(16).toUpperCase().padStart(2, '0')}`;
default:
return `$${operand.toString(16).toUpperCase()}`;
}
}
}
exports.WLADXFormatter = WLADXFormatter;
class BassFormatter extends OutputFormatter {
getName() {
return 'bass';
}
getFileExtension() {
return '.asm';
}
format(lines) {
const output = [];
// Add bass specific directives
output.push(...this.generateHeader());
output.push('; bass Assembler Configuration');
output.push('arch 65816 ; Set 65816 architecture');
// Add memory mapping
switch (this.rom.cartridgeInfo.type) {
case 'LoROM':
output.push('base $8000 ; LoROM base address');
break;
case 'HiROM':
output.push('base $C000 ; HiROM base address');
break;
}
output.push('');
// Add symbols
if (this.options.includeSymbols && this.symbols.size > 0) {
output.push('; Symbol Definitions');
for (const [address, symbol] of this.symbols) {
if (symbol.scope === 'GLOBAL' || symbol.scope === 'EXPORT') {
output.push(`define ${symbol.name} $${address.toString(16).toUpperCase()}`);
}
}
output.push('');
}
// Format disassembly lines
for (const line of lines) {
const formattedLine = this.formatBassLine(line);
if (formattedLine) {
output.push(formattedLine);
}
}
return output.join('\n');
}
formatBassLine(line) {
let output = '';
// Add label if present
if (line.label) {
output += `${line.label}:\n`;
}
// Format instruction
const mnemonic = this.options.uppercase ?
line.instruction.mnemonic.toUpperCase() :
line.instruction.mnemonic.toLowerCase();
output += ` ${mnemonic}`;
// Format operand with bass syntax
const operandStr = this.formatBassOperand(line);
if (operandStr) {
output += ` ${operandStr}`;
}
// Add comment
if (this.options.includeComments && line.comment) {
output = output.padEnd(40) + ` // ${line.comment}`;
}
return output;
}
formatBassOperand(line) {
if (line.operand === undefined) {
return '';
}
const operand = line.operand;
const symbolName = this.getSymbolName(operand);
if (symbolName) {
return `{${symbolName}}`; // bass uses {} for symbols
}
// bass specific formatting
switch (line.instruction.addressingMode) {
case '#': // Immediate
return `#$${operand.toString(16).toUpperCase().padStart(2, '0')}`;
case 'abs': // Absolute
return `$${operand.toString(16).toUpperCase().padStart(4, '0')}`;
case 'long': // Absolute Long
return `$${operand.toString(16).toUpperCase().padStart(6, '0')}`;
default:
return `$${operand.toString(16).toUpperCase()}`;
}
}
}
exports.BassFormatter = BassFormatter;
class OutputFormatterFactory {
static create(format, rom, symbols, crossRefs, options) {
switch (format.toLowerCase()) {
case 'ca65':
return new CA65Formatter(rom, symbols, crossRefs, options);
case 'wla-dx':
case 'wladx':
return new WLADXFormatter(rom, symbols, crossRefs, options);
case 'bass':
return new BassFormatter(rom, symbols, crossRefs, options);
default:
throw new Error(`Unsupported output format: ${format}`);
}
}
static getSupportedFormats() {
return ['ca65', 'wla-dx', 'bass'];
}
}
//# sourceMappingURL=output-formatters.js.map