UNPKG

snes-disassembler

Version:

A Super Nintendo (SNES) ROM disassembler for 65816 assembly

419 lines 15.6 kB
"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