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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/NESDecompiler.GUI/InputDialog.xaml.cs b/NESDecompiler.GUI/InputDialog.xaml.cs
new file mode 100644
index 0000000..cc3a209
--- /dev/null
+++ b/NESDecompiler.GUI/InputDialog.xaml.cs
@@ -0,0 +1,35 @@
+using System.Windows;
+
+namespace NESDecompiler.GUI
+{
+ public partial class InputDialog : Window
+ {
+ public string ResponseText
+ {
+ get { return ResponseTextBox.Text; }
+ set { ResponseTextBox.Text = value; }
+ }
+
+ public InputDialog(string title, string prompt, string defaultResponse = "")
+ {
+ InitializeComponent();
+
+ Title = title;
+ PromptText.Text = prompt;
+ ResponseText = defaultResponse;
+
+ ResponseTextBox.SelectAll();
+ ResponseTextBox.Focus();
+ }
+
+ private void OKButton_Click(object sender, RoutedEventArgs e)
+ {
+ DialogResult = true;
+ }
+
+ private void CancelButton_Click(object sender, RoutedEventArgs e)
+ {
+ DialogResult = false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/NESDecompiler.GUI/MainWindow.xaml b/NESDecompiler.GUI/MainWindow.xaml
new file mode 100644
index 0000000..13f11d6
--- /dev/null
+++ b/NESDecompiler.GUI/MainWindow.xaml
@@ -0,0 +1,358 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/NESDecompiler.GUI/MainWindow.xaml.cs b/NESDecompiler.GUI/MainWindow.xaml.cs
new file mode 100644
index 0000000..af4cf18
--- /dev/null
+++ b/NESDecompiler.GUI/MainWindow.xaml.cs
@@ -0,0 +1,305 @@
+using System;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+using System.ComponentModel;
+using NESDecompiler.GUI.ViewModels;
+using ICSharpCode.AvalonEdit.Highlighting;
+using System.Xml;
+using System.IO;
+using System.Reflection;
+using ICSharpCode.AvalonEdit.Highlighting.Xshd;
+using static NESDecompiler.GUI.ViewModels.ProjectItemViewModel;
+using System.Windows.Media;
+using NESDecompiler.GUI.Themes;
+using System.Collections.Generic;
+using ICSharpCode.AvalonEdit;
+
+namespace NESDecompiler.GUI
+{
+ ///
+ /// Interaction logic for MainWindow.xaml
+ ///
+ public partial class MainWindow : Window
+ {
+ private MainViewModel viewModel;
+
+ public MainWindow()
+ {
+ InitializeComponent();
+
+ viewModel = new MainViewModel();
+ DataContext = viewModel;
+
+ LoadSyntaxHighlighting();
+
+ Loaded += MainWindow_Loaded;
+ Closing += MainWindow_Closing;
+ }
+
+ ///
+ /// Loads custom syntax highlighting for ASM 6502
+ ///
+ private void LoadSyntaxHighlighting()
+ {
+ try
+ {
+ using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("NESDecompiler.GUI.Resources.ASM6502.xshd"))
+ {
+ if (stream != null)
+ {
+ using (var reader = new XmlTextReader(stream))
+ {
+ var highlighting = HighlightingLoader.Load(reader, HighlightingManager.Instance);
+ HighlightingManager.Instance.RegisterHighlighting("ASM6502", new[] { ".asm" }, highlighting);
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show($"Failed to load syntax highlighting: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ }
+
+ private void MainWindow_Loaded(object sender, RoutedEventArgs e)
+ {
+ viewModel.Initialize();
+
+ if (viewModel is INotifyPropertyChanged notifyViewModel)
+ {
+ notifyViewModel.PropertyChanged += ViewModel_PropertyChanged;
+ }
+
+ cCodeEditor.SyntaxHighlighting = HighlightingManager.Instance.GetDefinition("C#");
+ headerEditor.SyntaxHighlighting = HighlightingManager.Instance.GetDefinition("C#");
+
+ try
+ {
+ disassemblyEditor.SyntaxHighlighting = HighlightingManager.Instance.GetDefinition("ASM6502");
+ }
+ catch (Exception)
+ {
+ disassemblyEditor.SyntaxHighlighting = HighlightingManager.Instance.GetDefinition("XML");
+ }
+ ThemeManager.Instance.ThemeChanged += (s, args) => ApplyThemeToEditors();
+ ApplyThemeToEditors();
+ SetupEditorEvents();
+ }
+
+ private void ApplyThemeToEditors()
+ {
+ bool isDarkTheme = ThemeManager.Instance.IsDarkTheme;
+
+ SolidColorBrush backgroundBrush = Application.Current.Resources["SecondaryBackgroundBrush"] as SolidColorBrush;
+ SolidColorBrush foregroundBrush = Application.Current.Resources["PrimaryForegroundBrush"] as SolidColorBrush;
+
+ if (backgroundBrush != null && foregroundBrush != null)
+ {
+ ApplyThemeToEditor(disassemblyEditor, backgroundBrush, foregroundBrush);
+ ApplyThemeToEditor(cCodeEditor, backgroundBrush, foregroundBrush);
+ ApplyThemeToEditor(headerEditor, backgroundBrush, foregroundBrush);
+ }
+ }
+
+ private void ApplyThemeToEditor(TextEditor editor, SolidColorBrush background, SolidColorBrush foreground)
+ {
+ if (editor != null)
+ {
+ editor.Background = background;
+ editor.Foreground = foreground;
+ }
+ }
+
+ private void ViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName == nameof(MainViewModel.DisassemblyText))
+ {
+ disassemblyEditor.Text = viewModel.DisassemblyText;
+ }
+ else if (e.PropertyName == nameof(MainViewModel.CCodeText))
+ {
+ cCodeEditor.Text = viewModel.CCodeText;
+ }
+ else if (e.PropertyName == nameof(MainViewModel.HeaderText))
+ {
+ headerEditor.Text = viewModel.HeaderText;
+ }
+ }
+
+ private void MainWindow_Closing(object sender, CancelEventArgs e)
+ {
+ if (viewModel is INotifyPropertyChanged notifyViewModel)
+ {
+ notifyViewModel.PropertyChanged -= ViewModel_PropertyChanged;
+ }
+
+ if (viewModel.HasUnsavedChanges)
+ {
+ var result = MessageBox.Show(
+ "You have unsaved changes. Do you want to save before exiting?",
+ "Save Changes",
+ MessageBoxButton.YesNoCancel,
+ MessageBoxImage.Question);
+
+ if (result == MessageBoxResult.Cancel)
+ {
+ e.Cancel = true;
+ return;
+ }
+
+ if (result == MessageBoxResult.Yes)
+ {
+ viewModel.SaveAllCommand.Execute(null);
+ }
+ }
+ }
+
+ private void TreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs