using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using Microsoft.Win32;
using NESDecompiler.Core.ROM;
using NESDecompiler.Core.Disassembly;
using NESDecompiler.Core.Decompilation;
using NESDecompiler.Core.Exceptions;
using NESDecompiler.GUI.Commands;
using System.Diagnostics;
using static NESDecompiler.GUI.ViewModels.ProjectItemViewModel;
using NESDecompiler.GUI.Themes;
namespace NESDecompiler.GUI.ViewModels
{
///
/// Main view model for the application
///
public class MainViewModel : INotifyPropertyChanged
{
#region Fields
private string statusText = "Ready";
private bool isProcessing = false;
private string processingText = "";
private bool isROMLoaded = false;
private bool isDisassembled = false;
private bool isDecompiled = false;
private string disassemblyText = "";
private string cCodeText = "";
private string headerText = "";
private bool hasUnsavedChanges = false;
private string currentFilePath = "";
private ProjectItemViewModel? selectedProjectItem;
private List recentFiles = new List();
private ROMInfo? romInfo;
private Disassembler? disassembler;
private Decompiler? decompiler;
private const string WORKSPACE_DIRECTORY = "Workspaces";
private const string DEFAULT_WORKSPACE_FILENAME = "default.nesworkspace";
private ObservableCollection variableList = new ObservableCollection();
private ObservableCollection functionList = new ObservableCollection();
private VariableViewModel? selectedVariable;
private FunctionViewModel? selectedFunction;
private string variableSearchText = string.Empty;
private string functionSearchText = string.Empty;
private Project currentProject;
private ROMProject activeROM;
private ObservableCollection recentProjects = new ObservableCollection();
private const int MAX_RECENT_ITEMS = 10;
private List recentWorkspaces = new List();
private List recentProjectsList = new List();
#endregion
#region Properties
///
/// Whether the application is using dark theme
///
public bool IsDarkTheme
{
get => ThemeManager.Instance.IsDarkTheme;
set
{
ThemeManager.Instance.SetTheme(value);
OnPropertyChanged(nameof(IsDarkTheme));
}
}
///
/// Status text for the status bar
///
public string StatusText
{
get => statusText;
set
{
statusText = value;
OnPropertyChanged(nameof(StatusText));
}
}
///
/// Whether a long-running process is in progress
///
public bool IsProcessing
{
get => isProcessing;
set
{
isProcessing = value;
OnPropertyChanged(nameof(IsProcessing));
}
}
///
/// Text to display during processing
///
public string ProcessingText
{
get => processingText;
set
{
processingText = value;
OnPropertyChanged(nameof(ProcessingText));
}
}
///
/// Whether a ROM is loaded
///
public bool IsROMLoaded
{
get => isROMLoaded;
set
{
isROMLoaded = value;
OnPropertyChanged(nameof(IsROMLoaded));
}
}
///
/// Whether the ROM has been disassembled
///
public bool IsDisassembled
{
get => isDisassembled;
set
{
isDisassembled = value;
OnPropertyChanged(nameof(IsDisassembled));
}
}
///
/// Whether the ROM has been decompiled
///
public bool IsDecompiled
{
get => isDecompiled;
set
{
isDecompiled = value;
OnPropertyChanged(nameof(IsDecompiled));
}
}
///
/// The disassembly text
///
public string DisassemblyText
{
get => disassemblyText;
set
{
disassemblyText = value;
OnPropertyChanged(nameof(DisassemblyText));
}
}
///
/// The C code text
///
public string CCodeText
{
get => cCodeText;
set
{
cCodeText = value;
OnPropertyChanged(nameof(CCodeText));
}
}
///
/// The header text
///
public string HeaderText
{
get => headerText;
set
{
headerText = value;
OnPropertyChanged(nameof(HeaderText));
}
}
///
/// Whether there are unsaved changes
///
public bool HasUnsavedChanges
{
get => hasUnsavedChanges;
set
{
hasUnsavedChanges = value;
OnPropertyChanged(nameof(HasUnsavedChanges));
}
}
///
/// Currently selected project item
///
public ProjectItemViewModel? SelectedProjectItem
{
get => selectedProjectItem;
set
{
selectedProjectItem = value;
OnPropertyChanged(nameof(SelectedProjectItem));
if (selectedProjectItem != null)
{
}
}
}
///
/// Project items for the tree view
///
public ObservableCollection ProjectItems { get; } = new ObservableCollection();
///
/// Recent files list
///
public List RecentFiles
{
get => recentFiles;
set
{
recentFiles = value;
OnPropertyChanged(nameof(RecentFiles));
}
}
public ObservableCollection VariableList
{
get => variableList;
set
{
variableList = value;
OnPropertyChanged(nameof(VariableList));
}
}
public ObservableCollection FunctionList
{
get => functionList;
set
{
functionList = value;
OnPropertyChanged(nameof(FunctionList));
}
}
public VariableViewModel? SelectedVariable
{
get => selectedVariable;
set
{
selectedVariable = value;
OnPropertyChanged(nameof(SelectedVariable));
}
}
public FunctionViewModel? SelectedFunction
{
get => selectedFunction;
set
{
selectedFunction = value;
OnPropertyChanged(nameof(SelectedFunction));
}
}
public string VariableSearchText
{
get => variableSearchText;
set
{
variableSearchText = value;
OnPropertyChanged(nameof(VariableSearchText));
}
}
public string FunctionSearchText
{
get => functionSearchText;
set
{
functionSearchText = value;
OnPropertyChanged(nameof(FunctionSearchText));
}
}
public Project CurrentProject
{
get => currentProject;
set
{
currentProject = value;
OnPropertyChanged(nameof(CurrentProject));
OnPropertyChanged(nameof(HasProject));
}
}
public ROMProject ActiveROM
{
get => activeROM;
set
{
if (activeROM != null && value != activeROM)
{
activeROM.DisassemblyText = DisassemblyText;
activeROM.CCodeText = CCodeText;
activeROM.HeaderText = HeaderText;
}
activeROM = value;
OnPropertyChanged(nameof(ActiveROM));
if (activeROM != null)
{
currentFilePath = activeROM.FilePath;
romInfo = activeROM.ROMInfo;
IsROMLoaded = true;
IsDisassembled = activeROM.IsDisassembled;
IsDecompiled = activeROM.IsDecompiled;
if (!string.IsNullOrEmpty(activeROM.DisassemblyText))
DisassemblyText = activeROM.DisassemblyText;
if (!string.IsNullOrEmpty(activeROM.CCodeText))
CCodeText = activeROM.CCodeText;
if (!string.IsNullOrEmpty(activeROM.HeaderText))
HeaderText = activeROM.HeaderText;
if (activeROM.Variables != null && activeROM.Variables.Count > 0)
{
VariableList.Clear();
foreach (var varData in activeROM.Variables.Values)
{
VariableList.Add(new VariableViewModel
{
Address = GetAddressFromDescription(varData.Description),
Name = varData.Name,
TypeName = varData.Type,
Size = GetSizeFromDescription(varData.Description),
Description = varData.Description
});
}
}
if (activeROM.Functions != null && activeROM.Functions.Count > 0)
{
FunctionList.Clear();
foreach (var funcData in activeROM.Functions.Values)
{
FunctionList.Add(new FunctionViewModel
{
Address = GetAddressFromDescription(funcData.Description),
Name = funcData.Name,
InstructionCount = GetInstructionCountFromDescription(funcData.Description),
Description = funcData.Description
});
}
}
OnPropertyChanged(nameof(IsDisassembled));
OnPropertyChanged(nameof(IsDecompiled));
UpdateProjectExplorer();
}
else
{
IsROMLoaded = false;
IsDisassembled = false;
IsDecompiled = false;
DisassemblyText = string.Empty;
CCodeText = string.Empty;
HeaderText = string.Empty;
}
}
}
private ushort GetAddressFromDescription(string description)
{
try
{
var match = System.Text.RegularExpressions.Regex.Match(description, @"Address: 0x([0-9A-F]{4})");
if (match.Success && match.Groups.Count > 1)
{
return Convert.ToUInt16(match.Groups[1].Value, 16);
}
}
catch { }
return 0;
}
private int GetSizeFromDescription(string description)
{
try
{
var match = System.Text.RegularExpressions.Regex.Match(description, @"Size: (\d+)");
if (match.Success && match.Groups.Count > 1)
{
return int.Parse(match.Groups[1].Value);
}
}
catch { }
return 1;
}
private int GetInstructionCountFromDescription(string description)
{
try
{
var match = System.Text.RegularExpressions.Regex.Match(description, @"Instructions: (\d+)");
if (match.Success && match.Groups.Count > 1)
{
return int.Parse(match.Groups[1].Value);
}
}
catch { }
return 0;
}
public ObservableCollection RecentProjects
{
get => recentProjects;
set
{
recentProjects = value;
OnPropertyChanged(nameof(RecentProjects));
}
}
public List RecentWorkspaces
{
get => recentWorkspaces;
set
{
recentWorkspaces = value;
OnPropertyChanged(nameof(RecentWorkspaces));
}
}
public List RecentProjectsList
{
get => recentProjectsList;
set
{
recentProjectsList = value;
OnPropertyChanged(nameof(RecentProjectsList));
}
}
public bool HasProject => CurrentProject != null;
#endregion
#region Commands
public ICommand OpenROMCommand { get; }
public ICommand SaveDisassemblyCommand { get; }
public ICommand SaveCCodeCommand { get; }
public ICommand SaveHeaderCommand { get; }
public ICommand SaveAllCommand { get; }
public ICommand ExitCommand { get; }
public ICommand DisassembleCommand { get; }
public ICommand DecompileCommand { get; }
public ICommand ViewROMInfoCommand { get; }
public ICommand ViewVariablesCommand { get; }
public ICommand ViewFunctionsCommand { get; }
public ICommand AboutCommand { get; }
public ICommand ExportToVSCommand { get; }
public ICommand JumpToVariableDefinitionCommand { get; }
public ICommand FindVariableReferencesCommand { get; }
public ICommand RenameVariableCommand { get; }
public ICommand JumpToFunctionDefinitionCommand { get; }
public ICommand FindFunctionReferencesCommand { get; }
public ICommand RenameFunctionCommand { get; }
public ICommand SearchVariablesCommand { get; }
public ICommand SearchFunctionsCommand { get; }
public ICommand SaveWorkspaceCommand { get; }
public ICommand NewProjectCommand { get; }
public ICommand OpenProjectCommand { get; }
public ICommand SaveProjectCommand { get; }
public ICommand SaveProjectAsCommand { get; }
public ICommand AddROMCommand { get; }
public ICommand RemoveROMCommand { get; }
public ICommand ToggleThemeCommand { get; }
#endregion
#region Constructor
///
/// Creates a new instance of the MainViewModel
///
public MainViewModel()
{
OpenROMCommand = new RelayCommand(OpenROM);
SaveDisassemblyCommand = new RelayCommand(SaveDisassembly, () => IsDisassembled);
SaveCCodeCommand = new RelayCommand(SaveCCode, () => IsDecompiled);
SaveHeaderCommand = new RelayCommand(SaveHeader, () => IsDecompiled);
SaveAllCommand = new RelayCommand(SaveAll, () => IsDisassembled || IsDecompiled);
ExitCommand = new RelayCommand(Exit);
DisassembleCommand = new RelayCommand(Disassemble, () => IsROMLoaded);
DecompileCommand = new RelayCommand(Decompile, () => IsDisassembled);
ViewROMInfoCommand = new RelayCommand(ViewROMInfo, () => IsROMLoaded);
ViewVariablesCommand = new RelayCommand(ViewVariables, () => IsDecompiled);
ViewFunctionsCommand = new RelayCommand(ViewFunctions, () => IsDecompiled);
AboutCommand = new RelayCommand(ShowAbout);
ExportToVSCommand = new RelayCommand(ExportToVisualStudio, () => IsDecompiled);
JumpToVariableDefinitionCommand = new RelayCommand(JumpToVariableDefinition, () => SelectedVariable != null);
FindVariableReferencesCommand = new RelayCommand(FindVariableReferences, () => SelectedVariable != null);
RenameVariableCommand = new RelayCommand(RenameVariable, () => SelectedVariable != null);
JumpToFunctionDefinitionCommand = new RelayCommand(JumpToFunctionDefinition, () => SelectedFunction != null);
FindFunctionReferencesCommand = new RelayCommand(FindFunctionReferences, () => SelectedFunction != null);
RenameFunctionCommand = new RelayCommand(RenameFunction, () => SelectedFunction != null);
SearchVariablesCommand = new RelayCommand(SearchVariables, () => !string.IsNullOrEmpty(VariableSearchText));
SearchFunctionsCommand = new RelayCommand(SearchFunctions, () => !string.IsNullOrEmpty(FunctionSearchText));
SaveWorkspaceCommand = new RelayCommand(SaveWorkspace, () => IsROMLoaded);
NewProjectCommand = new RelayCommand(NewProject);
OpenProjectCommand = new RelayCommand(OpenProject);
SaveProjectCommand = new RelayCommand(SaveProject, () => HasProject);
SaveProjectAsCommand = new RelayCommand(SaveProjectAs, () => HasProject);
AddROMCommand = new RelayCommand(AddROM, () => HasProject);
ToggleThemeCommand = new RelayCommand(ToggleTheme);
RemoveROMCommand = new RelayCommand(RemoveROM, () => IsROMLoaded);
}
#endregion
#region Methods
///
/// Initializes the view model
///
public void Initialize()
{
ThemeManager.Instance.ApplyTheme();
LoadRecentFiles();
LoadRecentWorkspaces();
LoadRecentProjects();
if (CurrentProject == null)
{
CurrentProject = new Project
{
Name = "New Project",
FilePath = "",
LastModified = DateTime.Now
};
UpdateProjectExplorer();
}
}
///
/// Loads the list of recent files
///
private void LoadRecentFiles()
{
try
{
string workspaceDir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"NESDecompiler", WORKSPACE_DIRECTORY);
string workspacePath = Path.Combine(workspaceDir, DEFAULT_WORKSPACE_FILENAME);
if (!Directory.Exists(workspaceDir))
{
Directory.CreateDirectory(workspaceDir);
}
if (File.Exists(workspacePath))
{
string json = File.ReadAllText(workspacePath);
var workspace = System.Text.Json.JsonSerializer.Deserialize(json);
if (workspace != null)
{
RecentFiles = workspace.RecentFiles ?? new List();
if (!string.IsNullOrEmpty(workspace.CurrentFilePath) && File.Exists(workspace.CurrentFilePath))
{
Task.Run(() => Application.Current.Dispatcher.Invoke(() =>
LoadROMFile(workspace.CurrentFilePath)));
}
}
}
else
{
RecentFiles = new List();
SaveRecentFiles();
}
}
catch (Exception ex)
{
MessageBox.Show($"Failed to load workspace: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
RecentFiles = new List();
}
}
///
/// Saves the list of recent files
///
private void SaveRecentFiles()
{
try
{
string workspaceDir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"NESDecompiler", WORKSPACE_DIRECTORY);
string workspacePath = Path.Combine(workspaceDir, DEFAULT_WORKSPACE_FILENAME);
if (!Directory.Exists(workspaceDir))
{
Directory.CreateDirectory(workspaceDir);
}
var workspace = new WorkspaceFile
{
CurrentFilePath = currentFilePath,
RecentFiles = recentFiles,
IsDisassembled = isDisassembled,
IsDecompiled = isDecompiled
};
if (decompiler != null)
{
foreach (var variable in decompiler.Variables.Values)
{
workspace.Variables[variable.Name] = new VariableWorkspaceData
{
Name = variable.Name,
Type = variable.Type.ToString(),
Description = $"Address: 0x{variable.Address:X4}, Size: {variable.Size}"
};
}
foreach (var function in decompiler.Functions.Values)
{
workspace.Functions[function.Name] = new FunctionWorkspaceData
{
Name = function.Name,
ReturnType = "void",
Parameters = new List(),
Description = $"Address: 0x{function.Address:X4}, Instructions: {function.Instructions.Count}"
};
}
}
string json = System.Text.Json.JsonSerializer.Serialize(workspace, new System.Text.Json.JsonSerializerOptions
{
WriteIndented = true
});
File.WriteAllText(workspacePath, json);
}
catch (Exception ex)
{
MessageBox.Show($"Failed to save workspace: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
///
/// Adds a file to the recent files list
///
/// The file path to add
private void AddRecentFile(string filePath)
{
try
{
recentFiles.Remove(filePath);
recentFiles.Insert(0, filePath);
if (recentFiles.Count > 10)
{
recentFiles.RemoveAt(recentFiles.Count - 1);
}
OnPropertyChanged(nameof(RecentFiles));
SaveRecentFiles();
}
catch (Exception ex)
{
MessageBox.Show($"Failed to add recent file: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
///
/// Opens a ROM file
///
private async void OpenROM()
{
try
{
var dialog = new OpenFileDialog
{
Title = "Open NES ROM",
Filter = "NES ROM Files (*.nes)|*.nes|All Files (*.*)|*.*",
FilterIndex = 1,
Multiselect = true
};
if (dialog.ShowDialog() == true)
{
if (CurrentProject == null)
{
CurrentProject = new Project
{
Name = "New Project",
FilePath = "",
LastModified = DateTime.Now
};
}
foreach (string filePath in dialog.FileNames)
{
await LoadROMFile(filePath);
}
}
}
catch (Exception ex)
{
MessageBox.Show($"Failed to open ROM: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
StatusText = "Failed to open ROM";
}
}
///
/// Opens a recent file
///
/// The file path to open
public async void OpenRecentFile(string filePath)
{
try
{
if (File.Exists(filePath))
{
await LoadROMFile(filePath);
}
else
{
MessageBox.Show($"The file '{filePath}' no longer exists.", "File Not Found", MessageBoxButton.OK, MessageBoxImage.Error);
recentFiles.Remove(filePath);
OnPropertyChanged(nameof(RecentFiles));
SaveRecentFiles();
}
}
catch (Exception ex)
{
MessageBox.Show($"Failed to open file: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
StatusText = "Failed to open file";
}
}
///
/// Loads a ROM file
///
/// The file path to load
private async Task LoadROMFile(string filePath)
{
try
{
IsProcessing = true;
ProcessingText = "Loading ROM...";
StatusText = $"Loading {Path.GetFileName(filePath)}...";
if (CurrentProject.ROMs.Any(r => r.FilePath.Equals(filePath, StringComparison.OrdinalIgnoreCase)))
{
MessageBox.Show($"ROM {Path.GetFileName(filePath)} is already loaded in this project.",
"ROM Already Loaded", MessageBoxButton.OK, MessageBoxImage.Information);
ActiveROM = CurrentProject.ROMs.First(r => r.FilePath.Equals(filePath, StringComparison.OrdinalIgnoreCase));
IsProcessing = false;
return;
}
var romProject = new ROMProject
{
Name = Path.GetFileName(filePath),
FilePath = filePath
};
bool loadSuccess = false;
await Task.Run(() =>
{
try
{
var romLoader = new ROMLoader();
romProject.ROMInfo = romLoader.LoadFromFile(filePath);
byte[] prgRomData = romLoader.GetPRGROMData();
if (prgRomData == null || prgRomData.Length == 0)
{
throw new InvalidOperationException("ROM contains no program data");
}
loadSuccess = true;
}
catch (Exception ex)
{
Application.Current.Dispatcher.Invoke(() =>
{
MessageBox.Show($"Error loading ROM: {ex.Message}", "ROM Loading Error", MessageBoxButton.OK, MessageBoxImage.Error);
});
}
});
if (loadSuccess)
{
CurrentProject.ROMs.Add(romProject);
ActiveROM = romProject;
StatusText = $"Loaded {Path.GetFileName(filePath)}";
AddRecentFile(filePath);
UpdateProjectExplorer();
await DisassembleAsync();
}
else
{
StatusText = "Failed to load ROM";
}
}
catch (Exception ex)
{
MessageBox.Show($"Failed to load ROM: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
StatusText = "Failed to load ROM";
}
finally
{
IsProcessing = false;
}
}
private void UpdateProjectExplorer()
{
var expandedPaths = new List();
foreach (var rootItem in ProjectItems)
{
CollectExpandedPaths(rootItem, "", expandedPaths);
}
ProjectItems.Clear();
if (CurrentProject == null)
return;
var projectRoot = new ProjectItemViewModel
{
Name = CurrentProject.Name,
Type = ProjectItemType.Project,
Path = CurrentProject.FilePath
};
ProjectItems.Add(projectRoot);
foreach (var rom in CurrentProject.ROMs)
{
var romItem = new ProjectItemViewModel
{
Name = rom.Name,
Type = ProjectItemType.ROM,
Path = rom.FilePath,
Tag = rom,
IsExpanded = rom.IsExpanded
};
projectRoot.Children.Add(romItem);
romItem.Children.Add(new ProjectItemViewModel
{
Name = "ROM Info",
Type = ProjectItemType.ROMInfo,
Path = rom.FilePath,
Tag = rom
});
if (rom.IsDisassembled)
{
romItem.Children.Add(new ProjectItemViewModel
{
Name = "Disassembly",
Type = ProjectItemType.Disassembly,
Path = rom.FilePath,
Tag = rom
});
}
if (rom.IsDecompiled)
{
romItem.Children.Add(new ProjectItemViewModel
{
Name = "C Code",
Type = ProjectItemType.CCode,
Path = rom.FilePath,
Tag = rom
});
romItem.Children.Add(new ProjectItemViewModel
{
Name = "Header",
Type = ProjectItemType.Header,
Path = rom.FilePath,
Tag = rom
});
romItem.Children.Add(new ProjectItemViewModel
{
Name = "Variables",
Type = ProjectItemType.Variables,
Path = rom.FilePath,
Tag = rom
});
romItem.Children.Add(new ProjectItemViewModel
{
Name = "Functions",
Type = ProjectItemType.Functions,
Path = rom.FilePath,
Tag = rom
});
}
}
RestoreExpandedPaths(ProjectItems[0], "", expandedPaths);
}
private void CollectExpandedPaths(ProjectItemViewModel item, string path, List expandedPaths)
{
if (item == null) return;
string currentPath = string.IsNullOrEmpty(path) ? item.Name : path + "/" + item.Name;
if (item.Tag is ROMProject rom)
{
if (rom.IsExpanded)
{
expandedPaths.Add(currentPath);
}
}
else if (item.IsExpanded)
{
expandedPaths.Add(currentPath);
}
foreach (var child in item.Children)
{
CollectExpandedPaths(child, currentPath, expandedPaths);
}
}
private void RestoreExpandedPaths(ProjectItemViewModel item, string path, List expandedPaths)
{
if (item == null) return;
string currentPath = string.IsNullOrEmpty(path) ? item.Name : path + "/" + item.Name;
if (expandedPaths.Contains(currentPath))
{
item.IsExpanded = true;
if (item.Tag is ROMProject rom)
{
rom.IsExpanded = true;
}
}
foreach (var child in item.Children)
{
RestoreExpandedPaths(child, currentPath, expandedPaths);
}
}
///
/// Saves the disassembly to a file
///
private void SaveDisassembly()
{
try
{
var dialog = new SaveFileDialog
{
Title = "Save Disassembly",
Filter = "Assembly Files (*.asm)|*.asm|Text Files (*.txt)|*.txt|All Files (*.*)|*.*",
FilterIndex = 1,
FileName = Path.GetFileNameWithoutExtension(currentFilePath) + ".asm"
};
if (dialog.ShowDialog() == true)
{
File.WriteAllText(dialog.FileName, DisassemblyText);
StatusText = $"Saved disassembly to {Path.GetFileName(dialog.FileName)}";
}
}
catch (Exception ex)
{
MessageBox.Show($"Failed to save disassembly: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
StatusText = "Failed to save disassembly";
}
}
///
/// Saves the C code to a file
///
private void SaveCCode()
{
try
{
var dialog = new SaveFileDialog
{
Title = "Save C Code",
Filter = "C Files (*.c)|*.c|Text Files (*.txt)|*.txt|All Files (*.*)|*.*",
FilterIndex = 1,
FileName = Path.GetFileNameWithoutExtension(currentFilePath) + ".c"
};
if (dialog.ShowDialog() == true)
{
File.WriteAllText(dialog.FileName, CCodeText);
StatusText = $"Saved C code to {Path.GetFileName(dialog.FileName)}";
}
}
catch (Exception ex)
{
MessageBox.Show($"Failed to save C code: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
StatusText = "Failed to save C code";
}
}
///
/// Saves the header to a file
///
private void SaveHeader()
{
try
{
var dialog = new SaveFileDialog
{
Title = "Save Header",
Filter = "C Header Files (*.h)|*.h|Text Files (*.txt)|*.txt|All Files (*.*)|*.*",
FilterIndex = 1,
FileName = Path.GetFileNameWithoutExtension(currentFilePath) + ".h"
};
if (dialog.ShowDialog() == true)
{
File.WriteAllText(dialog.FileName, HeaderText);
StatusText = $"Saved header to {Path.GetFileName(dialog.FileName)}";
}
}
catch (Exception ex)
{
MessageBox.Show($"Failed to save header: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
StatusText = "Failed to save header";
}
}
///
/// Saves all files
///
private void SaveAll()
{
try
{
string outputDir = Path.Combine(
Path.GetDirectoryName(currentFilePath) ?? "",
Path.GetFileNameWithoutExtension(currentFilePath));
if (!Directory.Exists(outputDir))
{
Directory.CreateDirectory(outputDir);
}
if (IsDisassembled)
{
string asmFile = Path.Combine(outputDir, Path.GetFileNameWithoutExtension(currentFilePath) + ".asm");
File.WriteAllText(asmFile, DisassemblyText);
}
if (IsDecompiled)
{
string cFile = Path.Combine(outputDir, Path.GetFileNameWithoutExtension(currentFilePath) + ".c");
File.WriteAllText(cFile, CCodeText);
string hFile = Path.Combine(outputDir, Path.GetFileNameWithoutExtension(currentFilePath) + ".h");
File.WriteAllText(hFile, HeaderText);
}
StatusText = $"Saved all files to {outputDir}";
HasUnsavedChanges = false;
}
catch (Exception ex)
{
MessageBox.Show($"Failed to save files: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
StatusText = "Failed to save files";
}
}
///
/// Exits the application
///
private void Exit()
{
Application.Current.Shutdown();
}
///
/// Disassembles the ROM
///
private async void Disassemble()
{
await DisassembleAsync();
}
///
/// Disassembles the ROM asynchronously
///
private async Task DisassembleAsync()
{
try
{
if (ActiveROM == null)
return;
IsProcessing = true;
ProcessingText = "Disassembling...";
StatusText = "Disassembling ROM...";
await Task.Run(() =>
{
if (ActiveROM.ROMInfo == null)
throw new InvalidOperationException("No ROM loaded");
var romLoader = new ROMLoader();
if (string.IsNullOrEmpty(ActiveROM.FilePath) || !File.Exists(ActiveROM.FilePath))
throw new InvalidOperationException("ROM file not found");
var romInfo = romLoader.LoadFromFile(ActiveROM.FilePath);
if (romInfo == null)
throw new InvalidOperationException("Failed to load ROM info");
byte[] prgRomData = romLoader.GetPRGROMData();
if (prgRomData == null || prgRomData.Length == 0)
throw new InvalidOperationException("No ROM data loaded");
var disassembler = new Disassembler(romInfo, prgRomData);
disassembler.Disassemble();
Application.Current.Dispatcher.Invoke(() =>
{
ActiveROM.DisassemblyText = disassembler.ToAssemblyString();
DisassemblyText = ActiveROM.DisassemblyText;
this.disassembler = disassembler;
this.romInfo = romInfo;
});
});
ActiveROM.IsDisassembled = true;
IsDisassembled = true;
HasUnsavedChanges = true;
StatusText = "Disassembly complete";
UpdateProjectExplorer();
await DecompileAsync();
}
catch (Exception ex)
{
MessageBox.Show($"Failed to disassemble ROM: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
StatusText = "Failed to disassemble ROM";
}
finally
{
IsProcessing = false;
}
}
///
/// Exports the decompiled code as a Visual Studio 2022 solution
///
private void ExportToVisualStudio()
{
try
{
var dialog = new SaveFileDialog
{
Title = "Export to Visual Studio 2022",
Filter = "Visual Studio Solution (*.sln)|*.sln",
FileName = Path.GetFileNameWithoutExtension(currentFilePath ?? "Decompiled"),
OverwritePrompt = true
};
if (dialog.ShowDialog() == true)
{
string rootPath = Path.GetDirectoryName(dialog.FileName) ?? Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
string solutionName = Path.GetFileNameWithoutExtension(dialog.FileName);
IsProcessing = true;
ProcessingText = "Exporting to Visual Studio...";
StatusText = "Exporting to Visual Studio...";
var exporter = new VisualStudioExporter(solutionName, rootPath, CCodeText, HeaderText);
if (exporter.ExportToVisualStudio())
{
StatusText = "Exported to Visual Studio successfully";
// broken for me, idk why
var result = MessageBox.Show(
"The project has been exported successfully. Would you like to open it in Visual Studio?",
"Export Complete",
MessageBoxButton.YesNo,
MessageBoxImage.Question);
if (result == MessageBoxResult.Yes)
{
string solutionPath = Path.Combine(rootPath, solutionName, $"{solutionName}.sln");
VisualStudioExporter.OpenInVisualStudio(solutionPath);
}
}
else
{
StatusText = "Failed to export to Visual Studio";
}
}
}
catch (Exception ex)
{
MessageBox.Show($"Failed to export to Visual Studio: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
StatusText = "Failed to export to Visual Studio";
}
finally
{
IsProcessing = false;
}
}
///
/// Decompiles the ROM
///
private async void Decompile()
{
await DecompileAsync();
}
///
/// Decompiles the ROM asynchronously
///
private async Task DecompileAsync()
{
try
{
if (ActiveROM == null)
return;
IsProcessing = true;
ProcessingText = "Decompiling...";
StatusText = "Decompiling ROM...";
string cCodeResult = string.Empty;
string headerTextResult = string.Empty;
await Task.Run(() =>
{
try
{
if (romInfo == null || disassembler == null)
throw new InvalidOperationException("No ROM disassembled");
decompiler = new Decompiler(romInfo, disassembler);
decompiler.Decompile();
cCodeResult = decompiler.GenerateCCode();
headerTextResult = GenerateHeaderFile(decompiler);
Application.Current.Dispatcher.Invoke(() =>
{
VariableList.Clear();
foreach (var variable in decompiler.Variables.Values)
{
VariableList.Add(new VariableViewModel
{
Address = variable.Address,
Name = variable.Name,
TypeName = variable.Type.ToString(),
Size = variable.Size,
IsRead = variable.IsRead,
IsWritten = variable.IsWritten,
Description = $"Location: {(variable.Address < 0x100 ? "Zero Page" : variable.Address < 0x800 ? "RAM" : "ROM")}"
});
ActiveROM.Variables[variable.Name] = new VariableWorkspaceData
{
Name = variable.Name,
Type = variable.Type.ToString(),
Description = $"Address: 0x{variable.Address:X4}, Size: {variable.Size}"
};
}
FunctionList.Clear();
foreach (var function in decompiler.Functions.Values)
{
FunctionList.Add(new FunctionViewModel
{
Address = function.Address,
Name = function.Name,
InstructionCount = function.Instructions.Count,
VariableCount = function.VariablesAccessed.Count,
CalledFunctionCount = function.CalledFunctions.Count,
Description = $"Entry point: {(decompiler.ROMInfo.EntryPoints.Contains(function.Address) ? "Yes" : "No")}"
});
ActiveROM.Functions[function.Name] = new FunctionWorkspaceData
{
Name = function.Name,
ReturnType = "void",
Parameters = new List(),
Description = $"Address: 0x{function.Address:X4}, Instructions: {function.Instructions.Count}"
};
}
});
}
catch (Exception ex)
{
Application.Current.Dispatcher.Invoke(() =>
{
throw new Exception($"Decompilation failed: {ex.Message}", ex);
});
}
});
CCodeText = cCodeResult;
HeaderText = headerTextResult;
ActiveROM.CCodeText = cCodeResult;
ActiveROM.HeaderText = headerTextResult;
ActiveROM.IsDecompiled = true;
IsDecompiled = true;
OnPropertyChanged(nameof(IsDecompiled));
if (ProjectItems.Count > 0)
{
UpdateProjectExplorer();
}
HasUnsavedChanges = true;
StatusText = "Decompilation complete";
SaveRecentWorkspaces();
}
catch (Exception ex)
{
MessageBox.Show($"Failed to decompile ROM: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
StatusText = "Failed to decompile ROM";
}
finally
{
IsProcessing = false;
}
}
///
/// Generates a C header file for the decompiled ROM
///
/// The decompiler instance
/// The generated header code
private string GenerateHeaderFile(Decompiler decompiler)
{
StringWriter writer = new StringWriter();
// Add header
writer.WriteLine("/*");
writer.WriteLine(" * Decompiled NES ROM");
writer.WriteLine($" * ROM: {decompiler.ROMInfo}");
writer.WriteLine(" */");
writer.WriteLine();
// Include guard
string guardName = Path.GetFileNameWithoutExtension(currentFilePath).ToUpper() + "_H";
writer.WriteLine($"#ifndef {guardName}");
writer.WriteLine($"#define {guardName}");
writer.WriteLine();
// Add includes
writer.WriteLine("#include ");
writer.WriteLine("#include ");
writer.WriteLine();
// Add NES hardware definitions
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();
// Add external variable declarations
writer.WriteLine("// Variables");
foreach (var variable in decompiler.Variables.Values)
{
if (variable.Address < 0x2000 || variable.Address >= 0x8000)
{
if (variable.Type == VariableType.Array)
{
var cType = variable.GetCType();
var baseType = cType.Contains("[") ? cType.Substring(0, cType.IndexOf("[")) : cType;
writer.WriteLine($"extern {baseType} {variable.Name}[256]; // Size estimated");
}
else
{
writer.WriteLine($"extern {variable.GetCType()} {variable.Name};");
}
}
}
writer.WriteLine();
// Add function declarations
writer.WriteLine("// Functions");
foreach (var function in decompiler.Functions.Values)
{
writer.WriteLine($"void {function.Name}();");
}
writer.WriteLine();
// Close include guard
writer.WriteLine($"#endif // {guardName}");
return writer.ToString();
}
///
/// Views the ROM information
///
private void ViewROMInfo()
{
try
{
if (romInfo == null)
return;
string info = romInfo.ToString();
MessageBox.Show(info, "ROM Information", MessageBoxButton.OK, MessageBoxImage.Information);
}
catch (Exception ex)
{
MessageBox.Show($"Failed to view ROM info: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
///
/// Views the variables
///
private void ViewVariables()
{
try
{
if (decompiler == null)
return;
VariableList.Clear();
foreach (var variable in decompiler.Variables.Values)
{
VariableList.Add(new VariableViewModel
{
Address = variable.Address,
Name = variable.Name,
TypeName = variable.Type.ToString(),
Size = variable.Size,
IsRead = variable.IsRead,
IsWritten = variable.IsWritten,
Description = $"Location: {(variable.Address < 0x100 ? "Zero Page" : variable.Address < 0x800 ? "RAM" : "ROM")}"
});
}
ActivateTab("Variables");
}
catch (Exception ex)
{
MessageBox.Show($"Failed to view variables: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
///
/// Views the functions
///
private void ViewFunctions()
{
try
{
if (decompiler == null)
return;
FunctionList.Clear();
foreach (var function in decompiler.Functions.Values)
{
FunctionList.Add(new FunctionViewModel
{
Address = function.Address,
Name = function.Name,
InstructionCount = function.Instructions.Count,
VariableCount = function.VariablesAccessed.Count,
CalledFunctionCount = function.CalledFunctions.Count,
Description = $"Entry point: {(decompiler.ROMInfo.EntryPoints.Contains(function.Address) ? "Yes" : "No")}"
});
}
ActivateTab("Functions");
}
catch (Exception ex)
{
MessageBox.Show($"Failed to view functions: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
///
/// Shows the about dialog
///
private void ShowAbout()
{
MessageBox.Show(
"NES Decompiler\n" +
"==============\n\n" +
"A static decompiler for NES/6502 ROMs.\n\n" +
"Created with <3 by ApfelTeeSaft",
"About NES Decompiler",
MessageBoxButton.OK,
MessageBoxImage.Information);
}
///
/// Activate a Tab
///
private void ActivateTab(string tabName)
{
var mainWindow = Application.Current.MainWindow as MainWindow;
if (mainWindow != null)
{
mainWindow.ActivateTab(tabName);
}
}
private void JumpToVariableDefinition()
{
if (SelectedVariable == null)
return;
string variableName = SelectedVariable.Name;
string pattern = $"[\\s\\n\\r]({variableName})[\\s\\n\\r;]";
FindAndHighlightInCode(pattern, true, "cCodeEditor");
}
private void FindVariableReferences()
{
if (SelectedVariable == null)
return;
string variableName = SelectedVariable.Name;
string pattern = $"[\\s\\n\\r]({variableName})[\\s\\n\\r;]";
FindAllOccurrencesInCode(pattern, "Variable References", "cCodeEditor");
}
private void RenameVariable()
{
if (SelectedVariable == null)
return;
string oldName = SelectedVariable.Name;
var dialog = new InputDialog("Rename Variable", "Enter new name:", oldName);
if (dialog.ShowDialog() == true)
{
string newName = dialog.ResponseText;
if (string.IsNullOrWhiteSpace(newName))
{
MessageBox.Show("Variable name cannot be empty.", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
SelectedVariable.Name = newName;
ReplaceAllInCode(oldName, newName, "cCodeEditor");
ReplaceAllInCode(oldName, newName, "headerEditor");
HasUnsavedChanges = true;
if (decompiler != null && decompiler.Variables.ContainsKey(SelectedVariable.Address))
{
decompiler.Variables[SelectedVariable.Address].Name = newName;
}
// Save workspace
SaveRecentFiles();
}
}
private void JumpToFunctionDefinition()
{
if (SelectedFunction == null)
return;
string functionName = SelectedFunction.Name;
string pattern = $"void[\\s\\n\\r]+({functionName})[\\s\\n\\r]*\\(";
FindAndHighlightInCode(pattern, true, "cCodeEditor");
}
private void FindFunctionReferences()
{
if (SelectedFunction == null)
return;
string functionName = SelectedFunction.Name;
string pattern = $"[\\s\\n\\r]({functionName})\\(";
FindAllOccurrencesInCode(pattern, "Function References", "cCodeEditor");
}
private void RenameFunction()
{
if (SelectedFunction == null)
return;
string oldName = SelectedFunction.Name;
var dialog = new InputDialog("Rename Function", "Enter new name:", oldName);
if (dialog.ShowDialog() == true)
{
string newName = dialog.ResponseText;
if (string.IsNullOrWhiteSpace(newName))
{
MessageBox.Show("Function name cannot be empty.", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
SelectedFunction.Name = newName;
ReplaceAllInCode(oldName, newName, "cCodeEditor");
ReplaceAllInCode(oldName, newName, "headerEditor");
HasUnsavedChanges = true;
if (decompiler != null && decompiler.Functions.ContainsKey(SelectedFunction.Address))
{
decompiler.Functions[SelectedFunction.Address].Name = newName;
}
SaveRecentFiles();
}
}
private void SearchVariables()
{
if (string.IsNullOrEmpty(VariableSearchText))
return;
var filteredList = new ObservableCollection(
decompiler?.Variables.Values
.Where(v => v.Name.Contains(VariableSearchText, StringComparison.OrdinalIgnoreCase))
.Select(v => new VariableViewModel
{
Address = v.Address,
Name = v.Name,
TypeName = v.Type.ToString(),
Size = v.Size,
IsRead = v.IsRead,
IsWritten = v.IsWritten,
Description = $"Location: {(v.Address < 0x100 ? "Zero Page" : v.Address < 0x800 ? "RAM" : "ROM")}"
})
?? Array.Empty());
VariableList = filteredList;
}
private void SearchFunctions()
{
if (string.IsNullOrEmpty(FunctionSearchText))
return;
var filteredList = new ObservableCollection(
decompiler?.Functions.Values
.Where(f => f.Name.Contains(FunctionSearchText, StringComparison.OrdinalIgnoreCase))
.Select(f => new FunctionViewModel
{
Address = f.Address,
Name = f.Name,
InstructionCount = f.Instructions.Count,
VariableCount = f.VariablesAccessed.Count,
CalledFunctionCount = f.CalledFunctions.Count,
Description = $"Entry point: {(decompiler.ROMInfo.EntryPoints.Contains(f.Address) ? "Yes" : "No")}"
})
?? Array.Empty());
FunctionList = filteredList;
}
private void FindAndHighlightInCode(string pattern, bool useRegex, string editorName)
{
var mainWindow = Application.Current.MainWindow as MainWindow;
if (mainWindow == null)
return;
var editor = mainWindow.GetType().GetField(editorName, System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?.GetValue(mainWindow) as ICSharpCode.AvalonEdit.TextEditor;
if (editor == null)
return;
if (editorName == "cCodeEditor")
ActivateTab("C Code");
else if (editorName == "headerEditor")
ActivateTab("Header");
else if (editorName == "disassemblyEditor")
ActivateTab("Disassembly");
string text = editor.Text;
int startIndex = 0;
System.Text.RegularExpressions.Match match;
if (useRegex)
{
var regex = new System.Text.RegularExpressions.Regex(pattern);
match = regex.Match(text, startIndex);
}
else
{
int index = text.IndexOf(pattern, startIndex, StringComparison.OrdinalIgnoreCase);
if (index >= 0)
{
match = System.Text.RegularExpressions.Regex.Match(text, pattern,
System.Text.RegularExpressions.RegexOptions.IgnoreCase,
System.TimeSpan.FromSeconds(1));
}
else
{
match = System.Text.RegularExpressions.Match.Empty;
}
}
if (match.Success)
{
editor.Select(match.Index, match.Length);
editor.ScrollToLine(editor.Document.GetLineByOffset(match.Index).LineNumber);
}
else
{
MessageBox.Show($"Could not find the pattern in the code.", "Not Found", MessageBoxButton.OK, MessageBoxImage.Information);
}
}
private void FindAllOccurrencesInCode(string pattern, string title, string editorName)
{
var mainWindow = Application.Current.MainWindow as MainWindow;
if (mainWindow == null)
return;
var editor = mainWindow.GetType().GetField(editorName, System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?.GetValue(mainWindow) as ICSharpCode.AvalonEdit.TextEditor;
if (editor == null)
return;
string text = editor.Text;
var regex = new System.Text.RegularExpressions.Regex(pattern);
var matches = regex.Matches(text);
if (matches.Count > 0)
{
var sb = new System.Text.StringBuilder();
sb.AppendLine($"Found {matches.Count} occurrences:");
sb.AppendLine();
foreach (System.Text.RegularExpressions.Match match in matches)
{
int lineNumber = editor.Document.GetLineByOffset(match.Index).LineNumber;
var docLine = editor.Document.GetLineByNumber(lineNumber);
string line = editor.Document.GetText(docLine.Offset, docLine.Length).Trim();
sb.AppendLine($"Line {lineNumber}: {line}");
}
MessageBox.Show(sb.ToString(), title, MessageBoxButton.OK, MessageBoxImage.Information);
}
else
{
MessageBox.Show($"No occurrences found.", title, MessageBoxButton.OK, MessageBoxImage.Information);
}
}
private void ReplaceAllInCode(string oldText, string newText, string editorName)
{
var mainWindow = Application.Current.MainWindow as MainWindow;
if (mainWindow == null)
return;
var editor = mainWindow.GetType().GetField(editorName, System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?.GetValue(mainWindow) as ICSharpCode.AvalonEdit.TextEditor;
if (editor == null)
return;
string text = editor.Text;
var regex = new System.Text.RegularExpressions.Regex($"\\b{System.Text.RegularExpressions.Regex.Escape(oldText)}\\b");
string newContent = regex.Replace(text, newText);
if (text != newContent)
{
editor.Text = newContent;
if (editorName == "cCodeEditor")
CCodeText = newContent;
else if (editorName == "headerEditor")
HeaderText = newContent;
else if (editorName == "disassemblyEditor")
DisassemblyText = newContent;
}
}
private void SaveWorkspace()
{
try
{
var dialog = new SaveFileDialog
{
Title = "Save Workspace",
Filter = "NES Decompiler Workspace (*.nesworkspace)|*.nesworkspace",
DefaultExt = ".nesworkspace",
FileName = Path.GetFileNameWithoutExtension(currentFilePath) + ".nesworkspace"
};
if (dialog.ShowDialog() == true)
{
string workspacePath = dialog.FileName;
var workspace = new WorkspaceFile
{
CurrentFilePath = currentFilePath,
RecentFiles = recentFiles,
IsDisassembled = isDisassembled,
IsDecompiled = isDecompiled
};
if (decompiler != null)
{
foreach (var variable in decompiler.Variables.Values)
{
workspace.Variables[variable.Name] = new VariableWorkspaceData
{
Name = variable.Name,
Type = variable.Type.ToString(),
Description = $"Address: 0x{variable.Address:X4}, Size: {variable.Size}"
};
}
foreach (var function in decompiler.Functions.Values)
{
workspace.Functions[function.Name] = new FunctionWorkspaceData
{
Name = function.Name,
ReturnType = "void",
Parameters = new List(),
Description = $"Address: 0x{function.Address:X4}, Instructions: {function.Instructions.Count}"
};
}
}
string json = System.Text.Json.JsonSerializer.Serialize(workspace, new System.Text.Json.JsonSerializerOptions
{
WriteIndented = true
});
File.WriteAllText(workspacePath, json);
AddRecentWorkspace(workspacePath);
StatusText = $"Workspace saved to {Path.GetFileName(workspacePath)}";
}
}
catch (Exception ex)
{
MessageBox.Show($"Failed to save workspace: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
StatusText = "Failed to save workspace";
}
}
private void SaveWorkspaceToFile(string filePath)
{
try
{
var workspace = new WorkspaceFile
{
CurrentFilePath = currentFilePath,
RecentFiles = recentFiles,
IsDisassembled = isDisassembled,
IsDecompiled = isDecompiled
};
if (decompiler != null)
{
foreach (var variable in decompiler.Variables.Values)
{
workspace.Variables[variable.Name] = new VariableWorkspaceData
{
Name = variable.Name,
Type = variable.Type.ToString(),
Description = $"Address: 0x{variable.Address:X4}, Size: {variable.Size}"
};
}
foreach (var function in decompiler.Functions.Values)
{
workspace.Functions[function.Name] = new FunctionWorkspaceData
{
Name = function.Name,
ReturnType = "void",
Parameters = new List(),
Description = $"Address: 0x{function.Address:X4}, Instructions: {function.Instructions.Count}"
};
}
}
string json = System.Text.Json.JsonSerializer.Serialize(workspace, new System.Text.Json.JsonSerializerOptions
{
WriteIndented = true
});
File.WriteAllText(filePath, json);
AddRecentWorkspace(filePath);
}
catch (Exception ex)
{
throw new Exception($"Failed to save workspace to file: {ex.Message}", ex);
}
}
private void NewProject()
{
if (HasUnsavedChanges)
{
var result = MessageBox.Show(
"You have unsaved changes. Do you want to save before creating a new project?",
"Save Changes",
MessageBoxButton.YesNoCancel,
MessageBoxImage.Question);
if (result == MessageBoxResult.Cancel)
return;
if (result == MessageBoxResult.Yes)
SaveProject();
}
CurrentProject = new Project
{
Name = "New Project",
FilePath = "",
LastModified = DateTime.Now
};
ActiveROM = null;
IsROMLoaded = false;
IsDisassembled = false;
IsDecompiled = false;
DisassemblyText = "";
CCodeText = "";
HeaderText = "";
HasUnsavedChanges = false;
UpdateProjectExplorer();
StatusText = "New project created";
}
private void OpenProject()
{
try
{
if (HasUnsavedChanges)
{
var result = MessageBox.Show(
"You have unsaved changes. Do you want to save before opening another project?",
"Save Changes",
MessageBoxButton.YesNoCancel,
MessageBoxImage.Question);
if (result == MessageBoxResult.Cancel)
return;
if (result == MessageBoxResult.Yes)
SaveProject();
}
var dialog = new OpenFileDialog
{
Title = "Open Project",
Filter = "NES Decompiler Project (*.nesproj)|*.nesproj",
DefaultExt = ".nesproj"
};
if (dialog.ShowDialog() == true)
{
LoadProject(dialog.FileName);
}
}
catch (Exception ex)
{
MessageBox.Show($"Failed to open project: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
StatusText = "Failed to open project";
}
}
private void SaveProject()
{
try
{
if (CurrentProject == null)
return;
if (string.IsNullOrEmpty(CurrentProject.FilePath))
{
SaveProjectAs();
return;
}
SaveProjectToFile(CurrentProject.FilePath);
StatusText = $"Project saved to {Path.GetFileName(CurrentProject.FilePath)}";
}
catch (Exception ex)
{
MessageBox.Show($"Failed to save project: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
StatusText = "Failed to save project";
}
}
private void SaveProjectAs()
{
try
{
if (CurrentProject == null)
return;
var dialog = new SaveFileDialog
{
Title = "Save Project As",
Filter = "NES Decompiler Project (*.nesproj)|*.nesproj",
DefaultExt = ".nesproj",
FileName = string.IsNullOrEmpty(CurrentProject.Name) ? "New Project" : CurrentProject.Name
};
if (dialog.ShowDialog() == true)
{
CurrentProject.FilePath = dialog.FileName;
CurrentProject.Name = Path.GetFileNameWithoutExtension(dialog.FileName);
SaveProjectToFile(CurrentProject.FilePath);
UpdateProjectExplorer();
StatusText = $"Project saved to {Path.GetFileName(CurrentProject.FilePath)}";
}
}
catch (Exception ex)
{
MessageBox.Show($"Failed to save project: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
StatusText = "Failed to save project";
}
}
private void AddROM()
{
OpenROM();
}
private void RemoveROM()
{
try
{
if (ActiveROM == null || CurrentProject == null)
return;
var result = MessageBox.Show(
$"Are you sure you want to remove '{ActiveROM.Name}' from the project?",
"Remove ROM",
MessageBoxButton.YesNo,
MessageBoxImage.Question);
if (result == MessageBoxResult.Yes)
{
CurrentProject.ROMs.Remove(ActiveROM);
ActiveROM = CurrentProject.ROMs.FirstOrDefault();
UpdateProjectExplorer();
StatusText = "ROM removed from project";
}
}
catch (Exception ex)
{
MessageBox.Show($"Failed to remove ROM: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
StatusText = "Failed to remove ROM";
}
}
private void SaveProjectToFile(string filePath)
{
try
{
if (CurrentProject == null)
return;
CurrentProject.LastModified = DateTime.Now;
string json = System.Text.Json.JsonSerializer.Serialize(CurrentProject, new System.Text.Json.JsonSerializerOptions
{
WriteIndented = true
});
File.WriteAllText(filePath, json);
HasUnsavedChanges = false;
AddRecentProject(filePath);
}
catch (Exception ex)
{
throw new Exception($"Failed to save project to file: {ex.Message}", ex);
}
}
private void LoadProject(string filePath)
{
try
{
IsProcessing = true;
ProcessingText = "Loading project...";
StatusText = $"Loading {Path.GetFileName(filePath)}...";
string json = File.ReadAllText(filePath);
var project = System.Text.Json.JsonSerializer.Deserialize(json);
if (project == null)
throw new Exception("Invalid project file");
CurrentProject = project;
foreach (var rom in CurrentProject.ROMs.ToList())
{
if (!File.Exists(rom.FilePath))
{
var result = MessageBox.Show(
$"ROM file '{rom.FilePath}' could not be found. Would you like to remove it from the project?",
"ROM File Not Found",
MessageBoxButton.YesNo,
MessageBoxImage.Question);
if (result == MessageBoxResult.Yes)
{
CurrentProject.ROMs.Remove(rom);
}
}
}
ActiveROM = CurrentProject.ROMs.FirstOrDefault();
UpdateProjectExplorer();
AddRecentProject(filePath);
StatusText = $"Project '{CurrentProject.Name}' loaded";
}
catch (Exception ex)
{
MessageBox.Show($"Failed to load project: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
StatusText = "Failed to load project";
}
finally
{
IsProcessing = false;
}
}
///
/// Saves recent workspaces to application settings
///
private void SaveRecentWorkspacesToSettings()
{
try
{
string appDataPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"NESDecompiler");
if (!Directory.Exists(appDataPath))
Directory.CreateDirectory(appDataPath);
string filePath = Path.Combine(appDataPath, "recentworkspaces.json");
string json = System.Text.Json.JsonSerializer.Serialize(RecentWorkspaces);
File.WriteAllText(filePath, json);
}
catch (Exception ex)
{
Debug.WriteLine($"Error saving recent workspaces: {ex.Message}");
}
}
///
/// Opens a recent workspace file
///
/// The workspace file path to open
public void OpenRecentWorkspace(string filePath)
{
try
{
if (!File.Exists(filePath))
{
MessageBox.Show($"The workspace file '{filePath}' could not be found.",
"File Not Found",
MessageBoxButton.OK,
MessageBoxImage.Error);
var updatedList = new List(RecentWorkspaces);
updatedList.Remove(filePath);
RecentWorkspaces = updatedList;
SaveRecentWorkspacesToSettings();
return;
}
LoadWorkspaceFile(filePath);
AddRecentWorkspace(filePath);
}
catch (Exception ex)
{
MessageBox.Show($"Failed to open workspace: {ex.Message}",
"Error",
MessageBoxButton.OK,
MessageBoxImage.Error);
StatusText = "Failed to open workspace";
}
}
///
/// Loads a workspace file
///
/// The workspace file path to load
private void LoadWorkspaceFile(string filePath)
{
try
{
IsProcessing = true;
ProcessingText = "Loading workspace...";
StatusText = $"Loading {Path.GetFileName(filePath)}...";
string json = File.ReadAllText(filePath);
var workspace = System.Text.Json.JsonSerializer.Deserialize(json);
if (workspace == null)
throw new Exception("Invalid workspace file");
if (!string.IsNullOrEmpty(workspace.CurrentFilePath) && File.Exists(workspace.CurrentFilePath))
{
Task.Run(() => Application.Current.Dispatcher.Invoke(async () =>
await LoadROMFile(workspace.CurrentFilePath)
)).Wait();
if (IsROMLoaded)
{
IsDisassembled = workspace.IsDisassembled;
IsDecompiled = workspace.IsDecompiled;
if (IsDecompiled && decompiler != null)
{
foreach (var varEntry in workspace.Variables)
{
var variable = decompiler.Variables.Values.FirstOrDefault(v => v.Name == varEntry.Key);
if (variable != null)
{
variable.Name = varEntry.Value.Name;
if (Enum.TryParse(varEntry.Value.Type, out var type))
{
variable.Type = type;
}
}
}
foreach (var funcEntry in workspace.Functions)
{
var function = decompiler.Functions.Values.FirstOrDefault(f => f.Name == funcEntry.Key);
if (function != null)
{
function.Name = funcEntry.Value.Name;
}
}
VariableList.Clear();
foreach (var variable in decompiler.Variables.Values)
{
VariableList.Add(new VariableViewModel
{
Address = variable.Address,
Name = variable.Name,
TypeName = variable.Type.ToString(),
Size = variable.Size,
IsRead = variable.IsRead,
IsWritten = variable.IsWritten,
Description = workspace.Variables.TryGetValue(variable.Name, out var varData) ?
varData.Description :
$"Location: {(variable.Address < 0x100 ? "Zero Page" : variable.Address < 0x800 ? "RAM" : "ROM")}"
});
}
FunctionList.Clear();
foreach (var function in decompiler.Functions.Values)
{
FunctionList.Add(new FunctionViewModel
{
Address = function.Address,
Name = function.Name,
InstructionCount = function.Instructions.Count,
VariableCount = function.VariablesAccessed.Count,
CalledFunctionCount = function.CalledFunctions.Count,
Description = workspace.Functions.TryGetValue(function.Name, out var funcData) ?
funcData.Description :
$"Entry point: {(romInfo?.EntryPoints.Contains(function.Address) ?? false ? "Yes" : "No")}"
});
}
}
}
}
else
{
MessageBox.Show($"The ROM file '{workspace.CurrentFilePath}' referenced in the workspace could not be found.",
"ROM File Not Found",
MessageBoxButton.OK,
MessageBoxImage.Warning);
}
if (workspace.RecentFiles != null)
{
RecentFiles = workspace.RecentFiles.Where(File.Exists).ToList();
SaveRecentFiles();
}
StatusText = $"Workspace '{Path.GetFileName(filePath)}' loaded";
}
catch (Exception ex)
{
MessageBox.Show($"Failed to load workspace: {ex.Message}",
"Error",
MessageBoxButton.OK,
MessageBoxImage.Error);
StatusText = "Failed to load workspace";
}
finally
{
IsProcessing = false;
}
}
///
/// Adds a project to the recent projects list
///
/// The project file path to add
private void AddRecentProject(string filePath)
{
try
{
if (!File.Exists(filePath))
return;
string projectName = (CurrentProject != null && CurrentProject.FilePath == filePath) ?
CurrentProject.Name : Path.GetFileNameWithoutExtension(filePath);
var projectInfo = new RecentProjectInfo
{
FilePath = filePath,
Name = projectName,
LastOpened = DateTime.Now
};
var updatedList = new List(RecentProjectsList);
updatedList.RemoveAll(p => p.FilePath.Equals(filePath, StringComparison.OrdinalIgnoreCase));
updatedList.Insert(0, projectInfo);
if (updatedList.Count > MAX_RECENT_ITEMS)
updatedList = updatedList.Take(MAX_RECENT_ITEMS).ToList();
RecentProjectsList = updatedList;
SaveRecentProjectsToSettings();
StatusText = $"Added '{projectName}' to recent projects";
}
catch (Exception ex)
{
Debug.WriteLine($"Error adding recent project: {ex.Message}");
}
}
///
/// Loads recent projects from application settings
///
private void LoadRecentProjects()
{
try
{
string appDataPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"NESDecompiler");
string filePath = Path.Combine(appDataPath, "recentprojects.json");
if (File.Exists(filePath))
{
string json = File.ReadAllText(filePath);
var projects = System.Text.Json.JsonSerializer.Deserialize>(json);
if (projects != null)
{
RecentProjectsList = projects.Where(p => File.Exists(p.FilePath)).ToList();
}
}
}
catch (Exception ex)
{
Debug.WriteLine($"Error loading recent projects: {ex.Message}");
RecentProjectsList = new List();
}
}
///
/// Saves recent projects to application settings
///
private void SaveRecentProjectsToSettings()
{
try
{
string appDataPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"NESDecompiler");
if (!Directory.Exists(appDataPath))
Directory.CreateDirectory(appDataPath);
string filePath = Path.Combine(appDataPath, "recentprojects.json");
string json = System.Text.Json.JsonSerializer.Serialize(RecentProjectsList);
File.WriteAllText(filePath, json);
}
catch (Exception ex)
{
Debug.WriteLine($"Error saving recent projects: {ex.Message}");
}
}
///
/// Opens a recent project
///
/// The project info to open
public void OpenRecentProject(RecentProjectInfo projectInfo)
{
try
{
if (projectInfo == null)
return;
if (!File.Exists(projectInfo.FilePath))
{
MessageBox.Show($"The project file '{projectInfo.FilePath}' could not be found.",
"File Not Found",
MessageBoxButton.OK,
MessageBoxImage.Error);
var updatedList = new List(RecentProjectsList);
updatedList.RemoveAll(p => p.FilePath.Equals(projectInfo.FilePath, StringComparison.OrdinalIgnoreCase));
RecentProjectsList = updatedList;
SaveRecentProjectsToSettings();
return;
}
if (HasUnsavedChanges)
{
var result = MessageBox.Show(
"You have unsaved changes. Do you want to save before opening another project?",
"Save Changes",
MessageBoxButton.YesNoCancel,
MessageBoxImage.Question);
if (result == MessageBoxResult.Cancel)
return;
if (result == MessageBoxResult.Yes)
SaveProject();
}
LoadProject(projectInfo.FilePath);
projectInfo.LastOpened = DateTime.Now;
AddRecentProject(projectInfo.FilePath);
}
catch (Exception ex)
{
MessageBox.Show($"Failed to open project: {ex.Message}",
"Error",
MessageBoxButton.OK,
MessageBoxImage.Error);
StatusText = "Failed to open project";
}
}
///
/// Saves the list of recent workspaces
///
private void SaveRecentWorkspaces()
{
try
{
string appDataPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"NESDecompiler");
if (!Directory.Exists(appDataPath))
Directory.CreateDirectory(appDataPath);
string filePath = Path.Combine(appDataPath, "recentworkspaces.json");
string json = System.Text.Json.JsonSerializer.Serialize(RecentWorkspaces);
File.WriteAllText(filePath, json);
}
catch (Exception ex)
{
MessageBox.Show($"Failed to save recent workspaces: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
///
/// Adds a workspace file to the recent workspaces list
///
/// The workspace file path to add
private void AddRecentWorkspace(string filePath)
{
try
{
var updatedList = new List(RecentWorkspaces);
updatedList.RemoveAll(p => p.Equals(filePath, StringComparison.OrdinalIgnoreCase));
updatedList.Insert(0, filePath);
if (updatedList.Count > MAX_RECENT_ITEMS)
updatedList = updatedList.Take(MAX_RECENT_ITEMS).ToList();
RecentWorkspaces = updatedList;
SaveRecentWorkspaces();
}
catch (Exception ex)
{
MessageBox.Show($"Failed to add recent workspace: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
///
/// Loads the list of recent workspaces
///
private void LoadRecentWorkspaces()
{
try
{
string appDataPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"NESDecompiler");
string filePath = Path.Combine(appDataPath, "recentworkspaces.json");
if (File.Exists(filePath))
{
string json = File.ReadAllText(filePath);
var workspaces = System.Text.Json.JsonSerializer.Deserialize>(json);
if (workspaces != null)
{
RecentWorkspaces = workspaces.Where(File.Exists).ToList();
}
}
}
catch (Exception ex)
{
MessageBox.Show($"Failed to load recent workspaces: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
RecentWorkspaces = new List();
}
}
///
/// Toggles between dark and light theme
///
private void ToggleTheme()
{
ThemeManager.Instance.ToggleTheme();
OnPropertyChanged(nameof(IsDarkTheme));
}
#endregion
#region INotifyPropertyChanged
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
}