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 + + + + + + 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