From 55397b6d5273c54da5ebdf1fa3790a593506fb23 Mon Sep 17 00:00:00 2001 From: "Admiral H. Curtiss" Date: Sun, 22 Aug 2021 07:13:00 +0200 Subject: [PATCH] DolphinQt: Rewrite cheat search GUI. --- Source/Core/DolphinQt/CMakeLists.txt | 6 + .../DolphinQt/CheatSearchFactoryWidget.cpp | 187 +++++ .../Core/DolphinQt/CheatSearchFactoryWidget.h | 51 ++ Source/Core/DolphinQt/CheatSearchWidget.cpp | 529 +++++++++++++ Source/Core/DolphinQt/CheatSearchWidget.h | 80 ++ Source/Core/DolphinQt/CheatsManager.cpp | 706 ++---------------- Source/Core/DolphinQt/CheatsManager.h | 62 +- Source/Core/DolphinQt/DolphinQt.vcxproj | 6 + .../QtUtils/PartiallyClosableTabWidget.cpp | 19 + .../QtUtils/PartiallyClosableTabWidget.h | 17 + 10 files changed, 955 insertions(+), 708 deletions(-) create mode 100644 Source/Core/DolphinQt/CheatSearchFactoryWidget.cpp create mode 100644 Source/Core/DolphinQt/CheatSearchFactoryWidget.h create mode 100644 Source/Core/DolphinQt/CheatSearchWidget.cpp create mode 100644 Source/Core/DolphinQt/CheatSearchWidget.h create mode 100644 Source/Core/DolphinQt/QtUtils/PartiallyClosableTabWidget.cpp create mode 100644 Source/Core/DolphinQt/QtUtils/PartiallyClosableTabWidget.h diff --git a/Source/Core/DolphinQt/CMakeLists.txt b/Source/Core/DolphinQt/CMakeLists.txt index 93b6f73257..1c8a41d606 100644 --- a/Source/Core/DolphinQt/CMakeLists.txt +++ b/Source/Core/DolphinQt/CMakeLists.txt @@ -16,6 +16,10 @@ set(CMAKE_AUTOMOC ON) add_executable(dolphin-emu AboutDialog.cpp AboutDialog.h + CheatSearchFactoryWidget.cpp + CheatSearchFactoryWidget.h + CheatSearchWidget.cpp + CheatSearchWidget.h CheatsManager.cpp CheatsManager.h ConvertDialog.cpp @@ -269,6 +273,8 @@ add_executable(dolphin-emu QtUtils/ModalMessageBox.cpp QtUtils/ModalMessageBox.h QtUtils/ParallelProgressDialog.h + QtUtils/PartiallyClosableTabWidget.cpp + QtUtils/PartiallyClosableTabWidget.h QtUtils/ImageConverter.cpp QtUtils/ImageConverter.h QtUtils/UTF8CodePointCountValidator.cpp diff --git a/Source/Core/DolphinQt/CheatSearchFactoryWidget.cpp b/Source/Core/DolphinQt/CheatSearchFactoryWidget.cpp new file mode 100644 index 0000000000..4e0f8f2f1b --- /dev/null +++ b/Source/Core/DolphinQt/CheatSearchFactoryWidget.cpp @@ -0,0 +1,187 @@ +// Copyright 2021 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "DolphinQt/CheatSearchFactoryWidget.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Common/StringUtil.h" +#include "Core/CheatSearch.h" +#include "Core/Config/MainSettings.h" +#include "Core/ConfigManager.h" +#include "Core/HW/Memmap.h" +#include "Core/PowerPC/MMU.h" + +CheatSearchFactoryWidget::CheatSearchFactoryWidget() +{ + CreateWidgets(); + ConnectWidgets(); + RefreshGui(); +} + +CheatSearchFactoryWidget::~CheatSearchFactoryWidget() = default; + +Q_DECLARE_METATYPE(Cheats::DataType); + +void CheatSearchFactoryWidget::CreateWidgets() +{ + auto* layout = new QVBoxLayout(); + + auto* address_space_group = new QGroupBox(tr("Address Space")); + auto* address_space_layout = new QVBoxLayout(); + address_space_group->setLayout(address_space_layout); + + m_standard_address_space = new QRadioButton(tr("Typical GameCube/Wii Address Space")); + m_standard_address_space->setChecked(true); + m_custom_address_space = new QRadioButton(tr("Custom Address Space")); + + QLabel* label_standard_address_space = + new QLabel(tr("Sets up the search using standard MEM1 and (on Wii) MEM2 mappings in virtual " + "address space. This will work for the vast majority of games.")); + label_standard_address_space->setWordWrap(true); + + auto* custom_address_space_layout = new QVBoxLayout(); + custom_address_space_layout->setMargin(6); + auto* custom_address_space_button_group = new QButtonGroup(); + m_custom_virtual_address_space = new QRadioButton(tr("Use virtual addresses when possible")); + m_custom_virtual_address_space->setChecked(true); + m_custom_physical_address_space = new QRadioButton(tr("Use physical addresses")); + m_custom_effective_address_space = + new QRadioButton(tr("Use memory mapper configuration at time of scan")); + custom_address_space_button_group->addButton(m_custom_virtual_address_space); + custom_address_space_button_group->addButton(m_custom_physical_address_space); + custom_address_space_button_group->addButton(m_custom_effective_address_space); + custom_address_space_layout->addWidget(m_custom_virtual_address_space); + custom_address_space_layout->addWidget(m_custom_physical_address_space); + custom_address_space_layout->addWidget(m_custom_effective_address_space); + + QLabel* label_range_start = new QLabel(tr("Range Start: ")); + m_custom_address_start = new QLineEdit(QStringLiteral("0x80000000")); + QLabel* label_range_end = new QLabel(tr("Range End: ")); + m_custom_address_end = new QLineEdit(QStringLiteral("0x81800000")); + custom_address_space_layout->addWidget(label_range_start); + custom_address_space_layout->addWidget(m_custom_address_start); + custom_address_space_layout->addWidget(label_range_end); + custom_address_space_layout->addWidget(m_custom_address_end); + + address_space_layout->addWidget(m_standard_address_space); + address_space_layout->addWidget(label_standard_address_space); + address_space_layout->addWidget(m_custom_address_space); + address_space_layout->addLayout(custom_address_space_layout); + + layout->addWidget(address_space_group); + + auto* data_type_group = new QGroupBox(tr("Data Type")); + auto* data_type_layout = new QVBoxLayout(); + data_type_group->setLayout(data_type_layout); + + m_data_type_dropdown = new QComboBox(); + m_data_type_dropdown->addItem(tr("8-bit Unsigned Integer"), + QVariant::fromValue(Cheats::DataType::U8)); + m_data_type_dropdown->addItem(tr("16-bit Unsigned Integer"), + QVariant::fromValue(Cheats::DataType::U16)); + m_data_type_dropdown->addItem(tr("32-bit Unsigned Integer"), + QVariant::fromValue(Cheats::DataType::U32)); + m_data_type_dropdown->addItem(tr("64-bit Unsigned Integer"), + QVariant::fromValue(Cheats::DataType::U64)); + m_data_type_dropdown->addItem(tr("8-bit Signed Integer"), + QVariant::fromValue(Cheats::DataType::S8)); + m_data_type_dropdown->addItem(tr("16-bit Signed Integer"), + QVariant::fromValue(Cheats::DataType::S16)); + m_data_type_dropdown->addItem(tr("32-bit Signed Integer"), + QVariant::fromValue(Cheats::DataType::S32)); + m_data_type_dropdown->addItem(tr("64-bit Signed Integer"), + QVariant::fromValue(Cheats::DataType::S64)); + m_data_type_dropdown->addItem(tr("32-bit Float"), QVariant::fromValue(Cheats::DataType::F32)); + m_data_type_dropdown->addItem(tr("64-bit Float"), QVariant::fromValue(Cheats::DataType::F64)); + m_data_type_dropdown->setCurrentIndex(6); // select 32bit signed int by default + + data_type_layout->addWidget(m_data_type_dropdown); + + m_data_type_aligned = new QCheckBox(tr("Aligned to data type length")); + m_data_type_aligned->setChecked(true); + + data_type_layout->addWidget(m_data_type_aligned); + + layout->addWidget(data_type_group); + + m_new_search = new QPushButton(tr("New Search")); + layout->addWidget(m_new_search); + + setLayout(layout); +} + +void CheatSearchFactoryWidget::ConnectWidgets() +{ + connect(m_new_search, &QPushButton::clicked, this, &CheatSearchFactoryWidget::OnNewSearchClicked); + connect(m_standard_address_space, &QPushButton::toggled, this, + &CheatSearchFactoryWidget::OnAddressSpaceRadioChanged); + connect(m_custom_address_space, &QRadioButton::toggled, this, + &CheatSearchFactoryWidget::OnAddressSpaceRadioChanged); +} + +void CheatSearchFactoryWidget::RefreshGui() +{ + bool enable_custom = m_custom_address_space->isChecked(); + m_custom_virtual_address_space->setEnabled(enable_custom); + m_custom_physical_address_space->setEnabled(enable_custom); + m_custom_effective_address_space->setEnabled(enable_custom); + m_custom_address_start->setEnabled(enable_custom); + m_custom_address_end->setEnabled(enable_custom); +} + +void CheatSearchFactoryWidget::OnAddressSpaceRadioChanged() +{ + RefreshGui(); +} + +void CheatSearchFactoryWidget::OnNewSearchClicked() +{ + std::vector memory_ranges; + PowerPC::RequestedAddressSpace address_space; + if (m_standard_address_space->isChecked()) + { + memory_ranges.emplace_back(0x80000000, Memory::GetRamSizeReal()); + if (SConfig::GetInstance().bWii) + memory_ranges.emplace_back(0x90000000, Memory::GetExRamSizeReal()); + address_space = PowerPC::RequestedAddressSpace::Virtual; + } + else + { + const std::string address_start_str = m_custom_address_start->text().toStdString(); + const std::string address_end_str = m_custom_address_end->text().toStdString(); + + u64 address_start; + u64 address_end; + if (!TryParse(address_start_str, &address_start) || !TryParse(address_end_str, &address_end)) + return; + if (address_end <= address_start || address_end > 0x1'0000'0000) + return; + + memory_ranges.emplace_back(static_cast(address_start), address_end - address_start); + + if (m_custom_virtual_address_space->isChecked()) + address_space = PowerPC::RequestedAddressSpace::Virtual; + else if (m_custom_physical_address_space->isChecked()) + address_space = PowerPC::RequestedAddressSpace::Physical; + else + address_space = PowerPC::RequestedAddressSpace::Effective; + } + + bool aligned = m_data_type_aligned->isChecked(); + auto data_type = m_data_type_dropdown->currentData().value(); + auto session = Cheats::MakeSession(std::move(memory_ranges), address_space, aligned, data_type); + if (session) + emit NewSessionCreated(*session); +} diff --git a/Source/Core/DolphinQt/CheatSearchFactoryWidget.h b/Source/Core/DolphinQt/CheatSearchFactoryWidget.h new file mode 100644 index 0000000000..576f7f97ad --- /dev/null +++ b/Source/Core/DolphinQt/CheatSearchFactoryWidget.h @@ -0,0 +1,51 @@ +// Copyright 2021 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include + +#include "Core/CheatSearch.h" + +class QCheckBox; +class QComboBox; +class QLineEdit; +class QPushButton; +class QRadioButton; + +class CheatSearchFactoryWidget : public QWidget +{ + Q_OBJECT +public: + explicit CheatSearchFactoryWidget(); + ~CheatSearchFactoryWidget() override; + +signals: + void NewSessionCreated(const Cheats::CheatSearchSessionBase& session); + +private: + void CreateWidgets(); + void ConnectWidgets(); + + void RefreshGui(); + + void OnAddressSpaceRadioChanged(); + void OnNewSearchClicked(); + + QRadioButton* m_standard_address_space; + QRadioButton* m_custom_address_space; + + QRadioButton* m_custom_virtual_address_space; + QRadioButton* m_custom_physical_address_space; + QRadioButton* m_custom_effective_address_space; + + QLineEdit* m_custom_address_start; + QLineEdit* m_custom_address_end; + + QComboBox* m_data_type_dropdown; + QCheckBox* m_data_type_aligned; + + QPushButton* m_new_search; +}; diff --git a/Source/Core/DolphinQt/CheatSearchWidget.cpp b/Source/Core/DolphinQt/CheatSearchWidget.cpp new file mode 100644 index 0000000000..54bb058ce5 --- /dev/null +++ b/Source/Core/DolphinQt/CheatSearchWidget.cpp @@ -0,0 +1,529 @@ +// Copyright 2021 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "DolphinQt/CheatSearchWidget.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "Common/Align.h" +#include "Common/BitUtils.h" +#include "Common/StringUtil.h" +#include "Common/Swap.h" + +#include "Core/ActionReplay.h" +#include "Core/CheatGeneration.h" +#include "Core/CheatSearch.h" +#include "Core/ConfigManager.h" +#include "Core/PowerPC/PowerPC.h" + +#include "DolphinQt/Config/CheatCodeEditor.h" +#include "DolphinQt/Config/CheatWarningWidget.h" + +#include "UICommon/GameFile.h" + +constexpr size_t TABLE_MAX_ROWS = 1000; + +constexpr int ADDRESS_TABLE_ADDRESS_ROLE = Qt::UserRole; +constexpr int ADDRESS_TABLE_RESULT_INDEX_ROLE = Qt::UserRole + 1; +constexpr int ADDRESS_TABLE_COLUMN_INDEX_DESCRIPTION = 0; +constexpr int ADDRESS_TABLE_COLUMN_INDEX_ADDRESS = 1; +constexpr int ADDRESS_TABLE_COLUMN_INDEX_LAST_VALUE = 2; +constexpr int ADDRESS_TABLE_COLUMN_INDEX_CURRENT_VALUE = 3; + +CheatSearchWidget::CheatSearchWidget(std::unique_ptr session) + : m_session(std::move(session)) +{ + setAttribute(Qt::WA_DeleteOnClose); + CreateWidgets(); + ConnectWidgets(); + UpdateGuiTable(); +} + +CheatSearchWidget::~CheatSearchWidget() = default; + +Q_DECLARE_METATYPE(Cheats::CompareType); +Q_DECLARE_METATYPE(Cheats::FilterType); + +void CheatSearchWidget::CreateWidgets() +{ + QLabel* session_info_label = new QLabel(); + { + QString ranges; + size_t range_count = m_session->GetMemoryRangeCount(); + switch (range_count) + { + case 1: + { + auto m = m_session->GetMemoryRange(0); + ranges = + tr("[%1, %2]") + .arg(QString::fromStdString(fmt::format("0x{0:08x}", m.m_start))) + .arg(QString::fromStdString(fmt::format("0x{0:08x}", m.m_start + m.m_length - 1))); + break; + } + case 2: + { + auto m0 = m_session->GetMemoryRange(0); + auto m1 = m_session->GetMemoryRange(1); + ranges = + tr("[%1, %2] and [%3, %4]") + .arg(QString::fromStdString(fmt::format("0x{0:08x}", m0.m_start))) + .arg(QString::fromStdString(fmt::format("0x{0:08x}", m0.m_start + m0.m_length - 1))) + .arg(QString::fromStdString(fmt::format("0x{0:08x}", m1.m_start))) + .arg(QString::fromStdString(fmt::format("0x{0:08x}", m1.m_start + m1.m_length - 1))); + break; + } + default: + ranges = tr("%1 memory ranges").arg(range_count); + break; + } + + QString space; + switch (m_session->GetAddressSpace()) + { + case PowerPC::RequestedAddressSpace::Effective: + space = tr("Address space by CPU state"); + break; + case PowerPC::RequestedAddressSpace::Physical: + space = tr("Physical address space"); + break; + case PowerPC::RequestedAddressSpace::Virtual: + space = tr("Virtual address space"); + break; + default: + space = tr("Unknown address space"); + break; + } + + QString type; + switch (m_session->GetDataType()) + { + case Cheats::DataType::U8: + type = tr("8-bit Unsigned Integer"); + break; + case Cheats::DataType::U16: + type = tr("16-bit Unsigned Integer"); + break; + case Cheats::DataType::U32: + type = tr("32-bit Unsigned Integer"); + break; + case Cheats::DataType::U64: + type = tr("64-bit Unsigned Integer"); + break; + case Cheats::DataType::S8: + type = tr("8-bit Signed Integer"); + break; + case Cheats::DataType::S16: + type = tr("16-bit Signed Integer"); + break; + case Cheats::DataType::S32: + type = tr("32-bit Signed Integer"); + break; + case Cheats::DataType::S64: + type = tr("64-bit Signed Integer"); + break; + case Cheats::DataType::F32: + type = tr("32-bit Float"); + break; + case Cheats::DataType::F64: + type = tr("64-bit Float"); + break; + default: + type = tr("Unknown data type"); + break; + } + QString aligned = m_session->GetAligned() ? tr("aligned") : tr("unaligned"); + session_info_label->setText(tr("%1, %2, %3, %4").arg(ranges).arg(space).arg(type).arg(aligned)); + } + + auto* value_layout = new QHBoxLayout(); + + auto* instructions_label = new QLabel(tr("Keep addresses where value in memory")); + value_layout->addWidget(instructions_label); + + m_compare_type_dropdown = new QComboBox(); + m_compare_type_dropdown->addItem(tr("is equal to"), + QVariant::fromValue(Cheats::CompareType::Equal)); + m_compare_type_dropdown->addItem(tr("is not equal to"), + QVariant::fromValue(Cheats::CompareType::NotEqual)); + m_compare_type_dropdown->addItem(tr("is less than"), + QVariant::fromValue(Cheats::CompareType::Less)); + m_compare_type_dropdown->addItem(tr("is less than or equal to"), + QVariant::fromValue(Cheats::CompareType::LessOrEqual)); + m_compare_type_dropdown->addItem(tr("is greater than"), + QVariant::fromValue(Cheats::CompareType::Greater)); + m_compare_type_dropdown->addItem(tr("is greater than or equal to"), + QVariant::fromValue(Cheats::CompareType::GreaterOrEqual)); + value_layout->addWidget(m_compare_type_dropdown); + + m_value_source_dropdown = new QComboBox(); + m_value_source_dropdown->addItem( + tr("this value:"), QVariant::fromValue(Cheats::FilterType::CompareAgainstSpecificValue)); + m_value_source_dropdown->addItem( + tr("last value"), QVariant::fromValue(Cheats::FilterType::CompareAgainstLastValue)); + m_value_source_dropdown->addItem(tr("any value"), + QVariant::fromValue(Cheats::FilterType::DoNotFilter)); + value_layout->addWidget(m_value_source_dropdown); + + m_given_value_text = new QLineEdit(); + value_layout->addWidget(m_given_value_text); + + auto* button_layout = new QHBoxLayout(); + m_next_scan_button = new QPushButton(tr("Search and Filter")); + button_layout->addWidget(m_next_scan_button); + m_refresh_values_button = new QPushButton(tr("Refresh Current Values")); + button_layout->addWidget(m_refresh_values_button); + m_reset_button = new QPushButton(tr("Reset Results")); + button_layout->addWidget(m_reset_button); + + m_address_table = new QTableWidget(); + m_address_table->setContextMenuPolicy(Qt::CustomContextMenu); + + m_info_label_1 = new QLabel(tr("Waiting for first scan...")); + m_info_label_2 = new QLabel(); + + m_display_values_in_hex_checkbox = new QCheckBox(tr("Display values in Hex")); + + QVBoxLayout* layout = new QVBoxLayout(); + layout->addWidget(session_info_label); + layout->addLayout(value_layout); + layout->addLayout(button_layout); + layout->addWidget(m_display_values_in_hex_checkbox); + layout->addWidget(m_info_label_1); + layout->addWidget(m_info_label_2); + layout->addWidget(m_address_table); + setLayout(layout); +} + +void CheatSearchWidget::ConnectWidgets() +{ + connect(m_next_scan_button, &QPushButton::clicked, this, &CheatSearchWidget::OnNextScanClicked); + connect(m_refresh_values_button, &QPushButton::clicked, this, + &CheatSearchWidget::OnRefreshClicked); + connect(m_reset_button, &QPushButton::clicked, this, &CheatSearchWidget::OnResetClicked); + connect(m_address_table, &QTableWidget::itemChanged, this, + &CheatSearchWidget::OnAddressTableItemChanged); + connect(m_address_table, &QTableWidget::customContextMenuRequested, this, + &CheatSearchWidget::OnAddressTableContextMenu); + connect(m_value_source_dropdown, &QComboBox::currentTextChanged, this, + &CheatSearchWidget::OnValueSourceChanged); + connect(m_display_values_in_hex_checkbox, &QCheckBox::toggled, this, + &CheatSearchWidget::OnHexCheckboxStateChanged); +} + +void CheatSearchWidget::OnNextScanClicked() +{ + const bool had_old_results = m_session->WasFirstSearchDone(); + const size_t old_count = m_session->GetResultCount(); + + const auto filter_type = m_value_source_dropdown->currentData().value(); + if (filter_type == Cheats::FilterType::CompareAgainstLastValue && + !m_session->WasFirstSearchDone()) + { + m_info_label_1->setText(tr("Cannot compare against last value on first search.")); + return; + } + m_session->SetFilterType(filter_type); + m_session->SetCompareType(m_compare_type_dropdown->currentData().value()); + if (filter_type == Cheats::FilterType::CompareAgainstSpecificValue) + { + if (!m_session->SetValueFromString(m_given_value_text->text().toStdString())) + { + m_info_label_1->setText(tr("Failed to parse given value into target data type.")); + return; + } + } + Cheats::SearchErrorCode error_code = m_session->RunSearch(); + + if (error_code == Cheats::SearchErrorCode::Success) + { + const size_t new_count = m_session->GetResultCount(); + const size_t new_valid_count = m_session->GetValidValueCount(); + m_info_label_1->setText(tr("Scan succeeded.")); + + if (had_old_results) + { + const QString removed_str = + tr("%n address(es) were removed.", "", static_cast(old_count - new_count)); + const QString remain_str = tr("%n address(es) remain.", "", static_cast(new_count)); + + if (new_valid_count == new_count) + { + m_info_label_2->setText(tr("%1 %2").arg(removed_str).arg(remain_str)); + } + else + { + const QString inaccessible_str = + tr("%n address(es) could not be accessed in emulated memory.", "", + static_cast(new_count - new_valid_count)); + + m_info_label_2->setText( + tr("%1 %2 %3").arg(removed_str).arg(remain_str).arg(inaccessible_str)); + } + } + else + { + const QString found_str = tr("Found %n address(es).", "", static_cast(new_count)); + + if (new_valid_count == new_count) + { + m_info_label_2->setText(found_str); + } + else + { + const QString inaccessible_str = + tr("%n address(es) could not be accessed in emulated memory.", "", + static_cast(new_count - new_valid_count)); + + m_info_label_2->setText(tr("%1 %2").arg(found_str).arg(inaccessible_str)); + } + } + + m_address_table_current_values.clear(); + const bool show_in_hex = m_display_values_in_hex_checkbox->isChecked(); + const bool too_many_results = new_count > TABLE_MAX_ROWS; + const size_t result_count_to_display = too_many_results ? TABLE_MAX_ROWS : new_count; + for (size_t i = 0; i < result_count_to_display; ++i) + { + m_address_table_current_values[m_session->GetResultAddress(i)] = + m_session->GetResultValueAsString(i, show_in_hex); + } + + UpdateGuiTable(); + } + else + { + switch (error_code) + { + case Cheats::SearchErrorCode::NoEmulationActive: + m_info_label_1->setText(tr("No game is running.")); + break; + case Cheats::SearchErrorCode::InvalidParameters: + m_info_label_1->setText(tr("Invalid parameters given to search.")); + break; + case Cheats::SearchErrorCode::VirtualAddressesCurrentlyNotAccessible: + m_info_label_1->setText( + tr("Search currently not possible in virtual address space. Please run " + "the game for a bit and try again.")); + break; + default: + m_info_label_1->setText(tr("Unknown error occurred.")); + break; + } + } +} + +bool CheatSearchWidget::RefreshValues() +{ + const size_t result_count = m_session->GetResultCount(); + if (result_count == 0) + { + m_info_label_1->setText(tr("Cannot refresh without results.")); + return false; + } + + const bool too_many_results = result_count > TABLE_MAX_ROWS; + std::unique_ptr tmp; + if (too_many_results) + { + std::vector value_indices; + value_indices.reserve(TABLE_MAX_ROWS); + for (size_t i = 0; i < TABLE_MAX_ROWS; ++i) + value_indices.push_back(i); + tmp = m_session->ClonePartial(value_indices); + } + else + { + tmp = m_session->Clone(); + } + + tmp->SetFilterType(Cheats::FilterType::DoNotFilter); + if (tmp->RunSearch() != Cheats::SearchErrorCode::Success) + { + m_info_label_1->setText(tr("Refresh failed. Please run the game for a bit and try again.")); + return false; + } + + m_address_table_current_values.clear(); + const bool show_in_hex = m_display_values_in_hex_checkbox->isChecked(); + const size_t result_count_to_display = tmp->GetResultCount(); + for (size_t i = 0; i < result_count_to_display; ++i) + { + m_address_table_current_values[tmp->GetResultAddress(i)] = + tmp->GetResultValueAsString(i, show_in_hex); + } + + UpdateGuiTable(); + m_info_label_1->setText(tr("Refreshed current values.")); + return true; +} + +void CheatSearchWidget::OnRefreshClicked() +{ + RefreshValues(); +} + +void CheatSearchWidget::OnResetClicked() +{ + m_session->ResetResults(); + m_address_table_current_values.clear(); + + UpdateGuiTable(); + m_info_label_1->setText(tr("Waiting for first scan...")); + m_info_label_2->clear(); +} + +void CheatSearchWidget::OnAddressTableItemChanged(QTableWidgetItem* item) +{ + const u32 address = item->data(ADDRESS_TABLE_ADDRESS_ROLE).toUInt(); + const int column = item->column(); + + switch (column) + { + case ADDRESS_TABLE_COLUMN_INDEX_DESCRIPTION: + { + m_address_table_user_data[address].m_description = item->text().toStdString(); + break; + } + default: + break; + } +} + +void CheatSearchWidget::OnAddressTableContextMenu() +{ + if (m_address_table->selectedItems().isEmpty()) + return; + + QMenu* menu = new QMenu(this); + + menu->addAction(tr("Generate Action Replay Code"), this, &CheatSearchWidget::GenerateARCode); + + menu->exec(QCursor::pos()); +} + +void CheatSearchWidget::OnValueSourceChanged() +{ + const auto filter_type = m_value_source_dropdown->currentData().value(); + m_given_value_text->setEnabled(filter_type == Cheats::FilterType::CompareAgainstSpecificValue); +} + +void CheatSearchWidget::OnHexCheckboxStateChanged() +{ + if (!m_session->WasFirstSearchDone()) + return; + + if (!RefreshValues()) + UpdateGuiTable(); +} + +void CheatSearchWidget::GenerateARCode() +{ + if (m_address_table->selectedItems().isEmpty()) + return; + + auto* item = m_address_table->selectedItems()[0]; + if (!item) + return; + + const u32 index = item->data(ADDRESS_TABLE_RESULT_INDEX_ROLE).toUInt(); + auto result = Cheats::GenerateActionReplayCode(*m_session, index); + if (result) + { + emit ActionReplayCodeGenerated(*result); + m_info_label_1->setText(tr("Generated AR code.")); + } + else + { + switch (result.Error()) + { + case Cheats::GenerateActionReplayCodeErrorCode::NotVirtualMemory: + m_info_label_1->setText(tr("Can only generate AR code for values in virtual memory.")); + break; + case Cheats::GenerateActionReplayCodeErrorCode::InvalidAddress: + m_info_label_1->setText(tr("Cannot generate AR code for this address.")); + break; + default: + m_info_label_1->setText(tr("Internal error while generating AR code.")); + break; + } + } +} + +void CheatSearchWidget::UpdateGuiTable() +{ + const QSignalBlocker blocker(m_address_table); + + m_address_table->clear(); + m_address_table->setColumnCount(4); + m_address_table->setHorizontalHeaderLabels( + {tr("Description"), tr("Address"), tr("Last Value"), tr("Current Value")}); + + const size_t result_count = m_session->GetResultCount(); + const bool too_many_results = result_count > TABLE_MAX_ROWS; + const int result_count_to_display = int(too_many_results ? TABLE_MAX_ROWS : result_count); + m_address_table->setRowCount(result_count_to_display); + + for (int i = 0; i < result_count_to_display; ++i) + { + const u32 address = m_session->GetResultAddress(i); + const auto user_data_it = m_address_table_user_data.find(address); + const bool has_user_data = user_data_it != m_address_table_user_data.end(); + + auto* description_item = new QTableWidgetItem(); + description_item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable); + if (has_user_data) + description_item->setText(QString::fromStdString(user_data_it->second.m_description)); + description_item->setData(ADDRESS_TABLE_ADDRESS_ROLE, address); + description_item->setData(ADDRESS_TABLE_RESULT_INDEX_ROLE, static_cast(i)); + m_address_table->setItem(i, ADDRESS_TABLE_COLUMN_INDEX_DESCRIPTION, description_item); + + auto* address_item = new QTableWidgetItem(); + address_item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable); + address_item->setText(QStringLiteral("0x%1").arg(address, 8, 16, QLatin1Char('0'))); + address_item->setData(ADDRESS_TABLE_ADDRESS_ROLE, address); + address_item->setData(ADDRESS_TABLE_RESULT_INDEX_ROLE, static_cast(i)); + m_address_table->setItem(i, ADDRESS_TABLE_COLUMN_INDEX_ADDRESS, address_item); + + const bool show_in_hex = m_display_values_in_hex_checkbox->isChecked(); + auto* last_value_item = new QTableWidgetItem(); + last_value_item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable); + last_value_item->setText( + QString::fromStdString(m_session->GetResultValueAsString(i, show_in_hex))); + last_value_item->setData(ADDRESS_TABLE_ADDRESS_ROLE, address); + last_value_item->setData(ADDRESS_TABLE_RESULT_INDEX_ROLE, static_cast(i)); + m_address_table->setItem(i, ADDRESS_TABLE_COLUMN_INDEX_LAST_VALUE, last_value_item); + + auto* current_value_item = new QTableWidgetItem(); + current_value_item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable); + const auto curr_val_it = m_address_table_current_values.find(address); + if (curr_val_it != m_address_table_current_values.end()) + current_value_item->setText(QString::fromStdString(curr_val_it->second)); + else + current_value_item->setText(QStringLiteral("---")); + current_value_item->setData(ADDRESS_TABLE_ADDRESS_ROLE, address); + current_value_item->setData(ADDRESS_TABLE_RESULT_INDEX_ROLE, static_cast(i)); + m_address_table->setItem(i, ADDRESS_TABLE_COLUMN_INDEX_CURRENT_VALUE, current_value_item); + } +} diff --git a/Source/Core/DolphinQt/CheatSearchWidget.h b/Source/Core/DolphinQt/CheatSearchWidget.h new file mode 100644 index 0000000000..74ee2fc2d7 --- /dev/null +++ b/Source/Core/DolphinQt/CheatSearchWidget.h @@ -0,0 +1,80 @@ +// Copyright 2021 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include +#include +#include +#include +#include + +#include "Common/CommonTypes.h" +#include "Core/CheatSearch.h" + +namespace ActionReplay +{ +struct ARCode; +} + +class QCheckBox; +class QComboBox; +class QLabel; +class QLineEdit; +class QPushButton; +class QTableWidget; +class QTableWidgetItem; + +struct CheatSearchTableUserData +{ + std::string m_description; +}; + +class CheatSearchWidget : public QWidget +{ + Q_OBJECT +public: + explicit CheatSearchWidget(std::unique_ptr session); + ~CheatSearchWidget() override; + +signals: + void ActionReplayCodeGenerated(const ActionReplay::ARCode& ar_code); + +private: + void CreateWidgets(); + void ConnectWidgets(); + + void OnNextScanClicked(); + void OnRefreshClicked(); + void OnResetClicked(); + void OnAddressTableItemChanged(QTableWidgetItem* item); + void OnAddressTableContextMenu(); + void OnValueSourceChanged(); + void OnHexCheckboxStateChanged(); + + bool RefreshValues(); + void UpdateGuiTable(); + void GenerateARCode(); + + std::unique_ptr m_session; + + // storage for the 'Current Value' column's data + std::unordered_map m_address_table_current_values; + + // storage for user-entered metadata such as text descriptions for memory addresses + // this is intentionally NOT cleared when updating values or resetting or similar + std::unordered_map m_address_table_user_data; + + QComboBox* m_compare_type_dropdown; + QComboBox* m_value_source_dropdown; + QLineEdit* m_given_value_text; + QLabel* m_info_label_1; + QLabel* m_info_label_2; + QPushButton* m_next_scan_button; + QPushButton* m_refresh_values_button; + QPushButton* m_reset_button; + QCheckBox* m_display_values_in_hex_checkbox; + QTableWidget* m_address_table; +}; diff --git a/Source/Core/DolphinQt/CheatsManager.cpp b/Source/Core/DolphinQt/CheatsManager.cpp index 7c3e51a482..ba7a9cbc1a 100644 --- a/Source/Core/DolphinQt/CheatsManager.cpp +++ b/Source/Core/DolphinQt/CheatsManager.cpp @@ -3,152 +3,24 @@ #include "DolphinQt/CheatsManager.h" -#include -#include +#include -#include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include #include "Core/ActionReplay.h" +#include "Core/CheatSearch.h" #include "Core/ConfigManager.h" #include "Core/Core.h" -#include "Core/Debugger/PPCDebugInterface.h" -#include "Core/HW/Memmap.h" -#include "Core/PowerPC/MMU.h" -#include "Core/PowerPC/PowerPC.h" #include "UICommon/GameFile.h" +#include "DolphinQt/CheatSearchFactoryWidget.h" +#include "DolphinQt/CheatSearchWidget.h" #include "DolphinQt/Config/ARCodeWidget.h" #include "DolphinQt/Config/GeckoCodeWidget.h" #include "DolphinQt/Settings.h" - -constexpr u32 MAX_RESULTS = 50; - -constexpr int INDEX_ROLE = Qt::UserRole; -constexpr int COLUMN_ROLE = Qt::UserRole + 1; - -constexpr int AR_SET_BYTE_CMD = 0x00; -constexpr int AR_SET_SHORT_CMD = 0x02; -constexpr int AR_SET_INT_CMD = 0x04; - -enum class CompareType : int -{ - Equal = 0, - NotEqual = 1, - Less = 2, - LessEqual = 3, - More = 4, - MoreEqual = 5 -}; - -enum class DataType : int -{ - Byte = 0, - Short = 1, - Int = 2, - Float = 3, - Double = 4, - String = 5 -}; - -struct Result -{ - u32 address; - DataType type; - QString name; - bool locked = false; - u32 locked_value; -}; - -static u32 GetResultValue(Result result) -{ - switch (result.type) - { - case DataType::Byte: - return PowerPC::HostRead_U8(result.address); - case DataType::Short: - return PowerPC::HostRead_U16(result.address); - case DataType::Int: - return PowerPC::HostRead_U32(result.address); - default: - return 0; - } -} - -static void UpdatePatch(Result result) -{ - PowerPC::debug_interface.UnsetPatch(result.address); - if (result.locked) - { - switch (result.type) - { - case DataType::Byte: - PowerPC::debug_interface.SetPatch(result.address, - std::vector{static_cast(result.locked_value)}); - break; - default: - PowerPC::debug_interface.SetPatch(result.address, result.locked_value); - break; - } - } -} - -static ActionReplay::AREntry ResultToAREntry(Result result) -{ - u8 cmd; - - switch (result.type) - { - case DataType::Byte: - cmd = AR_SET_BYTE_CMD; - break; - case DataType::Short: - cmd = AR_SET_SHORT_CMD; - break; - default: - case DataType::Int: - cmd = AR_SET_INT_CMD; - break; - } - - u32 address = result.address & 0xffffff; - - return ActionReplay::AREntry(cmd << 24 | address, result.locked_value); -} - -template -static bool Compare(T mem_value, T value, CompareType op) -{ - switch (op) - { - case CompareType::Equal: - return mem_value == value; - case CompareType::NotEqual: - return mem_value != value; - case CompareType::Less: - return mem_value < value; - case CompareType::LessEqual: - return mem_value <= value; - case CompareType::More: - return mem_value > value; - case CompareType::MoreEqual: - return mem_value >= value; - default: - return false; - } -} +#include "QtUtils/PartiallyClosableTabWidget.h" CheatsManager::CheatsManager(QWidget* parent) : QDialog(parent) { @@ -162,8 +34,6 @@ CheatsManager::CheatsManager(QWidget* parent) : QDialog(parent) CreateWidgets(); ConnectWidgets(); - Reset(); - Update(); } CheatsManager::~CheatsManager() = default; @@ -175,7 +45,7 @@ void CheatsManager::OnStateChanged(Core::State state) const auto& game_id = SConfig::GetInstance().GetGameID(); const auto& game_tdb_id = SConfig::GetInstance().GetGameTDBID(); - u16 revision = SConfig::GetInstance().GetRevision(); + const u16 revision = SConfig::GetInstance().GetRevision(); if (m_game_id == game_id && m_game_tdb_id == game_tdb_id && m_revision == revision) return; @@ -184,32 +54,40 @@ void CheatsManager::OnStateChanged(Core::State state) m_game_tdb_id = game_tdb_id; m_revision = revision; - if (m_tab_widget->count() == 3) + if (m_ar_code) { - m_tab_widget->removeTab(0); - m_tab_widget->removeTab(0); + const int tab_index = m_tab_widget->indexOf(m_ar_code); + if (tab_index != -1) + m_tab_widget->removeTab(tab_index); + m_ar_code->deleteLater(); + m_ar_code = nullptr; } - if (m_tab_widget->count() == 1) + if (m_gecko_code) { - if (m_ar_code) - m_ar_code->deleteLater(); - - m_ar_code = new ARCodeWidget(m_game_id, m_revision, false); - m_tab_widget->insertTab(0, m_ar_code, tr("AR Code")); - auto* gecko_code = new GeckoCodeWidget(m_game_id, m_game_tdb_id, m_revision, false); - m_tab_widget->insertTab(1, gecko_code, tr("Gecko Codes")); + const int tab_index = m_tab_widget->indexOf(m_gecko_code); + if (tab_index != -1) + m_tab_widget->removeTab(tab_index); + m_gecko_code->deleteLater(); + m_gecko_code = nullptr; } + + m_ar_code = new ARCodeWidget(m_game_id, m_revision, false); + m_gecko_code = new GeckoCodeWidget(m_game_id, m_game_tdb_id, m_revision, false); + m_tab_widget->insertTab(0, m_ar_code, tr("AR Code")); + m_tab_widget->insertTab(1, m_gecko_code, tr("Gecko Codes")); + m_tab_widget->setTabUnclosable(0); + m_tab_widget->setTabUnclosable(1); } void CheatsManager::CreateWidgets() { - m_tab_widget = new QTabWidget; + m_tab_widget = new PartiallyClosableTabWidget; m_button_box = new QDialogButtonBox(QDialogButtonBox::Close); - m_cheat_search = CreateCheatSearch(); - - m_tab_widget->addTab(m_cheat_search, tr("Cheat Search")); + m_cheat_search_new = new CheatSearchFactoryWidget(); + m_tab_widget->addTab(m_cheat_search_new, tr("Start New Cheat Search")); + m_tab_widget->setTabUnclosable(0); auto* layout = new QVBoxLayout; layout->addWidget(m_tab_widget); @@ -218,513 +96,27 @@ void CheatsManager::CreateWidgets() setLayout(layout); } +void CheatsManager::OnNewSessionCreated(const Cheats::CheatSearchSessionBase& session) +{ + auto* w = new CheatSearchWidget(session.Clone()); + const int tab_index = m_tab_widget->addTab(w, tr("Cheat Search")); + w->connect(w, &CheatSearchWidget::ActionReplayCodeGenerated, this, + [this](const ActionReplay::ARCode& ar_code) { + if (m_ar_code) + m_ar_code->AddCode(ar_code); + }); + m_tab_widget->setCurrentIndex(tab_index); +} + +void CheatsManager::OnTabCloseRequested(int index) +{ + m_tab_widget->removeTab(index); +} + void CheatsManager::ConnectWidgets() { connect(m_button_box, &QDialogButtonBox::rejected, this, &QDialog::reject); - - connect(m_match_new, &QPushButton::clicked, this, &CheatsManager::NewSearch); - connect(m_match_next, &QPushButton::clicked, this, &CheatsManager::NextSearch); - connect(m_match_refresh, &QPushButton::clicked, this, &CheatsManager::Update); - connect(m_match_reset, &QPushButton::clicked, this, &CheatsManager::Reset); - - m_match_table->setContextMenuPolicy(Qt::CustomContextMenu); - m_watch_table->setContextMenuPolicy(Qt::CustomContextMenu); - - connect(m_match_table, &QTableWidget::customContextMenuRequested, this, - &CheatsManager::OnMatchContextMenu); - connect(m_watch_table, &QTableWidget::customContextMenuRequested, this, - &CheatsManager::OnWatchContextMenu); - connect(m_watch_table, &QTableWidget::itemChanged, this, &CheatsManager::OnWatchItemChanged); -} - -void CheatsManager::OnWatchContextMenu() -{ - if (m_watch_table->selectedItems().isEmpty()) - return; - - QMenu* menu = new QMenu(this); - - menu->addAction(tr("Remove from Watch"), this, [this] { - auto* item = m_watch_table->selectedItems()[0]; - - int index = item->data(INDEX_ROLE).toInt(); - - m_watch.erase(m_watch.begin() + index); - - Update(); - }); - - menu->addSeparator(); - - menu->addAction(tr("Generate Action Replay Code"), this, &CheatsManager::GenerateARCode); - - menu->exec(QCursor::pos()); -} - -void CheatsManager::OnMatchContextMenu() -{ - if (m_match_table->selectedItems().isEmpty()) - return; - - QMenu* menu = new QMenu(this); - - menu->addAction(tr("Add to Watch"), this, [this] { - auto* item = m_match_table->selectedItems()[0]; - - int index = item->data(INDEX_ROLE).toInt(); - - m_results[index].locked_value = GetResultValue(m_results[index]); - - m_watch.push_back(m_results[index]); - - Update(); - }); - - menu->exec(QCursor::pos()); -} - -void CheatsManager::GenerateARCode() -{ - if (!m_ar_code) - return; - - auto* item = m_watch_table->selectedItems()[0]; - - int index = item->data(INDEX_ROLE).toInt(); - ActionReplay::ARCode ar_code; - - ar_code.enabled = true; - ar_code.user_defined = true; - ar_code.name = tr("Generated by search (Address %1)") - .arg(m_watch[index].address, 8, 16, QLatin1Char('0')) - .toStdString(); - - ar_code.ops.push_back(ResultToAREntry(m_watch[index])); - - m_ar_code->AddCode(ar_code); -} - -void CheatsManager::OnWatchItemChanged(QTableWidgetItem* item) -{ - if (m_updating) - return; - - int index = item->data(INDEX_ROLE).toInt(); - int column = item->data(COLUMN_ROLE).toInt(); - - switch (column) - { - case 0: - m_watch[index].name = item->text(); - break; - case 2: - m_watch[index].locked = item->checkState() == Qt::Checked; - - if (m_watch[index].locked) - m_watch[index].locked_value = GetResultValue(m_results[index]); - - UpdatePatch(m_watch[index]); - break; - case 3: - { - const auto text = item->text(); - u32 value = 0; - - switch (m_watch[index].type) - { - case DataType::Byte: - value = text.toUShort(nullptr, 16) & 0xFF; - break; - case DataType::Short: - value = text.toUShort(nullptr, 16); - break; - case DataType::Int: - value = text.toUInt(nullptr, 16); - break; - case DataType::Float: - { - float f = text.toFloat(); - std::memcpy(&value, &f, sizeof(float)); - break; - } - default: - break; - } - - m_watch[index].locked_value = value; - - UpdatePatch(m_watch[index]); - - break; - } - } - - Update(); -} - -QWidget* CheatsManager::CreateCheatSearch() -{ - m_match_table = new QTableWidget; - m_watch_table = new QTableWidget; - - m_match_table->setTabKeyNavigation(false); - m_watch_table->setTabKeyNavigation(false); - - m_match_table->verticalHeader()->hide(); - m_watch_table->verticalHeader()->hide(); - - m_match_table->setSelectionBehavior(QAbstractItemView::SelectRows); - m_watch_table->setSelectionBehavior(QAbstractItemView::SelectRows); - - // Options - m_result_label = new QLabel; - m_match_length = new QComboBox; - m_match_operation = new QComboBox; - m_match_value = new QLineEdit; - m_match_new = new QPushButton(tr("New Search")); - m_match_next = new QPushButton(tr("Next Search")); - m_match_refresh = new QPushButton(tr("Refresh")); - m_match_reset = new QPushButton(tr("Reset")); - - auto* options = new QWidget; - auto* layout = new QVBoxLayout; - options->setLayout(layout); - - for (const auto& option : {tr("8-bit Integer"), tr("16-bit Integer"), tr("32-bit Integer"), - tr("Float"), tr("Double"), tr("String")}) - { - m_match_length->addItem(option); - } - - for (const auto& option : {tr("Equals to"), tr("Not equals to"), tr("Less than"), - tr("Less or equal to"), tr("More than"), tr("More or equal to")}) - { - m_match_operation->addItem(option); - } - - auto* group_box = new QGroupBox(tr("Type")); - auto* group_layout = new QHBoxLayout; - group_box->setLayout(group_layout); - - // i18n: The base 10 numeral system. Not related to non-integer numbers - m_match_decimal = new QRadioButton(tr("Decimal")); - m_match_hexadecimal = new QRadioButton(tr("Hexadecimal")); - m_match_octal = new QRadioButton(tr("Octal")); - - group_layout->addWidget(m_match_decimal); - group_layout->addWidget(m_match_hexadecimal); - group_layout->addWidget(m_match_octal); - - layout->addWidget(m_result_label); - layout->addWidget(m_match_length); - layout->addWidget(m_match_operation); - layout->addWidget(m_match_value); - layout->addWidget(group_box); - layout->addWidget(m_match_new); - layout->addWidget(m_match_next); - layout->addWidget(m_match_refresh); - layout->addWidget(m_match_reset); - - // Splitters - m_option_splitter = new QSplitter(Qt::Horizontal); - m_table_splitter = new QSplitter(Qt::Vertical); - - m_table_splitter->addWidget(m_match_table); - m_table_splitter->addWidget(m_watch_table); - - m_option_splitter->addWidget(m_table_splitter); - m_option_splitter->addWidget(options); - - return m_option_splitter; -} - -size_t CheatsManager::GetTypeSize() const -{ - switch (static_cast(m_match_length->currentIndex())) - { - case DataType::Byte: - return sizeof(u8); - case DataType::Short: - return sizeof(u16); - case DataType::Int: - return sizeof(u32); - case DataType::Float: - return sizeof(float); - case DataType::Double: - return sizeof(double); - default: - return m_match_value->text().toStdString().size(); - } -} - -std::function CheatsManager::CreateMatchFunction() -{ - const QString text = m_match_value->text(); - - if (text.isEmpty()) - { - m_result_label->setText(tr("No search value entered.")); - return nullptr; - } - - const CompareType op = static_cast(m_match_operation->currentIndex()); - - const int base = - (m_match_decimal->isChecked() ? 10 : (m_match_hexadecimal->isChecked() ? 16 : 8)); - - bool conversion_succeeded = false; - std::function matches_func; - switch (static_cast(m_match_length->currentIndex())) - { - case DataType::Byte: - { - u8 comparison_value = text.toUShort(&conversion_succeeded, base) & 0xFF; - matches_func = [=](u32 addr) { - return Compare(PowerPC::HostRead_U8(addr), comparison_value, op); - }; - break; - } - case DataType::Short: - { - u16 comparison_value = text.toUShort(&conversion_succeeded, base); - matches_func = [=](u32 addr) { - return Compare(PowerPC::HostRead_U16(addr), comparison_value, op); - }; - break; - } - case DataType::Int: - { - u32 comparison_value = text.toUInt(&conversion_succeeded, base); - matches_func = [=](u32 addr) { - return Compare(PowerPC::HostRead_U32(addr), comparison_value, op); - }; - break; - } - case DataType::Float: - { - float comparison_value = text.toFloat(&conversion_succeeded); - matches_func = [=](u32 addr) { - return Compare(PowerPC::HostRead_F32(addr), comparison_value, op); - }; - break; - } - case DataType::Double: - { - double comparison_value = text.toDouble(&conversion_succeeded); - matches_func = [=](u32 addr) { - return Compare(PowerPC::HostRead_F64(addr), comparison_value, op); - }; - break; - } - case DataType::String: - { - if (op != CompareType::Equal && op != CompareType::NotEqual) - { - m_result_label->setText(tr("String values can only be compared using equality.")); - return nullptr; - } - - conversion_succeeded = true; - - const QString lambda_text = m_match_value->text(); - const QByteArray utf8_bytes = lambda_text.toUtf8(); - - matches_func = [op, utf8_bytes](u32 addr) { - bool is_equal = std::equal(utf8_bytes.cbegin(), utf8_bytes.cend(), - reinterpret_cast(Memory::m_pRAM + addr - 0x80000000)); - switch (op) - { - case CompareType::Equal: - return is_equal; - case CompareType::NotEqual: - return !is_equal; - default: - // This should never occur since we've already checked the type of op - return false; - } - }; - break; - } - } - - if (conversion_succeeded) - return matches_func; - - m_result_label->setText(tr("Cannot interpret the given value.\nHave you chosen the right type?")); - return nullptr; -} - -void CheatsManager::NewSearch() -{ - m_results.clear(); - const u32 base_address = 0x80000000; - - if (!Memory::m_pRAM) - { - m_result_label->setText(tr("Memory Not Ready")); - return; - } - - std::function matches_func = CreateMatchFunction(); - if (matches_func == nullptr) - return; - - Core::RunAsCPUThread([&] { - for (u32 i = 0; i < Memory::GetRamSizeReal() - GetTypeSize(); i++) - { - if (PowerPC::HostIsRAMAddress(base_address + i) && matches_func(base_address + i)) - m_results.push_back( - {base_address + i, static_cast(m_match_length->currentIndex())}); - } - }); - - m_match_next->setEnabled(true); - - Update(); -} - -void CheatsManager::NextSearch() -{ - if (!Memory::m_pRAM) - { - m_result_label->setText(tr("Memory Not Ready")); - return; - } - - std::function matches_func = CreateMatchFunction(); - if (matches_func == nullptr) - return; - - Core::RunAsCPUThread([this, matches_func] { - m_results.erase(std::remove_if(m_results.begin(), m_results.end(), - [matches_func](Result r) { - return !PowerPC::HostIsRAMAddress(r.address) || - !matches_func(r.address); - }), - m_results.end()); - }); - - Update(); -} - -static QString GetResultString(const Result& result) -{ - if (!PowerPC::HostIsRAMAddress(result.address)) - { - return QStringLiteral("---"); - } - switch (result.type) - { - case DataType::Byte: - return QStringLiteral("%1").arg(PowerPC::HostRead_U8(result.address), 2, 16, QLatin1Char('0')); - case DataType::Short: - return QStringLiteral("%1").arg(PowerPC::HostRead_U16(result.address), 4, 16, QLatin1Char('0')); - case DataType::Int: - return QStringLiteral("%1").arg(PowerPC::HostRead_U32(result.address), 8, 16, QLatin1Char('0')); - case DataType::Float: - return QString::number(PowerPC::HostRead_F32(result.address)); - case DataType::Double: - return QString::number(PowerPC::HostRead_F64(result.address)); - case DataType::String: - return QObject::tr("String Match"); - default: - return {}; - } -} - -void CheatsManager::Update() -{ - m_match_table->clear(); - m_watch_table->clear(); - m_match_table->setColumnCount(2); - m_watch_table->setColumnCount(4); - - m_match_table->setHorizontalHeaderLabels({tr("Address"), tr("Value")}); - m_watch_table->setHorizontalHeaderLabels({tr("Name"), tr("Address"), tr("Lock"), tr("Value")}); - - if (m_results.size() > MAX_RESULTS) - { - m_result_label->setText(tr("Too many matches to display (%1)").arg(m_results.size())); - return; - } - - m_result_label->setText(tr("%n Match(es)", "", static_cast(m_results.size()))); - m_match_table->setRowCount(static_cast(m_results.size())); - - if (m_results.empty()) - return; - - m_updating = true; - - Core::RunAsCPUThread([this] { - for (size_t i = 0; i < m_results.size(); i++) - { - auto* address_item = new QTableWidgetItem( - QStringLiteral("%1").arg(m_results[i].address, 8, 16, QLatin1Char('0'))); - auto* value_item = new QTableWidgetItem; - - address_item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable); - value_item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable); - - value_item->setText(GetResultString(m_results[i])); - - address_item->setData(INDEX_ROLE, static_cast(i)); - value_item->setData(INDEX_ROLE, static_cast(i)); - - m_match_table->setItem(static_cast(i), 0, address_item); - m_match_table->setItem(static_cast(i), 1, value_item); - } - - m_watch_table->setRowCount(static_cast(m_watch.size())); - - for (size_t i = 0; i < m_watch.size(); i++) - { - auto* name_item = new QTableWidgetItem(m_watch[i].name); - auto* address_item = new QTableWidgetItem( - QStringLiteral("%1").arg(m_watch[i].address, 8, 16, QLatin1Char('0'))); - auto* lock_item = new QTableWidgetItem; - auto* value_item = new QTableWidgetItem; - - value_item->setText(GetResultString(m_results[i])); - - name_item->setData(INDEX_ROLE, static_cast(i)); - name_item->setData(COLUMN_ROLE, 0); - - address_item->setData(INDEX_ROLE, static_cast(i)); - address_item->setData(COLUMN_ROLE, 1); - - lock_item->setData(INDEX_ROLE, static_cast(i)); - lock_item->setData(COLUMN_ROLE, 2); - - value_item->setData(INDEX_ROLE, static_cast(i)); - value_item->setData(COLUMN_ROLE, 3); - - name_item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable); - address_item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable); - lock_item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsUserCheckable | Qt::ItemIsSelectable); - value_item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable); - - lock_item->setCheckState(m_watch[i].locked ? Qt::Checked : Qt::Unchecked); - - m_watch_table->setItem(static_cast(i), 0, name_item); - m_watch_table->setItem(static_cast(i), 1, address_item); - m_watch_table->setItem(static_cast(i), 2, lock_item); - m_watch_table->setItem(static_cast(i), 3, value_item); - } - }); - - m_updating = false; -} - -void CheatsManager::Reset() -{ - m_results.clear(); - m_watch.clear(); - m_match_next->setEnabled(false); - m_match_table->clear(); - m_watch_table->clear(); - m_match_decimal->setChecked(true); - m_result_label->clear(); - - Update(); + connect(m_cheat_search_new, &CheatSearchFactoryWidget::NewSessionCreated, this, + &CheatsManager::OnNewSessionCreated); + connect(m_tab_widget, &QTabWidget::tabCloseRequested, this, &CheatsManager::OnTabCloseRequested); } diff --git a/Source/Core/DolphinQt/CheatsManager.h b/Source/Core/DolphinQt/CheatsManager.h index 6cf4179974..7cf0bb874f 100644 --- a/Source/Core/DolphinQt/CheatsManager.h +++ b/Source/Core/DolphinQt/CheatsManager.h @@ -5,6 +5,7 @@ #include #include +#include #include #include @@ -12,29 +13,19 @@ #include "Common/CommonTypes.h" #include "DolphinQt/GameList/GameListModel.h" +#include "Core/CheatSearch.h" + class ARCodeWidget; -class QComboBox; +class GeckoCodeWidget; +class CheatSearchFactoryWidget; class QDialogButtonBox; -class QLabel; -class QLineEdit; -class QPushButton; -class QRadioButton; -class QSplitter; -class QTabWidget; -class QTableWidget; -class QTableWidgetItem; -struct Result; +class PartiallyClosableTabWidget; namespace Core { enum class State; } -namespace UICommon -{ -class GameFile; -} - class CheatsManager : public QDialog { Q_OBJECT @@ -43,51 +34,20 @@ public: ~CheatsManager(); private: - QWidget* CreateCheatSearch(); void CreateWidgets(); void ConnectWidgets(); void OnStateChanged(Core::State state); - - size_t GetTypeSize() const; - std::function CreateMatchFunction(); - - void Reset(); - void NewSearch(); - void NextSearch(); - void Update(); - void GenerateARCode(); - - void OnWatchContextMenu(); - void OnMatchContextMenu(); - void OnWatchItemChanged(QTableWidgetItem* item); + void OnNewSessionCreated(const Cheats::CheatSearchSessionBase& session); + void OnTabCloseRequested(int index); std::string m_game_id; std::string m_game_tdb_id; u16 m_revision = 0; - std::vector m_results; - std::vector m_watch; QDialogButtonBox* m_button_box; - QTabWidget* m_tab_widget = nullptr; + PartiallyClosableTabWidget* m_tab_widget = nullptr; - QWidget* m_cheat_search; ARCodeWidget* m_ar_code = nullptr; - - QLabel* m_result_label; - QTableWidget* m_match_table; - QTableWidget* m_watch_table; - QSplitter* m_option_splitter; - QSplitter* m_table_splitter; - QComboBox* m_match_length; - QComboBox* m_match_operation; - QLineEdit* m_match_value; - QPushButton* m_match_new; - QPushButton* m_match_next; - QPushButton* m_match_refresh; - QPushButton* m_match_reset; - - QRadioButton* m_match_decimal; - QRadioButton* m_match_hexadecimal; - QRadioButton* m_match_octal; - bool m_updating = false; + GeckoCodeWidget* m_gecko_code = nullptr; + CheatSearchFactoryWidget* m_cheat_search_new = nullptr; }; diff --git a/Source/Core/DolphinQt/DolphinQt.vcxproj b/Source/Core/DolphinQt/DolphinQt.vcxproj index 028980080b..c8c0061bbe 100644 --- a/Source/Core/DolphinQt/DolphinQt.vcxproj +++ b/Source/Core/DolphinQt/DolphinQt.vcxproj @@ -45,6 +45,8 @@ + + @@ -168,6 +170,7 @@ + @@ -227,6 +230,8 @@ + + @@ -340,6 +345,7 @@ + diff --git a/Source/Core/DolphinQt/QtUtils/PartiallyClosableTabWidget.cpp b/Source/Core/DolphinQt/QtUtils/PartiallyClosableTabWidget.cpp new file mode 100644 index 0000000000..b8d3ef0dbd --- /dev/null +++ b/Source/Core/DolphinQt/QtUtils/PartiallyClosableTabWidget.cpp @@ -0,0 +1,19 @@ +// Copyright 2021 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "DolphinQt/QtUtils/PartiallyClosableTabWidget.h" + +#include +#include + +PartiallyClosableTabWidget::PartiallyClosableTabWidget(QWidget* parent) : QTabWidget(parent) +{ + setTabsClosable(true); +} + +void PartiallyClosableTabWidget::setTabUnclosable(int index) +{ + QTabBar::ButtonPosition closeSide = (QTabBar::ButtonPosition)style()->styleHint( + QStyle::SH_TabBar_CloseButtonPosition, nullptr, this); + tabBar()->setTabButton(index, closeSide, nullptr); +} diff --git a/Source/Core/DolphinQt/QtUtils/PartiallyClosableTabWidget.h b/Source/Core/DolphinQt/QtUtils/PartiallyClosableTabWidget.h new file mode 100644 index 0000000000..7fa1f3e160 --- /dev/null +++ b/Source/Core/DolphinQt/QtUtils/PartiallyClosableTabWidget.h @@ -0,0 +1,17 @@ +// Copyright 2021 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +class QEvent; + +class PartiallyClosableTabWidget : public QTabWidget +{ + Q_OBJECT +public: + PartiallyClosableTabWidget(QWidget* parent = nullptr); + + void setTabUnclosable(int index); +};