// Copyright 2018 Dolphin Emulator Project // Licensed under GPLv2+ // Refer to the license.txt file included. #include "DolphinQt2/CheatsManager.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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 "DolphinQt2/Config/ARCodeWidget.h" #include "DolphinQt2/Config/GeckoCodeWidget.h" #include "DolphinQt2/GameList/GameListModel.h" #include "DolphinQt2/QtUtils/ActionHelper.h" #include "DolphinQt2/Settings.h" constexpr u32 MAX_RESULTS = 50; constexpr int INDEX_ROLE = Qt::UserRole; constexpr int COLUMN_ROLE = Qt::UserRole + 1; CheatsManager::CheatsManager(QWidget* parent) : QDialog(parent) { setWindowTitle(tr("Cheats Manager")); setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); connect(&Settings::Instance(), &Settings::EmulationStateChanged, this, &CheatsManager::OnStateChanged); OnStateChanged(Core::GetState()); CreateWidgets(); ConnectWidgets(); Reset(); Update(); } void CheatsManager::OnStateChanged(Core::State state) { if (state != Core::State::Running && state != Core::State::Paused) return; auto* model = Settings::Instance().GetGameListModel(); for (int i = 0; i < model->rowCount(QModelIndex()); i++) { auto file = model->GetGameFile(i); if (file->GetGameID() == SConfig::GetInstance().GetGameID()) { m_game_file = file; if (m_tab_widget->count() == 3) { m_tab_widget->removeTab(0); m_tab_widget->removeTab(0); } if (m_tab_widget->count() == 1) { if (m_ar_code) m_ar_code->deleteLater(); m_ar_code = new ARCodeWidget(*m_game_file, false); m_tab_widget->insertTab(0, m_ar_code, tr("AR Code")); m_tab_widget->insertTab(1, new GeckoCodeWidget(*m_game_file, false), tr("Gecko Codes")); } } } } void CheatsManager::CreateWidgets() { m_tab_widget = new QTabWidget; m_button_box = new QDialogButtonBox(QDialogButtonBox::Close); m_cheat_search = CreateCheatSearch(); m_tab_widget->addTab(m_cheat_search, tr("Cheat Search")); auto* layout = new QVBoxLayout; layout->addWidget(m_tab_widget); layout->addWidget(m_button_box); setLayout(layout); } void CheatsManager::ConnectWidgets() { connect(m_button_box, &QDialogButtonBox::rejected, this, &QDialog::reject); connect(m_match_new, &QPushButton::pressed, this, &CheatsManager::NewSearch); connect(m_match_next, &QPushButton::pressed, this, &CheatsManager::NextSearch); connect(m_match_refresh, &QPushButton::pressed, this, &CheatsManager::Update); connect(m_match_reset, &QPushButton::pressed, 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); AddAction(menu, tr("Remove from Watch"), this, [this] { auto* item = m_match_table->selectedItems()[0]; int index = item->data(INDEX_ROLE).toInt(); m_watch.erase(m_watch.begin() + index); Update(); }); menu->addSeparator(); AddAction(menu, 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); AddAction(menu, tr("Add to Watch"), this, [this] { auto* item = m_match_table->selectedItems()[0]; int index = item->data(INDEX_ROLE).toInt(); m_watch.push_back(m_results[index]); Update(); }); menu->exec(QCursor::pos()); } static ActionReplay::AREntry ResultToAREntry(Result result) { u8 cmd; switch (result.type) { case DataType::Byte: cmd = 0x00; break; case DataType::Short: cmd = 0x02; break; default: case DataType::Int: cmd = 0x04; break; } u32 address = result.address & 0xffffff; return ActionReplay::AREntry(cmd << 24 | address, result.locked_value); } void CheatsManager::GenerateARCode() { if (!m_ar_code) return; auto* item = m_match_table->selectedItems()[0]; int index = item->data(INDEX_ROLE).toInt(); ActionReplay::ARCode ar_code; ar_code.active = 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 3: m_watch[index].locked = item->checkState() == Qt::Checked; break; case 4: { const auto text = item->text(); u32 value = 0; switch (static_cast(m_match_length->currentIndex())) { 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; break; } } Update(); } QWidget* CheatsManager::CreateCheatSearch() { m_match_table = new QTableWidget; m_watch_table = new QTableWidget; 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); 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(); } } 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 <= mem_value; case CompareType::More: return value > mem_value; case CompareType::MoreEqual: return value >= mem_value; default: return false; } } bool CheatsManager::MatchesSearch(u32 addr) const { const auto text = m_match_value->text(); const auto op = static_cast(m_match_operation->currentIndex()); const int base = (m_match_decimal->isChecked() ? 10 : (m_match_hexadecimal->isChecked() ? 16 : 8)); switch (static_cast(m_match_length->currentIndex())) { case DataType::Byte: return Compare(PowerPC::HostRead_U8(addr), text.toUShort(nullptr, base) & 0xFF, op); case DataType::Short: return Compare(PowerPC::HostRead_U16(addr), text.toUShort(nullptr, base), op); case DataType::Int: return Compare(PowerPC::HostRead_U32(addr), text.toUInt(nullptr, base), op); case DataType::Float: return Compare(PowerPC::HostRead_F32(addr), text.toFloat(), op); case DataType::Double: return Compare(PowerPC::HostRead_F64(addr), text.toDouble(), op); case DataType::String: { bool is_equal = std::equal(text.toUtf8().cbegin(), text.toUtf8().cend(), reinterpret_cast(Memory::m_pRAM + addr - 0x80000000)); // String only supports equals and not equals comparisons because the other ones frankly don't // make any sense here switch (op) { case CompareType::Equal: return is_equal; case CompareType::NotEqual: return !is_equal; default: return false; } } } return false; } void CheatsManager::NewSearch() { m_results.clear(); const u32 base_address = 0x80000000; if (!Memory::m_pRAM) { m_result_label->setText(tr("Memory Not Ready")); return; } Core::RunAsCPUThread([&] { for (u32 i = 0; i < Memory::REALRAM_SIZE - GetTypeSize(); i++) { if (PowerPC::HostIsRAMAddress(base_address + i) && MatchesSearch(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; } Core::RunAsCPUThread([this] { m_results.erase(std::remove_if(m_results.begin(), m_results.end(), [this](Result r) { return !PowerPC::HostIsRAMAddress(r.address) || !MatchesSearch(r.address); }), m_results.end()); }); Update(); } 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("%1 Match(es)").arg(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); if (PowerPC::HostIsRAMAddress(m_results[i].address)) { switch (m_results[i].type) { case DataType::Byte: value_item->setText(QStringLiteral("%1").arg(PowerPC::HostRead_U8(m_results[i].address), 2, 16, QLatin1Char('0'))); break; case DataType::Short: value_item->setText(QStringLiteral("%1").arg(PowerPC::HostRead_U16(m_results[i].address), 4, 16, QLatin1Char('0'))); break; case DataType::Int: value_item->setText(QStringLiteral("%1").arg(PowerPC::HostRead_U32(m_results[i].address), 8, 16, QLatin1Char('0'))); break; case DataType::Float: value_item->setText(QString::number(PowerPC::HostRead_F32(m_results[i].address))); break; case DataType::Double: value_item->setText(QString::number(PowerPC::HostRead_F64(m_results[i].address))); break; case DataType::String: value_item->setText(tr("String Match")); break; } } else { value_item->setText(QStringLiteral("---")); } 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; if (PowerPC::HostIsRAMAddress(m_watch[i].address)) { if (m_watch[i].locked) { PowerPC::debug_interface.Patch(m_watch[i].address, m_watch[i].locked_value); } switch (m_watch[i].type) { case DataType::Byte: value_item->setText(QStringLiteral("%1").arg(PowerPC::HostRead_U8(m_watch[i].address), 2, 16, QLatin1Char('0'))); break; case DataType::Short: value_item->setText(QStringLiteral("%1").arg(PowerPC::HostRead_U16(m_watch[i].address), 4, 16, QLatin1Char('0'))); break; case DataType::Int: value_item->setText(QStringLiteral("%1").arg(PowerPC::HostRead_U32(m_watch[i].address), 8, 16, QLatin1Char('0'))); break; case DataType::Float: value_item->setText(QString::number(PowerPC::HostRead_F32(m_watch[i].address))); break; case DataType::Double: value_item->setText(QString::number(PowerPC::HostRead_F64(m_watch[i].address))); break; case DataType::String: value_item->setText(tr("String Match")); break; } } else { value_item->setText(QStringLiteral("---")); } 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); value_item->setData(INDEX_ROLE, static_cast(i)); value_item->setData(COLUMN_ROLE, 2); lock_item->setData(INDEX_ROLE, static_cast(i)); lock_item->setData(COLUMN_ROLE, 3); value_item->setData(INDEX_ROLE, static_cast(i)); value_item->setData(COLUMN_ROLE, 4); name_item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable); address_item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable); lock_item->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsSelectable); value_item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable); 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->setText(QStringLiteral("")); Update(); }