UNPKG

snes-disassembler

Version:

A Super Nintendo (SNES) ROM disassembler for 65816 assembly

530 lines (528 loc) 23.7 kB
"use strict"; /** * Automated Documentation Generator * * Analyzes SNES ROM data and generates comprehensive MD files with: * - AI-powered assembly code analysis * - Pattern recognition insights * - SNES-specific hardware documentation * - Contextual explanations for beginners and experts * - Cross-references to related assets */ 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.MarkdownExporter = exports.AIDocumentationGenerator = void 0; /** * AI-Enhanced Documentation Generator for SNES ROMs */ class AIDocumentationGenerator { constructor(configManager, patternRecognizer, nameSuggester) { this.configManager = configManager; this.patternRecognizer = patternRecognizer; this.nameSuggester = nameSuggester; this.snesKnowledge = new SNESKnowledgeBase(); } /** * Generate comprehensive documentation for SNES assets */ async generateDocumentation(data, offset, length, assemblyCode) { const config = this.configManager.getConfig(); if (!config.documentationGeneration.enabled) { return this.generateBasicDocumentation(data, offset, length); } // Analyze the data region const analysisResult = await this.patternRecognizer.classifyDataRegion(data, offset, length); // Generate contextual asset information const assetContext = { offset, size: length, bank: offset >> 16, classification: { graphics: analysisResult.graphics, audio: analysisResult.audio, text: analysisResult.text } }; const suggestedNames = await this.nameSuggester.suggestNames(assetContext); // Generate assembly comments if code is provided let codeComments = []; if (assemblyCode && config.documentationGeneration.generateComments) { codeComments = await this.generateAssemblyComments(assemblyCode, data, offset); } // Build comprehensive documentation return this.buildDetailedDocumentation(analysisResult, suggestedNames, codeComments, config, assetContext); } /** * Generate AI-powered assembly code comments */ async generateAssemblyComments(assemblyCode, data, baseOffset) { const comments = []; const lines = assemblyCode.split('\n'); for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); if (!line || line.startsWith(';')) continue; // Skip empty lines and comments const comment = await this.analyzeAssemblyLine(line, data, baseOffset + i * 2); if (comment) { comments.push(comment); } } return comments; } /** * Analyze a single assembly line and generate intelligent comments */ async analyzeAssemblyLine(line, data, offset) { const instruction = this.parseInstruction(line); if (!instruction) return null; const comment = this.generateInstructionComment(instruction, offset); const snesContext = this.getSNESContext(instruction); return { offset, data: line, comment, confidence: 0.8, // Base confidence for pattern-based analysis snesContext }; } parseInstruction(line) { const match = line.match(/^\s*([A-Z]{3})\s*(.*)$/i); if (!match) return null; return { mnemonic: match[1].toUpperCase(), operand: match[2]?.trim() }; } generateInstructionComment(instruction, offset) { const { mnemonic, operand } = instruction; // AI-informed instruction analysis based on SNES patterns switch (mnemonic) { case 'LDA': return `Load value ${operand || ''} into accumulator A`; case 'STA': if (operand?.includes('$21')) { return `Store accumulator to PPU register ${operand} (graphics/display control)`; } return `Store accumulator value to ${operand || 'memory'}`; case 'JSR': return `Call subroutine at ${operand || 'address'}`; case 'RTS': return 'Return from subroutine'; case 'LDX': return `Load value ${operand || ''} into X register (often used for indexing)`; case 'LDY': return `Load value ${operand || ''} into Y register (often used for indexing)`; case 'CMP': return `Compare accumulator with ${operand || 'value'} (sets flags for branching)`; case 'BEQ': return `Branch to ${operand || 'address'} if equal (Z flag set)`; case 'BNE': return `Branch to ${operand || 'address'} if not equal (Z flag clear)`; case 'JMP': return `Jump unconditionally to ${operand || 'address'}`; default: return `${mnemonic} instruction with operand ${operand || 'none'}`; } } getSNESContext(instruction) { const { mnemonic, operand } = instruction; // Provide SNES-specific context if (operand?.includes('$21')) { return 'PPU (Picture Processing Unit) register access - controls graphics rendering'; } if (operand?.includes('$40')) { return 'CPU register access - processor control and DMA'; } if (operand?.includes('$7E')) { return 'Work RAM access - temporary data storage'; } if (mnemonic === 'JSR' || mnemonic === 'JMP') { return 'Control flow - program execution jumps or calls'; } return ''; } buildDetailedDocumentation(analysis, names, codeComments, config, context) { const { graphics, audio, text, compression, mostLikely, confidence } = analysis; // Build comprehensive title let title = 'SNES Asset Analysis'; if (mostLikely !== 'unknown') { title = `${mostLikely.charAt(0).toUpperCase() + mostLikely.slice(1)} Asset Analysis`; } // Generate summary const summary = this.generateSummary(analysis, context, config); // Generate detailed description const details = this.generateComprehensiveDetails(analysis, names, codeComments, config, context); // Extract hardware context const hardwareContext = this.extractHardwareContext(analysis, context); // Find related assets (simplified for now) const relatedAssets = this.findRelatedAssets(context); return { title, summary, details, confidence, codeComments: codeComments.length > 0 ? codeComments : undefined, hardwareContext, relatedAssets: relatedAssets.length > 0 ? relatedAssets : undefined, recommendations: this.generateRecommendations(analysis, config) }; } generateSummary(analysis, context, config) { const { mostLikely, confidence } = analysis; const bankHex = context.bank.toString(16).toUpperCase().padStart(2, '0'); const offsetHex = context.offset.toString(16).toUpperCase().padStart(6, '0'); let summary = `Asset located at bank $${bankHex}, offset $${offsetHex} (${context.size} bytes).\n`; summary += `AI analysis indicates this is most likely **${mostLikely}** data `; summary += `with ${(confidence * 100).toFixed(1)}% confidence.\n\n`; if (config.documentationGeneration.style === 'beginner') { summary += 'This analysis uses AI-powered pattern recognition to identify the type and purpose of data in the SNES ROM.'; } else if (config.documentationGeneration.style === 'technical') { summary += 'Classification based on entropy analysis, pattern recognition, and SNES-specific heuristics.'; } return summary; } generateComprehensiveDetails(analysis, names, codeComments, config, context) { let details = ''; // Asset Classification Section details += '## Asset Classification\n\n'; if (analysis.graphics) { details += this.generateGraphicsDetails(analysis.graphics, config); } if (analysis.audio) { details += this.generateAudioDetails(analysis.audio, config); } if (analysis.text) { details += this.generateTextDetails(analysis.text, config); } if (analysis.compression && analysis.compression.type !== 'none') { details += this.generateCompressionDetails(analysis.compression, config); } // Naming Suggestions Section if (names.length > 0) { details += '\n## Suggested Names\n\n'; for (const name of names.slice(0, 5)) { // Top 5 suggestions details += `- **${name.name}** (${(name.confidence * 100).toFixed(1)}% confidence)\n`; details += ` - Reasoning: ${name.reasoning}\n`; if (name.alternatives && name.alternatives.length > 0) { details += ` - Alternatives: ${name.alternatives.slice(0, 3).join(', ')}\n`; } } } // Assembly Comments Section if (codeComments.length > 0) { details += '\n## Assembly Code Analysis\n\n'; details += '```assembly\n'; for (const comment of codeComments.slice(0, 10)) { // First 10 comments details += `${comment.data.padEnd(20)} ; ${comment.comment}\n`; if (comment.snesContext) { details += `${' '.repeat(20)} ; SNES: ${comment.snesContext}\n`; } } details += '```\n'; } // Technical Details Section details += '\n## Technical Details\n\n'; details += `- **Memory Location**: Bank $${context.bank.toString(16).toUpperCase().padStart(2, '0')}, Offset $${context.offset.toString(16).toUpperCase().padStart(6, '0')}\n`; details += `- **Size**: ${context.size} bytes (0x${context.size.toString(16).toUpperCase()})\n`; details += `- **ROM Address**: $${context.offset.toString(16).toUpperCase().padStart(6, '0')}\n`; // Add confidence information if requested if (config.documentationGeneration.includeConfidence) { details += '\n## AI Analysis Confidence\n\n'; details += 'This analysis uses machine learning models trained on SNES patterns:\n\n'; if (analysis.graphics) { details += `- Graphics classification: ${(analysis.graphics.confidence * 100).toFixed(1)}%\n`; } if (analysis.audio) { details += `- Audio classification: ${(analysis.audio.confidence * 100).toFixed(1)}%\n`; } if (analysis.text) { details += `- Text classification: ${(analysis.text.confidence * 100).toFixed(1)}%\n`; } } return details; } generateGraphicsDetails(graphics, config) { let details = '### Graphics Data\n\n'; details += `**Type**: ${graphics.type}\n`; details += `**Confidence**: ${(graphics.confidence * 100).toFixed(1)}%\n`; if (graphics.format) { details += `**Format**: ${graphics.format} (${graphics.format === '2bpp' ? '4 colors' : graphics.format === '4bpp' ? '16 colors' : '256 colors'})\n`; } if (graphics.dimensions) { details += `**Dimensions**: ${graphics.dimensions.width}x${graphics.dimensions.height} pixels\n`; } // Add SNES-specific context details += '\n**SNES Context**: '; switch (graphics.type) { case 'sprite': details += 'Sprite graphics for moving objects (characters, enemies, items). Stored in VRAM and controlled by OAM.\n'; break; case 'background': details += 'Background layer graphics. SNES supports up to 4 background layers with various modes.\n'; break; case 'tile': details += 'Individual tile graphics. SNES uses 8x8 pixel tiles as building blocks for backgrounds and sprites.\n'; break; case 'font': details += 'Character font data for text display. Often stored as tiles and referenced by text engines.\n'; break; case 'palette': details += 'Color palette data. SNES supports 256 colors total, organized in sub-palettes.\n'; break; } return details + '\n'; } generateAudioDetails(audio, config) { let details = '### Audio Data\n\n'; details += `**Type**: ${audio.type}\n`; details += `**Confidence**: ${(audio.confidence * 100).toFixed(1)}%\n`; if (audio.encoding) { details += `**Encoding**: ${audio.encoding}\n`; } if (audio.sampleRate) { details += `**Sample Rate**: ${audio.sampleRate} Hz\n`; } if (audio.channels) { details += `**Channels**: ${audio.channels}\n`; } // Add SNES-specific context details += '\n**SNES Context**: '; switch (audio.type) { case 'brr_sample': details += 'BRR (Bit Rate Reduction) compressed audio sample. SNES SPC700 audio processor native format.\n'; break; case 'sequence': details += 'Music sequence data. Contains note patterns, timing, and instrument references.\n'; break; case 'spc_code': details += 'SPC700 processor code. Audio driver or sound effect generation routines.\n'; break; case 'instrument': details += 'Instrument definition data. Describes how samples are played (ADSR, tuning, etc.).\n'; break; } return details + '\n'; } generateTextDetails(text, config) { let details = '### Text Data\n\n'; details += `**Type**: ${text.type}\n`; details += `**Confidence**: ${(text.confidence * 100).toFixed(1)}%\n`; details += `**Encoding**: ${text.encoding}\n`; if (text.compression && text.compression !== 'none') { details += `**Compression**: ${text.compression}\n`; } // Add SNES-specific context details += '\n**SNES Context**: '; switch (text.type) { case 'dialogue': details += 'Game dialogue text. Often compressed to save ROM space. May include control codes for formatting.\n'; break; case 'menu': details += 'Menu text and interface strings. Usually uncompressed for quick access.\n'; break; case 'item_name': details += 'Item or object names. Typically stored in tables for easy lookup.\n'; break; case 'credits': details += 'Credits text, usually displayed at game completion. Often in ASCII format.\n'; break; } if (text.compression && text.compression !== 'none') { details += '\n**Compression Details**: '; switch (text.compression) { case 'dte': details += 'Dual Tile Encoding - common character pairs replaced with single bytes to save space.\n'; break; case 'dictionary': details += 'Dictionary compression - frequently used words/phrases stored in lookup table.\n'; break; } } return details + '\n'; } generateCompressionDetails(compression, config) { let details = '### Compression Analysis\n\n'; details += `**Type**: ${compression.type}\n`; details += `**Confidence**: ${(compression.confidence * 100).toFixed(1)}%\n`; if (compression.decompressHint) { details += `**Hint**: ${compression.decompressHint}\n`; } return details + '\n'; } extractHardwareContext(analysis, context) { const hardware = {}; // Determine mapping mode based on bank if (context.bank <= 0x3F || (context.bank >= 0x80 && context.bank <= 0xBF)) { hardware.mappingMode = 'LoROM'; } else if (context.bank >= 0x40 && context.bank <= 0x7F) { hardware.mappingMode = 'HiROM'; } // Add relevant registers based on data type if (analysis.graphics) { hardware.registers = ['$2100-$213F (PPU registers)', '$2140-$217F (APU communication)']; hardware.ppuFeatures = this.getPPUFeatures(analysis.graphics.type); } if (analysis.audio) { hardware.registers = ['$2140-$2143 (SPC700 communication)', '$2180-$2183 (WRAM access)']; } // Bank usage explanation hardware.bankUsage = this.getBankUsageExplanation(context.bank); return Object.keys(hardware).length > 0 ? hardware : undefined; } getPPUFeatures(graphicsType) { switch (graphicsType) { case 'sprite': return ['OAM (Object Attribute Memory)', 'Sprite priorities', 'VRAM tile data']; case 'background': return ['Background layers', 'Tilemap data', 'Scroll registers']; case 'palette': return ['CGRAM (Color Generator RAM)', 'Color math']; default: return ['VRAM access', 'DMA transfers']; } } getBankUsageExplanation(bank) { if (bank <= 0x3F) { return 'LoROM banks $00-$3F: Contains ROM data, mirrors to $80-$BF'; } else if (bank >= 0x40 && bank <= 0x7F) { return 'HiROM banks $40-$7F: High ROM area, direct mapping'; } else if (bank >= 0x80 && bank <= 0xBF) { return 'LoROM mirror banks $80-$BF: Fast ROM access area'; } else if (bank >= 0xC0) { return 'High banks $C0+: Extended ROM or special mapping'; } return 'Unknown bank usage pattern'; } findRelatedAssets(context) { // Simplified related asset detection const related = []; // Look for nearby assets (same bank, nearby offsets) const nearbyOffset1 = context.offset + context.size; const nearbyOffset2 = context.offset - 0x1000; if (nearbyOffset1 < 0x10000) { related.push(`Potential next asset at $${nearbyOffset1.toString(16).toUpperCase().padStart(6, '0')}`); } if (nearbyOffset2 > 0) { related.push(`Potential previous asset at $${nearbyOffset2.toString(16).toUpperCase().padStart(6, '0')}`); } return related; } generateRecommendations(analysis, config) { const recommendations = []; if (analysis.graphics) { recommendations.push('Use a graphics viewer to visualize the tile data'); recommendations.push('Check for associated palette data nearby'); } if (analysis.audio) { recommendations.push('Use an SPC player to preview audio data'); recommendations.push('Look for sequence data or driver code in adjacent banks'); } if (analysis.text) { recommendations.push('Apply text extraction with appropriate encoding'); if (analysis.text.compression !== 'none') { recommendations.push('Implement decompression algorithm before text extraction'); } } if (analysis.confidence < 0.7) { recommendations.push('Low confidence - verify analysis with manual inspection'); recommendations.push('Consider adjusting AI model parameters or using heuristic analysis'); } return recommendations; } generateBasicDocumentation(data, offset, length) { const bankHex = (offset >> 16).toString(16).toUpperCase().padStart(2, '0'); const offsetHex = offset.toString(16).toUpperCase().padStart(6, '0'); return { title: 'Basic Asset Documentation', summary: `Data region at bank $${bankHex}, offset $${offsetHex} (${length} bytes). AI analysis disabled.`, details: `## Basic Information\n\n- **Location**: Bank $${bankHex}, Offset $${offsetHex}\n- **Size**: ${length} bytes\n\nTo enable AI-powered analysis, configure AI features in ai-config.json.`, confidence: 0.1 }; } } exports.AIDocumentationGenerator = AIDocumentationGenerator; /** * SNES Knowledge Base for contextual information */ class SNESKnowledgeBase { getRegisterInfo(address) { // Returns information about SNES registers if (address >= 0x2100 && address <= 0x213F) { return 'PPU (Picture Processing Unit) registers - graphics and display control'; } if (address >= 0x4000 && address <= 0x43FF) { return 'CPU registers - processor control, DMA, and I/O'; } return 'Unknown register range'; } getMemoryRegionInfo(bank) { if (bank <= 0x3F) { return 'LoROM area - contains cartridge ROM and system areas'; } if (bank >= 0x7E && bank <= 0x7F) { return 'Work RAM (WRAM) - temporary data storage'; } if (bank >= 0x80 && bank <= 0xBF) { return 'Fast ROM area - mirror of banks $00-$3F with faster access'; } return 'Extended or special memory area'; } } /** * Markdown file output for generated documentation */ class MarkdownExporter { /** Export documentation to a markdown file */ static async exportToMarkdown(doc, outputPath) { const mdContent = `# ${doc.title} ## Summary ${doc.summary} ## Details ${doc.details} `; const fs = await Promise.resolve().then(() => __importStar(require('fs/promises'))); await fs.writeFile(outputPath, mdContent, 'utf8'); console.log(`✅ Documentation generated at ${outputPath}`); } } exports.MarkdownExporter = MarkdownExporter; //# sourceMappingURL=ai-documentation.js.map