diff --git a/qt/cheatswindow.cxx b/qt/cheatswindow.cxx index 8b9f8a11..4128a19b 100644 --- a/qt/cheatswindow.cxx +++ b/qt/cheatswindow.cxx @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -28,26 +29,56 @@ static std::vector cheat_to_bytes(const std::string& cheat) return bytes; } -// TODO: does this close gracefully when mainwindow closes? +class CheatEntryWidget : public QWidget +{ +public: + CheatEntryWidget(std::shared_ptr wrapper, + std::shared_ptr metadata, QListWidget* parent); + + std::shared_ptr GetMetadata() + { + return metadata_; + } + + void Update() + { + lbl_name_->setText(metadata_->name.c_str()); + update(); + } + +private: + std::shared_ptr metadata_; + std::shared_ptr wrapper_; + QLabel* lbl_name_; +}; + class CheatEditDialog : public QDialog { public: - CheatEditDialog(hydra::ICheat* cheat_interface, CheatMetadata& metadata) - : QDialog(), cheat_interface_(cheat_interface), metadata_(metadata) + CheatEditDialog(std::shared_ptr wrapper, CheatEntryWidget& entry) + : QDialog(), wrapper_(wrapper), entry_(entry), metadata_(entry.GetMetadata()) { setModal(true); QVBoxLayout* layout = new QVBoxLayout; + + QLineEdit* txt_name = new QLineEdit; + txt_name->setText(metadata_->name.c_str()); + txt_name->setPlaceholderText(tr("Cheat name")); + connect(txt_name, &QLineEdit::textChanged, this, + [this, txt_name]() { metadata_->name = txt_name->text().toStdString(); }); + layout->addWidget(txt_name); + QTextEdit* txt_code = new QTextEdit; QFont font; font.setFamily("Courier"); font.setFixedPitch(true); font.setPointSize(10); txt_code->setFont(font); - - if (metadata.code.size() != 0) + txt_code->setPlaceholderText(tr("Cheat code")); + if (metadata_->code.size() != 0) { - printf("Setting code to %s\n", metadata.code.c_str()); - txt_code->setText(metadata.code.c_str()); + printf("Setting code to %s\n", metadata_->code.c_str()); + txt_code->setText(metadata_->code.c_str()); } connect(txt_code, &QTextEdit::textChanged, this, [this, txt_code]() { QString new_text = txt_code->toPlainText(); @@ -67,64 +98,85 @@ class CheatEditDialog : public QDialog txt_code->moveCursor(QTextCursor::End); txt_code->blockSignals(false); }); - is_editing_ = metadata_.handle != hydra::BAD_CHEAT; + is_editing_ = metadata_->handle != hydra::BAD_CHEAT; layout->addWidget(txt_code); setLayout(layout); QDialogButtonBox* button_box = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); layout->addWidget(button_box); + connect(button_box, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(button_box, &QDialogButtonBox::rejected, this, &QDialog::reject); + connect(this, &QDialog::accepted, this, [this, txt_code]() { + QString code = txt_code->toPlainText(); + code.replace(QRegularExpression("[^0-9a-fA-F]"), ""); + metadata_->code = code.toStdString(); + std::vector bytes = cheat_to_bytes(metadata_->code); + hydra::ICheat* cheat_interface = wrapper_->shell->asICheat(); + if (is_editing_) + { + cheat_interface->removeCheat(metadata_->handle); + metadata_->handle = cheat_interface->addCheat(bytes.data(), bytes.size()); + } + else + { + if (metadata_->name.empty()) + { + metadata_->name = tr("My cheat code").toStdString(); + } + metadata_->handle = cheat_interface->addCheat(bytes.data(), bytes.size()); + } + entry_.Update(); + }); } private: - CheatMetadata& metadata_; - hydra::ICheat* cheat_interface_; + CheatEntryWidget& entry_; + std::shared_ptr metadata_; + std::shared_ptr wrapper_; bool is_editing_; }; -class CheatWidget : public QWidget +CheatEntryWidget::CheatEntryWidget(std::shared_ptr wrapper, + std::shared_ptr metadata, QListWidget* parent) + : QWidget(), wrapper_(wrapper), metadata_(metadata) { -public: - CheatWidget(hydra::ICheat* cheat_interface, const CheatMetadata& metadata, QListWidget* parent) - : QWidget(), metadata_(metadata) - { - QHBoxLayout* layout = new QHBoxLayout; - QCheckBox* chk_enabled = new QCheckBox; + QHBoxLayout* layout = new QHBoxLayout; + QCheckBox* chk_enabled = new QCheckBox; - connect(chk_enabled, &QCheckBox::stateChanged, this, [this](int state) { - metadata_.enabled = state == Qt::Checked; - if (metadata_.enabled) - { - cheat_interface_->enableCheat(metadata_.handle); - } - else - { - cheat_interface_->disableCheat(metadata_.handle); - } - }); - - QLabel* lbl_name = new QLabel(metadata_.name.c_str()); - QPushButton* btn_edit = new QPushButton("Edit"); + connect(chk_enabled, &QCheckBox::stateChanged, this, [this](int state) { + metadata_->enabled = state == Qt::Checked; + hydra::ICheat* cheat_interface = wrapper_->shell->asICheat(); + if (metadata_->enabled) + { + cheat_interface->enableCheat(metadata_->handle); + } + else + { + cheat_interface->disableCheat(metadata_->handle); + } + }); - connect(btn_edit, &QPushButton::clicked, this, [this]() { + chk_enabled->setChecked(metadata_->enabled); - }); + lbl_name_ = new QLabel(metadata_->name.c_str()); + QPushButton* btn_edit = new QPushButton(tr("Edit")); - layout->addWidget(chk_enabled); - layout->addWidget(lbl_name); - layout->addWidget(btn_edit); - setLayout(layout); + connect(btn_edit, &QPushButton::clicked, this, [this]() { + CheatEditDialog* dialog = new CheatEditDialog(wrapper_, *this); + dialog->show(); + }); - QListWidgetItem* list_item = new QListWidgetItem; - list_item->setSizeHint(sizeHint()); - parent->addItem(list_item); - parent->setItemWidget(list_item, this); - } + layout->addWidget(chk_enabled); + layout->addWidget(lbl_name_); + layout->addWidget(btn_edit); + setLayout(layout); -private: - CheatMetadata metadata_; - hydra::ICheat* cheat_interface_; -}; + QListWidgetItem* list_item = new QListWidgetItem; + list_item->setSizeHint(sizeHint()); + parent->addItem(list_item); + parent->setItemWidget(list_item, this); +} CheatsWindow::CheatsWindow(std::shared_ptr wrapper, bool& open, const std::string& hash, QWidget* parent) @@ -139,10 +191,6 @@ CheatsWindow::CheatsWindow(std::shared_ptr wrapper, bool return; } } - else - { - just_created = true; - } if (!wrapper_->shell->hasInterface(hydra::InterfaceType::ICheat)) { @@ -153,6 +201,44 @@ CheatsWindow::CheatsWindow(std::shared_ptr wrapper, bool auto cheat_interface = wrapper_->shell->asICheat(); cheat_path_ = Settings::GetSavePath() / "cheats" / (hash + ".json"); + QVBoxLayout* layout = new QVBoxLayout; + layout->setContentsMargins(6, 6, 6, 6); + setLayout(layout); + + cheat_list_ = new QListWidget; + layout->addWidget(cheat_list_); + + QWidget* button_box = new QWidget; + QHBoxLayout* button_layout = new QHBoxLayout; + + QPushButton* btn_add = new QPushButton(tr("Add")); + connect(btn_add, &QPushButton::clicked, this, [this]() { + CheatEntryWidget* entry = + new CheatEntryWidget(wrapper_, std::make_shared(), cheat_list_); + CheatEditDialog* dialog = new CheatEditDialog(wrapper_, *entry); + dialog->show(); + }); + + QPushButton* btn_remove = new QPushButton(tr("Remove")); + connect(btn_remove, &QPushButton::clicked, this, [this]() { + QListWidgetItem* item = cheat_list_->currentItem(); + if (item == nullptr) + { + return; + } + + CheatEntryWidget* entry = (CheatEntryWidget*)cheat_list_->itemWidget(item); + wrapper_->shell->asICheat()->removeCheat(entry->GetMetadata()->handle); + cheat_list_->takeItem(cheat_list_->row(item)); + entry->deleteLater(); + }); + + button_layout->addWidget(btn_add); + button_layout->addWidget(btn_remove); + button_box->setLayout(button_layout); + + layout->addWidget(button_box); + if (!just_created) { // Check if this game already has saved cheats @@ -167,13 +253,13 @@ CheatsWindow::CheatsWindow(std::shared_ptr wrapper, bool CheatMetadata cheat_metadata; cheat_metadata.enabled = cheat["enabled"] == "true"; cheat_metadata.name = cheat["name"]; - cheat_metadata.description = cheat["description"]; cheat_metadata.code = cheat["code"]; std::vector bytes = cheat_to_bytes(cheat_metadata.code); cheat_metadata.handle = cheat_interface->addCheat(bytes.data(), bytes.size()); if (cheat_metadata.handle != hydra::BAD_CHEAT) { - cheats_.push_back(cheat_metadata); + new CheatEntryWidget(wrapper_, std::make_shared(cheat_metadata), + cheat_list_); } else { @@ -183,42 +269,43 @@ CheatsWindow::CheatsWindow(std::shared_ptr wrapper, bool } } - QVBoxLayout* layout = new QVBoxLayout; - layout->setContentsMargins(6, 6, 6, 6); - setLayout(layout); - - cheats_.resize(1); - CheatMetadata& metadata = cheats_[0]; - metadata.enabled = true; - metadata.name = "Infinite Health"; - metadata.description = "Infinite health cheat"; - - QListWidget* list = new QListWidget; - new CheatWidget(wrapper->shell->asICheat(), metadata, list); - layout->addWidget(list); - - QPushButton* btn_add = new QPushButton("Add"); - connect(btn_add, &QPushButton::clicked, this, [this, &metadata]() { - CheatEditDialog* dialog = new CheatEditDialog(wrapper_->shell->asICheat(), metadata); - dialog->show(); - }); - layout->addWidget(btn_add); - show(); open_ = true; } CheatsWindow::~CheatsWindow() { - // Save the cheats + save_cheats(); +} + +void CheatsWindow::save_cheats() +{ nlohmann::json cheat_json; - for (auto& cheat : cheats_) + for (int i = 0; i < cheat_list_->count(); i++) { + CheatEntryWidget* entry = (CheatEntryWidget*)cheat_list_->itemWidget(cheat_list_->item(i)); + auto cheat = *entry->GetMetadata(); cheat_json.push_back({{"enabled", cheat.enabled ? "true" : "false"}, {"name", cheat.name}, - {"description", cheat.description}, {"code", cheat.code}}); } std::ofstream cheat_file(cheat_path_); cheat_file << cheat_json.dump(4); } + +void CheatsWindow::closeEvent(QCloseEvent* event) +{ + Hide(); +} + +void CheatsWindow::Hide() +{ + open_ = false; + hide(); +} + +void CheatsWindow::Show() +{ + open_ = true; + show(); +} diff --git a/qt/cheatswindow.hxx b/qt/cheatswindow.hxx index 87371e2f..258e0cc4 100644 --- a/qt/cheatswindow.hxx +++ b/qt/cheatswindow.hxx @@ -5,11 +5,12 @@ #include #include +class QListWidget; + struct CheatMetadata { bool enabled = true; std::string name{}; - std::string description{}; std::string code{}; uint32_t handle = hydra::BAD_CHEAT; }; @@ -28,9 +29,16 @@ public: const std::string& hash, QWidget* parent = nullptr); ~CheatsWindow(); + void Show(); + void Hide(); + private: + void closeEvent(QCloseEvent* event) override; + bool& open_; - std::vector cheats_; + QListWidget* cheat_list_; std::shared_ptr wrapper_; std::filesystem::path cheat_path_; + + void save_cheats(); }; diff --git a/qt/mainwindow.cxx b/qt/mainwindow.cxx index 460baffc..811ab4f9 100644 --- a/qt/mainwindow.cxx +++ b/qt/mainwindow.cxx @@ -1,6 +1,5 @@ #include "mainwindow.hxx" #include "aboutwindow.hxx" -#include "cheatswindow.hxx" #include "downloaderwindow.hxx" #include "input.hxx" #include "qthelper.hxx" @@ -35,6 +34,9 @@ #include #include #include +// TODO: remove this +#define OSSL_DEPRECATEDIN_3_0 +#include enum class EmulatorState { @@ -285,7 +287,7 @@ void MainWindow::create_actions() cheats_act_ = new QAction(tr("&Cheats"), this); cheats_act_->setShortcut(Qt::Key_F8); cheats_act_->setStatusTip("Open the cheats window"); - connect(cheats_act_, &QAction::triggered, this, &MainWindow::open_cheats); + connect(cheats_act_, &QAction::triggered, this, &MainWindow::toggle_cheats_window); recent_act_ = new QAction(tr("&Recent files"), this); for (int i = 0; i < 10; i++) { @@ -475,6 +477,28 @@ void MainWindow::open_file_impl(const std::string& path) fmt::format("Failed to find core info for core {}... This shouldn't happen?", core_path) .c_str()); } + { + unsigned char result[MD5_DIGEST_LENGTH]; + std::ifstream file(path, std::ifstream::binary); + MD5_CTX md5Context; + MD5_Init(&md5Context); + char buf[1024 * 16]; + while (file.good()) + { + file.read(buf, sizeof(buf)); + MD5_Update(&md5Context, buf, file.gcount()); + } + MD5_Final(result, &md5Context); + std::stringstream md5stream; + md5stream << std::hex << std::setfill('0'); + for (const auto& byte : result) + { + md5stream << std::setw(2) << (int)byte; + } + game_hash_ = md5stream.str(); + } + // TODO: such resets don't belong in open_file_impl, but in a separate function + cheats_window_.reset(); emulator_ = hydra::EmulatorFactory::Create(core_path); if (!emulator_) throw ErrorFactory::generate_exception(__func__, __LINE__, "Failed to create emulator"); @@ -570,12 +594,23 @@ void MainWindow::open_terminal() }); } -void MainWindow::open_cheats() +void MainWindow::toggle_cheats_window() { qt_may_throw([this]() { - if (!cheats_open_) + if (!cheats_window_) + { + cheats_window_.reset(new CheatsWindow(emulator_, cheats_open_, game_hash_, this)); + } + else { - new CheatsWindow(emulator_, cheats_open_, game_hash_, this); + if (!cheats_open_) + { + cheats_window_->Show(); + } + else + { + cheats_window_->Hide(); + } } }); } diff --git a/qt/mainwindow.hxx b/qt/mainwindow.hxx index 9831fe7c..610d64da 100644 --- a/qt/mainwindow.hxx +++ b/qt/mainwindow.hxx @@ -8,6 +8,7 @@ #include #define MA_NO_DECODING #define MA_NO_ENCODING +#include "cheatswindow.hxx" #include #include #include @@ -20,6 +21,7 @@ #include class DownloaderWindow; +class CheatsWindow; class MainWindow : public QMainWindow { @@ -41,7 +43,7 @@ private: void open_shaders(); void open_scripts(); void open_terminal(); - void open_cheats(); + void toggle_cheats_window(); void run_script(const std::string& script, bool safe_mode); void screenshot(); void add_recent(const std::string& path); @@ -98,6 +100,7 @@ private: DownloaderWindow* downloader_; ma_device sound_device_{}; bool frontend_driven_ = false; + std::unique_ptr cheats_window_ = nullptr; std::shared_ptr emulator_; std::unique_ptr info_; std::string game_hash_;