diff --git a/CMakeLists.txt b/CMakeLists.txt index 52e5630e..53043c83 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -79,6 +79,7 @@ set(HYDRA_QT_FILES qt/keypicker.cxx qt/terminalwindow.cxx qt/downloaderwindow.cxx + qt/cheatswindow.cxx vendored/miniaudio.c vendored/stb_image_write.c vendored/miniz/miniz.c diff --git a/core b/core index 1cdb1eda..8c15cad6 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 1cdb1eda5f368481e216416a119c85664e8c72ab +Subproject commit 8c15cad622b7bc7764046cbc80af42fa083b30de diff --git a/include/core_loader.hxx b/include/core_loader.hxx index 2dd7b92b..9605476b 100644 --- a/include/core_loader.hxx +++ b/include/core_loader.hxx @@ -146,7 +146,7 @@ namespace hydra struct EmulatorFactory { - static std::unique_ptr Create(const std::string& path) + static std::shared_ptr Create(const std::string& path) { dynlib_handle_t handle = dynlib_open(path.c_str()); @@ -183,11 +183,11 @@ namespace hydra return nullptr; } - return std::unique_ptr( + return std::shared_ptr( new EmulatorWrapper(create_emu_p(), handle, destroy_emu_p, get_info_p)); } - static std::unique_ptr Create(const std::filesystem::path& path) + static std::shared_ptr Create(const std::filesystem::path& path) { return Create(path.string()); } diff --git a/include/settings.hxx b/include/settings.hxx index 1cce7a78..fe553f6f 100644 --- a/include/settings.hxx +++ b/include/settings.hxx @@ -137,6 +137,13 @@ public: { Settings::Set("core_path", (std::filesystem::current_path()).string()); } + + if (!std::filesystem::exists(Settings::Get("core_path"))) + { + printf("Failed to find initialize core info\n"); + return; + } + std::filesystem::directory_iterator it(Settings::Get("core_path")); std::filesystem::directory_iterator end; while (it != end) diff --git a/qt/cheatswindow.cxx b/qt/cheatswindow.cxx new file mode 100644 index 00000000..4128a19b --- /dev/null +++ b/qt/cheatswindow.cxx @@ -0,0 +1,311 @@ +#include "cheatswindow.hxx" + +#include "core_loader.hxx" +#include "hydra/core.hxx" +#include "json.hpp" +#include "settings.hxx" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static std::vector cheat_to_bytes(const std::string& cheat) +{ + std::vector bytes; + for (size_t i = 0; i < cheat.size(); i += 2) + { + std::string hex = cheat.substr(i, 2); + bytes.push_back((uint8_t)std::stoul(hex, nullptr, 16)); + } + return bytes; +} + +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(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); + 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()); + } + connect(txt_code, &QTextEdit::textChanged, this, [this, txt_code]() { + QString new_text = txt_code->toPlainText(); + new_text.replace(QRegularExpression("[^0-9a-fA-F]"), ""); + QStringList tokens; + for (int i = 0; i < new_text.length(); i += 8) + { + tokens << new_text.mid(i, 8); + } + txt_code->blockSignals(true); + new_text = tokens.join(" "); + for (int i = 17; i < new_text.length(); i += 18) + { + new_text[i] = '\n'; + } + txt_code->setText(new_text); + txt_code->moveCursor(QTextCursor::End); + txt_code->blockSignals(false); + }); + 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: + CheatEntryWidget& entry_; + std::shared_ptr metadata_; + std::shared_ptr wrapper_; + bool is_editing_; +}; + +CheatEntryWidget::CheatEntryWidget(std::shared_ptr wrapper, + std::shared_ptr metadata, QListWidget* parent) + : QWidget(), wrapper_(wrapper), metadata_(metadata) +{ + QHBoxLayout* layout = new QHBoxLayout; + QCheckBox* chk_enabled = new QCheckBox; + + 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); + } + }); + + chk_enabled->setChecked(metadata_->enabled); + + lbl_name_ = new QLabel(metadata_->name.c_str()); + QPushButton* btn_edit = new QPushButton(tr("Edit")); + + connect(btn_edit, &QPushButton::clicked, this, [this]() { + CheatEditDialog* dialog = new CheatEditDialog(wrapper_, *this); + dialog->show(); + }); + + layout->addWidget(chk_enabled); + layout->addWidget(lbl_name_); + layout->addWidget(btn_edit); + setLayout(layout); + + 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) + : QWidget(parent, Qt::Window), open_(open), wrapper_(wrapper) +{ + bool just_created = false; + if (!std::filesystem::create_directories(Settings::GetSavePath() / "cheats")) + { + if (!std::filesystem::exists(Settings::GetSavePath() / "cheats")) + { + printf("Failed to create cheats directory\n"); + return; + } + } + + if (!wrapper_->shell->hasInterface(hydra::InterfaceType::ICheat)) + { + printf("Emulator does not have cheat interface, this dialog shouldn't have been opened?\n"); + return; + } + + 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 + if (std::filesystem::exists(cheat_path_)) + { + // Load the cheats + std::ifstream cheat_file(cheat_path_); + nlohmann::json cheat_json; + cheat_file >> cheat_json; + for (auto& cheat : cheat_json) + { + CheatMetadata cheat_metadata; + cheat_metadata.enabled = cheat["enabled"] == "true"; + cheat_metadata.name = cheat["name"]; + 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) + { + new CheatEntryWidget(wrapper_, std::make_shared(cheat_metadata), + cheat_list_); + } + else + { + printf("Failed to add cheat %s\n", cheat_metadata.name.c_str()); + } + } + } + } + + show(); + open_ = true; +} + +CheatsWindow::~CheatsWindow() +{ + save_cheats(); +} + +void CheatsWindow::save_cheats() +{ + nlohmann::json cheat_json; + 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}, + {"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 new file mode 100644 index 00000000..143f9359 --- /dev/null +++ b/qt/cheatswindow.hxx @@ -0,0 +1,40 @@ +#pragma once + +#include "hydra/core.hxx" +#include +#include +#include +#include + +class QListWidget; + +struct CheatMetadata +{ + bool enabled = true; + std::string name{}; + std::string code{}; + uint32_t handle = hydra::BAD_CHEAT; +}; + +class CheatsWindow : public QWidget +{ + Q_OBJECT + +public: + CheatsWindow(std::shared_ptr wrapper, bool& open, + const std::string& hash, QWidget* parent = nullptr); + ~CheatsWindow(); + + void Show(); + void Hide(); + +private: + void closeEvent(QCloseEvent* event) override; + + bool& open_; + 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 7e02985d..7359868e 100644 --- a/qt/mainwindow.cxx +++ b/qt/mainwindow.cxx @@ -30,10 +30,15 @@ #include #include #include +#ifdef HYDRA_USE_LUA #include +#endif #include #include #include +// TODO: remove this +#define OSSL_DEPRECATEDIN_3_0 +#include enum class EmulatorState { @@ -281,6 +286,10 @@ void MainWindow::create_actions() terminal_act_->setStatusTip("Open the terminal"); terminal_act_->setIcon(QIcon(":/images/terminal.png")); connect(terminal_act_, &QAction::triggered, this, &MainWindow::open_terminal); + 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::toggle_cheats_window); recent_act_ = new QAction(tr("&Recent files"), this); for (int i = 0; i < 10; i++) { @@ -330,6 +339,7 @@ void MainWindow::create_menus() emulation_menu_->addAction(mute_act_); tools_menu_ = menuBar()->addMenu(tr("&Tools")); tools_menu_->addAction(terminal_act_); + tools_menu_->addAction(cheats_act_); tools_menu_->addAction(scripts_act_); tools_menu_->addAction(shaders_act_); help_menu_ = menuBar()->addMenu(tr("&Help")); @@ -469,6 +479,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"); @@ -564,6 +596,27 @@ void MainWindow::open_terminal() }); } +void MainWindow::toggle_cheats_window() +{ + qt_may_throw([this]() { + if (!cheats_window_) + { + cheats_window_.reset(new CheatsWindow(emulator_, cheats_open_, game_hash_, this)); + } + else + { + if (!cheats_open_) + { + cheats_window_->Show(); + } + else + { + cheats_window_->Hide(); + } + } + }); +} + // TODO: compiler option to turn off lua support void MainWindow::run_script(const std::string& script, bool safe_mode) { @@ -661,6 +714,10 @@ void MainWindow::enable_emulation_actions(bool should) { stop_act_->setEnabled(should); reset_act_->setEnabled(should); + scripts_act_->setEnabled(should); + terminal_act_->setEnabled(should); + cheats_act_->setEnabled(should); + shaders_act_->setEnabled(should); if (should) screen_->show(); else @@ -761,6 +818,21 @@ void MainWindow::init_emulator() shell_log->setLogCallback(hydra::LogTarget::Info, TerminalWindow::log_info); shell_log->setLogCallback(hydra::LogTarget::Debug, TerminalWindow::log_debug); shell_log->setLogCallback(hydra::LogTarget::Error, log_fatal); + terminal_act_->setEnabled(true); + } + else + { + terminal_act_->setEnabled(false); + } + + // Initialize cheats + if (emulator_->shell->hasInterface(hydra::InterfaceType::ICheat)) + { + cheats_act_->setEnabled(true); + } + else + { + cheats_act_->setEnabled(false); } if (emulator_->shell->hasInterface(hydra::InterfaceType::ISelfDriven)) diff --git a/qt/mainwindow.hxx b/qt/mainwindow.hxx index 3b975e67..b1ea7f96 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 @@ -41,6 +42,7 @@ private: void open_shaders(); void open_scripts(); void open_terminal(); + void toggle_cheats_window(); void run_script(const std::string& script, bool safe_mode); void screenshot(); void add_recent(const std::string& path); @@ -89,6 +91,7 @@ private: QAction* screenshot_act_; QAction* shaders_act_; QAction* scripts_act_; + QAction* cheats_act_; QAction* terminal_act_; QAction* recent_act_; QTimer* emulator_timer_; @@ -96,14 +99,17 @@ private: DownloaderWindow* downloader_; ma_device sound_device_{}; bool frontend_driven_ = false; - std::unique_ptr emulator_; + std::unique_ptr cheats_window_ = nullptr; + std::shared_ptr emulator_; std::unique_ptr info_; + std::string game_hash_; std::vector queued_audio_; bool settings_open_ = false; bool about_open_ = false; bool shaders_open_ = false; bool scripts_open_ = false; bool terminal_open_ = false; + bool cheats_open_ = false; bool paused_ = false; std::mutex emulator_mutex_; std::mutex audio_mutex_; diff --git a/qt/scripteditor.cxx b/qt/scripteditor.cxx index 7c35994c..8ad6f18a 100644 --- a/qt/scripteditor.cxx +++ b/qt/scripteditor.cxx @@ -45,7 +45,7 @@ void ScriptHighlighter::highlightBlock(const QString& text) { return; } - for (const HighlightingRule& rule : qAsConst(highlighting_rules_)) + for (const HighlightingRule& rule : highlighting_rules_) { QRegularExpressionMatchIterator matchIterator = rule.pattern.globalMatch(text); while (matchIterator.hasNext()) @@ -176,4 +176,4 @@ void ScriptEditor::safe_mode_changed(int state) } safe_mode_ = state == Qt::Checked; Settings::Set("lua_safe_mode", safe_mode_ ? "true" : "false"); -} \ No newline at end of file +} diff --git a/qt/shadereditor.cxx b/qt/shadereditor.cxx index 1bf192e2..0130a960 100644 --- a/qt/shadereditor.cxx +++ b/qt/shadereditor.cxx @@ -165,7 +165,7 @@ void ShaderHighlighter::highlightBlock(const QString& text) { return; } - for (const HighlightingRule& rule : qAsConst(highlighting_rules_)) + for (const HighlightingRule& rule : highlighting_rules_) { QRegularExpressionMatchIterator matchIterator = rule.pattern.globalMatch(text); while (matchIterator.hasNext()) @@ -256,4 +256,4 @@ void ShaderEditor::autocompile() ShaderEditor::~ShaderEditor() { open_ = false; -} \ No newline at end of file +}