using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; using Avalonia.Threading; using System; using Mesen.Debugger.Controls; using Mesen.Debugger.ViewModels; using Mesen.Interop; using System.ComponentModel; using Avalonia.Interactivity; using Mesen.Debugger.Utilities; using System.IO; using Mesen.Utilities; using Mesen.Config; using Mesen.Debugger.Labels; using System.Linq; using System.Collections.Generic; using Mesen.Localization; using Avalonia.Input; using Mesen.Debugger.Disassembly; namespace Mesen.Debugger.Windows { public class MemoryToolsWindow : MesenWindow, INotificationHandler { private HexEditor _editor; private MemoryToolsViewModel _model; private MemoryViewerFindWindow? _searchWnd; private Point _prevMousePos; private int _prevByteOffset; private DynamicTooltip? _prevTooltip; public MemoryToolsWindow() { InitializeComponent(); #if DEBUG this.AttachDevTools(); #endif _editor = this.GetControl("Hex"); _model = new MemoryToolsViewModel(_editor); DataContext = _model; if(Design.IsDesignMode) { return; } _model.Config.LoadWindowSettings(this); _editor.ByteUpdated += editor_ByteUpdated; _editor.PointerMoved += editor_PointerMoved; } public static void ShowInMemoryTools(MemoryType memType, int address) { MemoryToolsWindow wnd = DebugWindowManager.GetOrOpenDebugWindow(() => new MemoryToolsWindow()); wnd.SetCursorPosition(memType, address); } public void SetCursorPosition(MemoryType memType, int address) { if(_model.AvailableMemoryTypes.Contains(memType)) { _model.Config.MemoryType = memType; _editor.SetCursorPosition(address, scrollToTop: true); _editor.Focus(); } } protected override void OnClosing(WindowClosingEventArgs e) { base.OnClosing(e); _model.Config.SaveWindowSettings(this); ConfigManager.Config.Debug.HexEditor = _model.Config; } protected override void OnOpened(EventArgs e) { base.OnOpened(e); InitializeActions(); _editor.Focus(); } protected override void OnGotFocus(GotFocusEventArgs e) { base.OnGotFocus(e); if(FocusManager?.GetFocusedElement() == this) { //Focus on editor whenever the window itself is focused _editor.Focus(); } } private void InitializeComponent() { AvaloniaXamlLoader.Load(this); } private void OnSettingsClick(object sender, RoutedEventArgs e) { _model.Config.ShowOptionPanel = !_model.Config.ShowOptionPanel; } private void editor_PointerMoved(object? sender, PointerEventArgs e) { Point point = e.GetPosition(_editor); if(point == _prevMousePos) { return; } _prevMousePos = point; int byteOffset = _model.Config.ShowTooltips ? _editor.GetByteOffset(point) : -1; if(byteOffset >= 0) { if(byteOffset != _prevByteOffset) { CpuType cpuType = _model.Config.MemoryType.ToCpuType(); AddressInfo addr = new AddressInfo() { Address = byteOffset, Type = _model.Config.MemoryType }; AddressInfo relAddr; AddressInfo absAddr; if(addr.Type.IsRelativeMemory()) { relAddr = addr; absAddr = DebugApi.GetAbsoluteAddress(relAddr); } else { absAddr = addr; relAddr = DebugApi.GetRelativeAddress(absAddr, cpuType); } CodeLabel? label = LabelManager.GetLabel(addr); LocationInfo locInfo = new() { RelAddress = relAddr.Address >= 0 ? relAddr : null, AbsAddress = absAddr.Address >= 0 ? absAddr : null, Label = label, LabelAddressOffset = label?.Length > 1 && absAddr.Address >= label.Address ? (absAddr.Address - (int)label.Address) : null }; _prevTooltip = CodeTooltipHelper.GetCodeAddressTooltip(cpuType, locInfo, !addr.Type.IsRelativeMemory()); TooltipHelper.ShowTooltip(_editor, _prevTooltip, 15); } else { TooltipHelper.ShowTooltip(_editor, _prevTooltip, 15); } } else { _prevTooltip = null; TooltipHelper.HideTooltip(_editor); } _prevByteOffset = byteOffset; } private void editor_ByteUpdated(object? sender, ByteUpdatedEventArgs e) { if(e.Values != null) { DebugApi.SetMemoryValues(_model.Config.MemoryType, (uint)e.ByteOffset, e.Values, e.Values.Length); } else if(e.Value != null) { if(e.Length > 0) { byte[] data = new byte[e.Length]; Array.Fill(data, e.Value.Value); DebugApi.SetMemoryValues(_model.Config.MemoryType, (uint)e.ByteOffset, data, data.Length); } else { DebugApi.SetMemoryValue(_model.Config.MemoryType, (uint)e.ByteOffset, e.Value.Value); } } } private void InitializeActions() { DebugConfig cfg = ConfigManager.Config.Debug; _model.AddDisposables(DebugShortcutManager.CreateContextMenu(_editor, new ContextMenuAction[] { GetMarkSelectionAction(), new ContextMenuSeparator(), GetAddWatchAction(), GetEditBreakpointAction(), GetEditLabelAction(), new ContextMenuSeparator(), GetViewInDebuggerAction(), GetViewInMemoryAction(), new ContextMenuSeparator() { IsVisible = () => _model.Config.MemoryType.SupportsFreezeAddress() }, GetFreezeAction(ActionType.FreezeMemory, DebuggerShortcut.MemoryViewer_Freeze), GetFreezeAction(ActionType.UnfreezeMemory, DebuggerShortcut.MemoryViewer_Unfreeze), new ContextMenuSeparator(), new ContextMenuAction() { ActionType = ActionType.Undo, IsEnabled = () => DebugApi.HasUndoHistory(), Shortcut = () => cfg.Shortcuts.Get(DebuggerShortcut.Undo), OnClick = () => { if(DebugApi.HasUndoHistory()){ DebugApi.PerformUndo(); _editor.InvalidateVisual(); } } }, new ContextMenuSeparator(), new ContextMenuAction() { ActionType = ActionType.Copy, IsEnabled = () => _editor.SelectionLength > 0, OnClick = () => _editor.CopySelection(), Shortcut = () => cfg.Shortcuts.Get(DebuggerShortcut.Copy) }, new ContextMenuAction() { ActionType = ActionType.Paste, OnClick = () => _editor.PasteSelection(), Shortcut = () => cfg.Shortcuts.Get(DebuggerShortcut.Paste) }, new ContextMenuSeparator(), new ContextMenuAction() { ActionType = ActionType.SelectAll, OnClick = () => _editor.SelectAll(), Shortcut = () => cfg.Shortcuts.Get(DebuggerShortcut.SelectAll) }, })); _model.FileMenuItems = _model.AddDisposables(new List() { GetImportAction(), GetExportAction(), new ContextMenuSeparator(), SaveRomActionHelper.GetSaveRomAction(this), SaveRomActionHelper.GetSaveRomAsAction(this), SaveRomActionHelper.GetSaveEditsAsIpsAction(this), new ContextMenuSeparator(), GetResetAccessCountersAction(), new ContextMenuSeparator(), new ContextMenuAction() { ActionType = ActionType.LoadTblFile, OnClick = async () => { string? filename = await FileDialogHelper.OpenFile(null, this, FileDialogHelper.TblExt); if(filename != null) { string[] tblData = File.ReadAllLines(filename); _model.TblConverter = TblLoader.Load(tblData); DebugWorkspaceManager.Workspace.TblMappings = tblData; } } }, new ContextMenuAction() { ActionType = ActionType.ResetTblMappings, OnClick = () => { _model.TblConverter = null; DebugWorkspaceManager.Workspace.TblMappings = Array.Empty(); } }, new ContextMenuSeparator(), new ContextMenuAction() { ActionType = ActionType.Exit, OnClick = () => Close() } }); _model.ViewMenuItems = _model.AddDisposables(new List() { new ContextMenuAction() { ActionType = ActionType.ShowSettingsPanel, Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.ToggleSettingsPanel), IsSelected = () => _model.Config.ShowOptionPanel, OnClick = () => _model.Config.ShowOptionPanel = !_model.Config.ShowOptionPanel }, }); _model.SearchMenuItems = _model.AddDisposables(new List() { new ContextMenuAction() { ActionType = ActionType.GoToAddress, Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.GoToAddress), OnClick = async () => { MemoryType memType = _model.Config.MemoryType; int? address = await new GoToWindow(memType.ToCpuType(), memType, DebugApi.GetMemorySize(memType) - 1).ShowCenteredDialog(this); if(address != null) { _editor.SetCursorPosition(address.Value, scrollToTop: true); _editor.Focus(); } } }, new ContextMenuAction() { ActionType = ActionType.GoToAll, Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.GoToAll), OnClick = async () => { GoToDestination? dest = await GoToAllWindow.Open(this, _model.Config.MemoryType.ToCpuType(), GoToAllOptions.ShowOutOfScope, DebugWorkspaceManager.SymbolProvider); if(dest?.RelativeAddress?.Type == _model.Config.MemoryType) { SetCursorPosition(dest.RelativeAddress.Value.Type, dest.RelativeAddress.Value.Address); } else if(dest?.AbsoluteAddress != null) { SetCursorPosition(dest.AbsoluteAddress.Value.Type, dest.AbsoluteAddress.Value.Address); } } }, new ContextMenuSeparator(), new ContextMenuAction() { ActionType = ActionType.Find, Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.Find), OnClick = () => OpenSearchWindow() }, new ContextMenuAction() { ActionType = ActionType.FindPrev, Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.FindPrev), OnClick = () => Find(SearchDirection.Backward) }, new ContextMenuAction() { ActionType = ActionType.FindNext, Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.FindNext), OnClick = () => Find(SearchDirection.Forward) }, new ContextMenuSeparator(), new ContextMenuAction() { ActionType = ActionType.NavigateTo, SubActions = new List { new ContextMenuAction() { ActionType = ActionType.GoToPrevRead, Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.MemoryViewer_GoToPrevRead), OnClick = () => _model.NavigateTo(NavType.PrevRead) }, new ContextMenuAction() { ActionType = ActionType.GoToNextRead, Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.MemoryViewer_GoToNextRead), OnClick = () => _model.NavigateTo(NavType.NextRead) }, new ContextMenuAction() { ActionType = ActionType.GoToPrevWrite, Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.MemoryViewer_GoToPrevWrite), OnClick = () => _model.NavigateTo(NavType.PrevWrite) }, new ContextMenuAction() { ActionType = ActionType.GoToNextWrite, Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.MemoryViewer_GoToNextWrite), OnClick = () => _model.NavigateTo(NavType.NextWrite) }, new ContextMenuAction() { ActionType = ActionType.GoToPrevExec, Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.MemoryViewer_GoToPrevExec), OnClick = () => _model.NavigateTo(NavType.PrevExec) }, new ContextMenuAction() { ActionType = ActionType.GoToNextExec, Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.MemoryViewer_GoToNextExec), OnClick = () => _model.NavigateTo(NavType.NextExec) }, new ContextMenuSeparator(), new ContextMenuAction() { ActionType = ActionType.GoToPrevCode, IsEnabled = () => _model.Config.MemoryType.SupportsCdl(), Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.MemoryViewer_GoToPrevCode), OnClick = () => _model.NavigateTo(NavType.PrevCode) }, new ContextMenuAction() { ActionType = ActionType.GoToNextCode, IsEnabled = () => _model.Config.MemoryType.SupportsCdl(), Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.MemoryViewer_GoToNextCode), OnClick = () => _model.NavigateTo(NavType.NextCode) }, new ContextMenuAction() { ActionType = ActionType.GoToPrevData, IsEnabled = () => _model.Config.MemoryType.SupportsCdl(), Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.MemoryViewer_GoToPrevData), OnClick = () => _model.NavigateTo(NavType.PrevData) }, new ContextMenuAction() { ActionType = ActionType.GoToNextData, IsEnabled = () => _model.Config.MemoryType.SupportsCdl(), Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.MemoryViewer_GoToNextData), OnClick = () => _model.NavigateTo(NavType.NextData) }, new ContextMenuAction() { ActionType = ActionType.GoToPrevUnknown, IsEnabled = () => _model.Config.MemoryType.SupportsCdl(), Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.MemoryViewer_GoToPrevUnknown), OnClick = () => _model.NavigateTo(NavType.PrevUnknown) }, new ContextMenuAction() { ActionType = ActionType.GoToNextUnknown, IsEnabled = () => _model.Config.MemoryType.SupportsCdl(), Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.MemoryViewer_GoToNextUnknown), OnClick = () => _model.NavigateTo(NavType.NextUnknown) }, } } }); _model.ToolbarItems = _model.AddDisposables(new List() { GetImportAction(), GetExportAction(), new ContextMenuSeparator(), GetResetAccessCountersAction() }); DebugShortcutManager.RegisterActions(this, _model.FileMenuItems); DebugShortcutManager.RegisterActions(this, _model.ViewMenuItems); DebugShortcutManager.RegisterActions(this, _model.SearchMenuItems); } private void OpenSearchWindow() { if(_searchWnd == null) { _searchWnd = new MemoryViewerFindWindow(_model.Search, _model); _searchWnd.Closed += (s, e) => _searchWnd = null; _searchWnd.ShowCenteredWithParent(this); } else { _searchWnd.BringToFront(); } } private void Find(SearchDirection direction) { if(!_model.Search.IsValid) { OpenSearchWindow(); } else { _model.Find(direction); } } private ContextMenuAction GetViewInMemoryAction() { AddressInfo? GetAddress() { MemoryType memType = _model.Config.MemoryType; if(_editor.SelectionLength <= 1) { if(memType.IsRelativeMemory()) { AddressInfo absAddr = DebugApi.GetAbsoluteAddress(new AddressInfo() { Address = _model.SelectionStart, Type = memType }); return absAddr.Address >= 0 ? absAddr : null; } else { AddressInfo relAddr = DebugApi.GetRelativeAddress(new AddressInfo() { Address = _model.SelectionStart, Type = memType }, memType.ToCpuType()); return relAddr.Address >= 0 ? relAddr : null; } } return null; } return new ContextMenuAction() { ActionType = ActionType.ViewInMemoryViewer, DynamicText = () => ResourceHelper.GetMessage("ViewInAction", (GetAddress() is AddressInfo addr) ? ResourceHelper.GetEnumText(addr.Type) : "[n/a]"), IsVisible = () => GetAddress() != null, HintText = () => "$" + _editor.SelectionStart.ToString("X2"), OnClick = () => { if(GetAddress() is AddressInfo addr) { SetCursorPosition(addr.Type, addr.Address); } }, Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.MemoryViewer_ViewInMemoryType) }; } private ContextMenuAction GetViewInDebuggerAction() { AddressInfo? GetAddress() { MemoryType memType = _model.Config.MemoryType; if(_editor.SelectionLength <= 1 && !memType.IsPpuMemory()) { AddressInfo relAddr; if(!memType.IsRelativeMemory()) { relAddr = DebugApi.GetRelativeAddress(new AddressInfo() { Address = _model.SelectionStart, Type = memType }, memType.ToCpuType()); return relAddr.Address >= 0 ? relAddr : null; } else { return new AddressInfo() { Address = _model.SelectionStart, Type = memType }; } } return null; } return new ContextMenuAction() { ActionType = ActionType.ViewInDebugger, Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.MemoryViewer_ViewInDebugger), IsEnabled = () => GetAddress() != null, HintText = () => "$" + _editor.SelectionStart.ToString("X2"), OnClick = () => { AddressInfo? relAddr = GetAddress(); if(relAddr?.Address >= 0) { CpuType cpuType = relAddr.Value.Type.ToCpuType(); DebuggerWindow.OpenWindowAtAddress(cpuType, relAddr.Value.Address); } } }; } private ContextMenuAction GetResetAccessCountersAction() { return new ContextMenuAction() { ActionType = ActionType.ResetAccessCounters, Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.MemoryViewer_ResetAccessCounters), OnClick = () => { DebugApi.ResetMemoryAccessCounts(); _editor.InvalidateVisual(); } }; } private ContextMenuAction GetImportAction() { return new ContextMenuAction() { ActionType = ActionType.Import, Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.MemoryViewer_Import), IsEnabled = () => !_model.Config.MemoryType.IsRelativeMemory(), OnClick = () => Import() }; } private ContextMenuAction GetExportAction() { return new ContextMenuAction() { ActionType = ActionType.Export, Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.MemoryViewer_Export), OnClick = () => Export() }; } public async void Import() { string? filename = await FileDialogHelper.OpenFile(ConfigManager.DebuggerFolder, this, FileDialogHelper.DmpExt); if(filename != null) { byte[]? dmpData = FileHelper.ReadAllBytes(filename); if(dmpData != null) { DebugApi.SetMemoryState(_model.Config.MemoryType, dmpData, dmpData.Length); } } } public async void Export() { string name = EmuApi.GetRomInfo().GetRomName() + " - " + _model.Config.MemoryType.ToString() + ".dmp"; string? filename = await FileDialogHelper.SaveFile(ConfigManager.DebuggerFolder, name, this, FileDialogHelper.DmpExt); if(filename != null) { FileHelper.WriteAllBytes(filename, DebugApi.GetMemoryState(_model.Config.MemoryType)); } } private ContextMenuAction GetEditLabelAction() { AddressInfo? GetAddress(MemoryType memType, int address) { if(memType.IsRelativeMemory()) { AddressInfo relAddress = new AddressInfo() { Address = address, Type = memType }; AddressInfo absAddress = DebugApi.GetAbsoluteAddress(relAddress); return absAddress.Address >= 0 && absAddress.Type.SupportsLabels() ? absAddress : null; } else { return memType.SupportsLabels() ? new AddressInfo() { Address = address, Type = memType } : null; } } return new ContextMenuAction() { ActionType = ActionType.EditLabel, HintText = () => "$" + _editor.SelectionStart.ToString("X2"), Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.MemoryViewer_EditLabel), IsEnabled = () => GetAddress(_model.Config.MemoryType, _editor.SelectionStart) != null, OnClick = () => { AddressInfo? addr = GetAddress(_model.Config.MemoryType, _editor.SelectionStart); if(addr == null) { return; } CodeLabel? label = LabelManager.GetLabel((uint)addr.Value.Address, addr.Value.Type); if(label == null) { int length = 1; if(_editor.SelectionLength > 1) { int end = addr.Value.Address + _editor.SelectionLength; int memSize = DebugApi.GetMemorySize(addr.Value.Type); if(end >= memSize) { end = memSize; } length = end - addr.Value.Address; } label = new CodeLabel() { Address = (uint)addr.Value.Address, MemoryType = addr.Value.Type, Length = (uint)length }; } LabelEditWindow.EditLabel(label.MemoryType.ToCpuType(), this, label); } }; } private ContextMenuAction GetAddWatchAction() { return new ContextMenuAction() { ActionType = ActionType.AddWatch, HintText = () => GetAddressRange(), Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.MemoryViewer_AddToWatch), IsEnabled = () => _model.Config.MemoryType.SupportsWatch(), OnClick = () => { string[] toAdd = Enumerable.Range(_editor.SelectionStart, Math.Max(1, _editor.SelectionLength)).Select((num) => $"[${num.ToString("X2")}]").ToArray(); WatchManager.GetWatchManager(_model.Config.MemoryType.ToCpuType()).AddWatch(toAdd); } }; } private ContextMenuAction GetEditBreakpointAction() { return new ContextMenuAction() { ActionType = ActionType.EditBreakpoint, HintText = () => GetAddressRange(), Shortcut = () => ConfigManager.Config.Debug.Shortcuts.Get(DebuggerShortcut.MemoryViewer_EditBreakpoint), OnClick = async () => { uint startAddress = (uint)_editor.SelectionStart; uint endAddress = (uint)(_editor.SelectionStart + Math.Max(1, _editor.SelectionLength) - 1); MemoryType memType = _model.Config.MemoryType; Breakpoint? bp = BreakpointManager.GetMatchingBreakpoint(startAddress, endAddress, memType); CpuType cpuType = memType.ToCpuType(); if(bp == null) { bp = new Breakpoint() { MemoryType = memType, CpuType = cpuType, StartAddress = startAddress, EndAddress = endAddress, BreakOnWrite = true, BreakOnRead = true }; if(bp.SupportsExec) { bp.BreakOnExec = true; } } bool result = await BreakpointEditWindow.EditBreakpointAsync(bp, this); if(result && DebugWindowManager.GetDebugWindow(x => x.CpuType == cpuType) == null) { DebuggerWindow.GetOrOpenWindow(cpuType); } } }; } private ContextMenuAction GetFreezeAction(ActionType action, DebuggerShortcut shortcut) { DebugConfig cfg = ConfigManager.Config.Debug; return new ContextMenuAction() { ActionType = action, HintText = () => GetAddressRange(), IsVisible = () => _model.Config.MemoryType.SupportsFreezeAddress(), OnClick = () => { UInt32 start = (UInt32)_editor.SelectionStart; UInt32 end = (UInt32)(_editor.SelectionStart + Math.Max(1, _editor.SelectionLength) - 1); DebugApi.UpdateFrozenAddresses(_model.Config.MemoryType.ToCpuType(), start, end, action == ActionType.FreezeMemory); }, Shortcut = () => cfg.Shortcuts.Get(shortcut) }; } private ContextMenuAction GetMarkSelectionAction() { return MarkSelectionHelper.GetAction( () => _model.Config.MemoryType, () => _model.SelectionStart, () => _model.SelectionStart + Math.Max(0, _model.SelectionLength - 1), () => { } ); } private string GetAddressRange() { string address = "$" + _editor.SelectionStart.ToString("X2"); if(_editor.SelectionLength > 1) { address += "-$" + (_editor.SelectionStart + _editor.SelectionLength - 1).ToString("X2"); } return address; } public void ProcessNotification(NotificationEventArgs e) { switch(e.NotificationType) { case ConsoleNotificationType.CodeBreak: case ConsoleNotificationType.StateLoaded: Dispatcher.UIThread.Post(() => { _editor.InvalidateVisual(); }); break; case ConsoleNotificationType.PpuFrameDone: if(!ToolRefreshHelper.LimitFps(this, 80)) { Dispatcher.UIThread.Post(() => { _editor.InvalidateVisual(); }); } break; case ConsoleNotificationType.GameLoaded: Dispatcher.UIThread.Post(() => { _model.UpdateAvailableMemoryTypes(); }); break; } } } }