commit 810f1458cbdb809d835a6f47f2c585f77d4aea8f Author: ApfelTeeSaft <91074565+ApfelTeeSaft@users.noreply.github.com> Date: Thu May 15 14:33:41 2025 +0200 Initial commit diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8a30d25 --- /dev/null +++ b/.gitignore @@ -0,0 +1,398 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b685437 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 ApfelTeeSaft + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/NESDecompiler.CLI/NESDecompiler.CLI.csproj b/NESDecompiler.CLI/NESDecompiler.CLI.csproj new file mode 100644 index 0000000..1cec136 --- /dev/null +++ b/NESDecompiler.CLI/NESDecompiler.CLI.csproj @@ -0,0 +1,15 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + diff --git a/NESDecompiler.CLI/Program.cs b/NESDecompiler.CLI/Program.cs new file mode 100644 index 0000000..9f2c13c --- /dev/null +++ b/NESDecompiler.CLI/Program.cs @@ -0,0 +1,214 @@ +using System; +using System.IO; +using CommandLine; +using NESDecompiler.Core.ROM; +using NESDecompiler.Core.Disassembly; +using NESDecompiler.Core.Decompilation; +using NESDecompiler.Core.Exceptions; +using System.Runtime.InteropServices; + +namespace NESDecompiler.CLI +{ + /// + /// Command-line options for the decompiler + /// + class Options + { + [Option('i', "input", Required = true, HelpText = "Input NES ROM file path")] + public string InputFile { get; set; } = string.Empty; + + [Option('o', "output", Required = false, HelpText = "Output directory for generated files")] + public string? OutputDirectory { get; set; } + + [Option('d', "disassemble", Required = false, Default = false, HelpText = "Generate disassembly output")] + public bool GenerateDisassembly { get; set; } + + [Option('c', "decompile", Required = false, Default = true, HelpText = "Generate C code output")] + public bool GenerateCCode { get; set; } + + [Option('v', "verbose", Required = false, Default = false, HelpText = "Set output to verbose messages")] + public bool Verbose { get; set; } + } + + /// + /// Main program class for the CLI interface + /// + class Program + { + /// + /// Entry point for the CLI application + /// + static int Main(string[] args) + { + return Parser.Default.ParseArguments(args) + .MapResult( + options => RunDecompiler(options), + errors => 1 + ); + } + + /// + /// Runs the decompiler with the specified options + /// + /// Command-line options + /// 0 for success, non-zero for failure + static int RunDecompiler(Options options) + { + try + { + Console.WriteLine($"NES ROM Decompiler"); + Console.WriteLine($"================="); + Console.WriteLine(); + + if (!File.Exists(options.InputFile)) + { + Console.Error.WriteLine($"Error: Input file '{options.InputFile}' does not exist"); + return 1; + } + + string outputDirectory = options.OutputDirectory ?? Path.GetDirectoryName(options.InputFile) ?? "."; + if (!Directory.Exists(outputDirectory)) + { + Directory.CreateDirectory(outputDirectory); + } + + Console.WriteLine($"Loading ROM: {options.InputFile}"); + var romLoader = new ROMLoader(); + var romInfo = romLoader.LoadFromFile(options.InputFile); + + if (options.Verbose) + { + Console.WriteLine(romInfo.ToString()); + } + + byte[] prgRomData = romLoader.GetPRGROMData(); + + Console.WriteLine("Disassembling code..."); + var disassembler = new Disassembler(romInfo, prgRomData); + disassembler.Disassemble(); + + if (options.Verbose) + { + Console.WriteLine($"Disassembled {disassembler.Instructions.Count} instructions"); + } + + if (options.GenerateDisassembly) + { + string disassemblyFile = Path.Combine(outputDirectory, Path.GetFileNameWithoutExtension(options.InputFile) + ".asm"); + Console.WriteLine($"Generating disassembly: {disassemblyFile}"); + + string disassembly = disassembler.ToAssemblyString(); + File.WriteAllText(disassemblyFile, disassembly); + } + + if (options.GenerateCCode) + { + Console.WriteLine("Decompiling to C code..."); + var decompiler = new Decompiler(romInfo, disassembler); + decompiler.Decompile(); + + if (options.Verbose) + { + Console.WriteLine($"Identified {decompiler.Variables.Count} variables and {decompiler.Functions.Count} functions"); + } + + string cCodeFile = Path.Combine(outputDirectory, Path.GetFileNameWithoutExtension(options.InputFile) + ".c"); + Console.WriteLine($"Generating C code: {cCodeFile}"); + + string cCode = decompiler.GenerateCCode(); + File.WriteAllText(cCodeFile, cCode); + + string headerFile = Path.Combine(outputDirectory, Path.GetFileNameWithoutExtension(options.InputFile) + ".h"); + Console.WriteLine($"Generating header file: {headerFile}"); + + string headerCode = GenerateHeaderFile(decompiler); + File.WriteAllText(headerFile, headerCode); + } + + Console.WriteLine("Decompilation completed successfully"); + return 0; + } + catch (Exception ex) + { + Exception currentEx = ex; + Console.Error.WriteLine($"Error: {currentEx.Message}"); + + if (options.Verbose) + { + while (currentEx.InnerException != null) + { + currentEx = currentEx.InnerException; + Console.Error.WriteLine($" Caused by: {currentEx.Message}"); + } + + Console.Error.WriteLine(); + Console.Error.WriteLine("Stack trace:"); + Console.Error.WriteLine(ex.StackTrace); + } + + return 1; + } + } + + /// + /// Generates a C header file for the decompiled ROM + /// + /// The decompiler instance + /// The generated header code + static string GenerateHeaderFile(Decompiler decompiler) + { + StringWriter writer = new StringWriter(); + + writer.WriteLine("/*"); + writer.WriteLine(" * Decompiled NES ROM"); + writer.WriteLine($" * ROM: {decompiler.ROMInfo}"); + writer.WriteLine(" */"); + writer.WriteLine(); + + string guardName = Path.GetFileNameWithoutExtension(decompiler.ROMInfo.RawData[0].ToString()).ToUpper() + "_H"; + writer.WriteLine($"#ifndef {guardName}"); + writer.WriteLine($"#define {guardName}"); + writer.WriteLine(); + + writer.WriteLine("#include "); + writer.WriteLine("#include "); + writer.WriteLine(); + + writer.WriteLine("// NES Hardware Registers"); + writer.WriteLine("#define PPUCTRL (*((volatile uint8_t*)0x2000))"); + writer.WriteLine("#define PPUMASK (*((volatile uint8_t*)0x2001))"); + writer.WriteLine("#define PPUSTATUS (*((volatile uint8_t*)0x2002))"); + writer.WriteLine("#define OAMADDR (*((volatile uint8_t*)0x2003))"); + writer.WriteLine("#define OAMDATA (*((volatile uint8_t*)0x2004))"); + writer.WriteLine("#define PPUSCROLL (*((volatile uint8_t*)0x2005))"); + writer.WriteLine("#define PPUADDR (*((volatile uint8_t*)0x2006))"); + writer.WriteLine("#define PPUDATA (*((volatile uint8_t*)0x2007))"); + writer.WriteLine("#define OAMDMA (*((volatile uint8_t*)0x4014))"); + writer.WriteLine("#define SND_CHN (*((volatile uint8_t*)0x4015))"); + writer.WriteLine("#define JOY1 (*((volatile uint8_t*)0x4016))"); + writer.WriteLine("#define JOY2 (*((volatile uint8_t*)0x4017))"); + writer.WriteLine(); + + writer.WriteLine("// Variables"); + foreach (var variable in decompiler.Variables.Values) + { + if (variable.Address < 0x2000 || variable.Address >= 0x8000) + { + writer.WriteLine($"extern {variable.GetCType()} {variable.Name};"); + } + } + writer.WriteLine(); + + writer.WriteLine("// Functions"); + foreach (var function in decompiler.Functions.Values) + { + writer.WriteLine($"void {function.Name}();"); + } + writer.WriteLine(); + + writer.WriteLine($"#endif // {guardName}"); + + return writer.ToString(); + } + } +} \ No newline at end of file diff --git a/NESDecompiler.Core/CPU/InstructionSet.cs b/NESDecompiler.Core/CPU/InstructionSet.cs new file mode 100644 index 0000000..e3f9f12 --- /dev/null +++ b/NESDecompiler.Core/CPU/InstructionSet.cs @@ -0,0 +1,400 @@ +using System; +using System.Collections.Generic; + +namespace NESDecompiler.Core.CPU +{ + /// + /// Addressing modes for 6502 CPU instructions + /// + public enum AddressingMode + { + Implied, // No operand (e.g., RTS) + Accumulator, // Operand is the accumulator (e.g., LSR A) + Immediate, // Operand is 8-bit value (e.g., LDA #$12) + ZeroPage, // Operand is 8-bit address (e.g., LDA $12) + ZeroPageX, // Operand is 8-bit address, X-indexed (e.g., LDA $12,X) + ZeroPageY, // Operand is 8-bit address, Y-indexed (e.g., LDX $12,Y) + Relative, // Operand is 8-bit signed offset (e.g., BNE $12) + Absolute, // Operand is 16-bit address (e.g., LDA $1234) + AbsoluteX, // Operand is 16-bit address, X-indexed (e.g., LDA $1234,X) + AbsoluteY, // Operand is 16-bit address, Y-indexed (e.g., LDA $1234,Y) + Indirect, // Operand is 16-bit address pointing to the address (e.g., JMP ($1234)) + IndexedIndirect, // Operand is 8-bit address, X-indexed, pointing to 16-bit address (e.g., LDA ($12,X)) + IndirectIndexed // Operand is 8-bit address pointing to 16-bit address, Y-indexed (e.g., LDA ($12),Y) + } + + /// + /// Instruction type categories + /// + public enum InstructionType + { + Load, // Load operations (LDA, LDX, LDY) + Store, // Store operations (STA, STX, STY) + Transfer, // Register transfers (TAX, TXA, etc.) + Stack, // Stack operations (PHA, PLA, PHP, PLP, TSX, TXS) + Arithmetic, // Arithmetic operations (ADC, SBC) + Increment, // Increment operations (INC, INX, INY) + Decrement, // Decrement operations (DEC, DEX, DEY) + Shift, // Shift operations (ASL, LSR, ROL, ROR) + Logic, // Logic operations (AND, EOR, ORA) + Compare, // Compare operations (CMP, CPX, CPY) + Branch, // Branch operations (BCC, BCS, BEQ, BMI, BNE, BPL, BVC, BVS) + Jump, // Jump operations (JMP, JSR) + Return, // Return operations (RTS, RTI) + Set, // Set flag operations (SEC, SED, SEI) + Clear, // Clear flag operations (CLC, CLD, CLI, CLV) + Interrupt, // Interrupt operations (BRK) + Other // Other operations (NOP, ???) + } + + /// + /// Information about a 6502 CPU instruction + /// + public class InstructionInfo + { + /// + /// The opcode value (0x00-0xFF) + /// + public byte Opcode { get; set; } + + /// + /// The mnemonic for this instruction (e.g., "LDA", "STA") + /// + public string Mnemonic { get; set; } + + /// + /// The addressing mode for this instruction + /// + public AddressingMode AddressingMode { get; set; } + + /// + /// The size of this instruction in bytes (including operands) + /// + public byte Size { get; set; } + + /// + /// The number of cycles this instruction takes to execute + /// + public byte Cycles { get; set; } + + /// + /// Whether this instruction can take an additional cycle on page boundary crossing + /// + public bool ExtraCycleOnPageCross { get; set; } + + /// + /// The instruction type category + /// + public InstructionType Type { get; set; } + + /// + /// Whether this instruction is a valid 6502 instruction + /// + public bool IsValid { get; set; } = true; + + public InstructionInfo(byte opcode, string mnemonic, AddressingMode addressingMode, + byte size, byte cycles, bool extraCycleOnPageCross, InstructionType type) + { + Opcode = opcode; + Mnemonic = mnemonic; + AddressingMode = addressingMode; + Size = size; + Cycles = cycles; + ExtraCycleOnPageCross = extraCycleOnPageCross; + Type = type; + } + + /// + /// Returns a string representation of the operand format for this addressing mode + /// + public string GetOperandFormat() + { + return AddressingMode switch + { + AddressingMode.Implied => "", + AddressingMode.Accumulator => "A", + AddressingMode.Immediate => "#$%02X", + AddressingMode.ZeroPage => "$%02X", + AddressingMode.ZeroPageX => "$%02X,X", + AddressingMode.ZeroPageY => "$%02X,Y", + AddressingMode.Relative => "$%02X", // Will be processed specially for branches + AddressingMode.Absolute => "$%04X", + AddressingMode.AbsoluteX => "$%04X,X", + AddressingMode.AbsoluteY => "$%04X,Y", + AddressingMode.Indirect => "($%04X)", + AddressingMode.IndexedIndirect => "($%02X,X)", + AddressingMode.IndirectIndexed => "($%02X),Y", + _ => "???" + }; + } + } + + /// + /// Defines the 6502 CPU instruction set + /// + public static class InstructionSet + { + // The full 6502 instruction set + private static readonly Dictionary instructions = new Dictionary(); + + // Maps mnemonic + addressing mode to an instruction info object + private static readonly Dictionary<(string, AddressingMode), InstructionInfo> mnemonicMap = + new Dictionary<(string, AddressingMode), InstructionInfo>(); + + /// + /// Static constructor to initialize the instruction set + /// + static InstructionSet() + { + InitializeInstructionSet(); + } + + /// + /// Gets an instruction by its opcode + /// + /// The opcode value + /// Information about the instruction + public static InstructionInfo GetInstruction(byte opcode) + { + if (instructions.TryGetValue(opcode, out var instruction)) + { + return instruction; + } + + // Return unknown instruction + return new InstructionInfo( + opcode, "???", AddressingMode.Implied, 1, 2, false, InstructionType.Other) + { + IsValid = false + }; + } + + /// + /// Gets an instruction by its mnemonic and addressing mode + /// + /// The instruction mnemonic + /// The addressing mode + /// Information about the instruction, or null if not found + public static InstructionInfo? GetInstruction(string mnemonic, AddressingMode addressingMode) + { + var key = (mnemonic.ToUpper(), addressingMode); + if (mnemonicMap.TryGetValue(key, out var instruction)) + { + return instruction; + } + return null; + } + + /// + /// Initializes the 6502 instruction set + /// + private static void InitializeInstructionSet() + { + // Load/Store Operations + Add(0xA9, "LDA", AddressingMode.Immediate, 2, 2, false, InstructionType.Load); + Add(0xA5, "LDA", AddressingMode.ZeroPage, 2, 3, false, InstructionType.Load); + Add(0xB5, "LDA", AddressingMode.ZeroPageX, 2, 4, false, InstructionType.Load); + Add(0xAD, "LDA", AddressingMode.Absolute, 3, 4, false, InstructionType.Load); + Add(0xBD, "LDA", AddressingMode.AbsoluteX, 3, 4, true, InstructionType.Load); + Add(0xB9, "LDA", AddressingMode.AbsoluteY, 3, 4, true, InstructionType.Load); + Add(0xA1, "LDA", AddressingMode.IndexedIndirect, 2, 6, false, InstructionType.Load); + Add(0xB1, "LDA", AddressingMode.IndirectIndexed, 2, 5, true, InstructionType.Load); + + Add(0xA2, "LDX", AddressingMode.Immediate, 2, 2, false, InstructionType.Load); + Add(0xA6, "LDX", AddressingMode.ZeroPage, 2, 3, false, InstructionType.Load); + Add(0xB6, "LDX", AddressingMode.ZeroPageY, 2, 4, false, InstructionType.Load); + Add(0xAE, "LDX", AddressingMode.Absolute, 3, 4, false, InstructionType.Load); + Add(0xBE, "LDX", AddressingMode.AbsoluteY, 3, 4, true, InstructionType.Load); + + Add(0xA0, "LDY", AddressingMode.Immediate, 2, 2, false, InstructionType.Load); + Add(0xA4, "LDY", AddressingMode.ZeroPage, 2, 3, false, InstructionType.Load); + Add(0xB4, "LDY", AddressingMode.ZeroPageX, 2, 4, false, InstructionType.Load); + Add(0xAC, "LDY", AddressingMode.Absolute, 3, 4, false, InstructionType.Load); + Add(0xBC, "LDY", AddressingMode.AbsoluteX, 3, 4, true, InstructionType.Load); + + Add(0x85, "STA", AddressingMode.ZeroPage, 2, 3, false, InstructionType.Store); + Add(0x95, "STA", AddressingMode.ZeroPageX, 2, 4, false, InstructionType.Store); + Add(0x8D, "STA", AddressingMode.Absolute, 3, 4, false, InstructionType.Store); + Add(0x9D, "STA", AddressingMode.AbsoluteX, 3, 5, false, InstructionType.Store); + Add(0x99, "STA", AddressingMode.AbsoluteY, 3, 5, false, InstructionType.Store); + Add(0x81, "STA", AddressingMode.IndexedIndirect, 2, 6, false, InstructionType.Store); + Add(0x91, "STA", AddressingMode.IndirectIndexed, 2, 6, false, InstructionType.Store); + + Add(0x86, "STX", AddressingMode.ZeroPage, 2, 3, false, InstructionType.Store); + Add(0x96, "STX", AddressingMode.ZeroPageY, 2, 4, false, InstructionType.Store); + Add(0x8E, "STX", AddressingMode.Absolute, 3, 4, false, InstructionType.Store); + + Add(0x84, "STY", AddressingMode.ZeroPage, 2, 3, false, InstructionType.Store); + Add(0x94, "STY", AddressingMode.ZeroPageX, 2, 4, false, InstructionType.Store); + Add(0x8C, "STY", AddressingMode.Absolute, 3, 4, false, InstructionType.Store); + + // Register Transfers + Add(0xAA, "TAX", AddressingMode.Implied, 1, 2, false, InstructionType.Transfer); + Add(0x8A, "TXA", AddressingMode.Implied, 1, 2, false, InstructionType.Transfer); + Add(0xA8, "TAY", AddressingMode.Implied, 1, 2, false, InstructionType.Transfer); + Add(0x98, "TYA", AddressingMode.Implied, 1, 2, false, InstructionType.Transfer); + Add(0xBA, "TSX", AddressingMode.Implied, 1, 2, false, InstructionType.Transfer); + Add(0x9A, "TXS", AddressingMode.Implied, 1, 2, false, InstructionType.Transfer); + + // Stack Operations + Add(0x48, "PHA", AddressingMode.Implied, 1, 3, false, InstructionType.Stack); + Add(0x68, "PLA", AddressingMode.Implied, 1, 4, false, InstructionType.Stack); + Add(0x08, "PHP", AddressingMode.Implied, 1, 3, false, InstructionType.Stack); + Add(0x28, "PLP", AddressingMode.Implied, 1, 4, false, InstructionType.Stack); + + // Logical Operations + Add(0x29, "AND", AddressingMode.Immediate, 2, 2, false, InstructionType.Logic); + Add(0x25, "AND", AddressingMode.ZeroPage, 2, 3, false, InstructionType.Logic); + Add(0x35, "AND", AddressingMode.ZeroPageX, 2, 4, false, InstructionType.Logic); + Add(0x2D, "AND", AddressingMode.Absolute, 3, 4, false, InstructionType.Logic); + Add(0x3D, "AND", AddressingMode.AbsoluteX, 3, 4, true, InstructionType.Logic); + Add(0x39, "AND", AddressingMode.AbsoluteY, 3, 4, true, InstructionType.Logic); + Add(0x21, "AND", AddressingMode.IndexedIndirect, 2, 6, false, InstructionType.Logic); + Add(0x31, "AND", AddressingMode.IndirectIndexed, 2, 5, true, InstructionType.Logic); + + Add(0x49, "EOR", AddressingMode.Immediate, 2, 2, false, InstructionType.Logic); + Add(0x45, "EOR", AddressingMode.ZeroPage, 2, 3, false, InstructionType.Logic); + Add(0x55, "EOR", AddressingMode.ZeroPageX, 2, 4, false, InstructionType.Logic); + Add(0x4D, "EOR", AddressingMode.Absolute, 3, 4, false, InstructionType.Logic); + Add(0x5D, "EOR", AddressingMode.AbsoluteX, 3, 4, true, InstructionType.Logic); + Add(0x59, "EOR", AddressingMode.AbsoluteY, 3, 4, true, InstructionType.Logic); + Add(0x41, "EOR", AddressingMode.IndexedIndirect, 2, 6, false, InstructionType.Logic); + Add(0x51, "EOR", AddressingMode.IndirectIndexed, 2, 5, true, InstructionType.Logic); + + Add(0x09, "ORA", AddressingMode.Immediate, 2, 2, false, InstructionType.Logic); + Add(0x05, "ORA", AddressingMode.ZeroPage, 2, 3, false, InstructionType.Logic); + Add(0x15, "ORA", AddressingMode.ZeroPageX, 2, 4, false, InstructionType.Logic); + Add(0x0D, "ORA", AddressingMode.Absolute, 3, 4, false, InstructionType.Logic); + Add(0x1D, "ORA", AddressingMode.AbsoluteX, 3, 4, true, InstructionType.Logic); + Add(0x19, "ORA", AddressingMode.AbsoluteY, 3, 4, true, InstructionType.Logic); + Add(0x01, "ORA", AddressingMode.IndexedIndirect, 2, 6, false, InstructionType.Logic); + Add(0x11, "ORA", AddressingMode.IndirectIndexed, 2, 5, true, InstructionType.Logic); + + Add(0x24, "BIT", AddressingMode.ZeroPage, 2, 3, false, InstructionType.Logic); + Add(0x2C, "BIT", AddressingMode.Absolute, 3, 4, false, InstructionType.Logic); + + // Arithmetic Operations + Add(0x69, "ADC", AddressingMode.Immediate, 2, 2, false, InstructionType.Arithmetic); + Add(0x65, "ADC", AddressingMode.ZeroPage, 2, 3, false, InstructionType.Arithmetic); + Add(0x75, "ADC", AddressingMode.ZeroPageX, 2, 4, false, InstructionType.Arithmetic); + Add(0x6D, "ADC", AddressingMode.Absolute, 3, 4, false, InstructionType.Arithmetic); + Add(0x7D, "ADC", AddressingMode.AbsoluteX, 3, 4, true, InstructionType.Arithmetic); + Add(0x79, "ADC", AddressingMode.AbsoluteY, 3, 4, true, InstructionType.Arithmetic); + Add(0x61, "ADC", AddressingMode.IndexedIndirect, 2, 6, false, InstructionType.Arithmetic); + Add(0x71, "ADC", AddressingMode.IndirectIndexed, 2, 5, true, InstructionType.Arithmetic); + + Add(0xE9, "SBC", AddressingMode.Immediate, 2, 2, false, InstructionType.Arithmetic); + Add(0xE5, "SBC", AddressingMode.ZeroPage, 2, 3, false, InstructionType.Arithmetic); + Add(0xF5, "SBC", AddressingMode.ZeroPageX, 2, 4, false, InstructionType.Arithmetic); + Add(0xED, "SBC", AddressingMode.Absolute, 3, 4, false, InstructionType.Arithmetic); + Add(0xFD, "SBC", AddressingMode.AbsoluteX, 3, 4, true, InstructionType.Arithmetic); + Add(0xF9, "SBC", AddressingMode.AbsoluteY, 3, 4, true, InstructionType.Arithmetic); + Add(0xE1, "SBC", AddressingMode.IndexedIndirect, 2, 6, false, InstructionType.Arithmetic); + Add(0xF1, "SBC", AddressingMode.IndirectIndexed, 2, 5, true, InstructionType.Arithmetic); + + Add(0xC9, "CMP", AddressingMode.Immediate, 2, 2, false, InstructionType.Compare); + Add(0xC5, "CMP", AddressingMode.ZeroPage, 2, 3, false, InstructionType.Compare); + Add(0xD5, "CMP", AddressingMode.ZeroPageX, 2, 4, false, InstructionType.Compare); + Add(0xCD, "CMP", AddressingMode.Absolute, 3, 4, false, InstructionType.Compare); + Add(0xDD, "CMP", AddressingMode.AbsoluteX, 3, 4, true, InstructionType.Compare); + Add(0xD9, "CMP", AddressingMode.AbsoluteY, 3, 4, true, InstructionType.Compare); + Add(0xC1, "CMP", AddressingMode.IndexedIndirect, 2, 6, false, InstructionType.Compare); + Add(0xD1, "CMP", AddressingMode.IndirectIndexed, 2, 5, true, InstructionType.Compare); + + Add(0xE0, "CPX", AddressingMode.Immediate, 2, 2, false, InstructionType.Compare); + Add(0xE4, "CPX", AddressingMode.ZeroPage, 2, 3, false, InstructionType.Compare); + Add(0xEC, "CPX", AddressingMode.Absolute, 3, 4, false, InstructionType.Compare); + + Add(0xC0, "CPY", AddressingMode.Immediate, 2, 2, false, InstructionType.Compare); + Add(0xC4, "CPY", AddressingMode.ZeroPage, 2, 3, false, InstructionType.Compare); + Add(0xCC, "CPY", AddressingMode.Absolute, 3, 4, false, InstructionType.Compare); + + // Increments & Decrements + Add(0xE6, "INC", AddressingMode.ZeroPage, 2, 5, false, InstructionType.Increment); + Add(0xF6, "INC", AddressingMode.ZeroPageX, 2, 6, false, InstructionType.Increment); + Add(0xEE, "INC", AddressingMode.Absolute, 3, 6, false, InstructionType.Increment); + Add(0xFE, "INC", AddressingMode.AbsoluteX, 3, 7, false, InstructionType.Increment); + + Add(0xE8, "INX", AddressingMode.Implied, 1, 2, false, InstructionType.Increment); + Add(0xC8, "INY", AddressingMode.Implied, 1, 2, false, InstructionType.Increment); + + Add(0xC6, "DEC", AddressingMode.ZeroPage, 2, 5, false, InstructionType.Decrement); + Add(0xD6, "DEC", AddressingMode.ZeroPageX, 2, 6, false, InstructionType.Decrement); + Add(0xCE, "DEC", AddressingMode.Absolute, 3, 6, false, InstructionType.Decrement); + Add(0xDE, "DEC", AddressingMode.AbsoluteX, 3, 7, false, InstructionType.Decrement); + + Add(0xCA, "DEX", AddressingMode.Implied, 1, 2, false, InstructionType.Decrement); + Add(0x88, "DEY", AddressingMode.Implied, 1, 2, false, InstructionType.Decrement); + + // Shifts + Add(0x0A, "ASL", AddressingMode.Accumulator, 1, 2, false, InstructionType.Shift); + Add(0x06, "ASL", AddressingMode.ZeroPage, 2, 5, false, InstructionType.Shift); + Add(0x16, "ASL", AddressingMode.ZeroPageX, 2, 6, false, InstructionType.Shift); + Add(0x0E, "ASL", AddressingMode.Absolute, 3, 6, false, InstructionType.Shift); + Add(0x1E, "ASL", AddressingMode.AbsoluteX, 3, 7, false, InstructionType.Shift); + + Add(0x4A, "LSR", AddressingMode.Accumulator, 1, 2, false, InstructionType.Shift); + Add(0x46, "LSR", AddressingMode.ZeroPage, 2, 5, false, InstructionType.Shift); + Add(0x56, "LSR", AddressingMode.ZeroPageX, 2, 6, false, InstructionType.Shift); + Add(0x4E, "LSR", AddressingMode.Absolute, 3, 6, false, InstructionType.Shift); + Add(0x5E, "LSR", AddressingMode.AbsoluteX, 3, 7, false, InstructionType.Shift); + + Add(0x2A, "ROL", AddressingMode.Accumulator, 1, 2, false, InstructionType.Shift); + Add(0x26, "ROL", AddressingMode.ZeroPage, 2, 5, false, InstructionType.Shift); + Add(0x36, "ROL", AddressingMode.ZeroPageX, 2, 6, false, InstructionType.Shift); + Add(0x2E, "ROL", AddressingMode.Absolute, 3, 6, false, InstructionType.Shift); + Add(0x3E, "ROL", AddressingMode.AbsoluteX, 3, 7, false, InstructionType.Shift); + + Add(0x6A, "ROR", AddressingMode.Accumulator, 1, 2, false, InstructionType.Shift); + Add(0x66, "ROR", AddressingMode.ZeroPage, 2, 5, false, InstructionType.Shift); + Add(0x76, "ROR", AddressingMode.ZeroPageX, 2, 6, false, InstructionType.Shift); + Add(0x6E, "ROR", AddressingMode.Absolute, 3, 6, false, InstructionType.Shift); + Add(0x7E, "ROR", AddressingMode.AbsoluteX, 3, 7, false, InstructionType.Shift); + + // Jumps & Calls + Add(0x4C, "JMP", AddressingMode.Absolute, 3, 3, false, InstructionType.Jump); + Add(0x6C, "JMP", AddressingMode.Indirect, 3, 5, false, InstructionType.Jump); + Add(0x20, "JSR", AddressingMode.Absolute, 3, 6, false, InstructionType.Jump); + Add(0x60, "RTS", AddressingMode.Implied, 1, 6, false, InstructionType.Return); + Add(0x40, "RTI", AddressingMode.Implied, 1, 6, false, InstructionType.Return); + + // Branches + Add(0x90, "BCC", AddressingMode.Relative, 2, 2, true, InstructionType.Branch); + Add(0xB0, "BCS", AddressingMode.Relative, 2, 2, true, InstructionType.Branch); + Add(0xF0, "BEQ", AddressingMode.Relative, 2, 2, true, InstructionType.Branch); + Add(0x30, "BMI", AddressingMode.Relative, 2, 2, true, InstructionType.Branch); + Add(0xD0, "BNE", AddressingMode.Relative, 2, 2, true, InstructionType.Branch); + Add(0x10, "BPL", AddressingMode.Relative, 2, 2, true, InstructionType.Branch); + Add(0x50, "BVC", AddressingMode.Relative, 2, 2, true, InstructionType.Branch); + Add(0x70, "BVS", AddressingMode.Relative, 2, 2, true, InstructionType.Branch); + + // Status Flag Changes + Add(0x18, "CLC", AddressingMode.Implied, 1, 2, false, InstructionType.Clear); + Add(0xD8, "CLD", AddressingMode.Implied, 1, 2, false, InstructionType.Clear); + Add(0x58, "CLI", AddressingMode.Implied, 1, 2, false, InstructionType.Clear); + Add(0xB8, "CLV", AddressingMode.Implied, 1, 2, false, InstructionType.Clear); + Add(0x38, "SEC", AddressingMode.Implied, 1, 2, false, InstructionType.Set); + Add(0xF8, "SED", AddressingMode.Implied, 1, 2, false, InstructionType.Set); + Add(0x78, "SEI", AddressingMode.Implied, 1, 2, false, InstructionType.Set); + + // System Functions + Add(0x00, "BRK", AddressingMode.Implied, 1, 7, false, InstructionType.Interrupt); + Add(0xEA, "NOP", AddressingMode.Implied, 1, 2, false, InstructionType.Other); + + // Unofficial/Illegal opcodes are added here if needed by further forks, however this should cover all of the NES 6502 instructions + } + + /// + /// Adds an instruction to the instruction set + /// + private static void Add(byte opcode, string mnemonic, AddressingMode addressingMode, + byte size, byte cycles, bool extraCycleOnPageCross, InstructionType type) + { + var instruction = new InstructionInfo( + opcode, mnemonic, addressingMode, size, cycles, extraCycleOnPageCross, type); + + instructions[opcode] = instruction; + mnemonicMap[(mnemonic, addressingMode)] = instruction; + } + } +} \ No newline at end of file diff --git a/NESDecompiler.Core/Decompilation/Decompiler.cs b/NESDecompiler.Core/Decompilation/Decompiler.cs new file mode 100644 index 0000000..9de2f6e --- /dev/null +++ b/NESDecompiler.Core/Decompilation/Decompiler.cs @@ -0,0 +1,1733 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NESDecompiler.Core.CPU; +using NESDecompiler.Core.Disassembly; +using NESDecompiler.Core.Exceptions; +using NESDecompiler.Core.ROM; + +namespace NESDecompiler.Core.Decompilation +{ + /// + /// Type of variable in the decompiled code + /// + public enum VariableType + { + Byte, + Word, + Array, + Pointer, + Unknown + } + + /// + /// A variable identified during decompilation + /// + public class Variable + { + /// + /// The CPU address of this variable + /// + public ushort Address { get; set; } + + /// + /// The name of this variable + /// + public string Name { get; set; } + + /// + /// The type of this variable + /// + public VariableType Type { get; set; } + + /// + /// The size of this variable in bytes + /// + public int Size { get; set; } + + /// + /// Whether this variable is accessed for reading + /// + public bool IsRead { get; set; } + + /// + /// Whether this variable is accessed for writing + /// + public bool IsWritten { get; set; } + + /// + /// Creates a new variable + /// + public Variable(ushort address, string name, VariableType type, int size = 1) + { + Address = address; + Name = name; + Type = type; + Size = size; + } + + /// + /// Returns the C type of this variable + /// + public string GetCType() + { + return Type switch + { + VariableType.Byte => "uint8_t", + VariableType.Word => "uint16_t", + VariableType.Pointer => "uint8_t*", + VariableType.Array => "uint8_t", + _ => "uint8_t" + }; + } + } + + /// + /// Represents a decompiled function + /// + public class Function + { + /// + /// The address of this function + /// + public ushort Address { get; set; } + + /// + /// The name of this function + /// + public string Name { get; set; } + + /// + /// The addresses of instructions in this function + /// + public HashSet Instructions { get; } = new HashSet(); + + /// + /// The variables accessed by this function + /// + public HashSet VariablesAccessed { get; } = new HashSet(); + + /// + /// The functions called by this function + /// + public HashSet CalledFunctions { get; } = new HashSet(); + + /// + /// Creates a new function + /// + public Function(ushort address, string name) + { + Address = address; + Name = name; + } + } + + /// + /// A block of code representing a logical unit + /// + public class CodeBlock + { + /// + /// The starting address of this block + /// + public ushort StartAddress { get; set; } + + /// + /// The ending address of this block + /// + public ushort EndAddress { get; set; } + + /// + /// The instructions in this block + /// + public List Instructions { get; } = new List(); + + /// + /// The blocks that can follow this one + /// + public List Successors { get; } = new List(); + + /// + /// Creates a new code block + /// + public CodeBlock(ushort startAddress) + { + StartAddress = startAddress; + EndAddress = startAddress; + } + } + + public class WorkspaceFile + { + public string CurrentFilePath { get; set; } = string.Empty; + public List RecentFiles { get; set; } = new List(); + public bool IsDisassembled { get; set; } + public bool IsDecompiled { get; set; } + public Dictionary Variables { get; set; } = new Dictionary(); + public Dictionary Functions { get; set; } = new Dictionary(); + } + + public class VariableWorkspaceData + { + public string Name { get; set; } = string.Empty; + public string Type { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + } + + public class FunctionWorkspaceData + { + public string Name { get; set; } = string.Empty; + public string ReturnType { get; set; } = string.Empty; + public List Parameters { get; set; } = new List(); + public string Description { get; set; } = string.Empty; + } + + /// + /// Decompiles 6502 assembly code to C code + /// + public class Decompiler + { + private readonly ROMInfo romInfo; + private readonly Disassembler disassembler; + private readonly Dictionary variables; + private readonly Dictionary functions; + private readonly Dictionary codeBlocks; + private int variableCounter; + + /// + /// Information about the ROM being decompiled + /// + public ROMInfo ROMInfo => romInfo; + + /// + /// The disassembler used by this decompiler + /// + public Disassembler Disassembler => disassembler; + + /// + /// The variables identified during decompilation + /// + public IReadOnlyDictionary Variables => variables; + + /// + /// The functions identified during decompilation + /// + public IReadOnlyDictionary Functions => functions; + + /// + /// The code blocks identified during decompilation + /// + public IReadOnlyDictionary CodeBlocks => codeBlocks; + + /// + /// Creates a new decompiler for the specified ROM + /// + /// Information about the ROM + /// The disassembler to use + public Decompiler(ROMInfo romInfo, Disassembler disassembler) + { + this.romInfo = romInfo ?? throw new ArgumentNullException(nameof(romInfo)); + this.disassembler = disassembler ?? throw new ArgumentNullException(nameof(disassembler)); + + variables = new Dictionary(); + functions = new Dictionary(); + codeBlocks = new Dictionary(); + variableCounter = 0; + } + + /// + /// Decompiles the ROM + /// + public void Decompile() + { + try + { + AnalyzeControlFlow(); + IdentifyVariables(); + IdentifyFunctions(); + AnalyzeDataDependencies(); + } + catch (Exception ex) + { + throw new DecompilationException($"Error during decompilation: {ex.Message}", ex); + } + } + + /// + /// Analyzes the control flow of the program + /// + private void AnalyzeControlFlow() + { + try + { + IdentifyBasicBlocks(); + BuildControlFlowGraph(); + } + catch (Exception ex) + { + throw new DecompilationException($"Error during control flow analysis: {ex.Message}", ex); + } + } + + /// + /// Identifies basic blocks in the code + /// + private void IdentifyBasicBlocks() + { + try + { + // A basic block is a sequence of instructions with no branches in or out + // except at the beginning and end + + var leaders = new HashSet(); + + if (disassembler.Instructions.Count > 0) + { + leaders.Add(disassembler.Instructions[0].CPUAddress); + } + + foreach (var entryPoint in disassembler.EntryPoints) + { + leaders.Add(entryPoint); + } + + foreach (var instruction in disassembler.Instructions) + { + if (instruction.IsBranch || instruction.IsJump) + { + if (instruction.TargetAddress.HasValue) + { + leaders.Add(instruction.TargetAddress.Value); + } + + if (instruction.Info.Mnemonic != "JMP") + { + ushort nextAddress = (ushort)(instruction.CPUAddress + instruction.Info.Size); + leaders.Add(nextAddress); + } + } + } + + ushort currentStart = 0; + + foreach (var instruction in disassembler.Instructions.OrderBy(i => i.CPUAddress)) + { + ushort address = instruction.CPUAddress; + + if (leaders.Contains(address)) + { + var newBlock = new CodeBlock(address); + codeBlocks[address] = newBlock; + currentStart = address; + } + + if (codeBlocks.TryGetValue(currentStart, out var currentBlock)) + { + currentBlock.Instructions.Add(instruction); + currentBlock.EndAddress = address; + } + + if (instruction.IsBranch || instruction.IsJump || instruction.IsFunctionExit) + { + currentStart = 0; + } + } + } + catch (Exception ex) + { + throw new DecompilationException($"Error during basic block identification: {ex.Message}", ex); + } + } + + /// + /// Builds the control flow graph + /// + private void BuildControlFlowGraph() + { + try + { + foreach (var block in codeBlocks.Values) + { + if (block.Instructions.Count == 0) + { + continue; + } + + var lastInstruction = block.Instructions[^1]; + + if (lastInstruction.IsBranch) + { + if (lastInstruction.TargetAddress.HasValue) + { + ushort targetAddress = lastInstruction.TargetAddress.Value; + if (codeBlocks.TryGetValue(targetAddress, out var targetBlock)) + { + block.Successors.Add(targetBlock); + } + } + ushort nextAddress = (ushort)(lastInstruction.CPUAddress + lastInstruction.Info.Size); + if (codeBlocks.TryGetValue(nextAddress, out var nextBlock)) + { + block.Successors.Add(nextBlock); + } + } + else if (lastInstruction.Info.Mnemonic == "JMP") + { + if (lastInstruction.TargetAddress.HasValue) + { + ushort targetAddress = lastInstruction.TargetAddress.Value; + if (codeBlocks.TryGetValue(targetAddress, out var targetBlock)) + { + block.Successors.Add(targetBlock); + } + } + } + else if (lastInstruction.Info.Mnemonic == "JSR") + { + ushort nextAddress = (ushort)(lastInstruction.CPUAddress + lastInstruction.Info.Size); + if (codeBlocks.TryGetValue(nextAddress, out var nextBlock)) + { + block.Successors.Add(nextBlock); + } + } + else if (!lastInstruction.IsFunctionExit) + { + ushort nextAddress = (ushort)(lastInstruction.CPUAddress + lastInstruction.Info.Size); + if (codeBlocks.TryGetValue(nextAddress, out var nextBlock)) + { + block.Successors.Add(nextBlock); + } + } + } + } + catch (Exception ex) + { + throw new DecompilationException($"Error during control flow graph construction: {ex.Message}", ex); + } + } + + /// + /// Identifies variables accessed by the code + /// + private void IdentifyVariables() + { + try + { + // NES memory map + Dictionary knownAddresses = new Dictionary + { + { 0x2000, "PPUCTRL" }, + { 0x2001, "PPUMASK" }, + { 0x2002, "PPUSTATUS" }, + { 0x2003, "OAMADDR" }, + { 0x2004, "OAMDATA" }, + { 0x2005, "PPUSCROLL" }, + { 0x2006, "PPUADDR" }, + { 0x2007, "PPUDATA" }, + { 0x4000, "SQ1_VOL" }, + { 0x4001, "SQ1_SWEEP" }, + { 0x4002, "SQ1_LO" }, + { 0x4003, "SQ1_HI" }, + { 0x4004, "SQ2_VOL" }, + { 0x4005, "SQ2_SWEEP" }, + { 0x4006, "SQ2_LO" }, + { 0x4007, "SQ2_HI" }, + { 0x4008, "TRI_LINEAR" }, + { 0x400A, "TRI_LO" }, + { 0x400B, "TRI_HI" }, + { 0x400C, "NOISE_VOL" }, + { 0x400E, "NOISE_LO" }, + { 0x400F, "NOISE_HI" }, + { 0x4010, "DMC_FREQ" }, + { 0x4011, "DMC_RAW" }, + { 0x4012, "DMC_START" }, + { 0x4013, "DMC_LEN" }, + { 0x4014, "OAMDMA" }, + { 0x4015, "SND_CHN" }, + { 0x4016, "JOY1" }, + { 0x4017, "JOY2" } + }; + + foreach (var instruction in disassembler.Instructions) + { + if (instruction.Info.AddressingMode == AddressingMode.Implied || + instruction.Info.AddressingMode == AddressingMode.Accumulator || + instruction.Info.AddressingMode == AddressingMode.Immediate || + instruction.Info.AddressingMode == AddressingMode.Relative) + { + continue; + } + + ushort? address = null; + + if (instruction.Info.AddressingMode == AddressingMode.ZeroPage || + instruction.Info.AddressingMode == AddressingMode.ZeroPageX || + instruction.Info.AddressingMode == AddressingMode.ZeroPageY) + { + address = instruction.Operands[0]; + } + else if (instruction.Info.AddressingMode == AddressingMode.Absolute || + instruction.Info.AddressingMode == AddressingMode.AbsoluteX || + instruction.Info.AddressingMode == AddressingMode.AbsoluteY) + { + address = (ushort)((instruction.Operands[1] << 8) | instruction.Operands[0]); + } + else if (instruction.Info.AddressingMode == AddressingMode.IndexedIndirect || + instruction.Info.AddressingMode == AddressingMode.IndirectIndexed) + { + address = instruction.Operands[0]; + } + + if (address.HasValue) + { + ushort addr = address.Value; + + if (!variables.TryGetValue(addr, out var variable)) + { + string name; + + if (knownAddresses.TryGetValue(addr, out var knownName)) + { + name = knownName; + } + else if (addr < 0x100) + { + // Zero page + name = $"zp_{addr:X2}"; + } + else if (addr < 0x800) + { + // RAM + name = $"ram_{addr:X4}"; + } + else if (addr >= 0x8000) + { + // ROM + name = $"rom_{addr:X4}"; + } + else + { + // Other memory + name = $"var_{variableCounter++:X4}"; + } + + VariableType type = VariableType.Byte; + + if (instruction.Info.AddressingMode == AddressingMode.IndexedIndirect || + instruction.Info.AddressingMode == AddressingMode.IndirectIndexed) + { + type = VariableType.Pointer; + } + + variable = new Variable(addr, name, type); + variables[addr] = variable; + } + + if (instruction.Info.Type == InstructionType.Store) + { + variable.IsWritten = true; + } + else + { + variable.IsRead = true; + } + } + } + } + catch (Exception ex) + { + throw new DecompilationException($"Error during variable identification: {ex.Message}", ex); + } + } + + /// + /// Identifies functions in the code + /// + private void IdentifyFunctions() + { + try + { + foreach (var entryPoint in disassembler.EntryPoints) + { + if (!functions.ContainsKey(entryPoint)) + { + string name = disassembler.Labels.TryGetValue(entryPoint, out var label) ? + label : $"func_{entryPoint:X4}"; + + functions[entryPoint] = new Function(entryPoint, name); + } + } + + foreach (var function in functions.Values.ToList()) + { + AnalyzeFunction(function); + } + } + catch (Exception ex) + { + throw new DecompilationException($"Error during function identification: {ex.Message}", ex); + } + } + + /// + /// Analyzes a function to identify its instructions, variables, and called functions + /// + /// The function to analyze + private void AnalyzeFunction(Function function) + { + try + { + var toProcess = new Queue(); + toProcess.Enqueue(function.Address); + + while (toProcess.Count > 0) + { + ushort address = toProcess.Dequeue(); + + if (function.Instructions.Contains(address)) + { + continue; + } + + if (!disassembler.AddressToInstruction.TryGetValue(address, out var instruction)) + { + continue; + } + + function.Instructions.Add(address); + + if (instruction.Info.AddressingMode != AddressingMode.Implied && + instruction.Info.AddressingMode != AddressingMode.Accumulator && + instruction.Info.AddressingMode != AddressingMode.Immediate && + instruction.Info.AddressingMode != AddressingMode.Relative) + { + ushort? varAddress = null; + + if (instruction.Info.AddressingMode == AddressingMode.ZeroPage || + instruction.Info.AddressingMode == AddressingMode.ZeroPageX || + instruction.Info.AddressingMode == AddressingMode.ZeroPageY) + { + varAddress = instruction.Operands[0]; + } + else if (instruction.Info.AddressingMode == AddressingMode.Absolute || + instruction.Info.AddressingMode == AddressingMode.AbsoluteX || + instruction.Info.AddressingMode == AddressingMode.AbsoluteY) + { + varAddress = (ushort)((instruction.Operands[1] << 8) | instruction.Operands[0]); + } + else if (instruction.Info.AddressingMode == AddressingMode.IndexedIndirect || + instruction.Info.AddressingMode == AddressingMode.IndirectIndexed) + { + varAddress = instruction.Operands[0]; + } + + if (varAddress.HasValue) + { + function.VariablesAccessed.Add(varAddress.Value); + } + } + + if (instruction.Info.Mnemonic == "JSR" && instruction.TargetAddress.HasValue) + { + ushort target = instruction.TargetAddress.Value; + + function.CalledFunctions.Add(target); + + if (!functions.ContainsKey(target)) + { + string name = disassembler.Labels.TryGetValue(target, out var label) ? + label : $"func_{target:X4}"; + + functions[target] = new Function(target, name); + + AnalyzeFunction(functions[target]); + } + + ushort nextAddress = (ushort)(address + instruction.Info.Size); + toProcess.Enqueue(nextAddress); + } + else if (instruction.Info.Mnemonic == "JMP" && instruction.TargetAddress.HasValue) + { + ushort target = instruction.TargetAddress.Value; + toProcess.Enqueue(target); + } + else if (instruction.IsBranch && instruction.TargetAddress.HasValue) + { + ushort target = instruction.TargetAddress.Value; + ushort nextAddress = (ushort)(address + instruction.Info.Size); + + toProcess.Enqueue(target); + toProcess.Enqueue(nextAddress); + } + else if (!instruction.IsFunctionExit) + { + ushort nextAddress = (ushort)(address + instruction.Info.Size); + toProcess.Enqueue(nextAddress); + } + } + } + catch (Exception ex) + { + throw new DecompilationException($"Error during function analysis: {ex.Message}", ex); + } + } + + /// + /// Analyzes data dependencies between functions + /// + private void AnalyzeDataDependencies() + { + try + { + foreach (var function in functions.Values) + { + foreach (var varAddress in function.VariablesAccessed) + { + if (variables.TryGetValue(varAddress, out var variable)) + { + foreach (var instructionAddress in function.Instructions) + { + var instruction = disassembler.AddressToInstruction[instructionAddress]; + + ushort? accessedAddress = null; + + if (instruction.Info.AddressingMode == AddressingMode.ZeroPage || + instruction.Info.AddressingMode == AddressingMode.ZeroPageX || + instruction.Info.AddressingMode == AddressingMode.ZeroPageY) + { + accessedAddress = instruction.Operands[0]; + } + else if (instruction.Info.AddressingMode == AddressingMode.Absolute || + instruction.Info.AddressingMode == AddressingMode.AbsoluteX || + instruction.Info.AddressingMode == AddressingMode.AbsoluteY) + { + accessedAddress = (ushort)((instruction.Operands[1] << 8) | instruction.Operands[0]); + } + else if (instruction.Info.AddressingMode == AddressingMode.IndexedIndirect || + instruction.Info.AddressingMode == AddressingMode.IndirectIndexed) + { + accessedAddress = instruction.Operands[0]; + } + + if (accessedAddress == varAddress) + { + if (instruction.Info.Type == InstructionType.Store) + { + variable.IsWritten = true; + } + else + { + variable.IsRead = true; + } + + if (instruction.Info.AddressingMode == AddressingMode.AbsoluteX || + instruction.Info.AddressingMode == AddressingMode.AbsoluteY || + instruction.Info.AddressingMode == AddressingMode.ZeroPageX || + instruction.Info.AddressingMode == AddressingMode.ZeroPageY || + instruction.Info.AddressingMode == AddressingMode.IndexedIndirect || + instruction.Info.AddressingMode == AddressingMode.IndirectIndexed) + { + if (variable.Type != VariableType.Pointer) + { + variable.Type = VariableType.Array; + variable.Size = 256; + } + } + } + } + } + } + } + } + catch (Exception ex) + { + throw new DecompilationException($"Error during data dependency analysis: {ex.Message}", ex); + } + } + + /// + /// Generates C code for the decompiled ROM + /// + /// The generated C code + public string GenerateCCode() + { + try + { + var sb = new StringBuilder(); + + sb.AppendLine("/*"); + sb.AppendLine(" * Decompiled NES ROM"); + sb.AppendLine($" * ROM: {romInfo}"); + sb.AppendLine(" */"); + sb.AppendLine(); + + sb.AppendLine("#include "); + sb.AppendLine("#include "); + sb.AppendLine("#include "); + sb.AppendLine("#include "); + sb.AppendLine(); + + sb.AppendLine("// 6502 CPU Status Flag Constants"); + sb.AppendLine("#define CARRY_FLAG 0x01"); + sb.AppendLine("#define ZERO_FLAG 0x02"); + sb.AppendLine("#define INTERRUPT_FLAG 0x04"); + sb.AppendLine("#define DECIMAL_FLAG 0x08"); + sb.AppendLine("#define BREAK_FLAG 0x10"); + sb.AppendLine("#define UNUSED_FLAG 0x20"); + sb.AppendLine("#define OVERFLOW_FLAG 0x40"); + sb.AppendLine("#define NEGATIVE_FLAG 0x80"); + sb.AppendLine(); + + sb.AppendLine("// CPU Registers"); + sb.AppendLine("static uint8_t a; // Accumulator"); + sb.AppendLine("static uint8_t x; // X Register"); + sb.AppendLine("static uint8_t y; // Y Register"); + sb.AppendLine("static uint8_t status; // Status Register"); + sb.AppendLine("static uint16_t pc; // Program Counter"); + sb.AppendLine("static uint8_t sp; // Stack Pointer"); + sb.AppendLine(); + + sb.AppendLine("// Memory"); + sb.AppendLine("static uint8_t memory[0x10000]; // 64KB memory"); + sb.AppendLine("static uint8_t stack[0x100]; // Stack (0x0100-0x01FF)"); + sb.AppendLine(); + + sb.AppendLine("// NES Hardware Registers"); + sb.AppendLine("#define PPUCTRL (*((volatile uint8_t*)0x2000))"); + sb.AppendLine("#define PPUMASK (*((volatile uint8_t*)0x2001))"); + sb.AppendLine("#define PPUSTATUS (*((volatile uint8_t*)0x2002))"); + sb.AppendLine("#define OAMADDR (*((volatile uint8_t*)0x2003))"); + sb.AppendLine("#define OAMDATA (*((volatile uint8_t*)0x2004))"); + sb.AppendLine("#define PPUSCROLL (*((volatile uint8_t*)0x2005))"); + sb.AppendLine("#define PPUADDR (*((volatile uint8_t*)0x2006))"); + sb.AppendLine("#define PPUDATA (*((volatile uint8_t*)0x2007))"); + sb.AppendLine("#define OAMDMA (*((volatile uint8_t*)0x4014))"); + sb.AppendLine("#define SND_CHN (*((volatile uint8_t*)0x4015))"); + sb.AppendLine("#define JOY1 (*((volatile uint8_t*)0x4016))"); + sb.AppendLine("#define JOY2 (*((volatile uint8_t*)0x4017))"); + sb.AppendLine(); + + sb.AppendLine("// Variables"); + foreach (var variable in variables.Values) + { + if (variable.Address < 0x2000 || variable.Address >= 0x8000) + { + sb.AppendLine($"static {variable.GetCType()} {variable.Name}[256];"); + } + } + sb.AppendLine(); + + sb.AppendLine("// Function prototypes"); + foreach (var function in functions.Values) + { + sb.AppendLine($"void {function.Name}();"); + } + sb.AppendLine(); + + foreach (var function in functions.Values) + { + sb.AppendLine($"void {function.Name}() {{"); + + GenerateFunctionBody(function, sb); + + sb.AppendLine("}"); + sb.AppendLine(); + } + + sb.AppendLine("int main() {"); + sb.AppendLine(" // Initialize CPU state"); + sb.AppendLine(" a = 0;"); + sb.AppendLine(" x = 0;"); + sb.AppendLine(" y = 0;"); + sb.AppendLine(" status = UNUSED_FLAG; // Bit 5 is always set"); + sb.AppendLine(" sp = 0xFF;"); + sb.AppendLine(" pc = 0x8000; // Start of PRG ROM"); + sb.AppendLine(); + sb.AppendLine(" // Initialize memory"); + sb.AppendLine(" memset(memory, 0, sizeof(memory));"); + sb.AppendLine(" memset(stack, 0, sizeof(stack));"); + sb.AppendLine(); + + if (functions.TryGetValue(romInfo.ResetVector, out var resetFunction)) + { + sb.AppendLine($" // Call the reset function"); + sb.AppendLine($" {resetFunction.Name}();"); + sb.AppendLine(); + } + + sb.AppendLine(" // Main loop"); + sb.AppendLine(" while (1) {"); + sb.AppendLine(" // Handle NMI, IRQ, etc."); + sb.AppendLine(" // Read input"); + sb.AppendLine(" // Update game state"); + sb.AppendLine(" // Render graphics"); + sb.AppendLine(" }"); + sb.AppendLine(); + sb.AppendLine(" return 0;"); + sb.AppendLine("}"); + + return sb.ToString(); + } + catch (Exception ex) + { + throw new DecompilationException($"Error during C code generation: {ex.Message}", ex); + } + } + + /// + /// Generates C code for a function + /// + /// The function to generate code for + /// The string builder to append to + private void GenerateFunctionBody(Function function, StringBuilder sb) + { + try + { + var functionBlocks = new List(); + + foreach (var block in codeBlocks.Values) + { + if (function.Instructions.Contains(block.StartAddress)) + { + functionBlocks.Add(block); + } + } + + functionBlocks.Sort((a, b) => a.StartAddress.CompareTo(b.StartAddress)); + + sb.AppendLine(" // Function labels"); + var usedLabels = new HashSet(); + + foreach (var block in functionBlocks) + { + if (disassembler.Labels.TryGetValue(block.StartAddress, out var label)) + { + usedLabels.Add(label); + } + + foreach (var instruction in block.Instructions) + { + if ((instruction.IsBranch || instruction.IsJump) && instruction.TargetAddress.HasValue) + { + ushort target = instruction.TargetAddress.Value; + if (disassembler.Labels.TryGetValue(target, out var targetLabel)) + { + usedLabels.Add(targetLabel); + } + } + } + } + + foreach (var label in usedLabels) + { + sb.AppendLine($" static void* {label} = &&{label}_impl; // Forward declaration for computed goto"); + } + sb.AppendLine(); + + foreach (var block in functionBlocks) + { + if (disassembler.Labels.TryGetValue(block.StartAddress, out var label)) + { + sb.AppendLine($"{label}_impl: // Address: 0x{block.StartAddress:X4}"); + } + + foreach (var instruction in block.Instructions) + { + string asmComment = instruction.ToString() + .Replace("#", "0x") // Replace # with 0x to avoid preprocessor issues + .Replace("$", "0x") // Replace $ with 0x for hex values + .Replace("%", ""); // Remove % format specifiers + + sb.AppendLine($" // {asmComment}"); + + GenerateInstructionCode(instruction, sb); + } + + sb.AppendLine(); + } + } + catch (Exception ex) + { + throw new DecompilationException($"Error during function body generation: {ex.Message}", ex); + } + } + + /// + /// Generates C code for an instruction + /// + /// The instruction to generate code for + /// The string builder to append to + private void GenerateInstructionCode(DisassembledInstruction instruction, StringBuilder sb) + { + try + { + switch (instruction.Info.Type) + { + case InstructionType.Load: + GenerateLoadCode(instruction, sb); + break; + case InstructionType.Store: + GenerateStoreCode(instruction, sb); + break; + case InstructionType.Transfer: + GenerateTransferCode(instruction, sb); + break; + case InstructionType.Stack: + GenerateStackCode(instruction, sb); + break; + case InstructionType.Arithmetic: + GenerateArithmeticCode(instruction, sb); + break; + case InstructionType.Increment: + GenerateIncrementCode(instruction, sb); + break; + case InstructionType.Decrement: + GenerateDecrementCode(instruction, sb); + break; + case InstructionType.Shift: + GenerateShiftCode(instruction, sb); + break; + case InstructionType.Logic: + GenerateLogicCode(instruction, sb); + break; + case InstructionType.Compare: + GenerateCompareCode(instruction, sb); + break; + case InstructionType.Branch: + GenerateBranchCode(instruction, sb); + break; + case InstructionType.Jump: + GenerateJumpCode(instruction, sb); + break; + case InstructionType.Return: + GenerateReturnCode(instruction, sb); + break; + case InstructionType.Set: + case InstructionType.Clear: + GenerateFlagCode(instruction, sb); + break; + case InstructionType.Interrupt: + GenerateInterruptCode(instruction, sb); + break; + case InstructionType.Other: + GenerateOtherCode(instruction, sb); + break; + } + } + catch (Exception ex) + { + throw new DecompilationException($"Error during instruction code generation: {ex.Message}", ex); + } + } + + + private void GenerateLoadCode(DisassembledInstruction instruction, StringBuilder sb) + { + // Process LDA, LDX, LDY + string register = instruction.Info.Mnemonic.Substring(2); + string variableName = GetVariableName(instruction); + + if (variableName != null) + { + sb.AppendLine($" {register.ToLower()} = {variableName};"); + } + else if (instruction.Info.AddressingMode == AddressingMode.Immediate) + { + sb.AppendLine($" {register.ToLower()} = 0x{instruction.Operands[0]:X2};"); + } + else + { + sb.AppendLine($" // TODO: Load {register} with value at {GetAddressString(instruction)}"); + } + } + + private void GenerateStoreCode(DisassembledInstruction instruction, StringBuilder sb) + { + // Process STA, STX, STY + string register = instruction.Info.Mnemonic.Substring(2); + string variableName = GetVariableName(instruction); + + if (variableName != null) + { + sb.AppendLine($" {variableName} = {register.ToLower()};"); + } + else + { + sb.AppendLine($" // TODO: Store {register} to {GetAddressString(instruction)}"); + } + } + + private void GenerateTransferCode(DisassembledInstruction instruction, StringBuilder sb) + { + // Process TAX, TXA, TAY, TYA, TSX, TXS + string source = instruction.Info.Mnemonic.Substring(1, 1).ToLower(); + string dest = instruction.Info.Mnemonic.Substring(2, 1).ToLower(); + + sb.AppendLine($" {dest} = {source};"); + } + + private void GenerateJumpCode(DisassembledInstruction instruction, StringBuilder sb) + { + // Handle jump operations: JMP, JSR + if (instruction.TargetAddress.HasValue) + { + ushort target = instruction.TargetAddress.Value; + + if (disassembler.Labels.TryGetValue(target, out var label)) + { + if (instruction.Info.Mnemonic == "JSR") + { + sb.AppendLine($" {label}(); // Call subroutine"); + } + else // JMP + { + sb.AppendLine($" goto *{label}; // Unconditional jump using computed goto"); + } + } + else + { + string dynamicLabel = $"dynamic_label_0x{target:X4}"; + + if (instruction.Info.Mnemonic == "JSR") + { + // Try to find a corresponding function by pattern matching + string potentialFuncName = $"sub_{target:X4}"; + + sb.AppendLine($" // Call function at target address 0x{target:X4}"); + sb.AppendLine($" // Try to call function if it exists, otherwise use inline code"); + sb.AppendLine($" #ifdef {potentialFuncName}"); + sb.AppendLine($" {potentialFuncName}();"); + sb.AppendLine($" #else"); + sb.AppendLine($" // Warning: No function definition found for JSR target 0x{target:X4}"); + sb.AppendLine($" // Treating as inline code section - may need manual adjustment"); + sb.AppendLine($" {{ // Create scope for local variables"); + sb.AppendLine($" // First save return address"); + sb.AppendLine($" uint16_t return_addr = pc + 3; // 3 bytes for JSR instruction"); + sb.AppendLine($" stack[sp--] = (return_addr >> 8) & 0xFF; // Push high byte"); + sb.AppendLine($" stack[sp--] = return_addr & 0xFF; // Push low byte"); + sb.AppendLine($" // Jump to target"); + sb.AppendLine($" pc = 0x{target:X4};"); + sb.AppendLine($" // Note: You may need to manually implement the called function here"); + sb.AppendLine($" }}"); + sb.AppendLine($" #endif"); + } + else // JMP + { + sb.AppendLine($" // Jump to target address 0x{target:X4}"); + sb.AppendLine($" // Define a local label for the jump target"); + sb.AppendLine($" #ifdef {dynamicLabel}"); + sb.AppendLine($" goto *{dynamicLabel}; // Jump to predefined label if available"); + sb.AppendLine($" #else"); + sb.AppendLine($" // Warning: No label found for JMP target 0x{target:X4}"); + sb.AppendLine($" // Using direct PC assignment instead"); + sb.AppendLine($" pc = 0x{target:X4};"); + sb.AppendLine($" // Note: You may need to implement a dynamic jump target here"); + sb.AppendLine($" #endif"); + } + } + } + else if (instruction.Info.AddressingMode == AddressingMode.Indirect) + { + // Handle JMP indirect - this is commonly used for tables and vectors + if (instruction.Operands.Length == 2) + { + ushort indirectAddr = (ushort)((instruction.Operands[1] << 8) | instruction.Operands[0]); + + sb.AppendLine($" // Indirect jump via address 0x{indirectAddr:X4}"); + sb.AppendLine($" {{"); + sb.AppendLine($" // Read jump target from the indirect address (little-endian)"); + sb.AppendLine($" uint16_t target_addr = memory[0x{indirectAddr:X4}] | (memory[0x{indirectAddr:X4} + 1] << 8);"); + sb.AppendLine($" pc = target_addr; // Jump to the target address"); + sb.AppendLine($" // Note: This might be a vector table jump, consider adding special handling"); + sb.AppendLine($" }}"); + } + else + { + sb.AppendLine($" // Indirect jump with unrecognized operand format"); + sb.AppendLine($" // Note: This needs manual investigation"); + } + } + else + { + // Handle other complex jump situations - such as indexed indirect + sb.AppendLine($" // {instruction.Info.Mnemonic} with dynamic or complex target"); + sb.AppendLine($" // Complex addressing mode: {instruction.Info.AddressingMode}"); + sb.AppendLine($" // This requires special handling - review the original assembly code"); + + // Add implementation guidance based on addressing mode + switch (instruction.Info.AddressingMode) + { + case AddressingMode.IndexedIndirect: + sb.AppendLine($" // This is likely a jump table indexed by X"); + sb.AppendLine($" // Example implementation:"); + sb.AppendLine($" // uint16_t addr = memory[x + {GetImmediateValue(instruction)}] | (memory[x + {GetImmediateValue(instruction)} + 1] << 8);"); + sb.AppendLine($" // pc = addr;"); + break; + case AddressingMode.IndirectIndexed: + sb.AppendLine($" // This is likely a jump to an address indexed by Y"); + sb.AppendLine($" // Example implementation:"); + sb.AppendLine($" // uint16_t base_addr = memory[{GetImmediateValue(instruction)}] | (memory[{GetImmediateValue(instruction)} + 1] << 8);"); + sb.AppendLine($" // pc = base_addr + y;"); + break; + default: + sb.AppendLine($" // Unsupported addressing mode for jump instruction"); + sb.AppendLine($" // Review original assembly and implement manually"); + break; + } + } + } + + private string GetImmediateValue(DisassembledInstruction instruction) + { + if (instruction.Operands.Length > 0) + { + return $"0x{instruction.Operands[0]:X2}"; + } + return "0x00"; + } + + private void GenerateReturnCode(DisassembledInstruction instruction, StringBuilder sb) + { + // Process RTS, RTI + sb.AppendLine(" return;"); + } + + + private string? GetVariableName(DisassembledInstruction instruction) + { + if (instruction.Info.AddressingMode == AddressingMode.ZeroPage || + instruction.Info.AddressingMode == AddressingMode.ZeroPageX || + instruction.Info.AddressingMode == AddressingMode.ZeroPageY) + { + // Zero page addressing + ushort address = instruction.Operands[0]; + + if (variables.TryGetValue(address, out var variable)) + { + return GetIndexedVariableName(variable, instruction.Info.AddressingMode); + } + } + else if (instruction.Info.AddressingMode == AddressingMode.Absolute || + instruction.Info.AddressingMode == AddressingMode.AbsoluteX || + instruction.Info.AddressingMode == AddressingMode.AbsoluteY) + { + // Absolute addressing + ushort address = (ushort)((instruction.Operands[1] << 8) | instruction.Operands[0]); + + if (variables.TryGetValue(address, out var variable)) + { + return GetIndexedVariableName(variable, instruction.Info.AddressingMode); + } + } + + return null; + } + + private string GetIndexedVariableName(Variable variable, AddressingMode addressingMode) + { + if (addressingMode == AddressingMode.ZeroPageX || addressingMode == AddressingMode.AbsoluteX) + { + if (variable.Type == VariableType.Array) + { + return $"{variable.Name}[x]"; + } + else + { + return $"{variable.Name} + x"; + } + } + else if (addressingMode == AddressingMode.ZeroPageY || addressingMode == AddressingMode.AbsoluteY) + { + if (variable.Type == VariableType.Array) + { + return $"{variable.Name}[y]"; + } + else + { + return $"{variable.Name} + y"; + } + } + else + { + return variable.Name; + } + } + + private string GetAddressString(DisassembledInstruction instruction) + { + if (instruction.Info.AddressingMode == AddressingMode.ZeroPage || + instruction.Info.AddressingMode == AddressingMode.ZeroPageX || + instruction.Info.AddressingMode == AddressingMode.ZeroPageY) + { + // Zero page addressing + ushort address = instruction.Operands[0]; + return $"0x{address:X2}"; + } + else if (instruction.Info.AddressingMode == AddressingMode.Absolute || + instruction.Info.AddressingMode == AddressingMode.AbsoluteX || + instruction.Info.AddressingMode == AddressingMode.AbsoluteY) + { + // Absolute addressing + ushort address = (ushort)((instruction.Operands[1] << 8) | instruction.Operands[0]); + return $"0x{address:X4}"; + } + else if (instruction.Info.AddressingMode == AddressingMode.IndexedIndirect) + { + // Indexed indirect addressing (X-indexed) + return $"*(0x{instruction.Operands[0]:X2} + X)"; + } + else if (instruction.Info.AddressingMode == AddressingMode.IndirectIndexed) + { + // Indirect indexed addressing (Y-indexed) + return $"*(0x{instruction.Operands[0]:X2}) + Y"; + } + + return "unknown"; + } + + private void GenerateStackCode(DisassembledInstruction instruction, StringBuilder sb) + { + // Handle stack operations: PHA, PLA, PHP, PLP, etc. + switch (instruction.Info.Mnemonic) + { + case "PHA": + sb.AppendLine(" stack[sp--] = a; // Push accumulator to stack"); + break; + case "PHP": + sb.AppendLine(" stack[sp--] = status; // Push status register to stack"); + break; + case "PLA": + sb.AppendLine(" a = stack[++sp]; // Pull accumulator from stack"); + sb.AppendLine(" // Update zero and negative flags"); + sb.AppendLine(" status = (status & ~(ZERO_FLAG | NEGATIVE_FLAG)) | (a == 0 ? ZERO_FLAG : 0) | (a & 0x80);"); + break; + case "PLP": + sb.AppendLine(" status = stack[++sp]; // Pull status register from stack"); + break; + default: + sb.AppendLine($" // TODO: Implement {instruction.Info.Mnemonic} instruction"); + break; + } + } + + private void GenerateArithmeticCode(DisassembledInstruction instruction, StringBuilder sb) + { + // Handle arithmetic operations: ADC, SBC + string operand = GetOperandString(instruction); + if (operand == null) + return; + + switch (instruction.Info.Mnemonic) + { + case "ADC": + sb.AppendLine($" // ADC - Add with carry"); + sb.AppendLine($" {{"); + sb.AppendLine($" uint16_t result = a + {operand} + (status & CARRY_FLAG ? 1 : 0);"); + sb.AppendLine($" // Set carry flag if result > 255"); + sb.AppendLine($" status = (status & ~CARRY_FLAG) | (result > 0xFF ? CARRY_FLAG : 0);"); + sb.AppendLine($" // Set overflow flag if sign bit changes in an unexpected way"); + sb.AppendLine($" uint8_t overflow = (~(a ^ {operand}) & (a ^ (uint8_t)result) & 0x80) ? OVERFLOW_FLAG : 0;"); + sb.AppendLine($" status = (status & ~OVERFLOW_FLAG) | overflow;"); + sb.AppendLine($" a = (uint8_t)result;"); + sb.AppendLine($" // Update zero and negative flags"); + sb.AppendLine($" status = (status & ~(ZERO_FLAG | NEGATIVE_FLAG)) | (a == 0 ? ZERO_FLAG : 0) | (a & 0x80);"); + sb.AppendLine($" }}"); + break; + + case "SBC": + sb.AppendLine($" // SBC - Subtract with carry"); + sb.AppendLine($" {{"); + sb.AppendLine($" uint16_t result = a - {operand} - (status & CARRY_FLAG ? 0 : 1);"); + sb.AppendLine($" // Set carry flag if no borrow required (result >= 0)"); + sb.AppendLine($" status = (status & ~CARRY_FLAG) | (result < 0x100 ? CARRY_FLAG : 0);"); + sb.AppendLine($" // Set overflow flag if sign bit changes in an unexpected way"); + sb.AppendLine($" uint8_t overflow = ((a ^ {operand}) & (a ^ (uint8_t)result) & 0x80) ? OVERFLOW_FLAG : 0;"); + sb.AppendLine($" status = (status & ~OVERFLOW_FLAG) | overflow;"); + sb.AppendLine($" a = (uint8_t)result;"); + sb.AppendLine($" // Update zero and negative flags"); + sb.AppendLine($" status = (status & ~(ZERO_FLAG | NEGATIVE_FLAG)) | (a == 0 ? ZERO_FLAG : 0) | (a & 0x80);"); + sb.AppendLine($" }}"); + break; + + default: + sb.AppendLine($" // TODO: Implement {instruction.Info.Mnemonic} instruction"); + break; + } + } + + private void GenerateIncrementCode(DisassembledInstruction instruction, StringBuilder sb) + { + // Handle increment operations: INC, INX, INY + string operand = GetOperandString(instruction); + + switch (instruction.Info.Mnemonic) + { + case "INC": + sb.AppendLine($" // Increment memory location"); + sb.AppendLine($" {operand} = ({operand} + 1) & 0xFF;"); + sb.AppendLine($" // Update zero and negative flags"); + sb.AppendLine($" status = (status & ~(ZERO_FLAG | NEGATIVE_FLAG)) | ({operand} == 0 ? ZERO_FLAG : 0) | ({operand} & 0x80);"); + break; + + case "INX": + sb.AppendLine($" // Increment X register"); + sb.AppendLine($" x = (x + 1) & 0xFF;"); + sb.AppendLine($" // Update zero and negative flags"); + sb.AppendLine($" status = (status & ~(ZERO_FLAG | NEGATIVE_FLAG)) | (x == 0 ? ZERO_FLAG : 0) | (x & 0x80);"); + break; + + case "INY": + sb.AppendLine($" // Increment Y register"); + sb.AppendLine($" y = (y + 1) & 0xFF;"); + sb.AppendLine($" // Update zero and negative flags"); + sb.AppendLine($" status = (status & ~(ZERO_FLAG | NEGATIVE_FLAG)) | (y == 0 ? ZERO_FLAG : 0) | (y & 0x80);"); + break; + + default: + sb.AppendLine($" // TODO: Implement {instruction.Info.Mnemonic} instruction"); + break; + } + } + + private void GenerateDecrementCode(DisassembledInstruction instruction, StringBuilder sb) + { + // Handle decrement operations: DEC, DEX, DEY + string operand = GetOperandString(instruction); + + switch (instruction.Info.Mnemonic) + { + case "DEC": + sb.AppendLine($" // Decrement memory location"); + sb.AppendLine($" {operand} = ({operand} - 1) & 0xFF;"); + sb.AppendLine($" // Update zero and negative flags"); + sb.AppendLine($" status = (status & ~(ZERO_FLAG | NEGATIVE_FLAG)) | ({operand} == 0 ? ZERO_FLAG : 0) | ({operand} & 0x80);"); + break; + + case "DEX": + sb.AppendLine($" // Decrement X register"); + sb.AppendLine($" x = (x - 1) & 0xFF;"); + sb.AppendLine($" // Update zero and negative flags"); + sb.AppendLine($" status = (status & ~(ZERO_FLAG | NEGATIVE_FLAG)) | (x == 0 ? ZERO_FLAG : 0) | (x & 0x80);"); + break; + + case "DEY": + sb.AppendLine($" // Decrement Y register"); + sb.AppendLine($" y = (y - 1) & 0xFF;"); + sb.AppendLine($" // Update zero and negative flags"); + sb.AppendLine($" status = (status & ~(ZERO_FLAG | NEGATIVE_FLAG)) | (y == 0 ? ZERO_FLAG : 0) | (y & 0x80);"); + break; + + default: + sb.AppendLine($" // TODO: Implement {instruction.Info.Mnemonic} instruction"); + break; + } + } + + private void GenerateShiftCode(DisassembledInstruction instruction, StringBuilder sb) + { + // Handle shift operations: ASL, LSR, ROL, ROR + string operand; + + if (instruction.Info.AddressingMode == AddressingMode.Accumulator) + { + operand = "a"; + } + else + { + operand = GetOperandString(instruction); + if (operand == null) + return; + } + + switch (instruction.Info.Mnemonic) + { + case "ASL": + sb.AppendLine($" // Arithmetic shift left"); + sb.AppendLine($" status = (status & ~CARRY_FLAG) | (({operand} & 0x80) ? CARRY_FLAG : 0);"); + sb.AppendLine($" {operand} = ({operand} << 1) & 0xFF;"); + sb.AppendLine($" // Update zero and negative flags"); + sb.AppendLine($" status = (status & ~(ZERO_FLAG | NEGATIVE_FLAG)) | ({operand} == 0 ? ZERO_FLAG : 0) | ({operand} & 0x80);"); + break; + + case "LSR": + sb.AppendLine($" // Logical shift right"); + sb.AppendLine($" status = (status & ~CARRY_FLAG) | (({operand} & 0x01) ? CARRY_FLAG : 0);"); + sb.AppendLine($" {operand} = {operand} >> 1;"); + sb.AppendLine($" // Update zero and negative flags (negative always clear)"); + sb.AppendLine($" status = (status & ~(ZERO_FLAG | NEGATIVE_FLAG)) | ({operand} == 0 ? ZERO_FLAG : 0);"); + break; + + case "ROL": + sb.AppendLine($" // Rotate left"); + sb.AppendLine($" {{"); + sb.AppendLine($" uint8_t oldCarry = (status & CARRY_FLAG) ? 1 : 0;"); + sb.AppendLine($" status = (status & ~CARRY_FLAG) | (({operand} & 0x80) ? CARRY_FLAG : 0);"); + sb.AppendLine($" {operand} = (({operand} << 1) | oldCarry) & 0xFF;"); + sb.AppendLine($" // Update zero and negative flags"); + sb.AppendLine($" status = (status & ~(ZERO_FLAG | NEGATIVE_FLAG)) | ({operand} == 0 ? ZERO_FLAG : 0) | ({operand} & 0x80);"); + sb.AppendLine($" }}"); + break; + + case "ROR": + sb.AppendLine($" // Rotate right"); + sb.AppendLine($" {{"); + sb.AppendLine($" uint8_t oldCarry = (status & CARRY_FLAG) ? 0x80 : 0;"); + sb.AppendLine($" status = (status & ~CARRY_FLAG) | (({operand} & 0x01) ? CARRY_FLAG : 0);"); + sb.AppendLine($" {operand} = ({operand} >> 1) | oldCarry;"); + sb.AppendLine($" // Update zero and negative flags"); + sb.AppendLine($" status = (status & ~(ZERO_FLAG | NEGATIVE_FLAG)) | ({operand} == 0 ? ZERO_FLAG : 0) | ({operand} & 0x80);"); + sb.AppendLine($" }}"); + break; + + default: + sb.AppendLine($" // TODO: Implement {instruction.Info.Mnemonic} instruction"); + break; + } + } + + private void GenerateLogicCode(DisassembledInstruction instruction, StringBuilder sb) + { + // Handle logic operations: AND, ORA, EOR, BIT + string operand = GetOperandString(instruction); + if (operand == null) + return; + + switch (instruction.Info.Mnemonic) + { + case "AND": + sb.AppendLine($" // Logical AND"); + sb.AppendLine($" a &= {operand};"); + sb.AppendLine($" // Update zero and negative flags"); + sb.AppendLine($" status = (status & ~(ZERO_FLAG | NEGATIVE_FLAG)) | (a == 0 ? ZERO_FLAG : 0) | (a & 0x80);"); + break; + + case "ORA": + sb.AppendLine($" // Logical OR"); + sb.AppendLine($" a |= {operand};"); + sb.AppendLine($" // Update zero and negative flags"); + sb.AppendLine($" status = (status & ~(ZERO_FLAG | NEGATIVE_FLAG)) | (a == 0 ? ZERO_FLAG : 0) | (a & 0x80);"); + break; + + case "EOR": + sb.AppendLine($" // Logical exclusive OR"); + sb.AppendLine($" a ^= {operand};"); + sb.AppendLine($" // Update zero and negative flags"); + sb.AppendLine($" status = (status & ~(ZERO_FLAG | NEGATIVE_FLAG)) | (a == 0 ? ZERO_FLAG : 0) | (a & 0x80);"); + break; + + case "BIT": + sb.AppendLine($" // Bit test"); + sb.AppendLine($" // Set zero flag based on AND result"); + sb.AppendLine($" status = (status & ~ZERO_FLAG) | ((a & {operand}) == 0 ? ZERO_FLAG : 0);"); + sb.AppendLine($" // Copy bits 6 and 7 of operand to overflow and negative flags"); + sb.AppendLine($" status = (status & ~(OVERFLOW_FLAG | NEGATIVE_FLAG)) | ({operand} & 0x40) | ({operand} & 0x80);"); + break; + + default: + sb.AppendLine($" // TODO: Implement {instruction.Info.Mnemonic} instruction"); + break; + } + } + + private void GenerateCompareCode(DisassembledInstruction instruction, StringBuilder sb) + { + // Handle compare operations: CMP, CPX, CPY + string operand = GetOperandString(instruction); + if (operand == null) + return; + + string register = ""; + + switch (instruction.Info.Mnemonic) + { + case "CMP": + register = "a"; + break; + case "CPX": + register = "x"; + break; + case "CPY": + register = "y"; + break; + default: + sb.AppendLine($" // TODO: Implement {instruction.Info.Mnemonic} instruction"); + return; + } + + sb.AppendLine($" // Compare {register} with memory"); + sb.AppendLine($" {{"); + sb.AppendLine($" uint8_t result = {register} - {operand};"); + sb.AppendLine($" // Set carry flag if {register} >= memory"); + sb.AppendLine($" status = (status & ~CARRY_FLAG) | ({register} >= {operand} ? CARRY_FLAG : 0);"); + sb.AppendLine($" // Update zero and negative flags"); + sb.AppendLine($" status = (status & ~(ZERO_FLAG | NEGATIVE_FLAG)) | (result == 0 ? ZERO_FLAG : 0) | (result & 0x80);"); + sb.AppendLine($" }}"); + } + + private void GenerateBranchCode(DisassembledInstruction instruction, StringBuilder sb) + { + // Handle branch operations: BCC, BCS, BEQ, BMI, BNE, BPL, BVC, BVS + if (!instruction.TargetAddress.HasValue) + { + sb.AppendLine($" // TODO: Implement {instruction.Info.Mnemonic} with unknown target"); + return; + } + + string condition = ""; + string comment = ""; + + switch (instruction.Info.Mnemonic) + { + case "BCC": + condition = "(status & CARRY_FLAG) == 0"; + comment = "Branch if carry clear"; + break; + case "BCS": + condition = "(status & CARRY_FLAG) != 0"; + comment = "Branch if carry set"; + break; + case "BEQ": + condition = "(status & ZERO_FLAG) != 0"; + comment = "Branch if equal (zero set)"; + break; + case "BMI": + condition = "(status & NEGATIVE_FLAG) != 0"; + comment = "Branch if minus (negative set)"; + break; + case "BNE": + condition = "(status & ZERO_FLAG) == 0"; + comment = "Branch if not equal (zero clear)"; + break; + case "BPL": + condition = "(status & NEGATIVE_FLAG) == 0"; + comment = "Branch if plus (negative clear)"; + break; + case "BVC": + condition = "(status & OVERFLOW_FLAG) == 0"; + comment = "Branch if overflow clear"; + break; + case "BVS": + condition = "(status & OVERFLOW_FLAG) != 0"; + comment = "Branch if overflow set"; + break; + default: + sb.AppendLine($" // TODO: Implement {instruction.Info.Mnemonic} instruction"); + return; + } + + string label = disassembler.Labels.TryGetValue(instruction.TargetAddress.Value, out var targetLabel) ? + targetLabel : $"loc_{instruction.TargetAddress.Value:X4}"; + + sb.AppendLine($" // {comment}"); + sb.AppendLine($" if ({condition}) {{"); + sb.AppendLine($" goto *{label}; // Use computed goto for better branch handling"); + sb.AppendLine($" }}"); + } + + private void GenerateFlagCode(DisassembledInstruction instruction, StringBuilder sb) + { + // Handle flag operations: SEC, CLC, SEI, CLI, SED, CLD, CLV + switch (instruction.Info.Mnemonic) + { + case "SEC": + sb.AppendLine(" // Set carry flag"); + sb.AppendLine(" status |= CARRY_FLAG;"); + break; + case "CLC": + sb.AppendLine(" // Clear carry flag"); + sb.AppendLine(" status &= ~CARRY_FLAG;"); + break; + case "SEI": + sb.AppendLine(" // Set interrupt disable flag"); + sb.AppendLine(" status |= INTERRUPT_FLAG;"); + break; + case "CLI": + sb.AppendLine(" // Clear interrupt disable flag"); + sb.AppendLine(" status &= ~INTERRUPT_FLAG;"); + break; + case "SED": + sb.AppendLine(" // Set decimal flag"); + sb.AppendLine(" status |= DECIMAL_FLAG;"); + break; + case "CLD": + sb.AppendLine(" // Clear decimal flag"); + sb.AppendLine(" status &= ~DECIMAL_FLAG;"); + break; + case "CLV": + sb.AppendLine(" // Clear overflow flag"); + sb.AppendLine(" status &= ~OVERFLOW_FLAG;"); + break; + default: + sb.AppendLine($" // TODO: Implement {instruction.Info.Mnemonic} instruction"); + break; + } + } + + private void GenerateInterruptCode(DisassembledInstruction instruction, StringBuilder sb) + { + // Handle interrupt operations: BRK, RTI + switch (instruction.Info.Mnemonic) + { + case "BRK": + sb.AppendLine(" // Software interrupt (BRK)"); + sb.AppendLine(" // Push program counter + 2"); + sb.AppendLine(" stack[sp--] = (pc + 2) >> 8;"); // Push high byte + sb.AppendLine(" stack[sp--] = (pc + 2) & 0xFF;"); // Push low byte + sb.AppendLine(" // Push status register with break flag set"); + sb.AppendLine(" stack[sp--] = status | BREAK_FLAG;"); + sb.AppendLine(" // Set interrupt disable flag"); + sb.AppendLine(" status |= INTERRUPT_FLAG;"); + sb.AppendLine(" // Load interrupt vector from $FFFE-$FFFF"); + sb.AppendLine(" pc = memory[0xFFFE] | (memory[0xFFFF] << 8);"); + break; + case "RTI": + sb.AppendLine(" // Return from interrupt"); + sb.AppendLine(" // Pull status register"); + sb.AppendLine(" status = stack[++sp];"); + sb.AppendLine(" // Pull program counter"); + sb.AppendLine(" pc = stack[++sp];"); // Pull low byte + sb.AppendLine(" pc |= stack[++sp] << 8;"); // Pull high byte + break; + default: + sb.AppendLine($" // TODO: Implement {instruction.Info.Mnemonic} instruction"); + break; + } + } + + private void GenerateOtherCode(DisassembledInstruction instruction, StringBuilder sb) + { + // Handle other operations: NOP + switch (instruction.Info.Mnemonic) + { + case "NOP": + sb.AppendLine(" // No operation"); + sb.AppendLine(" // Do nothing"); + break; + default: + sb.AppendLine($" // TODO: Implement {instruction.Info.Mnemonic} instruction"); + break; + } + } + + private string GetOperandString(DisassembledInstruction instruction) + { + if (instruction.Info.AddressingMode == AddressingMode.Immediate) + { + if (instruction.Operands.Length > 0) + { + return $"0x{instruction.Operands[0]:X2}"; + } + return null; + } + + string? variableName = GetVariableName(instruction); + if (variableName != null) + { + return variableName; + } + + return GetAddressString(instruction); + } + } +} \ No newline at end of file diff --git a/NESDecompiler.Core/Disassembly/Disassembler.cs b/NESDecompiler.Core/Disassembly/Disassembler.cs new file mode 100644 index 0000000..4e22e5d --- /dev/null +++ b/NESDecompiler.Core/Disassembly/Disassembler.cs @@ -0,0 +1,459 @@ +using System; +using System.Collections.Generic; +using System.Text; +using NESDecompiler.Core.CPU; +using NESDecompiler.Core.Exceptions; +using NESDecompiler.Core.ROM; + +namespace NESDecompiler.Core.Disassembly +{ + /// + /// Represents a disassembled instruction with its address and operands + /// + public class DisassembledInstruction + { + /// + /// The address of this instruction in the ROM + /// + public ushort Address { get; set; } + + /// + /// The CPU memory address this instruction maps to + /// + public ushort CPUAddress { get; set; } + + /// + /// Information about this instruction's opcode + /// + public InstructionInfo Info { get; set; } + + /// + /// The raw bytes of this instruction (including operands) + /// + public byte[] Bytes { get; set; } + + /// + /// The operand bytes of this instruction + /// + public byte[] Operands => Bytes.Length > 1 ? Bytes[1..] : Array.Empty(); + + /// + /// The target address for branch and jump instructions + /// + public ushort? TargetAddress { get; set; } + + /// + /// Potential label for this instruction + /// + public string? Label { get; set; } + + /// + /// Potential comment for this instruction + /// + public string? Comment { get; set; } + + /// + /// Whether this instruction is a potential function entry point + /// + public bool IsFunctionEntry { get; set; } + + /// + /// Whether this instruction is a potential function exit point + /// + public bool IsFunctionExit => Info.Mnemonic == "RTS" || Info.Mnemonic == "RTI"; + + /// + /// Whether this instruction is a branch instruction + /// + public bool IsBranch => Info.Type == InstructionType.Branch; + + /// + /// Whether this instruction is a jump instruction + /// + public bool IsJump => Info.Mnemonic == "JMP" || Info.Mnemonic == "JSR"; + + /// + /// Returns a string representation of this instruction + /// + public override string ToString() + { + var sb = new StringBuilder(); + + if (!string.IsNullOrEmpty(Label)) + { + sb.AppendLine($"{Label}:"); + } + + sb.Append($"{CPUAddress:X4} "); + foreach (var b in Bytes) + { + sb.Append($"{b:X2} "); + } + + sb.Append(new string(' ', (3 - Bytes.Length) * 3 + 2)); + + sb.Append(Info.Mnemonic); + + if (Info.AddressingMode != AddressingMode.Implied && + Info.AddressingMode != AddressingMode.Accumulator) + { + sb.Append(' '); + + if (Info.AddressingMode == AddressingMode.Relative && TargetAddress.HasValue) + { + sb.Append($"${TargetAddress.Value:X4}"); + } + else if (Operands.Length == 1) + { + string operandFormat = Info.GetOperandFormat(); + sb.Append(string.Format(operandFormat, Operands[0])); + } + else if (Operands.Length == 2) + { + string operandFormat = Info.GetOperandFormat(); + ushort value = (ushort)((Operands[1] << 8) | Operands[0]); + sb.Append(string.Format(operandFormat, value)); + } + } + + if (!string.IsNullOrEmpty(Comment)) + { + sb.Append($" ; {Comment}"); + } + + return sb.ToString(); + } + } + + /// + /// Disassembles 6502 machine code into assembly language + /// + public class Disassembler + { + private ROMInfo romInfo; + private byte[] codeData; + private List instructions; + private Dictionary addressToInstruction; + private HashSet entryPoints; + private HashSet referencedAddresses; + private Dictionary labels; + private int labelCounter; + + /// + /// The list of disassembled instructions + /// + public IReadOnlyList Instructions => instructions; + + /// + /// Maps CPU addresses to disassembled instructions + /// + public IReadOnlyDictionary AddressToInstruction => addressToInstruction; + + /// + /// The list of entry points (e.g., reset vector, NMI vector) + /// + public IReadOnlySet EntryPoints => entryPoints; + + /// + /// The list of addresses referenced by the code + /// + public IReadOnlySet ReferencedAddresses => referencedAddresses; + + /// + /// Maps CPU addresses to labels + /// + public IReadOnlyDictionary Labels => labels; + + /// + /// Creates a new disassembler for the specified ROM + /// + /// Information about the ROM + /// The code data to disassemble + public Disassembler(ROMInfo romInfo, byte[] codeData) + { + this.romInfo = romInfo ?? throw new ArgumentNullException(nameof(romInfo)); + this.codeData = codeData ?? throw new ArgumentNullException(nameof(codeData)); + + instructions = new List(); + addressToInstruction = new Dictionary(); + entryPoints = new HashSet(); + referencedAddresses = new HashSet(); + labels = new Dictionary(); + labelCounter = 0; + + if (romInfo.ResetVector != 0) + { + entryPoints.Add(romInfo.ResetVector); + } + } + + /// + /// Disassembles the code data + /// + public void Disassemble() + { + LinearDisassembly(); + TraceExecution(); + IdentifyFunctions(); + GenerateLabels(); + } + + /// + /// Gets the disassembled instruction at the specified address + /// + /// The CPU address + /// The disassembled instruction, or null if not found + public DisassembledInstruction? GetInstructionAt(ushort address) + { + addressToInstruction.TryGetValue(address, out var instruction); + return instruction; + } + + /// + /// Performs a linear disassembly of the code data + /// + private void LinearDisassembly() + { + try + { + int offset = 0; + + ushort baseAddress = 0x8000; + + while (offset < codeData.Length) + { + ushort cpuAddress = (ushort)(baseAddress + offset); + + byte opcode = codeData[offset]; + + var instructionInfo = InstructionSet.GetInstruction(opcode); + + if (!instructionInfo.IsValid) + { + offset++; + continue; + } + + if (offset + instructionInfo.Size > codeData.Length) + { + offset++; + continue; + } + + byte[] bytes = new byte[instructionInfo.Size]; + Array.Copy(codeData, offset, bytes, 0, instructionInfo.Size); + + var instruction = new DisassembledInstruction + { + Address = (ushort)offset, + CPUAddress = cpuAddress, + Info = instructionInfo, + Bytes = bytes + }; + + CalculateTargetAddress(instruction); + + instructions.Add(instruction); + addressToInstruction[cpuAddress] = instruction; + offset += instructionInfo.Size; + } + } + catch (Exception ex) + { + throw new DisassemblyException($"Error during linear disassembly: {ex.Message}", ex); + } + } + + /// + /// Traces execution from known entry points + /// + private void TraceExecution() + { + try + { + var toTrace = new Queue(entryPoints); + var traced = new HashSet(); + + while (toTrace.Count > 0) + { + ushort address = toTrace.Dequeue(); + + if (traced.Contains(address)) + { + continue; + } + + traced.Add(address); + + if (!addressToInstruction.TryGetValue(address, out var instruction)) + { + continue; + } + + if (entryPoints.Contains(address)) + { + instruction.IsFunctionEntry = true; + } + + if (instruction.IsJump) + { + if (instruction.TargetAddress.HasValue) + { + ushort target = instruction.TargetAddress.Value; + + referencedAddresses.Add(target); + + if (instruction.Info.Mnemonic == "JSR") + { + entryPoints.Add(target); + + ushort returnAddress = (ushort)(address + instruction.Info.Size); + toTrace.Enqueue(returnAddress); + } + + toTrace.Enqueue(target); + + if (instruction.Info.Mnemonic == "JMP") + { + continue; + } + } + } + else if (instruction.IsBranch) + { + if (instruction.TargetAddress.HasValue) + { + ushort target = instruction.TargetAddress.Value; + + referencedAddresses.Add(target); + + toTrace.Enqueue(target); + } + } + else if (instruction.IsFunctionExit) + { + continue; + } + + ushort nextAddress = (ushort)(address + instruction.Info.Size); + toTrace.Enqueue(nextAddress); + } + } + catch (Exception ex) + { + throw new DisassemblyException($"Error during execution tracing: {ex.Message}", ex); + } + } + + /// + /// Identifies functions and their boundaries + /// + private void IdentifyFunctions() + { + try + { + foreach (ushort entryPoint in entryPoints) + { + if (addressToInstruction.TryGetValue(entryPoint, out var instruction)) + { + instruction.IsFunctionEntry = true; + } + } + } + catch (Exception ex) + { + throw new DisassemblyException($"Error during function identification: {ex.Message}", ex); + } + } + + /// + /// Generates labels for referenced addresses + /// + private void GenerateLabels() + { + try + { + foreach (ushort entryPoint in entryPoints) + { + if (addressToInstruction.TryGetValue(entryPoint, out var instruction)) + { + string label = $"sub_{entryPoint:X4}"; + instruction.Label = label; + labels[entryPoint] = label; + } + } + + foreach (ushort address in referencedAddresses) + { + if (!labels.ContainsKey(address) && addressToInstruction.TryGetValue(address, out var instruction)) + { + string label = $"loc_{labelCounter++:X4}"; + instruction.Label = label; + labels[address] = label; + } + } + + foreach (var instruction in instructions) + { + if (instruction.TargetAddress.HasValue) + { + ushort target = instruction.TargetAddress.Value; + if (labels.TryGetValue(target, out string label)) + { + instruction.Comment = $"-> {label}"; + } + } + } + } + catch (Exception ex) + { + throw new DisassemblyException($"Error during label generation: {ex.Message}", ex); + } + } + + /// + /// Calculates the target address for branch and jump instructions + /// + /// The instruction to process + private void CalculateTargetAddress(DisassembledInstruction instruction) + { + if (instruction.Info.AddressingMode == AddressingMode.Relative) + { + // Branch instructions use relative addressing + // The offset is signed and relative to the next instruction + sbyte offset = (sbyte)instruction.Operands[0]; + ushort nextAddress = (ushort)(instruction.CPUAddress + instruction.Info.Size); + instruction.TargetAddress = (ushort)(nextAddress + offset); + } + else if (instruction.IsJump && + (instruction.Info.AddressingMode == AddressingMode.Absolute || + instruction.Info.AddressingMode == AddressingMode.Indirect)) + { + if (instruction.Operands.Length == 2) + { + ushort target = (ushort)((instruction.Operands[1] << 8) | instruction.Operands[0]); + instruction.TargetAddress = target; + } + } + } + + /// + /// Returns the disassembly as a formatted string + /// + public string ToAssemblyString() + { + var sb = new StringBuilder(); + + sb.AppendLine("; 6502 Disassembly"); + sb.AppendLine($"; ROM: {romInfo}"); + sb.AppendLine(); + + foreach (var instruction in instructions) + { + sb.AppendLine(instruction.ToString()); + } + + return sb.ToString(); + } + } +} \ No newline at end of file diff --git a/NESDecompiler.Core/Exceptions/Exceptions.cs b/NESDecompiler.Core/Exceptions/Exceptions.cs new file mode 100644 index 0000000..3d8af52 --- /dev/null +++ b/NESDecompiler.Core/Exceptions/Exceptions.cs @@ -0,0 +1,64 @@ +using System; + +namespace NESDecompiler.Core.Exceptions +{ + /// + /// Base exception class for ROM-related errors + /// + public class ROMException : Exception + { + public ROMException(string message) : base(message) + { + } + + public ROMException(string message, Exception innerException) + : base(message, innerException) + { + } + } + + /// + /// Exception thrown when a ROM has an invalid format + /// + public class InvalidROMFormatException : ROMException + { + public InvalidROMFormatException(string message) : base(message) + { + } + + public InvalidROMFormatException(string message, Exception innerException) + : base(message, innerException) + { + } + } + + /// + /// Exception thrown during the disassembly process + /// + public class DisassemblyException : Exception + { + public DisassemblyException(string message) : base(message) + { + } + + public DisassemblyException(string message, Exception innerException) + : base(message, innerException) + { + } + } + + /// + /// Exception thrown during the decompilation process + /// + public class DecompilationException : Exception + { + public DecompilationException(string message) : base(message) + { + } + + public DecompilationException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} \ No newline at end of file diff --git a/NESDecompiler.Core/NESDecompiler.Core.csproj b/NESDecompiler.Core/NESDecompiler.Core.csproj new file mode 100644 index 0000000..fa71b7a --- /dev/null +++ b/NESDecompiler.Core/NESDecompiler.Core.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/NESDecompiler.Core/ROM/ROMInfo.cs b/NESDecompiler.Core/ROM/ROMInfo.cs new file mode 100644 index 0000000..8070747 --- /dev/null +++ b/NESDecompiler.Core/ROM/ROMInfo.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; + +namespace NESDecompiler.Core.ROM +{ + /// + /// Mirroring types for NES hardware + /// + public enum MirroringType + { + Horizontal, + Vertical, + FourScreen + } + + /// + /// Contains information about a loaded NES ROM + /// + public class ROMInfo + { + /// + /// Size of the PRG ROM in bytes + /// + public int PRGROMSize { get; set; } + + /// + /// Size of the CHR ROM in bytes + /// + public int CHRROMSize { get; set; } + + /// + /// Mapper number + /// + public byte MapperNumber { get; set; } + + /// + /// Type of mirroring used + /// + public MirroringType MirroringType { get; set; } + + /// + /// Whether the ROM has battery-backed RAM + /// + public bool HasBatteryBackedRAM { get; set; } + + /// + /// Whether the ROM has a trainer + /// + public bool HasTrainer { get; set; } + + /// + /// Whether the ROM uses four-screen VRAM + /// + public bool HasFourScreenVRAM { get; set; } + + /// + /// Whether the ROM is a VS System cartridge + /// + public bool IsVSSystemCart { get; set; } + + /// + /// Offset of the PRG ROM in the file + /// + public int PRGROMOffset { get; set; } + + /// + /// Offset of the CHR ROM in the file + /// + public int CHRROMOffset { get; set; } + + /// + /// The Reset Vector (entry point) address + /// + public ushort ResetVector { get; set; } + + /// + /// The raw ROM data for reference + /// + public byte[] RawData { get; set; } + + /// + /// List of identified entry points (including reset vector and NMI) + /// + public HashSet EntryPoints { get; } = new HashSet(); + + /// + /// Returns a string representation of the ROM information + /// + public override string ToString() + { + return $"ROM Info:\n" + + $" PRG ROM: {PRGROMSize} bytes\n" + + $" CHR ROM: {CHRROMSize} bytes\n" + + $" Mapper: {MapperNumber}\n" + + $" Mirroring: {MirroringType}\n" + + $" Battery-Backed RAM: {HasBatteryBackedRAM}\n" + + $" Reset Vector: ${ResetVector:X4}"; + } + } +} \ No newline at end of file diff --git a/NESDecompiler.Core/ROM/ROMLoader.cs b/NESDecompiler.Core/ROM/ROMLoader.cs new file mode 100644 index 0000000..c62e477 --- /dev/null +++ b/NESDecompiler.Core/ROM/ROMLoader.cs @@ -0,0 +1,171 @@ +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; + + // Identify entry points (reset vector) + 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 + int resetVectorOffset = romInfo.PRGROMOffset + romInfo.PRGROMSize - 4; + if (resetVectorOffset >= 0 && resetVectorOffset < romData.Length - 1) + { + romInfo.ResetVector = (ushort)(romData[resetVectorOffset] | (romData[resetVectorOffset + 1] << 8)); + } + } + + 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); + } + } +} \ No newline at end of file diff --git a/NESDecompiler.GUI/App.xaml b/NESDecompiler.GUI/App.xaml new file mode 100644 index 0000000..a7f17d7 --- /dev/null +++ b/NESDecompiler.GUI/App.xaml @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/NESDecompiler.GUI/App.xaml.cs b/NESDecompiler.GUI/App.xaml.cs new file mode 100644 index 0000000..770673a --- /dev/null +++ b/NESDecompiler.GUI/App.xaml.cs @@ -0,0 +1,14 @@ +using System.Configuration; +using System.Data; +using System.Windows; + +namespace NESDecompiler.GUI +{ + /// + /// Interaction logic for App.xaml + /// + public partial class App : Application + { + } + +} diff --git a/NESDecompiler.GUI/AssemblyInfo.cs b/NESDecompiler.GUI/AssemblyInfo.cs new file mode 100644 index 0000000..b0ec827 --- /dev/null +++ b/NESDecompiler.GUI/AssemblyInfo.cs @@ -0,0 +1,10 @@ +using System.Windows; + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] diff --git a/NESDecompiler.GUI/AvalonEditBehavior.cs b/NESDecompiler.GUI/AvalonEditBehavior.cs new file mode 100644 index 0000000..13729f9 --- /dev/null +++ b/NESDecompiler.GUI/AvalonEditBehavior.cs @@ -0,0 +1,82 @@ +using System; +using System.Windows; +using Microsoft.Xaml.Behaviors; +using ICSharpCode.AvalonEdit; + +namespace NESDecompiler.GUI +{ + /// + /// Behavior for binding the text of an AvalonEdit control + /// + public class AvalonEditBehavior : Behavior + { + public static readonly DependencyProperty TextProperty = + DependencyProperty.Register("Text", typeof(string), typeof(AvalonEditBehavior), + new FrameworkPropertyMetadata(default(string), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, PropertyChangedCallback)); + + /// + /// The bound text property + /// + public string Text + { + get { return (string)GetValue(TextProperty); } + set { SetValue(TextProperty, value); } + } + + /// + /// Called when the Text property changes + /// + private static void PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var behavior = d as AvalonEditBehavior; + if (behavior != null && behavior.AssociatedObject != null) + { + var editor = behavior.AssociatedObject; + + if (editor.Text != e.NewValue as string) + { + editor.Text = e.NewValue as string ?? string.Empty; + } + } + } + + /// + /// Called when the behavior is attached to the control + /// + protected override void OnAttached() + { + base.OnAttached(); + + if (AssociatedObject != null) + { + AssociatedObject.Text = Text ?? string.Empty; + + AssociatedObject.TextChanged += OnTextChanged; + } + } + + /// + /// Called when the behavior is detached from the control + /// + protected override void OnDetaching() + { + if (AssociatedObject != null) + { + AssociatedObject.TextChanged -= OnTextChanged; + } + + base.OnDetaching(); + } + + /// + /// Called when the text in the editor changes + /// + private void OnTextChanged(object? sender, EventArgs e) + { + if (AssociatedObject != null) + { + Text = AssociatedObject.Text; + } + } + } +} \ No newline at end of file diff --git a/NESDecompiler.GUI/AvalonEditTextBindingBehavior.cs b/NESDecompiler.GUI/AvalonEditTextBindingBehavior.cs new file mode 100644 index 0000000..331d7cd --- /dev/null +++ b/NESDecompiler.GUI/AvalonEditTextBindingBehavior.cs @@ -0,0 +1,73 @@ +using System; +using System.Windows; +using ICSharpCode.AvalonEdit; + +namespace NESDecompiler.GUI +{ + /// + /// Provides attached properties for binding text to AvalonEdit + /// + public static class AvalonEditTextBindingBehavior + { + public static readonly DependencyProperty TextProperty = + DependencyProperty.RegisterAttached( + "Text", + typeof(string), + typeof(AvalonEditTextBindingBehavior), + new FrameworkPropertyMetadata( + default(string), + FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, + OnTextChanged)); + + /// + /// Gets the text value + /// + public static string GetText(DependencyObject obj) + { + return (string)obj.GetValue(TextProperty); + } + + /// + /// Sets the text value + /// + public static void SetText(DependencyObject obj, string value) + { + obj.SetValue(TextProperty, value); + } + + /// + /// Called when the text property changes + /// + private static void OnTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is TextEditor editor) + { + if (editor.Text != (string)e.NewValue) + { + editor.Text = (string)e.NewValue ?? string.Empty; + } + + if (e.OldValue != null) + { + editor.TextChanged -= Editor_TextChanged; + } + + if (e.NewValue != null) + { + editor.TextChanged += Editor_TextChanged; + } + } + } + + /// + /// Called when the editor text changes + /// + private static void Editor_TextChanged(object sender, EventArgs e) + { + if (sender is TextEditor editor) + { + SetText(editor, editor.Text); + } + } + } +} \ No newline at end of file diff --git a/NESDecompiler.GUI/Commands/RelayCommand.cs b/NESDecompiler.GUI/Commands/RelayCommand.cs new file mode 100644 index 0000000..ffaed7b --- /dev/null +++ b/NESDecompiler.GUI/Commands/RelayCommand.cs @@ -0,0 +1,150 @@ +using System; +using System.Windows.Input; + +namespace NESDecompiler.GUI.Commands +{ + /// + /// A command whose sole purpose is to relay its functionality to other + /// objects by invoking delegates. + /// + public class RelayCommand : ICommand + { + private readonly Action execute; + private readonly Func? canExecute; + + /// + /// Creates a new command that can always execute. + /// + /// The execution logic. + public RelayCommand(Action execute) + : this(execute, null) + { + } + + /// + /// Creates a new command. + /// + /// The execution logic. + /// The execution status logic. + public RelayCommand(Action execute, Func? canExecute) + { + this.execute = execute ?? throw new ArgumentNullException(nameof(execute)); + this.canExecute = canExecute; + } + + /// + /// Determines whether this command can execute in its current state. + /// + /// + /// Data used by the command. If the command does not require data to be passed, + /// this object can be set to null. + /// + /// true if this command can be executed; otherwise, false. + public bool CanExecute(object? parameter) + { + return canExecute == null || canExecute(); + } + + /// + /// Executes the command on the current command target. + /// + /// + /// Data used by the command. If the command does not require data to be passed, + /// this object can be set to null. + /// + public void Execute(object? parameter) + { + execute(); + } + + /// + /// Occurs when changes occur that affect whether or not the command should execute. + /// + public event EventHandler? CanExecuteChanged + { + add { CommandManager.RequerySuggested += value; } + remove { CommandManager.RequerySuggested -= value; } + } + + /// + /// Raises the CanExecuteChanged event. + /// + public void RaiseCanExecuteChanged() + { + CommandManager.InvalidateRequerySuggested(); + } + } + + /// + /// A generic command whose sole purpose is to relay its functionality to other + /// objects by invoking delegates. + /// + /// The type of parameter that this command expects. + public class RelayCommand : ICommand + { + private readonly Action execute; + private readonly Predicate? canExecute; + + /// + /// Creates a new command that can always execute. + /// + /// The execution logic. + public RelayCommand(Action execute) + : this(execute, null) + { + } + + /// + /// Creates a new command. + /// + /// The execution logic. + /// The execution status logic. + public RelayCommand(Action execute, Predicate? canExecute) + { + this.execute = execute ?? throw new ArgumentNullException(nameof(execute)); + this.canExecute = canExecute; + } + + /// + /// Determines whether this command can execute in its current state. + /// + /// + /// Data used by the command. If the command does not require data to be passed, + /// this object can be set to null. + /// + /// true if this command can be executed; otherwise, false. + public bool CanExecute(object? parameter) + { + return canExecute == null || canExecute((T?)parameter); + } + + /// + /// Executes the command on the current command target. + /// + /// + /// Data used by the command. If the command does not require data to be passed, + /// this object can be set to null. + /// + public void Execute(object? parameter) + { + execute((T?)parameter); + } + + /// + /// Occurs when changes occur that affect whether or not the command should execute. + /// + public event EventHandler? CanExecuteChanged + { + add { CommandManager.RequerySuggested += value; } + remove { CommandManager.RequerySuggested -= value; } + } + + /// + /// Raises the CanExecuteChanged event. + /// + public void RaiseCanExecuteChanged() + { + CommandManager.InvalidateRequerySuggested(); + } + } +} \ No newline at end of file diff --git a/NESDecompiler.GUI/InputDialog.xaml b/NESDecompiler.GUI/InputDialog.xaml new file mode 100644 index 0000000..1ff3e2c --- /dev/null +++ b/NESDecompiler.GUI/InputDialog.xaml @@ -0,0 +1,23 @@ + + + + + + + + + + + + +