mirror of https://github.com/PCSX2/pcsx2.git
Debugger: Adds loading breakpoints/saved addresses from settings
Adds `/inis/debuggersettings/` settings folder to contain settings specifically for the debugger. Adds functionality to manually save (to settings) Breakpoints/Saved addresses and automatically load them upon launching the debugger.
This commit is contained in:
parent
b402f6a404
commit
52ccc609cd
|
@ -80,6 +80,7 @@ oprofile_data/
|
|||
/bin/gamesettings
|
||||
/bin/help
|
||||
/bin/inis
|
||||
/bin/inis/debuggersettings
|
||||
/bin/logs
|
||||
/bin/memcards
|
||||
/bin/plugins
|
||||
|
|
|
@ -149,6 +149,8 @@ target_sources(pcsx2-qt PRIVATE
|
|||
Debugger/CpuWidget.cpp
|
||||
Debugger/CpuWidget.h
|
||||
Debugger/CpuWidget.ui
|
||||
Debugger/DebuggerSettingsManager.cpp
|
||||
Debugger/DebuggerSettingsManager.h
|
||||
Debugger/DebuggerWindow.cpp
|
||||
Debugger/DebuggerWindow.h
|
||||
Debugger/DebuggerWindow.ui
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
#include "Models/BreakpointModel.h"
|
||||
#include "Models/ThreadModel.h"
|
||||
#include "Models/SavedAddressesModel.h"
|
||||
#include "Debugger/DebuggerSettingsManager.h"
|
||||
|
||||
#include "DebugTools/DebugInterface.h"
|
||||
#include "DebugTools/Breakpoints.h"
|
||||
|
@ -45,6 +46,15 @@ CpuWidget::CpuWidget(QWidget* parent, DebugInterface& cpu)
|
|||
m_ui.setupUi(this);
|
||||
|
||||
connect(g_emu_thread, &EmuThread::onVMPaused, this, &CpuWidget::onVMPaused);
|
||||
connect(g_emu_thread, &EmuThread::onGameChanged, [this](const QString& title) {
|
||||
if (title.isEmpty())
|
||||
return;
|
||||
// Don't overwrite users BPs/Saved Addresses unless they have a clean state.
|
||||
if (m_bpModel.rowCount() == 0)
|
||||
DebuggerSettingsManager::loadGameSettings(&m_bpModel);
|
||||
if (m_savedAddressesModel.rowCount() == 0)
|
||||
DebuggerSettingsManager::loadGameSettings(&m_savedAddressesModel);
|
||||
});
|
||||
|
||||
connect(m_ui.registerWidget, &RegisterWidget::gotoInDisasm, m_ui.disassemblyWidget, &DisassemblyWidget::gotoAddress);
|
||||
connect(m_ui.memoryviewWidget, &MemoryViewWidget::gotoInDisasm, m_ui.disassemblyWidget, &DisassemblyWidget::gotoAddress);
|
||||
|
@ -143,9 +153,12 @@ CpuWidget::CpuWidget(QWidget* parent, DebugInterface& cpu)
|
|||
m_ui.savedAddressesList->horizontalHeader()->setSectionResizeMode(i++, mode);
|
||||
}
|
||||
QTableView* savedAddressesTableView = m_ui.savedAddressesList;
|
||||
connect(m_ui.savedAddressesList->model(), &QAbstractItemModel::dataChanged, [savedAddressesTableView](const QModelIndex& topLeft) {
|
||||
connect(m_ui.savedAddressesList->model(), &QAbstractItemModel::dataChanged, [savedAddressesTableView](const QModelIndex& topLeft) {
|
||||
savedAddressesTableView->resizeColumnToContents(topLeft.column());
|
||||
});
|
||||
|
||||
DebuggerSettingsManager::loadGameSettings(&m_bpModel);
|
||||
DebuggerSettingsManager::loadGameSettings(&m_savedAddressesModel);
|
||||
}
|
||||
|
||||
CpuWidget::~CpuWidget() = default;
|
||||
|
@ -301,46 +314,63 @@ void CpuWidget::onBPListDoubleClicked(const QModelIndex& index)
|
|||
|
||||
void CpuWidget::onBPListContextMenu(QPoint pos)
|
||||
{
|
||||
if (!m_cpu.isAlive())
|
||||
return;
|
||||
|
||||
QMenu* contextMenu = new QMenu(tr("Breakpoint List Context Menu"), m_ui.breakpointList);
|
||||
|
||||
QAction* newAction = new QAction(tr("New"), m_ui.breakpointList);
|
||||
connect(newAction, &QAction::triggered, this, &CpuWidget::contextBPListNew);
|
||||
contextMenu->addAction(newAction);
|
||||
|
||||
const QItemSelectionModel* selModel = m_ui.breakpointList->selectionModel();
|
||||
|
||||
if (selModel->hasSelection())
|
||||
if (m_cpu.isAlive())
|
||||
{
|
||||
QAction* editAction = new QAction(tr("Edit"), m_ui.breakpointList);
|
||||
connect(editAction, &QAction::triggered, this, &CpuWidget::contextBPListEdit);
|
||||
contextMenu->addAction(editAction);
|
||||
|
||||
if (selModel->selectedIndexes().count() == 1)
|
||||
QAction* newAction = new QAction(tr("New"), m_ui.breakpointList);
|
||||
connect(newAction, &QAction::triggered, this, &CpuWidget::contextBPListNew);
|
||||
contextMenu->addAction(newAction);
|
||||
|
||||
const QItemSelectionModel* selModel = m_ui.breakpointList->selectionModel();
|
||||
|
||||
if (selModel->hasSelection())
|
||||
{
|
||||
QAction* copyAction = new QAction(tr("Copy"), m_ui.breakpointList);
|
||||
connect(copyAction, &QAction::triggered, this, &CpuWidget::contextBPListCopy);
|
||||
contextMenu->addAction(copyAction);
|
||||
}
|
||||
QAction* editAction = new QAction(tr("Edit"), m_ui.breakpointList);
|
||||
connect(editAction, &QAction::triggered, this, &CpuWidget::contextBPListEdit);
|
||||
contextMenu->addAction(editAction);
|
||||
|
||||
QAction* deleteAction = new QAction(tr("Delete"), m_ui.breakpointList);
|
||||
connect(deleteAction, &QAction::triggered, this, &CpuWidget::contextBPListDelete);
|
||||
contextMenu->addAction(deleteAction);
|
||||
if (selModel->selectedIndexes().count() == 1)
|
||||
{
|
||||
QAction* copyAction = new QAction(tr("Copy"), m_ui.breakpointList);
|
||||
connect(copyAction, &QAction::triggered, this, &CpuWidget::contextBPListCopy);
|
||||
contextMenu->addAction(copyAction);
|
||||
}
|
||||
|
||||
QAction* deleteAction = new QAction(tr("Delete"), m_ui.breakpointList);
|
||||
connect(deleteAction, &QAction::triggered, this, &CpuWidget::contextBPListDelete);
|
||||
contextMenu->addAction(deleteAction);
|
||||
}
|
||||
}
|
||||
|
||||
contextMenu->addSeparator();
|
||||
QAction* actionExport = new QAction(tr("Copy all as CSV"), m_ui.breakpointList);
|
||||
connect(actionExport, &QAction::triggered, [this]() {
|
||||
// It's important to use the Export Role here to allow pasting to be translation agnostic
|
||||
QGuiApplication::clipboard()->setText(QtUtils::AbstractItemModelToCSV(m_ui.breakpointList->model(), BreakpointModel::ExportRole, true));
|
||||
});
|
||||
contextMenu->addAction(actionExport);
|
||||
if (m_bpModel.rowCount() > 0)
|
||||
{
|
||||
QAction* actionExport = new QAction(tr("Copy all as CSV"), m_ui.breakpointList);
|
||||
connect(actionExport, &QAction::triggered, [this]() {
|
||||
// It's important to use the Export Role here to allow pasting to be translation agnostic
|
||||
QGuiApplication::clipboard()->setText(QtUtils::AbstractItemModelToCSV(m_ui.breakpointList->model(), BreakpointModel::ExportRole, true));
|
||||
});
|
||||
contextMenu->addAction(actionExport);
|
||||
}
|
||||
|
||||
QAction* actionImport = new QAction(tr("Paste from CSV"), m_ui.breakpointList);
|
||||
connect(actionImport, &QAction::triggered, this, &CpuWidget::contextBPListPasteCSV);
|
||||
contextMenu->addAction(actionImport);
|
||||
if (m_cpu.isAlive())
|
||||
{
|
||||
QAction* actionImport = new QAction(tr("Paste from CSV"), m_ui.breakpointList);
|
||||
connect(actionImport, &QAction::triggered, this, &CpuWidget::contextBPListPasteCSV);
|
||||
contextMenu->addAction(actionImport);
|
||||
|
||||
QAction* actionLoad = new QAction(tr("Load from Settings"), m_ui.breakpointList);
|
||||
connect(actionLoad, &QAction::triggered, [this]() {
|
||||
m_bpModel.clear();
|
||||
DebuggerSettingsManager::loadGameSettings(&m_bpModel);
|
||||
});
|
||||
contextMenu->addAction(actionLoad);
|
||||
|
||||
QAction* actionSave = new QAction(tr("Save to Settings"), m_ui.breakpointList);
|
||||
connect(actionSave, &QAction::triggered, this, &CpuWidget::saveBreakpointsToDebuggerSettings);
|
||||
contextMenu->addAction(actionSave);
|
||||
}
|
||||
|
||||
contextMenu->popup(m_ui.breakpointList->viewport()->mapToGlobal(pos));
|
||||
}
|
||||
|
@ -407,7 +437,7 @@ void CpuWidget::contextBPListPasteCSV()
|
|||
// In order to handle text with commas in them we must wrap values in quotes to mark
|
||||
// where a value starts and end so that text commas aren't identified as delimiters.
|
||||
// So matches each quote pair, parse it out, and removes the quotes to get the value.
|
||||
QRegularExpression eachQuotePair(R"("([^"]|\\.)*")");
|
||||
QRegularExpression eachQuotePair(R"("([^"]|\\.)*")");
|
||||
QRegularExpressionMatchIterator it = eachQuotePair.globalMatch(line);
|
||||
while (it.hasNext())
|
||||
{
|
||||
|
@ -415,98 +445,7 @@ void CpuWidget::contextBPListPasteCSV()
|
|||
QString matchedValue = match.captured(0);
|
||||
fields << matchedValue.mid(1, matchedValue.length() - 2);
|
||||
}
|
||||
|
||||
if (fields.size() != BreakpointModel::BreakpointColumns::COLUMN_COUNT)
|
||||
{
|
||||
Console.WriteLn("Debugger CSV Import: Invalid number of columns, skipping");
|
||||
continue;
|
||||
}
|
||||
|
||||
bool ok;
|
||||
const int type = fields[BreakpointModel::BreakpointColumns::TYPE].toUInt(&ok);
|
||||
if (!ok)
|
||||
{
|
||||
Console.WriteLn("Debugger CSV Import: Failed to parse type '%s', skipping", fields[BreakpointModel::BreakpointColumns::TYPE].toUtf8().constData());
|
||||
continue;
|
||||
}
|
||||
|
||||
// This is how we differentiate between breakpoints and memchecks
|
||||
if (type == MEMCHECK_INVALID)
|
||||
{
|
||||
BreakPoint bp;
|
||||
|
||||
// Address
|
||||
bp.addr = fields[BreakpointModel::BreakpointColumns::OFFSET].toUInt(&ok, 16);
|
||||
if (!ok)
|
||||
{
|
||||
Console.WriteLn("Debugger CSV Import: Failed to parse address '%s', skipping", fields[BreakpointModel::BreakpointColumns::OFFSET].toUtf8().constData());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Condition
|
||||
if (!fields[BreakpointModel::BreakpointColumns::CONDITION].isEmpty())
|
||||
{
|
||||
PostfixExpression expr;
|
||||
bp.hasCond = true;
|
||||
bp.cond.debug = &m_cpu;
|
||||
|
||||
if (!m_cpu.initExpression(fields[BreakpointModel::BreakpointColumns::CONDITION].toUtf8().constData(), expr))
|
||||
{
|
||||
Console.WriteLn("Debugger CSV Import: Failed to parse cond '%s', skipping", fields[BreakpointModel::BreakpointColumns::CONDITION].toUtf8().constData());
|
||||
continue;
|
||||
}
|
||||
bp.cond.expression = expr;
|
||||
strncpy(&bp.cond.expressionString[0], fields[BreakpointModel::BreakpointColumns::CONDITION].toUtf8().constData(), sizeof(bp.cond.expressionString));
|
||||
}
|
||||
|
||||
// Enabled
|
||||
bp.enabled = fields[BreakpointModel::BreakpointColumns::ENABLED].toUInt(&ok);
|
||||
if (!ok)
|
||||
{
|
||||
Console.WriteLn("Debugger CSV Import: Failed to parse enable flag '%s', skipping", fields[BreakpointModel::BreakpointColumns::ENABLED].toUtf8().constData());
|
||||
continue;
|
||||
}
|
||||
|
||||
m_bpModel.insertBreakpointRows(0, 1, {bp});
|
||||
}
|
||||
else
|
||||
{
|
||||
MemCheck mc;
|
||||
// Mode
|
||||
if (type >= MEMCHECK_INVALID)
|
||||
{
|
||||
Console.WriteLn("Debugger CSV Import: Failed to parse cond type '%s', skipping", fields [BreakpointModel::BreakpointColumns::TYPE].toUtf8().constData());
|
||||
continue;
|
||||
}
|
||||
mc.cond = static_cast<MemCheckCondition>(type);
|
||||
|
||||
// Address
|
||||
mc.start = fields[BreakpointModel::BreakpointColumns::OFFSET].toUInt(&ok, 16);
|
||||
if (!ok)
|
||||
{
|
||||
Console.WriteLn("Debugger CSV Import: Failed to parse address '%s', skipping", fields[BreakpointModel::BreakpointColumns::OFFSET].toUtf8().constData());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Size
|
||||
mc.end = fields[BreakpointModel::BreakpointColumns::SIZE_LABEL].toUInt(&ok) + mc.start;
|
||||
if (!ok)
|
||||
{
|
||||
Console.WriteLn("Debugger CSV Import: Failed to parse length '%s', skipping", fields[BreakpointModel::BreakpointColumns::SIZE_LABEL].toUtf8().constData());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Result
|
||||
const int result = fields [BreakpointModel::BreakpointColumns::ENABLED].toUInt(&ok);
|
||||
if (!ok)
|
||||
{
|
||||
Console.WriteLn("Debugger CSV Import: Failed to parse result flag '%s', skipping", fields [BreakpointModel::BreakpointColumns::ENABLED].toUtf8().constData());
|
||||
continue;
|
||||
}
|
||||
mc.result = static_cast<MemCheckResult>(result);
|
||||
|
||||
m_bpModel.insertBreakpointRows(0, 1, {mc});
|
||||
}
|
||||
m_bpModel.loadBreakpointFromFieldList(fields);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -561,7 +500,19 @@ void CpuWidget::onSavedAddressesListContextMenu(QPoint pos)
|
|||
connect(actionImportCSV, &QAction::triggered, this, &CpuWidget::contextSavedAddressesListPasteCSV);
|
||||
contextMenu->addAction(actionImportCSV);
|
||||
|
||||
contextMenu->popup(m_ui.savedAddressesList->viewport()->mapToGlobal(pos));
|
||||
if (m_cpu.isAlive())
|
||||
{
|
||||
QAction* actionLoad = new QAction(tr("Load from Settings"), m_ui.savedAddressesList);
|
||||
connect(actionLoad, &QAction::triggered, [this]() {
|
||||
m_savedAddressesModel.clear();
|
||||
DebuggerSettingsManager::loadGameSettings(&m_savedAddressesModel);
|
||||
});
|
||||
contextMenu->addAction(actionLoad);
|
||||
|
||||
QAction* actionSave = new QAction(tr("Save to Settings"), m_ui.savedAddressesList);
|
||||
connect(actionSave, &QAction::triggered, this, &CpuWidget::saveSavedAddressesToDebuggerSettings);
|
||||
contextMenu->addAction(actionSave);
|
||||
}
|
||||
|
||||
if (isIndexValid)
|
||||
{
|
||||
|
@ -571,6 +522,8 @@ void CpuWidget::onSavedAddressesListContextMenu(QPoint pos)
|
|||
});
|
||||
contextMenu->addAction(deleteAction);
|
||||
}
|
||||
|
||||
contextMenu->popup(m_ui.savedAddressesList->viewport()->mapToGlobal(pos));
|
||||
}
|
||||
|
||||
void CpuWidget::contextSavedAddressesListPasteCSV()
|
||||
|
@ -594,24 +547,7 @@ void CpuWidget::contextSavedAddressesListPasteCSV()
|
|||
fields << matchedValue.mid(1, matchedValue.length() - 2);
|
||||
}
|
||||
|
||||
if (fields.size() != SavedAddressesModel::HeaderColumns::COLUMN_COUNT)
|
||||
{
|
||||
Console.WriteLn("Debugger CSV Import: Invalid number of columns, skipping");
|
||||
continue;
|
||||
}
|
||||
|
||||
bool ok;
|
||||
const u32 address = fields[SavedAddressesModel::HeaderColumns::ADDRESS].toUInt(&ok, 16);
|
||||
if (!ok)
|
||||
{
|
||||
Console.WriteLn("Debugger CSV Import: Failed to parse address '%s', skipping", fields[SavedAddressesModel::HeaderColumns::ADDRESS].toUtf8().constData());
|
||||
continue;
|
||||
}
|
||||
|
||||
const QString label = fields[SavedAddressesModel::HeaderColumns::LABEL];
|
||||
const QString description = fields[SavedAddressesModel::HeaderColumns::DESCRIPTION];
|
||||
const SavedAddressesModel::SavedAddress importedAddress = {address, label, description};
|
||||
m_savedAddressesModel.addRow(importedAddress);
|
||||
m_savedAddressesModel.loadSavedAddressFromFieldList(fields);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1392,3 +1328,12 @@ void CpuWidget::loadSearchResults()
|
|||
}
|
||||
}
|
||||
|
||||
void CpuWidget::saveBreakpointsToDebuggerSettings()
|
||||
{
|
||||
DebuggerSettingsManager::saveGameSettings(&m_bpModel);
|
||||
}
|
||||
|
||||
void CpuWidget::saveSavedAddressesToDebuggerSettings()
|
||||
{
|
||||
DebuggerSettingsManager::saveGameSettings(&m_savedAddressesModel);
|
||||
}
|
||||
|
|
|
@ -118,6 +118,9 @@ public slots:
|
|||
void contextRemoveSearchResult();
|
||||
void onListSearchResultsContextMenu(QPoint pos);
|
||||
|
||||
void saveBreakpointsToDebuggerSettings();
|
||||
void saveSavedAddressesToDebuggerSettings();
|
||||
|
||||
private:
|
||||
std::vector<QTableWidget*> m_registerTableViews;
|
||||
std::vector<u32> m_searchResults;
|
||||
|
|
|
@ -0,0 +1,164 @@
|
|||
// SPDX-FileCopyrightText: 2002-2024 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: LGPL-3.0+
|
||||
|
||||
#include "DebuggerSettingsManager.h"
|
||||
|
||||
#include <QtCore/QJsonDocument>
|
||||
#include <QtCore/QJsonObject>
|
||||
#include <QtCore/QJsonArray>
|
||||
#include <QtCore/QFile>
|
||||
|
||||
#include "common/Console.h"
|
||||
#include "fmt/core.h"
|
||||
#include "VMManager.h"
|
||||
#include "Models/BreakpointModel.h"
|
||||
|
||||
std::mutex DebuggerSettingsManager::writeLock;
|
||||
const QString DebuggerSettingsManager::settingsFileVersion = "0.00";
|
||||
|
||||
QJsonObject DebuggerSettingsManager::loadGameSettingsJSON() {
|
||||
std::string path = VMManager::GetDebuggerSettingsFilePathForCurrentGame();
|
||||
QFile file(QString::fromStdString(path));
|
||||
if (!file.open(QIODevice::ReadOnly))
|
||||
{
|
||||
Console.WriteLnFmt("Debugger Settings Manager: No Debugger Settings file found for game at: '{}'", path);
|
||||
return QJsonObject();
|
||||
}
|
||||
QByteArray fileContent = file.readAll();
|
||||
file.close();
|
||||
|
||||
const QJsonDocument jsonDoc(QJsonDocument::fromJson(fileContent));
|
||||
if (jsonDoc.isNull() || !jsonDoc.isObject())
|
||||
{
|
||||
Console.WriteLnFmt("Debugger Settings Manager: Failed to load contents of settings file for file at: '{}'", path);
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
return jsonDoc.object();
|
||||
}
|
||||
|
||||
void DebuggerSettingsManager::writeJSONToPath(std::string path, QJsonDocument jsonDocument)
|
||||
{
|
||||
QFile file(QString::fromStdString(path));
|
||||
if (!file.open(QIODevice::WriteOnly))
|
||||
{
|
||||
Console.WriteLnFmt("Debugger Settings Manager: Failed to write Debugger Settings file to path: '{}'", path);
|
||||
return;
|
||||
}
|
||||
file.write(jsonDocument.toJson(QJsonDocument::Indented));
|
||||
file.close();
|
||||
}
|
||||
|
||||
void DebuggerSettingsManager::loadGameSettings(BreakpointModel* bpModel)
|
||||
{
|
||||
const std::string path = VMManager::GetDebuggerSettingsFilePathForCurrentGame();
|
||||
if (path.empty())
|
||||
return;
|
||||
|
||||
const QJsonValue breakpointsValue = loadGameSettingsJSON().value("Breakpoints");
|
||||
const QString valueToLoad = breakpointsValue.toString();
|
||||
if (breakpointsValue.isUndefined() || !breakpointsValue.isArray())
|
||||
{
|
||||
Console.WriteLnFmt("Debugger Settings Manager: Failed to read Breakpoints array from settings file: '{}'", path);
|
||||
return;
|
||||
}
|
||||
|
||||
const QJsonArray breakpointsArray = breakpointsValue.toArray();
|
||||
for (u32 row = 0; row < breakpointsArray.size(); row++)
|
||||
{
|
||||
const QJsonValue rowValue = breakpointsArray.at(row);
|
||||
if (rowValue.isUndefined() || !rowValue.isObject())
|
||||
{
|
||||
Console.WriteLn("Debugger Settings Manager: Failed to load invalid Breakpoint object.");
|
||||
continue;
|
||||
}
|
||||
const QJsonObject rowObject = rowValue.toObject();
|
||||
|
||||
QStringList fields;
|
||||
u32 col = 0;
|
||||
for (auto iter = rowObject.begin(); iter != rowObject.end(); iter++, col++)
|
||||
{
|
||||
QString headerColKey = bpModel->headerData(col, Qt::Horizontal, Qt::UserRole).toString();
|
||||
fields << rowObject.value(headerColKey).toString();
|
||||
}
|
||||
bpModel->loadBreakpointFromFieldList(fields);
|
||||
}
|
||||
}
|
||||
|
||||
void DebuggerSettingsManager::loadGameSettings(SavedAddressesModel* savedAddressesModel)
|
||||
{
|
||||
const std::string path = VMManager::GetDebuggerSettingsFilePathForCurrentGame();
|
||||
if (path.empty())
|
||||
return;
|
||||
|
||||
const QJsonValue savedAddressesValue = loadGameSettingsJSON().value("SavedAddresses");
|
||||
QString valueToLoad = savedAddressesValue.toString();
|
||||
if (savedAddressesValue.isUndefined() || !savedAddressesValue.isArray())
|
||||
{
|
||||
Console.WriteLnFmt("Debugger Settings Manager: Failed to read Saved Addresses array from settings file: '{}'", path);
|
||||
return;
|
||||
}
|
||||
|
||||
const QJsonArray breakpointsArray = savedAddressesValue.toArray();
|
||||
|
||||
for (u32 row = 0; row < breakpointsArray.size(); row++)
|
||||
{
|
||||
const QJsonValue rowValue = breakpointsArray.at(row);
|
||||
if (rowValue.isUndefined() || !rowValue.isObject())
|
||||
{
|
||||
Console.WriteLn("Debugger Settings Manager: Failed to load invalid Breakpoint object.");
|
||||
continue;
|
||||
}
|
||||
const QJsonObject rowObject = rowValue.toObject();
|
||||
QStringList fields;
|
||||
u32 col = 0;
|
||||
for (auto iter = rowObject.begin(); iter != rowObject.end(); iter++, col++)
|
||||
{
|
||||
QString headerColKey = savedAddressesModel->headerData(col, Qt::Horizontal, Qt::UserRole).toString();
|
||||
fields << rowObject.value(headerColKey).toString();
|
||||
}
|
||||
savedAddressesModel->loadSavedAddressFromFieldList(fields);
|
||||
}
|
||||
}
|
||||
|
||||
void DebuggerSettingsManager::saveGameSettings(BreakpointModel* bpModel)
|
||||
{
|
||||
saveGameSettings(bpModel, "Breakpoints", BreakpointModel::ExportRole);
|
||||
}
|
||||
|
||||
void DebuggerSettingsManager::saveGameSettings(SavedAddressesModel* savedAddressesModel)
|
||||
{
|
||||
saveGameSettings(savedAddressesModel, "SavedAddresses", Qt::DisplayRole);
|
||||
}
|
||||
|
||||
void DebuggerSettingsManager::saveGameSettings(QAbstractTableModel* abstractTableModel, QString settingsKey, u32 role)
|
||||
{
|
||||
const std::string path = VMManager::GetDebuggerSettingsFilePathForCurrentGame();
|
||||
if (path.empty())
|
||||
return;
|
||||
|
||||
const std::lock_guard<std::mutex> lock(writeLock);
|
||||
QJsonObject loadedSettings = loadGameSettingsJSON();
|
||||
QJsonArray rowsArray;
|
||||
QStringList keys;
|
||||
for (u32 col = 0; col < abstractTableModel->columnCount(); ++col)
|
||||
{
|
||||
keys << abstractTableModel->headerData(col, Qt::Horizontal, Qt::UserRole).toString();
|
||||
}
|
||||
|
||||
for (u32 row = 0; row < abstractTableModel->rowCount(); row++)
|
||||
{
|
||||
QJsonObject rowObject;
|
||||
for (u32 col = 0; col < abstractTableModel->columnCount(); col++)
|
||||
{
|
||||
const QModelIndex index = abstractTableModel->index(row, col);
|
||||
const QString data = abstractTableModel->data(index, role).toString();
|
||||
rowObject.insert(keys[col], QJsonValue::fromVariant(data));
|
||||
}
|
||||
rowsArray.append(rowObject);
|
||||
}
|
||||
loadedSettings.insert(settingsKey, rowsArray);
|
||||
loadedSettings.insert("Version", settingsFileVersion);
|
||||
QJsonDocument doc(loadedSettings);
|
||||
writeJSONToPath(path, doc);
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
// SPDX-FileCopyrightText: 2002-2024 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: LGPL-3.0+
|
||||
|
||||
#pragma once
|
||||
#include <QtWidgets/QDialog>
|
||||
|
||||
#include <mutex>
|
||||
|
||||
#include "Models/BreakpointModel.h"
|
||||
#include "Models/SavedAddressesModel.h"
|
||||
|
||||
class DebuggerSettingsManager final
|
||||
{
|
||||
public:
|
||||
DebuggerSettingsManager(QWidget* parent = nullptr);
|
||||
~DebuggerSettingsManager();
|
||||
|
||||
static void loadGameSettings(BreakpointModel* bpModel);
|
||||
static void loadGameSettings(SavedAddressesModel* savedAddressesModel);
|
||||
static void saveGameSettings(BreakpointModel* bpModel);
|
||||
static void saveGameSettings(SavedAddressesModel* savedAddressesModel);
|
||||
static void saveGameSettings(QAbstractTableModel* abstractTableModel, QString settingsKey, u32 role);
|
||||
|
||||
private:
|
||||
static std::mutex writeLock;
|
||||
static void writeJSONToPath(std::string path, QJsonDocument jsonDocument);
|
||||
static QJsonObject loadGameSettingsJSON();
|
||||
const static QString settingsFileVersion;
|
||||
};
|
|
@ -6,6 +6,7 @@
|
|||
#include "DebugTools/DebugInterface.h"
|
||||
#include "DebugTools/Breakpoints.h"
|
||||
#include "DebugTools/DisassemblyManager.h"
|
||||
#include "common/Console.h"
|
||||
|
||||
#include "QtHost.h"
|
||||
#include "QtUtils.h"
|
||||
|
@ -226,6 +227,28 @@ QVariant BreakpointModel::headerData(int section, Qt::Orientation orientation, i
|
|||
return QVariant();
|
||||
}
|
||||
}
|
||||
if (role == Qt::UserRole && orientation == Qt::Horizontal)
|
||||
{
|
||||
switch (section)
|
||||
{
|
||||
case BreakpointColumns::TYPE:
|
||||
return "TYPE";
|
||||
case BreakpointColumns::OFFSET:
|
||||
return "OFFSET";
|
||||
case BreakpointColumns::SIZE_LABEL:
|
||||
return "SIZE / LABEL";
|
||||
case BreakpointColumns::OPCODE:
|
||||
return "INSTRUCTION";
|
||||
case BreakpointColumns::CONDITION:
|
||||
return "CONDITION";
|
||||
case BreakpointColumns::HITS:
|
||||
return "HITS";
|
||||
case BreakpointColumns::ENABLED:
|
||||
return "X";
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
}
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
|
@ -402,3 +425,106 @@ void BreakpointModel::refreshData()
|
|||
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
void BreakpointModel::loadBreakpointFromFieldList(QStringList fields)
|
||||
{
|
||||
bool ok;
|
||||
if (fields.size() != BreakpointModel::BreakpointColumns::COLUMN_COUNT)
|
||||
{
|
||||
Console.WriteLn("Debugger Breakpoint Model: Invalid number of columns, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
const int type = fields[BreakpointModel::BreakpointColumns::TYPE].toUInt(&ok);
|
||||
if (!ok)
|
||||
{
|
||||
Console.WriteLn("Debugger Breakpoint Model: Failed to parse type '%s', skipping", fields[BreakpointModel::BreakpointColumns::TYPE].toUtf8().constData());
|
||||
return;
|
||||
}
|
||||
|
||||
// This is how we differentiate between breakpoints and memchecks
|
||||
if (type == MEMCHECK_INVALID)
|
||||
{
|
||||
BreakPoint bp;
|
||||
|
||||
// Address
|
||||
bp.addr = fields[BreakpointModel::BreakpointColumns::OFFSET].toUInt(&ok, 16);
|
||||
if (!ok)
|
||||
{
|
||||
Console.WriteLn("Debugger Breakpoint Model: Failed to parse address '%s', skipping", fields[BreakpointModel::BreakpointColumns::OFFSET].toUtf8().constData());
|
||||
return;
|
||||
}
|
||||
|
||||
// Condition
|
||||
if (!fields[BreakpointModel::BreakpointColumns::CONDITION].isEmpty())
|
||||
{
|
||||
PostfixExpression expr;
|
||||
bp.hasCond = true;
|
||||
bp.cond.debug = &m_cpu;
|
||||
|
||||
if (!m_cpu.initExpression(fields[BreakpointModel::BreakpointColumns::CONDITION].toUtf8().constData(), expr))
|
||||
{
|
||||
Console.WriteLn("Debugger Breakpoint Model: Failed to parse cond '%s', skipping", fields[BreakpointModel::BreakpointColumns::CONDITION].toUtf8().constData());
|
||||
return;
|
||||
}
|
||||
bp.cond.expression = expr;
|
||||
strncpy(&bp.cond.expressionString[0], fields[BreakpointModel::BreakpointColumns::CONDITION].toUtf8().constData(), sizeof(bp.cond.expressionString));
|
||||
}
|
||||
|
||||
// Enabled
|
||||
bp.enabled = fields[BreakpointModel::BreakpointColumns::ENABLED].toUInt(&ok);
|
||||
if (!ok)
|
||||
{
|
||||
Console.WriteLn("Debugger Breakpoint Model: Failed to parse enable flag '%s', skipping", fields[BreakpointModel::BreakpointColumns::ENABLED].toUtf8().constData());
|
||||
return;
|
||||
}
|
||||
|
||||
insertBreakpointRows(0, 1, {bp});
|
||||
}
|
||||
else
|
||||
{
|
||||
MemCheck mc;
|
||||
// Mode
|
||||
if (type >= MEMCHECK_INVALID)
|
||||
{
|
||||
Console.WriteLn("Debugger Breakpoint Model: Failed to parse cond type '%s', skipping", fields[BreakpointModel::BreakpointColumns::TYPE].toUtf8().constData());
|
||||
return;
|
||||
}
|
||||
mc.cond = static_cast<MemCheckCondition>(type);
|
||||
|
||||
// Address
|
||||
QString test = fields[BreakpointModel::BreakpointColumns::OFFSET];
|
||||
mc.start = fields[BreakpointModel::BreakpointColumns::OFFSET].toUInt(&ok, 16);
|
||||
if (!ok)
|
||||
{
|
||||
Console.WriteLn("Debugger Breakpoint Model: Failed to parse address '%s', skipping", fields[BreakpointModel::BreakpointColumns::OFFSET].toUtf8().constData());
|
||||
return;
|
||||
}
|
||||
|
||||
// Size
|
||||
mc.end = fields[BreakpointModel::BreakpointColumns::SIZE_LABEL].toUInt(&ok) + mc.start;
|
||||
if (!ok)
|
||||
{
|
||||
Console.WriteLn("Debugger Breakpoint Model: Failed to parse length '%s', skipping", fields[BreakpointModel::BreakpointColumns::SIZE_LABEL].toUtf8().constData());
|
||||
return;
|
||||
}
|
||||
|
||||
// Result
|
||||
const int result = fields[BreakpointModel::BreakpointColumns::ENABLED].toUInt(&ok);
|
||||
if (!ok)
|
||||
{
|
||||
Console.WriteLn("Debugger Breakpoint Model: Failed to parse result flag '%s', skipping", fields[BreakpointModel::BreakpointColumns::ENABLED].toUtf8().constData());
|
||||
return;
|
||||
}
|
||||
mc.result = static_cast<MemCheckResult>(result);
|
||||
|
||||
insertBreakpointRows(0, 1, {mc});
|
||||
}
|
||||
}
|
||||
|
||||
void BreakpointModel::clear()
|
||||
{
|
||||
beginResetModel();
|
||||
m_breakpoints.clear();
|
||||
endResetModel();
|
||||
}
|
||||
|
|
|
@ -55,10 +55,12 @@ public:
|
|||
bool setData(const QModelIndex& index, const QVariant& value, int role) override;
|
||||
bool removeRows(int row, int count, const QModelIndex& index = QModelIndex()) override;
|
||||
bool insertBreakpointRows(int row, int count, std::vector<BreakpointMemcheck> breakpoints, const QModelIndex& index = QModelIndex());
|
||||
void loadBreakpointFromFieldList(QStringList breakpointFields);
|
||||
|
||||
BreakpointMemcheck at(int row) const { return m_breakpoints.at(row); };
|
||||
|
||||
void refreshData();
|
||||
void clear();
|
||||
|
||||
private:
|
||||
DebugInterface& m_cpu;
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
#include "PrecompiledHeader.h"
|
||||
#include "SavedAddressesModel.h"
|
||||
|
||||
#include "common/Console.h"
|
||||
|
||||
SavedAddressesModel::SavedAddressesModel(DebugInterface& cpu, QObject* parent)
|
||||
: QAbstractTableModel(parent)
|
||||
, m_cpu(cpu)
|
||||
|
@ -96,20 +98,38 @@ bool SavedAddressesModel::setData(const QModelIndex& index, const QVariant& valu
|
|||
|
||||
QVariant SavedAddressesModel::headerData(int section, Qt::Orientation orientation, int role) const
|
||||
{
|
||||
if ((role != Qt::DisplayRole && role != Qt::EditRole) || orientation != Qt::Horizontal)
|
||||
if (orientation != Qt::Horizontal)
|
||||
return QVariant();
|
||||
|
||||
switch (section)
|
||||
if (role == Qt::DisplayRole)
|
||||
{
|
||||
case SavedAddressesModel::ADDRESS:
|
||||
return tr("MEMORY ADDRESS");
|
||||
case SavedAddressesModel::LABEL:
|
||||
return tr("LABEL");
|
||||
case SavedAddressesModel::DESCRIPTION:
|
||||
return tr("DESCRIPTION");
|
||||
default:
|
||||
return QVariant();
|
||||
switch (section)
|
||||
{
|
||||
case SavedAddressesModel::ADDRESS:
|
||||
return tr("MEMORY ADDRESS");
|
||||
case SavedAddressesModel::LABEL:
|
||||
return tr("LABEL");
|
||||
case SavedAddressesModel::DESCRIPTION:
|
||||
return tr("DESCRIPTION");
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
}
|
||||
if (role == Qt::UserRole)
|
||||
{
|
||||
switch (section)
|
||||
{
|
||||
case SavedAddressesModel::ADDRESS:
|
||||
return "MEMORY ADDRESS";
|
||||
case SavedAddressesModel::LABEL:
|
||||
return "LABEL";
|
||||
case SavedAddressesModel::DESCRIPTION:
|
||||
return "DESCRIPTION";
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
}
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
Qt::ItemFlags SavedAddressesModel::flags(const QModelIndex& index) const
|
||||
|
@ -150,3 +170,32 @@ int SavedAddressesModel::columnCount(const QModelIndex&) const
|
|||
{
|
||||
return HeaderColumns::COLUMN_COUNT;
|
||||
}
|
||||
|
||||
void SavedAddressesModel::loadSavedAddressFromFieldList(QStringList fields)
|
||||
{
|
||||
if (fields.size() != SavedAddressesModel::HeaderColumns::COLUMN_COUNT)
|
||||
{
|
||||
Console.WriteLn("Debugger Saved Addresses Model: Invalid number of columns, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
bool ok;
|
||||
const u32 address = fields[SavedAddressesModel::HeaderColumns::ADDRESS].toUInt(&ok, 16);
|
||||
if (!ok)
|
||||
{
|
||||
Console.WriteLn("Debugger Saved Addresses Model: Failed to parse address '%s', skipping", fields[SavedAddressesModel::HeaderColumns::ADDRESS].toUtf8().constData());
|
||||
return;
|
||||
}
|
||||
|
||||
const QString label = fields[SavedAddressesModel::HeaderColumns::LABEL];
|
||||
const QString description = fields[SavedAddressesModel::HeaderColumns::DESCRIPTION];
|
||||
const SavedAddressesModel::SavedAddress importedAddress = {address, label, description};
|
||||
addRow(importedAddress);
|
||||
}
|
||||
|
||||
void SavedAddressesModel::clear()
|
||||
{
|
||||
beginResetModel();
|
||||
m_savedAddresses.clear();
|
||||
endResetModel();
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ public:
|
|||
QString description;
|
||||
};
|
||||
|
||||
enum HeaderColumns : int
|
||||
enum HeaderColumns: int
|
||||
{
|
||||
ADDRESS = 0,
|
||||
LABEL,
|
||||
|
@ -45,6 +45,8 @@ public:
|
|||
void addRow(SavedAddress addresstoSave);
|
||||
bool removeRows(int row, int count, const QModelIndex& parent = QModelIndex()) override;
|
||||
bool setData(const QModelIndex& index, const QVariant& value, int role) override;
|
||||
void loadSavedAddressFromFieldList(QStringList fields);
|
||||
void clear();
|
||||
|
||||
private:
|
||||
DebugInterface& m_cpu;
|
||||
|
|
|
@ -106,6 +106,7 @@
|
|||
<ClCompile Include="Debugger\MemoryViewWidget.cpp" />
|
||||
<ClCompile Include="Debugger\RegisterWidget.cpp" />
|
||||
<ClCompile Include="Debugger\BreakpointDialog.cpp" />
|
||||
<ClCompile Include="Debugger\DebuggerSettingsManager.cpp" />
|
||||
<ClCompile Include="Debugger\Models\BreakpointModel.cpp" />
|
||||
<ClCompile Include="Debugger\Models\ThreadModel.cpp" />
|
||||
<ClCompile Include="Debugger\Models\StackModel.cpp" />
|
||||
|
@ -196,6 +197,7 @@
|
|||
<QtMoc Include="Debugger\MemoryViewWidget.h" />
|
||||
<QtMoc Include="Debugger\RegisterWidget.h" />
|
||||
<QtMoc Include="Debugger\BreakpointDialog.h" />
|
||||
<ClInclude Include="Debugger\DebuggerSettingsManager.h" />
|
||||
<QtMoc Include="Debugger\Models\BreakpointModel.h" />
|
||||
<QtMoc Include="Debugger\Models\ThreadModel.h" />
|
||||
<QtMoc Include="Debugger\Models\StackModel.h" />
|
||||
|
|
|
@ -346,6 +346,9 @@
|
|||
<ClCompile Include="$(IntDir)moc_LogWindow.cpp">
|
||||
<Filter>moc</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="Debugger\DebuggerSettingsManager.cpp">
|
||||
<Filter>Debugger</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Manifest Include="..\pcsx2\windows\PCSX2.manifest">
|
||||
|
@ -365,6 +368,9 @@
|
|||
<ClInclude Include="Settings\MemoryCardConvertWorker.h">
|
||||
<Filter>Settings</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="Debugger\DebuggerSettingsManager.h">
|
||||
<Filter>Debugger</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<QtMoc Include="MainWindow.h" />
|
||||
|
|
|
@ -1210,6 +1210,7 @@ namespace EmuFolders
|
|||
extern std::string AppRoot;
|
||||
extern std::string DataRoot;
|
||||
extern std::string Settings;
|
||||
extern std::string DebuggerSettings;
|
||||
extern std::string Bios;
|
||||
extern std::string Snapshots;
|
||||
extern std::string Savestates;
|
||||
|
|
|
@ -150,6 +150,7 @@ namespace EmuFolders
|
|||
std::string AppRoot;
|
||||
std::string DataRoot;
|
||||
std::string Settings;
|
||||
std::string DebuggerSettings;
|
||||
std::string Bios;
|
||||
std::string Snapshots;
|
||||
std::string Savestates;
|
||||
|
@ -2004,6 +2005,7 @@ void EmuFolders::LoadConfig(SettingsInterface& si)
|
|||
Textures = LoadPathFromSettings(si, DataRoot, "Textures", "textures");
|
||||
InputProfiles = LoadPathFromSettings(si, DataRoot, "InputProfiles", "inputprofiles");
|
||||
Videos = LoadPathFromSettings(si, DataRoot, "Videos", "videos");
|
||||
DebuggerSettings = LoadPathFromSettings(si, Settings, "DebuggerSettings", "debuggersettings");
|
||||
|
||||
Console.WriteLn("BIOS Directory: %s", Bios.c_str());
|
||||
Console.WriteLn("Snapshots Directory: %s", Snapshots.c_str());
|
||||
|
@ -2020,6 +2022,7 @@ void EmuFolders::LoadConfig(SettingsInterface& si)
|
|||
Console.WriteLn("Textures Directory: %s", Textures.c_str());
|
||||
Console.WriteLn("Input Profile Directory: %s", InputProfiles.c_str());
|
||||
Console.WriteLn("Video Dumping Directory: %s", Videos.c_str());
|
||||
Console.WriteLn("Debugger Settings Directory: %s", DebuggerSettings.c_str());
|
||||
}
|
||||
|
||||
bool EmuFolders::EnsureFoldersExist()
|
||||
|
@ -2035,6 +2038,7 @@ bool EmuFolders::EnsureFoldersExist()
|
|||
result = FileSystem::CreateDirectoryPath(Covers.c_str(), false) && result;
|
||||
result = FileSystem::CreateDirectoryPath(GameSettings.c_str(), false) && result;
|
||||
result = FileSystem::CreateDirectoryPath(UserResources.c_str(), false) && result;
|
||||
result = FileSystem::CreateDirectoryPath(DebuggerSettings.c_str(), false) && result;
|
||||
result = FileSystem::CreateDirectoryPath(Cache.c_str(), false) && result;
|
||||
result = FileSystem::CreateDirectoryPath(Textures.c_str(), false) && result;
|
||||
result = FileSystem::CreateDirectoryPath(InputProfiles.c_str(), false) && result;
|
||||
|
|
|
@ -729,6 +729,22 @@ std::string VMManager::GetInputProfilePath(const std::string_view& name)
|
|||
return Path::Combine(EmuFolders::InputProfiles, fmt::format("{}.ini", name));
|
||||
}
|
||||
|
||||
std::string VMManager::GetDebuggerSettingsFilePath(const std::string_view& game_serial, u32 game_crc)
|
||||
{
|
||||
std::string path;
|
||||
if (!game_serial.empty() && game_crc != 0)
|
||||
{
|
||||
auto lock = Host::GetSettingsLock();
|
||||
return Path::Combine(EmuFolders::DebuggerSettings, fmt::format("{}_{:08X}.json", game_serial, game_crc));
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
std::string VMManager::GetDebuggerSettingsFilePathForCurrentGame()
|
||||
{
|
||||
return GetDebuggerSettingsFilePath(s_disc_serial, s_current_crc);
|
||||
}
|
||||
|
||||
void VMManager::Internal::UpdateEmuFolders()
|
||||
{
|
||||
const std::string old_cheats_directory(EmuFolders::Cheats);
|
||||
|
|
|
@ -196,6 +196,12 @@ namespace VMManager
|
|||
/// Returns the path for the input profile ini file with the specified name (may not exist).
|
||||
std::string GetInputProfilePath(const std::string_view& name);
|
||||
|
||||
/// Returns the path for the debugger settings json file for the specified game serial and CRC.
|
||||
std::string GetDebuggerSettingsFilePath(const std::string_view& game_serial, u32 game_crc);
|
||||
|
||||
/// Returns the path for the debugger settings json file for the current game.
|
||||
std::string GetDebuggerSettingsFilePathForCurrentGame();
|
||||
|
||||
/// Resizes the render window to the display size, with an optional scale.
|
||||
/// If the scale is set to 0, the internal resolution will be used, otherwise it is treated as a multiplier to 1x.
|
||||
void RequestDisplaySize(float scale = 0.0f);
|
||||
|
|
Loading…
Reference in New Issue