From 573036b38eb963fdbe37896666d771492467354c Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Wed, 30 Jan 2019 23:03:44 +0800 Subject: [PATCH 1/2] core/cheats: Add and change a few functions Added a few interfaces for adding/deleting/replacing/saving cheats. The cheats list is guarded by a std::shared_mutex, and would only need a exclusive lock when it's being updated. I marked the `Execute` function as `const` to avoid accidentally changing the internal state of the cheat on execution, so that execution can be considered a "read" operation which only needs a shared lock. Whether a cheat is enabled or not is now saved by a special comment line `*citra_enabled`. --- src/core/cheats/cheat_base.h | 3 +- src/core/cheats/cheats.cpp | 62 ++++++++++++++++++++++++++++--- src/core/cheats/cheats.h | 10 ++++- src/core/cheats/gateway_cheat.cpp | 51 ++++++++++++++++++++++--- src/core/cheats/gateway_cheat.h | 7 +++- 5 files changed, 117 insertions(+), 16 deletions(-) diff --git a/src/core/cheats/cheat_base.h b/src/core/cheats/cheat_base.h index ee64a047f..d8a5924cd 100644 --- a/src/core/cheats/cheat_base.h +++ b/src/core/cheats/cheat_base.h @@ -14,7 +14,7 @@ namespace Cheats { class CheatBase { public: virtual ~CheatBase(); - virtual void Execute(Core::System& system) = 0; + virtual void Execute(Core::System& system) const = 0; virtual bool IsEnabled() const = 0; virtual void SetEnabled(bool enabled) = 0; @@ -22,6 +22,7 @@ public: virtual std::string GetComments() const = 0; virtual std::string GetName() const = 0; virtual std::string GetType() const = 0; + virtual std::string GetCode() const = 0; virtual std::string ToString() const = 0; }; diff --git a/src/core/cheats/cheats.cpp b/src/core/cheats/cheats.cpp index 612d1b5d9..8b4a30ca6 100644 --- a/src/core/cheats/cheats.cpp +++ b/src/core/cheats/cheats.cpp @@ -2,8 +2,10 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#include #include #include +#include "common/file_util.h" #include "core/cheats/cheats.h" #include "core/cheats/gateway_cheat.h" #include "core/core.h" @@ -26,10 +28,54 @@ CheatEngine::~CheatEngine() { system.CoreTiming().UnscheduleEvent(event, 0); } -const std::vector>& CheatEngine::GetCheats() const { +std::vector> CheatEngine::GetCheats() const { + std::shared_lock lock(cheats_list_mutex); return cheats_list; } +void CheatEngine::AddCheat(const std::shared_ptr& cheat) { + std::unique_lock lock(cheats_list_mutex); + cheats_list.push_back(cheat); +} + +void CheatEngine::RemoveCheat(int index) { + std::unique_lock lock(cheats_list_mutex); + if (index < 0 || index >= cheats_list.size()) { + LOG_ERROR(Core_Cheats, "Invalid index {}", index); + return; + } + cheats_list.erase(cheats_list.begin() + index); +} + +void CheatEngine::UpdateCheat(int index, const std::shared_ptr& new_cheat) { + std::unique_lock lock(cheats_list_mutex); + if (index < 0 || index >= cheats_list.size()) { + LOG_ERROR(Core_Cheats, "Invalid index {}", index); + return; + } + cheats_list[index] = new_cheat; +} + +void CheatEngine::SaveCheatFile() const { + const std::string cheat_dir = FileUtil::GetUserPath(FileUtil::UserPath::CheatsDir); + const std::string filepath = fmt::format( + "{}{:016X}.txt", cheat_dir, system.Kernel().GetCurrentProcess()->codeset->program_id); + + if (!FileUtil::IsDirectory(cheat_dir)) { + FileUtil::CreateDir(cheat_dir); + } + + std::ofstream file; + OpenFStream(file, filepath, std::ios_base::out); + + auto cheats = GetCheats(); + for (const auto& cheat : cheats) { + file << cheat->ToString(); + } + + file.flush(); +} + void CheatEngine::LoadCheatFile() { const std::string cheat_dir = FileUtil::GetUserPath(FileUtil::UserPath::CheatsDir); const std::string filepath = fmt::format( @@ -43,13 +89,19 @@ void CheatEngine::LoadCheatFile() { return; auto gateway_cheats = GatewayCheat::LoadFile(filepath); - std::move(gateway_cheats.begin(), gateway_cheats.end(), std::back_inserter(cheats_list)); + { + std::unique_lock lock(cheats_list_mutex); + std::move(gateway_cheats.begin(), gateway_cheats.end(), std::back_inserter(cheats_list)); + } } void CheatEngine::RunCallback([[maybe_unused]] u64 userdata, int cycles_late) { - for (auto& cheat : cheats_list) { - if (cheat->IsEnabled()) { - cheat->Execute(system); + { + std::shared_lock lock(cheats_list_mutex); + for (auto& cheat : cheats_list) { + if (cheat->IsEnabled()) { + cheat->Execute(system); + } } } system.CoreTiming().ScheduleEvent(run_interval_ticks - cycles_late, event); diff --git a/src/core/cheats/cheats.h b/src/core/cheats/cheats.h index 7e838de29..a8d373038 100644 --- a/src/core/cheats/cheats.h +++ b/src/core/cheats/cheats.h @@ -5,6 +5,7 @@ #pragma once #include +#include #include #include "common/common_types.h" @@ -25,12 +26,17 @@ class CheatEngine { public: explicit CheatEngine(Core::System& system); ~CheatEngine(); - const std::vector>& GetCheats() const; + std::vector> GetCheats() const; + void AddCheat(const std::shared_ptr& cheat); + void RemoveCheat(int index); + void UpdateCheat(int index, const std::shared_ptr& new_cheat); + void SaveCheatFile() const; private: void LoadCheatFile(); void RunCallback(u64 userdata, int cycles_late); - std::vector> cheats_list; + std::vector> cheats_list; + mutable std::shared_mutex cheats_list_mutex; Core::TimingEventType* event; Core::System& system; }; diff --git a/src/core/cheats/gateway_cheat.cpp b/src/core/cheats/gateway_cheat.cpp index af7e0f8cf..15a0e8ca9 100644 --- a/src/core/cheats/gateway_cheat.cpp +++ b/src/core/cheats/gateway_cheat.cpp @@ -176,6 +176,7 @@ GatewayCheat::CheatLine::CheatLine(const std::string& line) { type = CheatType::Null; cheat_line = line; LOG_ERROR(Core_Cheats, "Cheat contains invalid line: {}", line); + valid = false; return; } try { @@ -193,6 +194,7 @@ GatewayCheat::CheatLine::CheatLine(const std::string& line) { type = CheatType::Null; cheat_line = line; LOG_ERROR(Core_Cheats, "Cheat contains invalid line: {}", line); + valid = false; } } @@ -201,9 +203,23 @@ GatewayCheat::GatewayCheat(std::string name_, std::vector cheat_lines : name(std::move(name_)), cheat_lines(std::move(cheat_lines_)), comments(std::move(comments_)) { } +GatewayCheat::GatewayCheat(std::string name_, std::string code, std::string comments_) + : name(std::move(name_)), comments(std::move(comments_)) { + + std::vector code_lines; + Common::SplitString(code, '\n', code_lines); + + std::vector temp_cheat_lines; + for (std::size_t i = 0; i < code_lines.size(); ++i) { + if (!code_lines[i].empty()) + temp_cheat_lines.emplace_back(code_lines[i]); + } + cheat_lines = std::move(temp_cheat_lines); +} + GatewayCheat::~GatewayCheat() = default; -void GatewayCheat::Execute(Core::System& system) { +void GatewayCheat::Execute(Core::System& system) const { State state; Memory::MemorySystem& memory = system.Memory(); @@ -421,13 +437,28 @@ std::string GatewayCheat::GetType() const { return "Gateway"; } +std::string GatewayCheat::GetCode() const { + std::string result; + for (const auto& line : cheat_lines) + result += line.cheat_line + '\n'; + return result; +} + +/// A special marker used to keep track of enabled cheats +static constexpr char EnabledText[] = "*citra_enabled"; + std::string GatewayCheat::ToString() const { std::string result; result += '[' + name + "]\n"; - result += comments + '\n'; - for (const auto& line : cheat_lines) - result += line.cheat_line + '\n'; - result += '\n'; + if (enabled) { + result += EnabledText; + result += '\n'; + } + std::vector comment_lines; + Common::SplitString(comments, '\n', comment_lines); + for (const auto& comment_line : comment_lines) + result += "*" + comment_line + '\n'; + result += GetCode() + '\n'; return result; } @@ -443,6 +474,7 @@ std::vector> GatewayCheat::LoadFile(const std::string std::string comments; std::vector cheat_lines; std::string name; + bool enabled = false; while (!file.eof()) { std::string line; @@ -452,18 +484,25 @@ std::vector> GatewayCheat::LoadFile(const std::string if (line.length() >= 2 && line.front() == '[') { if (!cheat_lines.empty()) { cheats.push_back(std::make_unique(name, cheat_lines, comments)); + cheats.back()->SetEnabled(enabled); + enabled = false; } name = line.substr(1, line.length() - 2); cheat_lines.clear(); comments.erase(); } else if (!line.empty() && line.front() == '*') { - comments += line.substr(1, line.length() - 1) + '\n'; + if (line == EnabledText) { + enabled = true; + } else { + comments += line.substr(1, line.length() - 1) + '\n'; + } } else if (!line.empty()) { cheat_lines.emplace_back(std::move(line)); } } if (!cheat_lines.empty()) { cheats.push_back(std::make_unique(name, cheat_lines, comments)); + cheats.back()->SetEnabled(enabled); } return cheats; } diff --git a/src/core/cheats/gateway_cheat.h b/src/core/cheats/gateway_cheat.h index 5d7a8fcf9..46512fb96 100644 --- a/src/core/cheats/gateway_cheat.h +++ b/src/core/cheats/gateway_cheat.h @@ -50,12 +50,14 @@ public: u32 value; u32 first; std::string cheat_line; + bool valid = true; }; GatewayCheat(std::string name, std::vector cheat_lines, std::string comments); + GatewayCheat(std::string name, std::string code, std::string comments); ~GatewayCheat(); - void Execute(Core::System& system) override; + void Execute(Core::System& system) const override; bool IsEnabled() const override; void SetEnabled(bool enabled) override; @@ -63,6 +65,7 @@ public: std::string GetComments() const override; std::string GetName() const override; std::string GetType() const override; + std::string GetCode() const override; std::string ToString() const override; /// Gateway cheats look like: @@ -77,7 +80,7 @@ public: private: std::atomic enabled = false; const std::string name; - const std::vector cheat_lines; + std::vector cheat_lines; const std::string comments; }; } // namespace Cheats From 433176a9b9270f637b12f8a8e5ab971c07838db9 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Wed, 30 Jan 2019 23:06:01 +0800 Subject: [PATCH 2/2] citra_qt: Implement UI for adding/editing/deleting cheats The UI file is rewritten, to better make use of Qt's layouts (instead of depending on abstract geometry). "Add Cheat", "Save", "Delete" buttons are also added. The UI logic should be rather easy and usable (IMO), but the code may seem a bit dirty. If anyone has a better idea regarding UI logic design or code implementation, feel free to tell me about it. --- src/citra_qt/cheats.cpp | 189 ++++++++++++++++++++- src/citra_qt/cheats.h | 34 +++- src/citra_qt/cheats.ui | 352 +++++++++++++++++++++------------------- 3 files changed, 395 insertions(+), 180 deletions(-) diff --git a/src/citra_qt/cheats.cpp b/src/citra_qt/cheats.cpp index 720609205..fc6f102a2 100644 --- a/src/citra_qt/cheats.cpp +++ b/src/citra_qt/cheats.cpp @@ -3,10 +3,12 @@ // Refer to the license.txt file included. #include +#include #include #include "citra_qt/cheats.h" #include "core/cheats/cheat_base.h" #include "core/cheats/cheats.h" +#include "core/cheats/gateway_cheat.h" #include "core/core.h" #include "core/hle/kernel/process.h" #include "ui_cheats.h" @@ -21,14 +23,23 @@ CheatDialog::CheatDialog(QWidget* parent) ui->tableCheats->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Fixed); ui->tableCheats->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch); ui->tableCheats->horizontalHeader()->setSectionResizeMode(2, QHeaderView::Fixed); - ui->textDetails->setEnabled(false); + ui->lineName->setEnabled(false); + ui->textCode->setEnabled(false); ui->textNotes->setEnabled(false); const auto game_id = fmt::format( "{:016X}", Core::System::GetInstance().Kernel().GetCurrentProcess()->codeset->program_id); ui->labelTitle->setText(tr("Title ID: %1").arg(QString::fromStdString(game_id))); connect(ui->buttonClose, &QPushButton::released, this, &CheatDialog::OnCancel); + connect(ui->buttonAddCheat, &QPushButton::released, this, &CheatDialog::OnAddCheat); connect(ui->tableCheats, &QTableWidget::cellClicked, this, &CheatDialog::OnRowSelected); + connect(ui->lineName, &QLineEdit::textEdited, this, &CheatDialog::OnTextEdited); + connect(ui->textNotes, &QPlainTextEdit::textChanged, this, &CheatDialog::OnTextEdited); + connect(ui->textCode, &QPlainTextEdit::textChanged, this, &CheatDialog::OnTextEdited); + + connect(ui->buttonSave, &QPushButton::released, + [this] { SaveCheat(ui->tableCheats->currentRow()); }); + connect(ui->buttonDelete, &QPushButton::released, this, &CheatDialog::OnDeleteCheat); LoadCheats(); } @@ -36,7 +47,7 @@ CheatDialog::CheatDialog(QWidget* parent) CheatDialog::~CheatDialog() = default; void CheatDialog::LoadCheats() { - const auto& cheats = Core::System::GetInstance().CheatEngine().GetCheats(); + cheats = Core::System::GetInstance().CheatEngine().GetCheats(); ui->tableCheats->setRowCount(cheats.size()); @@ -56,20 +67,184 @@ void CheatDialog::LoadCheats() { } } +bool CheatDialog::CheckSaveCheat() { + auto answer = QMessageBox::warning( + this, tr("Cheats"), tr("Would you like to save the current cheat?"), + QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, QMessageBox::Cancel); + + if (answer == QMessageBox::Yes) { + return SaveCheat(last_row); + } else { + return answer != QMessageBox::Cancel; + } +} + +bool CheatDialog::SaveCheat(int row) { + if (ui->lineName->text().isEmpty()) { + QMessageBox::critical(this, tr("Save Cheat"), tr("Please enter a cheat name.")); + return false; + } + if (ui->textCode->toPlainText().isEmpty()) { + QMessageBox::critical(this, tr("Save Cheat"), tr("Please enter the cheat code.")); + return false; + } + + // Check if the cheat lines are valid + auto code_lines = ui->textCode->toPlainText().split("\n", QString::SkipEmptyParts); + for (int i = 0; i < code_lines.size(); ++i) { + Cheats::GatewayCheat::CheatLine cheat_line(code_lines[i].toStdString()); + if (cheat_line.valid) + continue; + + auto answer = QMessageBox::warning( + this, tr("Save Cheat"), + tr("Cheat code line %1 is not valid.\nWould you like to ignore the error and continue?") + .arg(i + 1), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + if (answer == QMessageBox::No) + return false; + } + + auto cheat = std::make_shared(ui->lineName->text().toStdString(), + ui->textCode->toPlainText().toStdString(), + ui->textNotes->toPlainText().toStdString()); + + if (newly_created) { + Core::System::GetInstance().CheatEngine().AddCheat(cheat); + newly_created = false; + } else { + Core::System::GetInstance().CheatEngine().UpdateCheat(row, cheat); + } + Core::System::GetInstance().CheatEngine().SaveCheatFile(); + + int previous_row = ui->tableCheats->currentRow(); + int previous_col = ui->tableCheats->currentColumn(); + LoadCheats(); + ui->tableCheats->setCurrentCell(previous_row, previous_col); + + edited = false; + ui->buttonSave->setEnabled(false); + ui->buttonAddCheat->setEnabled(true); + return true; +} + +void CheatDialog::closeEvent(QCloseEvent* event) { + if (edited && !CheckSaveCheat()) { + event->ignore(); + return; + } + event->accept(); +} + void CheatDialog::OnCancel() { close(); } void CheatDialog::OnRowSelected(int row, int column) { - ui->textDetails->setEnabled(true); + if (row == last_row) { + return; + } + if (edited && !CheckSaveCheat()) { + ui->tableCheats->setCurrentCell(last_row, last_col); + return; + } + if (row < cheats.size()) { + if (newly_created) { + // Remove the newly created dummy item + newly_created = false; + ui->tableCheats->setRowCount(ui->tableCheats->rowCount() - 1); + } + + const auto& current_cheat = cheats[row]; + ui->lineName->setText(QString::fromStdString(current_cheat->GetName())); + ui->textNotes->setPlainText(QString::fromStdString(current_cheat->GetComments())); + ui->textCode->setPlainText(QString::fromStdString(current_cheat->GetCode())); + } + + edited = false; + ui->buttonSave->setEnabled(false); + ui->buttonDelete->setEnabled(true); + ui->buttonAddCheat->setEnabled(true); + ui->lineName->setEnabled(true); + ui->textCode->setEnabled(true); ui->textNotes->setEnabled(true); - const auto& current_cheat = Core::System::GetInstance().CheatEngine().GetCheats()[row]; - ui->textNotes->setPlainText(QString::fromStdString(current_cheat->GetComments())); - ui->textDetails->setPlainText(QString::fromStdString(current_cheat->ToString())); + + last_row = row; + last_col = column; } void CheatDialog::OnCheckChanged(int state) { const QCheckBox* checkbox = qobject_cast(sender()); int row = static_cast(checkbox->property("row").toInt()); - Core::System::GetInstance().CheatEngine().GetCheats()[row]->SetEnabled(state); + cheats[row]->SetEnabled(state); + Core::System::GetInstance().CheatEngine().SaveCheatFile(); +} + +void CheatDialog::OnTextEdited() { + edited = true; + ui->buttonSave->setEnabled(true); +} + +void CheatDialog::OnDeleteCheat() { + if (newly_created) { + newly_created = false; + } else { + Core::System::GetInstance().CheatEngine().RemoveCheat(ui->tableCheats->currentRow()); + Core::System::GetInstance().CheatEngine().SaveCheatFile(); + } + + LoadCheats(); + if (cheats.empty()) { + ui->lineName->setText(""); + ui->textCode->setPlainText(""); + ui->textNotes->setPlainText(""); + ui->lineName->setEnabled(false); + ui->textCode->setEnabled(false); + ui->textNotes->setEnabled(false); + ui->buttonDelete->setEnabled(false); + last_row = last_col = -1; + } else { + if (last_row >= ui->tableCheats->rowCount()) { + last_row = ui->tableCheats->rowCount() - 1; + } + ui->tableCheats->setCurrentCell(last_row, last_col); + + const auto& current_cheat = cheats[last_row]; + ui->lineName->setText(QString::fromStdString(current_cheat->GetName())); + ui->textNotes->setPlainText(QString::fromStdString(current_cheat->GetComments())); + ui->textCode->setPlainText(QString::fromStdString(current_cheat->GetCode())); + } + + edited = false; + ui->buttonSave->setEnabled(false); + ui->buttonAddCheat->setEnabled(true); +} + +void CheatDialog::OnAddCheat() { + if (edited && !CheckSaveCheat()) { + return; + } + + int row = ui->tableCheats->rowCount(); + ui->tableCheats->setRowCount(row + 1); + ui->tableCheats->setCurrentCell(row, 1); + + // create a dummy item + ui->tableCheats->setItem(row, 1, new QTableWidgetItem(tr("[new cheat]"))); + ui->tableCheats->setItem(row, 2, new QTableWidgetItem("")); + ui->lineName->setText(""); + ui->lineName->setPlaceholderText(tr("[new cheat]")); + ui->textCode->setPlainText(""); + ui->textNotes->setPlainText(""); + ui->lineName->setEnabled(true); + ui->textCode->setEnabled(true); + ui->textNotes->setEnabled(true); + ui->buttonSave->setEnabled(true); + ui->buttonDelete->setEnabled(true); + ui->buttonAddCheat->setEnabled(false); + + edited = false; + newly_created = true; + last_row = row; + last_col = 1; } diff --git a/src/citra_qt/cheats.h b/src/citra_qt/cheats.h index d532175ab..89c08b1c5 100644 --- a/src/citra_qt/cheats.h +++ b/src/citra_qt/cheats.h @@ -7,6 +7,10 @@ #include #include +namespace Cheats { +class CheatBase; +} + namespace Ui { class CheatDialog; } // namespace Ui @@ -19,12 +23,38 @@ public: ~CheatDialog(); private: - std::unique_ptr ui; - + /** + * Loads the cheats from the CheatEngine, and populates the table. + */ void LoadCheats(); + /** + * Pops up a message box asking if the user wants to save the current cheat. + * If the user selected Yes, attempts to save the current cheat. + * @return true if the user selected No, or if the cheat was saved successfully + * false if the user selected Cancel, or if the user selected Yes but saving failed + */ + bool CheckSaveCheat(); + + /** + * Saves the current cheat as the row-th cheat in the cheat list. + * @return true if the cheat is saved successfully, false otherwise + */ + bool SaveCheat(int row); + + void closeEvent(QCloseEvent* event) override; + private slots: void OnCancel(); void OnRowSelected(int row, int column); void OnCheckChanged(int state); + void OnTextEdited(); + void OnDeleteCheat(); + void OnAddCheat(); + +private: + std::unique_ptr ui; + std::vector> cheats; + bool edited = false, newly_created = false; + int last_row = -1, last_col = -1; }; diff --git a/src/citra_qt/cheats.ui b/src/citra_qt/cheats.ui index 08c0da19f..b6bb35b9e 100644 --- a/src/citra_qt/cheats.ui +++ b/src/citra_qt/cheats.ui @@ -22,182 +22,192 @@ Cheats - - - - 10 - 10 - 300 - 31 - - - - - 10 - - - - Title ID: - - - - - - 10 - 570 - 841 - 41 - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Close - - - - - - - - - 10 - 80 - 551 - 471 - - - - - - - QAbstractItemView::NoEditTriggers - - - QAbstractItemView::SelectRows - - - false - - - 3 - - - true - - - false - - - - + + + + + + + + 10 + - - - Name + Title ID: - - + + + + + + Qt::Horizontal + + + + + - Type + Add Cheat - - - - - - - - - 10 - 60 - 121 - 16 - - - - Available Cheats: - - - - - - 580 - 440 - 271 - 111 - - - - - - - true - - - - - - - - - 580 - 420 - 111 - 16 - - - - Notes: - - - - - - 580 - 80 - 271 - 311 - - - - - - - true - - - - - - - - - 580 - 60 - 55 - 16 - - - - Code: - - + + + + + + + + + + + + + 10 + 60 + 121 + 16 + + + + Available Cheats: + + + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::SelectRows + + + QAbstractItemView::SingleSelection + + + false + + + 3 + + + true + + + false + + + + + + + + + Name + + + + + Type + + + + + + + + + + + + + + Qt::Horizontal + + + + + + + Save + + + false + + + + + + + Delete + + + false + + + + + + + + + + + + + Name: + + + + + + + + + + + + Notes: + + + + + + + + + + Code: + + + + + + + + + + + + + + + + + + Qt::Horizontal + + + + + + + Close + + + + + +