diff --git a/CMakeLists.txt b/CMakeLists.txt index baa37466..9748b8f8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,7 +14,7 @@ if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND CMAKE_CXX_COMPILER_VERSION VERSION endif() if(NOT CMAKE_BUILD_TYPE) - set(CMAKE_BUILD_TYPE Release) + set(CMAKE_BUILD_TYPE RelWithDebInfo) endif() project(Alber) @@ -197,10 +197,10 @@ set(RENDERER_SW_SOURCE_FILES src/core/renderer_sw/renderer_sw.cpp) if(NOT ANDROID) if(ENABLE_QT_GUI) set(FRONTEND_SOURCE_FILES src/panda_qt/main.cpp src/panda_qt/screen.cpp src/panda_qt/main_window.cpp src/panda_qt/about_window.cpp - src/panda_qt/config_window.cpp src/panda_qt/zep.cpp src/panda_qt/text_editor.cpp + src/panda_qt/config_window.cpp src/panda_qt/zep.cpp src/panda_qt/text_editor.cpp src/panda_qt/cheats_window.cpp ) set(FRONTEND_HEADER_FILES include/panda_qt/screen.hpp include/panda_qt/main_window.hpp include/panda_qt/about_window.hpp - include/panda_qt/config_window.hpp include/panda_qt/text_editor.hpp + include/panda_qt/config_window.hpp include/panda_qt/text_editor.hpp include/panda_qt/cheats_window.hpp ) source_group("Source Files\\Qt" FILES ${FRONTEND_SOURCE_FILES}) diff --git a/include/panda_qt/cheats_window.hpp b/include/panda_qt/cheats_window.hpp new file mode 100644 index 00000000..2160a1f6 --- /dev/null +++ b/include/panda_qt/cheats_window.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include +#include +#include +#include +#include "emulator.hpp" + +class QListWidget; + +class CheatsWindow final : public QWidget +{ + Q_OBJECT + +public: + CheatsWindow(Emulator* emu, const std::filesystem::path& path, QWidget* parent = nullptr); + ~CheatsWindow() = default; + +private: + void addEntry(); + void removeClicked(); + + QListWidget* cheatList; + std::filesystem::path cheatPath; + Emulator* emu; +}; diff --git a/include/panda_qt/main_window.hpp b/include/panda_qt/main_window.hpp index 7dfb91b7..39a8b35f 100644 --- a/include/panda_qt/main_window.hpp +++ b/include/panda_qt/main_window.hpp @@ -12,6 +12,7 @@ #include "emulator.hpp" #include "panda_qt/about_window.hpp" #include "panda_qt/config_window.hpp" +#include "panda_qt/cheats_window.hpp" #include "panda_qt/screen.hpp" #include "panda_qt/text_editor.hpp" #include "services/hid.hpp" @@ -54,15 +55,19 @@ class MainWindow : public QMainWindow { ScreenWidget screen; AboutWindow* aboutWindow; ConfigWindow* configWindow; + CheatsWindow* cheatsEditor; TextEditorWindow* luaEditor; QMenuBar* menuBar = nullptr; + QAction* cheatsEditorAction = nullptr; + void swapEmuBuffer(); void emuThreadMainLoop(); void selectLuaFile(); void selectROM(); void dumpRomFS(); void openLuaEditor(); + void openCheatsEditor(); void showAboutMenu(); void sendMessage(const EmulatorMessage& message); void dispatchMessage(const EmulatorMessage& message); diff --git a/src/panda_qt/cheats_window.cpp b/src/panda_qt/cheats_window.cpp new file mode 100644 index 00000000..909ba0cb --- /dev/null +++ b/src/panda_qt/cheats_window.cpp @@ -0,0 +1,313 @@ +#include "panda_qt/cheats_window.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "cheats.hpp" +#include "emulator.hpp" + +using CheatHandle = u32; + +CheatHandle BAD_CHEAT = 0xFFFFFFFF; + +struct CheatMetadata +{ + CheatHandle handle = BAD_CHEAT; + std::string name = "New cheat"; + std::string code; + bool enabled = true; +}; + +u32 addCheat(Emulator* emu, u8* data, size_t size) +{ + if ((size % 8) != 0) { + return BAD_CHEAT; + } + + Cheats::Cheat cheat; + cheat.enabled = true; + cheat.type = Cheats::CheatType::ActionReplay; + + for (size_t i = 0; i < size; i += 8) { + auto read32 = [](const u8* ptr) { return (u32(ptr[3]) << 24) | (u32(ptr[2]) << 16) | (u32(ptr[1]) << 8) | u32(ptr[0]); }; + + // Data is passed to us in big endian so we bswap + u32 firstWord = Common::swap32(read32(data + i)); + u32 secondWord = Common::swap32(read32(data + i + 4)); + cheat.instructions.insert(cheat.instructions.end(), {firstWord, secondWord}); + } + + return emu->getCheats().addCheat(cheat); +} + +class CheatEntryWidget : public QWidget +{ +public: + CheatEntryWidget(Emulator* emu, CheatMetadata metadata, QListWidget* parent); + + void Update() + { + name->setText(metadata.name.c_str()); + enabled->setChecked(metadata.enabled); + update(); + } + + void Remove() + { + emu->getCheats().removeCheat(metadata.handle); + cheatList->takeItem(cheatList->row(listItem)); + deleteLater(); + } + + const CheatMetadata& GetMetadata() + { + return metadata; + } + + void SetMetadata(const CheatMetadata& metadata) + { + this->metadata = metadata; + } + +private: + void checkboxChanged(int state); + void editClicked(); + + Emulator* emu; + CheatMetadata metadata; + u32 handle; + QLabel* name; + QCheckBox* enabled; + QListWidget* cheatList; + QListWidgetItem* listItem; +}; + +class CheatEditDialog : public QDialog +{ +public: + CheatEditDialog(Emulator* emu, CheatEntryWidget& cheatEntry); + + void accepted(); + void rejected(); +private: + Emulator* emu; + CheatEntryWidget& cheatEntry; + QTextEdit* codeEdit; + QLineEdit* nameEdit; +}; + +CheatEntryWidget::CheatEntryWidget(Emulator* emu, CheatMetadata metadata, QListWidget* parent) + : QWidget(), emu(emu), metadata(metadata), cheatList(parent) +{ + QHBoxLayout* layout = new QHBoxLayout; + + enabled = new QCheckBox; + enabled->setChecked(metadata.enabled); + + name = new QLabel(metadata.name.c_str()); + QPushButton* buttonEdit = new QPushButton(tr("Edit")); + + connect(enabled, &QCheckBox::stateChanged, this, &CheatEntryWidget::checkboxChanged); + connect(buttonEdit, &QPushButton::clicked, this, &CheatEntryWidget::editClicked); + + layout->addWidget(enabled); + layout->addWidget(name); + layout->addWidget(buttonEdit); + setLayout(layout); + + listItem = new QListWidgetItem; + listItem->setSizeHint(sizeHint()); + parent->addItem(listItem); + parent->setItemWidget(listItem, this); +} + +void CheatEntryWidget::checkboxChanged(int state) +{ + bool enabled = state == Qt::Checked; + if (metadata.handle == BAD_CHEAT) + { + printf("Cheat handle is bad, this shouldn't happen\n"); + return; + } + + if (enabled) + { + emu->getCheats().enableCheat(metadata.handle); + metadata.enabled = true; + } + else + { + emu->getCheats().disableCheat(metadata.handle); + metadata.enabled = false; + } +} + +void CheatEntryWidget::editClicked() +{ + CheatEditDialog* dialog = new CheatEditDialog(emu, *this); + dialog->show(); +} + +CheatEditDialog::CheatEditDialog(Emulator* emu, CheatEntryWidget& cheatEntry) : QDialog(), emu(emu), cheatEntry(cheatEntry) +{ + setAttribute(Qt::WA_DeleteOnClose); + setModal(true); + + QVBoxLayout* layout = new QVBoxLayout; + const CheatMetadata& metadata = cheatEntry.GetMetadata(); + codeEdit = new QTextEdit; + nameEdit = new QLineEdit; + nameEdit->setText(metadata.name.c_str()); + nameEdit->setPlaceholderText(tr("Cheat name")); + layout->addWidget(nameEdit); + + QFont font; + font.setFamily("Courier"); + font.setFixedPitch(true); + font.setPointSize(10); + codeEdit->setFont(font); + + if (metadata.code.size() != 0) + { + // Nicely format it like so: + // 01234567 89ABCDEF + // 01234567 89ABCDEF + std::string formattedCode; + for (size_t i = 0; i < metadata.code.size(); i += 2) + { + if (i != 0) { + if (i % 8 == 0 && i % 16 != 0) + { + formattedCode += " "; + } + else if (i % 16 == 0) + { + formattedCode += "\n"; + } + } + + formattedCode += metadata.code[i]; + formattedCode += metadata.code[i + 1]; + } + codeEdit->setText(formattedCode.c_str()); + } + + layout->addWidget(codeEdit); + setLayout(layout); + + auto buttons = QDialogButtonBox::Ok | QDialogButtonBox::Cancel; + QDialogButtonBox* button_box = new QDialogButtonBox(buttons); + layout->addWidget(button_box); + + connect(button_box, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(button_box, &QDialogButtonBox::rejected, this, &QDialog::reject); + connect(this, &QDialog::rejected, this, &CheatEditDialog::rejected); + connect(this, &QDialog::accepted, this, &CheatEditDialog::accepted); +} + +void CheatEditDialog::accepted() +{ + QString code = codeEdit->toPlainText(); + code.replace(QRegularExpression("[^0-9a-fA-F]"), ""); + + CheatMetadata metadata = cheatEntry.GetMetadata(); + bool isEditing = metadata.handle != BAD_CHEAT; + metadata.name = nameEdit->text().toStdString(); + metadata.code = code.toStdString(); + + std::vector bytes; + for (size_t i = 0; i < metadata.code.size(); i += 2) + { + std::string hex = metadata.code.substr(i, 2); + bytes.push_back((uint8_t)std::stoul(hex, nullptr, 16)); + } + + if (isEditing) + { + emu->getCheats().removeCheat(metadata.handle); + u32 handle = addCheat(emu, bytes.data(), bytes.size()); + metadata.handle = handle; + cheatEntry.SetMetadata(metadata); + } + else + { + if (metadata.name.empty()) + { + metadata.name = tr("Cheat code").toStdString(); + } + u32 handle = addCheat(emu, bytes.data(), bytes.size()); + metadata.handle = handle; + cheatEntry.SetMetadata(metadata); + } + + cheatEntry.Update(); +} + +void CheatEditDialog::rejected() +{ + bool isEditing = cheatEntry.GetMetadata().handle != BAD_CHEAT; + + if (!isEditing) + { + // Was adding a cheat but pressed cancel + cheatEntry.Remove(); + } +} + +CheatsWindow::CheatsWindow(Emulator* emu, const std::filesystem::path& cheatPath, QWidget* parent) +: QWidget(parent, Qt::Window), emu(emu), cheatPath(cheatPath) +{ + QVBoxLayout* layout = new QVBoxLayout; + layout->setContentsMargins(6, 6, 6, 6); + setLayout(layout); + + cheatList = new QListWidget; + layout->addWidget(cheatList); + + QWidget* buttonBox = new QWidget; + QHBoxLayout* buttonLayout = new QHBoxLayout; + + QPushButton* buttonAdd = new QPushButton(tr("Add")); + QPushButton* buttonRemove = new QPushButton(tr("Remove")); + + connect(buttonAdd, &QPushButton::clicked, this, &CheatsWindow::addEntry); + connect(buttonRemove, &QPushButton::clicked, this, &CheatsWindow::removeClicked); + + buttonLayout->addWidget(buttonAdd); + buttonLayout->addWidget(buttonRemove); + buttonBox->setLayout(buttonLayout); + + layout->addWidget(buttonBox); + + // TODO: load cheats from saved cheats per game + // for (const CheatMetadata& metadata : getSavedCheats()) + // { + // new CheatEntryWidget(emu, metadata, cheatList); + // } +} + +void CheatsWindow::addEntry() +{ + // CheatEntryWidget is added to the list when it's created + CheatEntryWidget* entry = new CheatEntryWidget(emu, {BAD_CHEAT, "New cheat", "", true}, cheatList); + CheatEditDialog* dialog = new CheatEditDialog(emu, *entry); + dialog->show(); +} + +void CheatsWindow::removeClicked() +{ + QListWidgetItem* item = cheatList->currentItem(); + if (item == nullptr) + { + return; + } + + CheatEntryWidget* entry = static_cast(cheatList->itemWidget(item)); + entry->Remove(); +} diff --git a/src/panda_qt/main_window.cpp b/src/panda_qt/main_window.cpp index 5c661119..be5e4fd6 100644 --- a/src/panda_qt/main_window.cpp +++ b/src/panda_qt/main_window.cpp @@ -48,20 +48,24 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent) auto dumpRomFSAction = toolsMenu->addAction(tr("Dump RomFS")); auto luaEditorAction = toolsMenu->addAction(tr("Open Lua Editor")); + cheatsEditorAction = toolsMenu->addAction(tr("Open Cheats Editor")); + cheatsEditorAction->setEnabled(false); connect(dumpRomFSAction, &QAction::triggered, this, &MainWindow::dumpRomFS); connect(luaEditorAction, &QAction::triggered, this, &MainWindow::openLuaEditor); + connect(cheatsEditorAction, &QAction::triggered, this, &MainWindow::openCheatsEditor); auto aboutAction = aboutMenu->addAction(tr("About Panda3DS")); connect(aboutAction, &QAction::triggered, this, &MainWindow::showAboutMenu); + emu = new Emulator(); + emu->setOutputSize(screen.surfaceWidth, screen.surfaceHeight); + // Set up misc objects aboutWindow = new AboutWindow(nullptr); configWindow = new ConfigWindow(this); + cheatsEditor = new CheatsWindow(emu, {}); luaEditor = new TextEditorWindow(this, "script.lua", ""); - emu = new Emulator(); - emu->setOutputSize(screen.surfaceWidth, screen.surfaceHeight); - auto args = QCoreApplication::arguments(); if (args.size() > 1) { auto romPath = std::filesystem::current_path() / args.at(1).toStdU16String(); @@ -184,6 +188,7 @@ MainWindow::~MainWindow() { delete menuBar; delete aboutWindow; delete configWindow; + delete cheatsEditor; delete luaEditor; } @@ -234,10 +239,13 @@ void MainWindow::showAboutMenu() { void MainWindow::openLuaEditor() { luaEditor->show(); } +void MainWindow::openCheatsEditor() { cheatsEditor->show(); } + void MainWindow::dispatchMessage(const EmulatorMessage& message) { switch (message.type) { case MessageType::LoadROM: emu->loadROM(*message.path.p); + cheatsEditorAction->setEnabled(true); // Clean up the allocated path delete message.path.p; break;