Merge pull request #388 from OFFTKP/cheatpicker

Add cheat picker window
This commit is contained in:
wheremyfoodat 2024-01-27 19:16:53 +00:00 committed by GitHub
commit 407411cc0b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 473 additions and 25 deletions

View file

@ -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})

View file

@ -24,6 +24,7 @@ class Cheats {
Cheats(Memory& mem, HIDService& hid);
u32 addCheat(const Cheat& cheat);
u32 addCheat(const u8* data, size_t size);
void removeCheat(u32 id);
void enableCheat(u32 id);
void disableCheat(u32 id);
@ -32,6 +33,7 @@ class Cheats {
void clear();
bool haveCheats() const { return cheatsLoaded; }
static constexpr u32 badCheatHandle = 0xFFFFFFFF;
private:
ActionReplay ar; // An ActionReplay cheat machine for executing CTRPF codes

View file

@ -0,0 +1,26 @@
#pragma once
#include <QAction>
#include <QWidget>
#include <filesystem>
#include <memory>
#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;
};

View file

@ -5,6 +5,7 @@
#include <QtWidgets>
#include <atomic>
#include <filesystem>
#include <functional>
#include <mutex>
#include <thread>
#include <vector>
@ -12,16 +13,36 @@
#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"
struct CheatMessage {
u32 handle;
std::vector<uint8_t> cheat;
std::function<void(u32)> callback;
};
class MainWindow : public QMainWindow {
Q_OBJECT
private:
// Types of messages we might send from the GUI thread to the emulator thread
enum class MessageType { LoadROM, Reset, Pause, Resume, TogglePause, DumpRomFS, PressKey, ReleaseKey, LoadLuaScript };
enum class MessageType {
LoadROM,
Reset,
Pause,
Resume,
TogglePause,
DumpRomFS,
PressKey,
ReleaseKey,
SetCirclePadX,
SetCirclePadY,
LoadLuaScript,
EditCheat,
};
// Tagged union representing our message queue messages
struct EmulatorMessage {
@ -36,9 +57,17 @@ class MainWindow : public QMainWindow {
u32 key;
} key;
struct {
s16 value;
} circlepad;
struct {
std::string* str;
} string;
struct {
CheatMessage* c;
} cheat;
};
};
@ -54,6 +83,7 @@ class MainWindow : public QMainWindow {
ScreenWidget screen;
AboutWindow* aboutWindow;
ConfigWindow* configWindow;
CheatsWindow* cheatsEditor;
TextEditorWindow* luaEditor;
QMenuBar* menuBar = nullptr;
@ -63,6 +93,7 @@ class MainWindow : public QMainWindow {
void selectROM();
void dumpRomFS();
void openLuaEditor();
void openCheatsEditor();
void showAboutMenu();
void sendMessage(const EmulatorMessage& message);
void dispatchMessage(const EmulatorMessage& message);
@ -78,4 +109,5 @@ class MainWindow : public QMainWindow {
void keyPressEvent(QKeyEvent* event) override;
void keyReleaseEvent(QKeyEvent* event) override;
void loadLuaScript(const std::string& code);
void editCheat(u32 handle, const std::vector<uint8_t>& cheat, const std::function<void(u32)>& callback);
};

View file

@ -139,6 +139,65 @@ void ActionReplay::executeDType(const Cheat& cheat, u32 instruction) {
switch (instruction) {
case 0xD3000000: offset1 = cheat[pc++]; break;
case 0xD3000001: offset2 = cheat[pc++]; break;
case 0xD6000000:
write32(*activeOffset + cheat[pc++], u32(*activeData));
*activeOffset += 4;
break;
case 0xD6000001:
write32(*activeOffset + cheat[pc++], u32(data1));
*activeOffset += 4;
break;
case 0xD6000002:
write32(*activeOffset + cheat[pc++], u32(data2));
*activeOffset += 4;
break;
case 0xD7000000:
write16(*activeOffset + cheat[pc++], u16(*activeData));
*activeOffset += 2;
break;
case 0xD7000001:
write16(*activeOffset + cheat[pc++], u16(data1));
*activeOffset += 2;
break;
case 0xD7000002:
write16(*activeOffset + cheat[pc++], u16(data2));
*activeOffset += 2;
break;
case 0xD8000000:
write8(*activeOffset + cheat[pc++], u8(*activeData));
*activeOffset += 1;
break;
case 0xD8000001:
write8(*activeOffset + cheat[pc++], u8(data1));
*activeOffset += 1;
break;
case 0xD8000002:
write8(*activeOffset + cheat[pc++], u8(data2));
*activeOffset += 1;
break;
case 0xD9000000: *activeData = read32(cheat[pc++] + *activeOffset); break;
case 0xD9000001: data1 = read32(cheat[pc++] + *activeOffset); break;
case 0xD9000002: data2 = read32(cheat[pc++] + *activeOffset); break;
case 0xDA000000: *activeData = read16(cheat[pc++] + *activeOffset); break;
case 0xDA000001: data1 = read16(cheat[pc++] + *activeOffset); break;
case 0xDA000002: data2 = read16(cheat[pc++] + *activeOffset); break;
case 0xDB000000: *activeData = read8(cheat[pc++] + *activeOffset); break;
case 0xDB000001: data1 = read8(cheat[pc++] + *activeOffset); break;
case 0xDB000002: data2 = read8(cheat[pc++] + *activeOffset); break;
case 0xDC000000: *activeOffset += cheat[pc++]; break;
// DD000000 XXXXXXXX - if KEYPAD has value XXXXXXXX execute next block

View file

@ -1,4 +1,5 @@
#include "cheats.hpp"
#include "swap.hpp"
Cheats::Cheats(Memory& mem, HIDService& hid) : ar(mem, hid) { reset(); }
@ -23,6 +24,27 @@ u32 Cheats::addCheat(const Cheat& cheat) {
return cheats.size() - 1;
}
u32 Cheats::addCheat(const u8* data, size_t size) {
if ((size % 8) != 0) {
return badCheatHandle;
}
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 addCheat(cheat);
}
void Cheats::removeCheat(u32 id) {
if (id >= cheats.size()) {
return;

View file

@ -134,25 +134,7 @@ void HydraCore::setPollInputCallback(void (*callback)()) { pollInputCallback = c
void HydraCore::setCheckButtonCallback(s32 (*callback)(u32 player, hydra::ButtonType button)) { checkButtonCallback = callback; }
u32 HydraCore::addCheat(const u8* data, u32 size) {
// Every 3DS cheat is a multiple of 64 bits == 8 bytes
if ((size % 8) != 0) {
return hydra::BAD_CHEAT;
}
Cheats::Cheat cheat;
cheat.enabled = true;
cheat.type = Cheats::CheatType::ActionReplay;
for (u32 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 emulator->getCheats().addCheat(cheat);
return emulator->getCheats().addCheat(data, size);
};
void HydraCore::removeCheat(u32 id) { emulator->getCheats().removeCheat(id); }

View file

@ -0,0 +1,268 @@
#include "panda_qt/cheats_window.hpp"
#include <QCheckBox>
#include <QDialog>
#include <QDialogButtonBox>
#include <QLabel>
#include <QLineEdit>
#include <QListWidget>
#include <QPushButton>
#include <QTextEdit>
#include <QVBoxLayout>
#include <QTimer>
#include <functional>
#include "cheats.hpp"
#include "emulator.hpp"
#include "panda_qt/main_window.hpp"
MainWindow* mainWindow = nullptr;
struct CheatMetadata {
u32 handle = Cheats::badCheatHandle;
std::string name = "New cheat";
std::string code;
bool enabled = true;
};
void dispatchToMainThread(std::function<void()> callback) {
QTimer* timer = new QTimer();
timer->moveToThread(qApp->thread());
timer->setSingleShot(true);
QObject::connect(timer, &QTimer::timeout, [=]()
{
callback();
timer->deleteLater();
});
QMetaObject::invokeMethod(timer, "start", Qt::QueuedConnection, Q_ARG(int, 0));
}
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 == Cheats::badCheatHandle) {
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* buttonBox = new QDialogButtonBox(buttons);
layout->addWidget(buttonBox);
connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(buttonBox, &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();
metadata.name = nameEdit->text().toStdString();
metadata.code = code.toStdString();
cheatEntry.setMetadata(metadata);
std::vector<u8> bytes;
for (size_t i = 0; i < metadata.code.size(); i += 2) {
std::string hex = metadata.code.substr(i, 2);
bytes.push_back((u8)std::stoul(hex, nullptr, 16));
}
mainWindow->editCheat(cheatEntry.getMetadata().handle, bytes, [this](u32 handle) {
dispatchToMainThread([this, handle]() {
if (handle == Cheats::badCheatHandle) {
cheatEntry.Remove();
return;
} else {
CheatMetadata metadata = cheatEntry.getMetadata();
metadata.handle = handle;
cheatEntry.setMetadata(metadata);
cheatEntry.Update();
}
});
});
}
void CheatEditDialog::rejected() {
bool isEditing = cheatEntry.getMetadata().handle != Cheats::badCheatHandle;
if (!isEditing) {
// Was adding a cheat but user pressed cancel
cheatEntry.Remove();
}
}
CheatsWindow::CheatsWindow(Emulator* emu, const std::filesystem::path& cheatPath, QWidget* parent)
: QWidget(parent, Qt::Window), emu(emu), cheatPath(cheatPath) {
mainWindow = static_cast<MainWindow*>(parent);
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, {Cheats::badCheatHandle, "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<CheatEntryWidget*>(cheatList->itemWidget(item));
entry->Remove();
}

View file

@ -6,6 +6,8 @@
#include <cstdio>
#include <fstream>
#include "cheats.hpp"
MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent), screen(this) {
setWindowTitle("Alber");
// Enable drop events for loading ROMs
@ -48,20 +50,23 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent)
auto dumpRomFSAction = toolsMenu->addAction(tr("Dump RomFS"));
auto luaEditorAction = toolsMenu->addAction(tr("Open Lua Editor"));
auto cheatsEditorAction = toolsMenu->addAction(tr("Open Cheats Editor"));
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, {}, this);
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 +189,7 @@ MainWindow::~MainWindow() {
delete menuBar;
delete aboutWindow;
delete configWindow;
delete cheatsEditor;
delete luaEditor;
}
@ -233,6 +239,7 @@ void MainWindow::showAboutMenu() {
}
void MainWindow::openLuaEditor() { luaEditor->show(); }
void MainWindow::openCheatsEditor() { cheatsEditor->show(); }
void MainWindow::dispatchMessage(const EmulatorMessage& message) {
switch (message.type) {
@ -247,12 +254,29 @@ void MainWindow::dispatchMessage(const EmulatorMessage& message) {
delete message.string.str;
break;
case MessageType::EditCheat: {
u32 handle = message.cheat.c->handle;
const std::vector<uint8_t>& cheat = message.cheat.c->cheat;
const std::function<void(u32)>& callback = message.cheat.c->callback;
bool isEditing = handle != Cheats::badCheatHandle;
if (isEditing) {
emu->getCheats().removeCheat(handle);
u32 handle = emu->getCheats().addCheat(cheat.data(), cheat.size());
} else {
u32 handle = emu->getCheats().addCheat(cheat.data(), cheat.size());
callback(handle);
}
delete message.cheat.c;
} break;
case MessageType::Pause: emu->pause(); break;
case MessageType::Resume: emu->resume(); break;
case MessageType::TogglePause: emu->togglePause(); break;
case MessageType::Reset: emu->reset(Emulator::ReloadOption::Reload); break;
case MessageType::PressKey: emu->getServiceManager().getHID().pressKey(message.key.key); break;
case MessageType::ReleaseKey: emu->getServiceManager().getHID().releaseKey(message.key.key); break;
case MessageType::SetCirclePadX: emu->getServiceManager().getHID().setCirclepadX(message.circlepad.value); break;
case MessageType::SetCirclePadY: emu->getServiceManager().getHID().setCirclepadY(message.circlepad.value); break;
}
}
@ -260,7 +284,12 @@ void MainWindow::keyPressEvent(QKeyEvent* event) {
auto pressKey = [this](u32 key) {
EmulatorMessage message{.type = MessageType::PressKey};
message.key.key = key;
sendMessage(message);
};
auto setCirclePad = [this](MessageType type, s16 value) {
EmulatorMessage message{.type = type};
message.circlepad.value = value;
sendMessage(message);
};
@ -273,6 +302,11 @@ void MainWindow::keyPressEvent(QKeyEvent* event) {
case Qt::Key_Q: pressKey(HID::Keys::L); break;
case Qt::Key_P: pressKey(HID::Keys::R); break;
case Qt::Key_W: setCirclePad(MessageType::SetCirclePadY, 0x9C); break;
case Qt::Key_A: setCirclePad(MessageType::SetCirclePadX, -0x9C); break;
case Qt::Key_S: setCirclePad(MessageType::SetCirclePadY, -0x9C); break;
case Qt::Key_D: setCirclePad(MessageType::SetCirclePadX, 0x9C); break;
case Qt::Key_Right: pressKey(HID::Keys::Right); break;
case Qt::Key_Left: pressKey(HID::Keys::Left); break;
case Qt::Key_Up: pressKey(HID::Keys::Up); break;
@ -289,7 +323,12 @@ void MainWindow::keyReleaseEvent(QKeyEvent* event) {
auto releaseKey = [this](u32 key) {
EmulatorMessage message{.type = MessageType::ReleaseKey};
message.key.key = key;
sendMessage(message);
};
auto releaseCirclePad = [this](MessageType type) {
EmulatorMessage message{.type = type};
message.circlepad.value = 0;
sendMessage(message);
};
@ -302,6 +341,12 @@ void MainWindow::keyReleaseEvent(QKeyEvent* event) {
case Qt::Key_Q: releaseKey(HID::Keys::L); break;
case Qt::Key_P: releaseKey(HID::Keys::R); break;
case Qt::Key_W:
case Qt::Key_S: releaseCirclePad(MessageType::SetCirclePadY); break;
case Qt::Key_A:
case Qt::Key_D: releaseCirclePad(MessageType::SetCirclePadX); break;
case Qt::Key_Right: releaseKey(HID::Keys::Right); break;
case Qt::Key_Left: releaseKey(HID::Keys::Left); break;
case Qt::Key_Up: releaseKey(HID::Keys::Up); break;
@ -318,4 +363,16 @@ void MainWindow::loadLuaScript(const std::string& code) {
// Make a copy of the code on the heap to send via the message queue
message.string.str = new std::string(code);
sendMessage(message);
}
void MainWindow::editCheat(u32 handle, const std::vector<uint8_t>& cheat, const std::function<void(u32)>& callback) {
EmulatorMessage message{.type = MessageType::EditCheat};
CheatMessage* c = new CheatMessage();
c->handle = handle;
c->cheat = cheat;
c->callback = callback;
message.cheat.c = c;
sendMessage(message);
}