diff --git a/.gitmodules b/.gitmodules index 8a6cac49..428ca1d1 100644 --- a/.gitmodules +++ b/.gitmodules @@ -43,3 +43,6 @@ [submodule "third_party/hydra_core"] path = third_party/hydra_core url = https://github.com/hydra-emu/core +[submodule "third_party/zep"] + path = third_party/zep + url = https://github.com/Panda3DS-emu/zep diff --git a/CMakeLists.txt b/CMakeLists.txt index 5e49c27c..732ec793 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -186,12 +186,20 @@ set(RENDERER_SW_SOURCE_FILES src/core/renderer_sw/renderer_sw.cpp) # Frontend source files 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) - 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) + 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 + ) + 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 + ) source_group("Source Files\\Qt" FILES ${FRONTEND_SOURCE_FILES}) source_group("Header Files\\Qt" FILES ${FRONTEND_HEADER_FILES}) include_directories(${Qt6Gui_PRIVATE_INCLUDE_DIRS}) + + include_directories(third_party/zep/include) # Include zep for text editor usage + configure_file(third_party/zep/cmake/config_app.h.cmake ${CMAKE_BINARY_DIR}/zep_config/config_app.h) + include_directories(${CMAKE_BINARY_DIR}/zep_config) else() set(FRONTEND_SOURCE_FILES src/panda_sdl/main.cpp src/panda_sdl/frontend_sdl.cpp) set(FRONTEND_HEADER_FILES "") @@ -428,6 +436,9 @@ endif() if(ENABLE_QT_GUI) target_compile_definitions(Alber PUBLIC "PANDA3DS_FRONTEND_QT=1") + target_compile_definitions(Alber PUBLIC "ZEP_QT=1") + target_compile_definitions(Alber PUBLIC "ZEP_FEATURE_CPP_FILE_SYSTEM=1") + target_link_libraries(Alber PRIVATE Qt6::Widgets) if(LINUX OR FREEBSD) diff --git a/include/emulator.hpp b/include/emulator.hpp index d992b331..fcf93329 100644 --- a/include/emulator.hpp +++ b/include/emulator.hpp @@ -119,6 +119,8 @@ class Emulator { EmulatorConfig& getConfig() { return config; } Cheats& getCheats() { return cheats; } ServiceManager& getServiceManager() { return kernel.getServiceManager(); } + LuaManager& getLua() { return lua; } + RendererType getRendererType() const { return config.rendererType; } Renderer* getRenderer() { return gpu.getRenderer(); } u64 getTicks() { return cpu.getTicks(); } diff --git a/include/lua_manager.hpp b/include/lua_manager.hpp index 7ef96f18..50b8dd61 100644 --- a/include/lua_manager.hpp +++ b/include/lua_manager.hpp @@ -1,4 +1,6 @@ #pragma once +#include + #include "helpers.hpp" #include "memory.hpp" @@ -36,6 +38,8 @@ class LuaManager { void initialize(); void initializeThunks(); void loadFile(const char* path); + void loadString(const std::string& code); + void reset(); void signalEvent(LuaEvent e) { if (haveScript) [[unlikely]] { @@ -52,6 +56,7 @@ class LuaManager { void close() {} void initialize() {} void loadFile(const char* path) {} + void loadString(const std::string& code) {} void reset() {} void signalEvent(LuaEvent e) {} }; diff --git a/include/panda_qt/main_window.hpp b/include/panda_qt/main_window.hpp index e2fd8596..7dfb91b7 100644 --- a/include/panda_qt/main_window.hpp +++ b/include/panda_qt/main_window.hpp @@ -13,6 +13,7 @@ #include "panda_qt/about_window.hpp" #include "panda_qt/config_window.hpp" #include "panda_qt/screen.hpp" +#include "panda_qt/text_editor.hpp" #include "services/hid.hpp" class MainWindow : public QMainWindow { @@ -20,9 +21,7 @@ class MainWindow : public QMainWindow { 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 - }; + enum class MessageType { LoadROM, Reset, Pause, Resume, TogglePause, DumpRomFS, PressKey, ReleaseKey, LoadLuaScript }; // Tagged union representing our message queue messages struct EmulatorMessage { @@ -36,6 +35,10 @@ class MainWindow : public QMainWindow { struct { u32 key; } key; + + struct { + std::string* str; + } string; }; }; @@ -43,7 +46,7 @@ class MainWindow : public QMainWindow { Emulator* emu = nullptr; std::thread emuThread; - std::atomic appRunning = true; // Is the application itself running? + std::atomic appRunning = true; // Is the application itself running? // Used for synchronizing messages between the emulator and UI std::mutex messageQueueMutex; std::vector messageQueue; @@ -51,12 +54,15 @@ class MainWindow : public QMainWindow { ScreenWidget screen; AboutWindow* aboutWindow; ConfigWindow* configWindow; + TextEditorWindow* luaEditor; QMenuBar* menuBar = nullptr; void swapEmuBuffer(); void emuThreadMainLoop(); + void selectLuaFile(); void selectROM(); void dumpRomFS(); + void openLuaEditor(); void showAboutMenu(); void sendMessage(const EmulatorMessage& message); void dispatchMessage(const EmulatorMessage& message); @@ -71,4 +77,5 @@ class MainWindow : public QMainWindow { void keyPressEvent(QKeyEvent* event) override; void keyReleaseEvent(QKeyEvent* event) override; + void loadLuaScript(const std::string& code); }; \ No newline at end of file diff --git a/include/panda_qt/text_editor.hpp b/include/panda_qt/text_editor.hpp new file mode 100644 index 00000000..0da98294 --- /dev/null +++ b/include/panda_qt/text_editor.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include +#include +#include +#include + +#include "zep.h" +#include "zep/mode_repl.h" +#include "zep/regress.h" + +class TextEditorWindow : public QDialog { + Q_OBJECT + + private: + Zep::ZepWidget_Qt zepWidget; + Zep::IZepReplProvider replProvider; + static constexpr float fontSize = 14.0f; + + public: + TextEditorWindow(QWidget* parent, const std::string& filename, const std::string& initialText); + void setText(const std::string& text) { zepWidget.GetEditor().GetMRUBuffer()->SetText(text); } +}; \ No newline at end of file diff --git a/src/core/services/apt.cpp b/src/core/services/apt.cpp index 754baa13..9d02bf94 100644 --- a/src/core/services/apt.cpp +++ b/src/core/services/apt.cpp @@ -2,6 +2,7 @@ #include "ipc.hpp" #include "kernel.hpp" +#include #include namespace APTCommands { @@ -311,12 +312,14 @@ void APTService::setApplicationCpuTimeLimit(u32 messagePointer) { log("APT::SetApplicationCpuTimeLimit (percentage = %d%%)\n", percentage); if (percentage < 5 || percentage > 89 || fixed != 1) { - Helpers::panic("Invalid parameters passed to APT::SetApplicationCpuTimeLimit"); - } else { - mem.write32(messagePointer, IPC::responseHeader(0x4F, 1, 0)); - mem.write32(messagePointer + 4, Result::Success); - cpuTimeLimit = percentage; + Helpers::warn("Invalid parameter passed to APT::SetApplicationCpuTimeLimit: (percentage, fixed) = (%d, %d)\n", percentage, fixed); + // TODO: Does the clamp operation happen? Verify with getApplicationCpuTimeLimit on hardware + percentage = std::clamp(percentage, 5, 89); } + + mem.write32(messagePointer, IPC::responseHeader(0x4F, 1, 0)); + mem.write32(messagePointer + 4, Result::Success); + cpuTimeLimit = percentage; } void APTService::getApplicationCpuTimeLimit(u32 messagePointer) { diff --git a/src/lua.cpp b/src/lua.cpp index 729b6581..ec1287bd 100644 --- a/src/lua.cpp +++ b/src/lua.cpp @@ -51,6 +51,31 @@ void LuaManager::loadFile(const char* path) { } } +void LuaManager::loadString(const std::string& code) { + // Initialize Lua if it has not been initialized + if (!initialized) { + initialize(); + } + + // If init failed, don't execute + if (!initialized) { + printf("Lua initialization failed, file won't run\n"); + haveScript = false; + + return; + } + + int status = luaL_loadstring(L, code.c_str()); // load Lua script + int ret = lua_pcall(L, 0, 0, 0); // tell Lua to run the script + + if (ret != 0) { + haveScript = false; + fprintf(stderr, "%s\n", lua_tostring(L, -1)); // tell us what mistake we made + } else { + haveScript = true; + } +} + void LuaManager::signalEventInternal(LuaEvent e) { lua_getglobal(L, "eventHandler"); // We want to call the event handler lua_pushnumber(L, static_cast(e)); // Push event type diff --git a/src/panda_qt/main_window.cpp b/src/panda_qt/main_window.cpp index 8dbfc7aa..672fc2a1 100644 --- a/src/panda_qt/main_window.cpp +++ b/src/panda_qt/main_window.cpp @@ -2,6 +2,7 @@ #include #include +#include MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent), screen(this) { setWindowTitle("Alber"); @@ -23,8 +24,10 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent) auto aboutMenu = menuBar->addMenu(tr("About")); // Create and bind actions for them - auto pandaAction = fileMenu->addAction(tr("Load game")); - connect(pandaAction, &QAction::triggered, this, &MainWindow::selectROM); + auto loadGameAction = fileMenu->addAction(tr("Load game")); + auto loadLuaAction = fileMenu->addAction(tr("Load Lua script")); + connect(loadGameAction, &QAction::triggered, this, &MainWindow::selectROM); + connect(loadLuaAction, &QAction::triggered, this, &MainWindow::selectLuaFile); auto pauseAction = emulationMenu->addAction(tr("Pause")); auto resumeAction = emulationMenu->addAction(tr("Resume")); @@ -36,7 +39,9 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent) connect(configureAction, &QAction::triggered, this, [this]() { configWindow->show(); }); auto dumpRomFSAction = toolsMenu->addAction(tr("Dump RomFS")); + auto luaEditorAction = toolsMenu->addAction(tr("Open Lua Editor")); connect(dumpRomFSAction, &QAction::triggered, this, &MainWindow::dumpRomFS); + connect(luaEditorAction, &QAction::triggered, this, &MainWindow::openLuaEditor); auto aboutAction = aboutMenu->addAction(tr("About Panda3DS")); connect(aboutAction, &QAction::triggered, this, &MainWindow::showAboutMenu); @@ -44,6 +49,7 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent) // Set up misc objects aboutWindow = new AboutWindow(nullptr); configWindow = new ConfigWindow(this); + luaEditor = new TextEditorWindow(this, "script.lua", ""); emu = new Emulator(); emu->setOutputSize(screen.surfaceWidth, screen.surfaceHeight); @@ -121,6 +127,34 @@ void MainWindow::selectROM() { } } +void MainWindow::selectLuaFile() { + auto path = QFileDialog::getOpenFileName(this, tr("Select Lua script to load"), "", tr("Lua scripts (*.lua *.txt)")); + + if (!path.isEmpty()) { + std::ifstream file(std::filesystem::path(path.toStdU16String()), std::ios::in); + + if (file.fail()) { + printf("Failed to load selected lua file\n"); + return; + } + + // Read whole file into an std::string string + // Get file size, preallocate std::string to avoid furthermemory allocations + std::string code; + file.seekg(0, std::ios::end); + code.resize(file.tellg()); + + // Rewind and read the whole file + file.seekg(0, std::ios::beg); + file.read(&code[0], code.size()); + file.close(); + + loadLuaScript(code); + // Copy the Lua script to the Lua editor + luaEditor->setText(code); + } +} + // Cleanup when the main window closes MainWindow::~MainWindow() { appRunning = false; // Set our running atomic to false in order to make the emulator thread stop, and join it @@ -133,6 +167,7 @@ MainWindow::~MainWindow() { delete menuBar; delete aboutWindow; delete configWindow; + delete luaEditor; } // Send a message to the emulator thread. Lock the mutex and just push back to the vector. @@ -181,6 +216,8 @@ void MainWindow::showAboutMenu() { about.exec(); } +void MainWindow::openLuaEditor() { luaEditor->show(); } + void MainWindow::dispatchMessage(const EmulatorMessage& message) { switch (message.type) { case MessageType::LoadROM: @@ -189,6 +226,11 @@ void MainWindow::dispatchMessage(const EmulatorMessage& message) { delete message.path.p; break; + case MessageType::LoadLuaScript: + emu->getLua().loadString(*message.string.str); + delete message.string.str; + break; + case MessageType::Pause: emu->pause(); break; case MessageType::Resume: emu->resume(); break; case MessageType::TogglePause: emu->togglePause(); break; @@ -253,3 +295,11 @@ void MainWindow::keyReleaseEvent(QKeyEvent* event) { case Qt::Key_Backspace: releaseKey(HID::Keys::Select); break; } } + +void MainWindow::loadLuaScript(const std::string& code) { + EmulatorMessage message{.type = MessageType::LoadLuaScript}; + + // Make a copy of the code on the heap to send via the message queue + message.string.str = new std::string(code); + sendMessage(message); +} \ No newline at end of file diff --git a/src/panda_qt/text_editor.cpp b/src/panda_qt/text_editor.cpp new file mode 100644 index 00000000..c189c2ce --- /dev/null +++ b/src/panda_qt/text_editor.cpp @@ -0,0 +1,44 @@ +#include "panda_qt/text_editor.hpp" + +#include +#include + +#include "panda_qt/main_window.hpp" + +using namespace Zep; + +TextEditorWindow::TextEditorWindow(QWidget* parent, const std::string& filename, const std::string& initialText) + : QDialog(parent), zepWidget(this, qApp->applicationDirPath().toStdString(), fontSize) { + resize(600, 600); + + // Register our extensions + ZepRegressExCommand::Register(zepWidget.GetEditor()); + ZepReplExCommand::Register(zepWidget.GetEditor(), &replProvider); + + // Default to standard mode instead of vim mode, initialize text box + zepWidget.GetEditor().SetGlobalMode(Zep::ZepMode_Standard::StaticName()); + zepWidget.GetEditor().InitWithText(filename, initialText); + + // Layout for widgets + QVBoxLayout* mainLayout = new QVBoxLayout(); + setLayout(mainLayout); + + QPushButton* button = new QPushButton(tr("Load script"), this); + button->setFixedSize(100, 20); + + // When the Load Script button is pressed, send the current text to the MainWindow, which will upload it to the emulator's lua object + connect(button, &QPushButton::pressed, this, [this]() { + if (parentWidget()) { + auto buffer = zepWidget.GetEditor().GetMRUBuffer(); + const std::string text = buffer->GetBufferText(buffer->Begin(), buffer->End()); + + static_cast(parentWidget())->loadLuaScript(text); + } else { + // This should be unreachable, only here for safety purposes + printf("Text editor does not have any parent widget, click doesn't work :(\n"); + } + }); + + mainLayout->addWidget(button); + mainLayout->addWidget(&zepWidget); +} \ No newline at end of file diff --git a/src/panda_qt/zep.cpp b/src/panda_qt/zep.cpp new file mode 100644 index 00000000..570f0e64 --- /dev/null +++ b/src/panda_qt/zep.cpp @@ -0,0 +1,2 @@ +#define ZEP_SINGLE_HEADER_BUILD +#include "zep.h" \ No newline at end of file diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/config/GlobalConfig.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/config/GlobalConfig.java index ddfee09e..d25e34c7 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/config/GlobalConfig.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/config/GlobalConfig.java @@ -57,4 +57,4 @@ public class GlobalConfig { this.defaultValue = defaultValue; } } -} \ No newline at end of file +} diff --git a/third_party/zep b/third_party/zep new file mode 160000 index 00000000..75406e1c --- /dev/null +++ b/third_party/zep @@ -0,0 +1 @@ +Subproject commit 75406e1c854b9fa6ede697d6165664e0e11b09ff