From f9dc9ac94dfcef87593c4907bd48775c904f421a Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Fri, 6 Oct 2023 09:33:15 +0300 Subject: [PATCH 1/5] Add mio submodule --- .gitmodules | 3 +++ third_party/mio | 1 + 2 files changed, 4 insertions(+) create mode 160000 third_party/mio diff --git a/.gitmodules b/.gitmodules index c884b29dd..462d67260 100644 --- a/.gitmodules +++ b/.gitmodules @@ -37,3 +37,6 @@ [submodule "third_party/LuaJIT"] path = third_party/LuaJIT url = https://github.com/Panda3DS-emu/LuaJIT +[submodule "third_party/mio"] + path = third_party/mio + url = https://github.com/vimpunk/mio diff --git a/third_party/mio b/third_party/mio new file mode 160000 index 000000000..8b6b7d878 --- /dev/null +++ b/third_party/mio @@ -0,0 +1 @@ +Subproject commit 8b6b7d878c89e81614d05edca7936de41ccdd2da From abe46754771f404949c16434ba9f294880692cd7 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Sat, 7 Oct 2023 21:23:05 +0300 Subject: [PATCH 2/5] Attempt to add RomFS dumping --- CMakeLists.txt | 5 +-- include/emulator.hpp | 2 ++ include/fs/romfs.hpp | 7 ++++ include/loader/ncch.hpp | 1 + include/memory_mapped_file.hpp | 42 ++++++++++++++++++++++ include/panda_qt/main_window.hpp | 1 + src/core/loader/ncch.cpp | 1 + src/emulator.cpp | 61 +++++++++++++++++++++++++++++++- src/memory_mapped_file.cpp | 37 +++++++++++++++++++ src/panda_qt/main_window.cpp | 31 +++++++++++++--- 10 files changed, 180 insertions(+), 8 deletions(-) create mode 100644 include/memory_mapped_file.hpp create mode 100644 src/memory_mapped_file.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 2c13d6346..10dd66a5c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -53,6 +53,7 @@ include_directories(third_party/xxhash/include) include_directories(third_party/httplib) include_directories(third_party/stb) include_directories(third_party/opengl) +include_directories(third_party/mio/single_include) add_compile_definitions(NOMINMAX) # Make windows.h not define min/max macros because third-party deps don't like it add_compile_definitions(WIN32_LEAN_AND_MEAN) # Make windows.h not include literally everything @@ -141,7 +142,7 @@ set(SOURCE_FILES src/emulator.cpp src/io_file.cpp src/config.cpp src/core/CPU/cpu_dynarmic.cpp src/core/CPU/dynarmic_cycles.cpp src/core/memory.cpp src/renderer.cpp src/core/renderer_null/renderer_null.cpp src/http_server.cpp src/stb_image_write.c src/core/cheats.cpp src/core/action_replay.cpp - src/discord_rpc.cpp src/lua.cpp + src/discord_rpc.cpp src/lua.cpp src/memory_mapped_file.cpp ) set(CRYPTO_SOURCE_FILES src/core/crypto/aes_engine.cpp) set(KERNEL_SOURCE_FILES src/core/kernel/kernel.cpp src/core/kernel/resource_limits.cpp @@ -219,7 +220,7 @@ set(HEADER_FILES include/emulator.hpp include/helpers.hpp include/termcolor.hpp include/applets/applet.hpp include/applets/mii_selector.hpp include/math_util.hpp include/services/soc.hpp include/services/news_u.hpp include/applets/software_keyboard.hpp include/applets/applet_manager.hpp include/fs/archive_user_save_data.hpp include/services/amiibo_device.hpp include/services/nfc_types.hpp include/swap.hpp include/services/csnd.hpp include/services/nwm_uds.hpp - include/fs/archive_system_save_data.hpp include/lua_manager.hpp + include/fs/archive_system_save_data.hpp include/lua_manager.hpp include/memory_mapped_file.hpp ) cmrc_add_resource_library( diff --git a/include/emulator.hpp b/include/emulator.hpp index 4a5fab754..1901e4254 100644 --- a/include/emulator.hpp +++ b/include/emulator.hpp @@ -12,6 +12,7 @@ #include "cpu.hpp" #include "crypto/aes_engine.hpp" #include "discord_rpc.hpp" +#include "fs/romfs.hpp" #include "io_file.hpp" #include "lua_manager.hpp" #include "memory.hpp" @@ -120,6 +121,7 @@ class Emulator { void initGraphicsContext() { gpu.initGraphicsContext(window); } #endif + RomFS::DumpingResult dumpRomFS(const std::filesystem::path& path); void setOutputSize(u32 width, u32 height) { gpu.setOutputSize(width, height); } EmulatorConfig& getConfig() { return config; } diff --git a/include/fs/romfs.hpp b/include/fs/romfs.hpp index 20213761a..114b1c1ef 100644 --- a/include/fs/romfs.hpp +++ b/include/fs/romfs.hpp @@ -18,5 +18,12 @@ namespace RomFS { std::vector> files; }; + // Result codes when dumping RomFS. These are used by the frontend to print appropriate error messages if RomFS dumping fails + enum class DumpingResult { + Success = 0, + InvalidFormat = 1, // ROM is a format that doesn't support RomFS, such as ELF + NoRomFS = 2 + }; + std::unique_ptr parseRomFSTree(uintptr_t romFS, u64 romFSSize); } // namespace RomFS \ No newline at end of file diff --git a/include/loader/ncch.hpp b/include/loader/ncch.hpp index 7f0ff37f6..5e2ad1d85 100644 --- a/include/loader/ncch.hpp +++ b/include/loader/ncch.hpp @@ -57,6 +57,7 @@ struct NCCH { FSInfo exeFS; FSInfo romFS; CodeSetInfo text, data, rodata; + FSInfo partitionInfo; // Contents of the .code file in the ExeFS std::vector codeFile; diff --git a/include/memory_mapped_file.hpp b/include/memory_mapped_file.hpp new file mode 100644 index 000000000..e83141555 --- /dev/null +++ b/include/memory_mapped_file.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include +#include + +#include "helpers.hpp" +#include "mio/mio.hpp" + +// Minimal RAII wrapper over memory mapped files + +class MemoryMappedFile { + std::filesystem::path filePath = ""; // path of our file + mio::mmap_sink map; // mmap sink for our file + + u8* pointer = nullptr; // Pointer to the contents of the memory mapped file + bool opened = false; + + public: + bool exists() const { return opened; } + u8* data() const { return pointer; } + + std::error_code flush(); + MemoryMappedFile(); + MemoryMappedFile(const std::filesystem::path& path); + + ~MemoryMappedFile(); + // Returns true on success + bool open(const std::filesystem::path& path); + void close(); + + // TODO: For memory-mapped output files we'll need some more stuff such as a constructor that takes path/size/shouldCreate as parameters + + u8& operator[](size_t index) { return pointer[index]; } + const u8& operator[](size_t index) const { return pointer[index]; } + + auto begin() { return map.begin(); } + auto end() { return map.end(); } + auto cbegin() { return map.cbegin(); } + auto cend() { return map.cend(); } + + mio::mmap_sink& getSink() { return map; } +}; \ No newline at end of file diff --git a/include/panda_qt/main_window.hpp b/include/panda_qt/main_window.hpp index 9f9f1014d..e4a454877 100644 --- a/include/panda_qt/main_window.hpp +++ b/include/panda_qt/main_window.hpp @@ -39,6 +39,7 @@ class MainWindow : public QMainWindow { void swapEmuBuffer(); void emuThreadMainLoop(); void selectROM(); + void dumpRomFS(); // Tracks whether we are using an OpenGL-backed renderer or a Vulkan-backed renderer bool usingGL = false; diff --git a/src/core/loader/ncch.cpp b/src/core/loader/ncch.cpp index d3d05839c..2546aa01f 100644 --- a/src/core/loader/ncch.cpp +++ b/src/core/loader/ncch.cpp @@ -26,6 +26,7 @@ bool NCCH::loadFromHeader(Crypto::AESEngine &aesEngine, IOFile& file, const FSIn codeFile.clear(); saveData.clear(); + partitionInfo = info; size = u64(*(u32*)&header[0x104]) * mediaUnit; // TODO: Maybe don't type pun because big endian will break exheaderSize = *(u32*)&header[0x180]; diff --git a/src/emulator.cpp b/src/emulator.cpp index 5d87fccd2..e0de4a292 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -581,4 +581,63 @@ void Emulator::updateDiscord() { } #else void Emulator::updateDiscord() {} -#endif \ No newline at end of file +#endif + +static void printNode(const RomFS::RomFSNode& node, int indentation) { + for (int i = 0; i < indentation; i++) { + printf(" "); + } + printf("%s/\n", std::string(node.name.begin(), node.name.end()).c_str()); + + for (auto& file : node.files) { + for (int i = 0; i <= indentation; i++) { + printf(" "); + } + printf("%s\n", std::string(file->name.begin(), file->name.end()).c_str()); + } + + indentation++; + for (auto& directory : node.directories) { + printNode(*directory, indentation); + } + indentation--; +} + +RomFS::DumpingResult Emulator::dumpRomFS(const std::filesystem::path& path) { + using namespace RomFS; + + if (romType != ROMType::NCSD && romType != ROMType::CXI && romType != ROMType::HB_3DSX) { + return DumpingResult::InvalidFormat; + } + + // Contents of RomFS as raw bytes + std::vector romFS; + u64 size; + + if (romType == ROMType::HB_3DSX) { + auto hb3dsx = memory.get3DSX(); + if (!hb3dsx->hasRomFs()) { + return DumpingResult::NoRomFS; + } + size = hb3dsx->romFSSize; + + romFS.resize(size); + hb3dsx->readRomFSBytes(&romFS[0], 0, size); + } else { + auto cxi = memory.getCXI(); + if (!cxi->hasRomFS()) { + return DumpingResult::NoRomFS; + } + + const u64 offset = cxi->romFS.offset; + size = cxi->romFS.size; + + romFS.resize(size); + cxi->readFromFile(memory.CXIFile, cxi->partitionInfo, &romFS[0], offset - cxi->fileOffset, size); + } + + std::unique_ptr node = parseRomFSTree((uintptr_t)&romFS[0], size); + printNode(*node, 0); + + return DumpingResult::Success; +} \ No newline at end of file diff --git a/src/memory_mapped_file.cpp b/src/memory_mapped_file.cpp new file mode 100644 index 000000000..e62b46363 --- /dev/null +++ b/src/memory_mapped_file.cpp @@ -0,0 +1,37 @@ +#include "memory_mapped_file.hpp" + +MemoryMappedFile::MemoryMappedFile() : opened(false), filePath(""), pointer(nullptr) {} +MemoryMappedFile::MemoryMappedFile(const std::filesystem::path& path) { open(path); } +MemoryMappedFile::~MemoryMappedFile() { close(); } + +// TODO: This should probably also return the error one way or another eventually +bool MemoryMappedFile::open(const std::filesystem::path& path) { + std::error_code error; + map = mio::make_mmap_sink(path.string(), 0, mio::map_entire_file, error); + + if (error) { + opened = false; + return false; + } + + filePath = path; + pointer = (u8*)map.data(); + opened = true; + return true; +} + +void MemoryMappedFile::close() { + if (opened) { + opened = false; + pointer = nullptr; // Set the pointer to nullptr to avoid errors related to lingering pointers + + map.unmap(); + } +} + +std::error_code MemoryMappedFile::flush() { + std::error_code ret; + map.sync(ret); + + return ret; +} \ No newline at end of file diff --git a/src/panda_qt/main_window.cpp b/src/panda_qt/main_window.cpp index b4887454f..977a7f80f 100644 --- a/src/panda_qt/main_window.cpp +++ b/src/panda_qt/main_window.cpp @@ -16,14 +16,20 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent) menuBar = new QMenuBar(this); setMenuBar(menuBar); + // Create menu bar menus auto fileMenu = menuBar->addMenu(tr("File")); - auto pandaAction = fileMenu->addAction(tr("panda...")); - connect(pandaAction, &QAction::triggered, this, &MainWindow::selectROM); - auto emulationMenu = menuBar->addMenu(tr("Emulation")); + auto toolsMenu = menuBar->addMenu(tr("Tools")); auto helpMenu = menuBar->addMenu(tr("Help")); auto aboutMenu = menuBar->addMenu(tr("About")); + // Create and bind actions for them + auto pandaAction = fileMenu->addAction(tr("panda...")); + connect(pandaAction, &QAction::triggered, this, &MainWindow::selectROM); + + auto dumpRomFSAction = toolsMenu->addAction(tr("Dump RomFS")); + connect(dumpRomFSAction, &QAction::triggered, this, &MainWindow::dumpRomFS); + // Set up theme selection setTheme(Theme::Dark); themeSelect = new QComboBox(this); @@ -71,6 +77,7 @@ void MainWindow::emuThreadMainLoop() { } needToLoadROM.store(false, std::memory_order::seq_cst); + emu->dumpRomFS(""); } emu->runFrame(); @@ -98,8 +105,8 @@ void MainWindow::selectROM() { return; } - auto path = - QFileDialog::getOpenFileName(this, tr("Select 3DS ROM to load"), "", tr("Nintendo 3DS ROMs (*.3ds *.cci *.cxi *.app *.3dsx *.elf *.axf)")); + auto path = QFileDialog::getOpenFileName( + this, tr("Select 3DS ROM to load"), {}, tr("Nintendo 3DS ROMs (*.3ds *.cci *.cxi *.app *.3dsx *.elf *.axf)"), {}); if (!path.isEmpty()) { romToLoad = path.toStdU16String(); @@ -175,4 +182,18 @@ void MainWindow::setTheme(Theme theme) { break; } } +} + +void MainWindow::dumpRomFS() { + // TODO: LOCK FILE MUTEX HERE + auto folder = QFileDialog::getExistingDirectory( + this, tr("Select folder to dump RomFS files to"), "", QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks + ); + + if (folder.isEmpty()) { + return; + } + + std::filesystem::path path(folder.toStdU16String()); + //RomFS::DumpingResult res = emu->dumpRomFS(path); } \ No newline at end of file From ab2ff1829092cc4cdf810e20c4d758bd02842a58 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Sat, 7 Oct 2023 21:52:47 +0300 Subject: [PATCH 3/5] Fix dumping --- src/emulator.cpp | 29 +++++++++++++++-------------- src/panda_qt/main_window.cpp | 7 +++---- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/emulator.cpp b/src/emulator.cpp index e0de4a292..7555580b4 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -583,24 +583,25 @@ void Emulator::updateDiscord() { void Emulator::updateDiscord() {} #endif -static void printNode(const RomFS::RomFSNode& node, int indentation) { - for (int i = 0; i < indentation; i++) { - printf(" "); - } - printf("%s/\n", std::string(node.name.begin(), node.name.end()).c_str()); - +static void printNode(const RomFS::RomFSNode& node, const char* romFSBase, const std::filesystem::path& path) { for (auto& file : node.files) { - for (int i = 0; i <= indentation; i++) { - printf(" "); - } - printf("%s\n", std::string(file->name.begin(), file->name.end()).c_str()); + const auto p = path / file->name; + std::ofstream outFile(p); + + outFile.write(romFSBase + file->dataOffset, file->dataSize); } - indentation++; for (auto& directory : node.directories) { - printNode(*directory, indentation); + const auto newPath = path / directory->name; + + // Create the directory for the new folder + std::error_code ec; + std::filesystem::create_directories(newPath, ec); + + if (!ec) { + printNode(*directory, romFSBase, newPath); + } } - indentation--; } RomFS::DumpingResult Emulator::dumpRomFS(const std::filesystem::path& path) { @@ -637,7 +638,7 @@ RomFS::DumpingResult Emulator::dumpRomFS(const std::filesystem::path& path) { } std::unique_ptr node = parseRomFSTree((uintptr_t)&romFS[0], size); - printNode(*node, 0); + printNode(*node, (const char*) &romFS[0], path); return DumpingResult::Success; } \ No newline at end of file diff --git a/src/panda_qt/main_window.cpp b/src/panda_qt/main_window.cpp index 977a7f80f..8c57e3355 100644 --- a/src/panda_qt/main_window.cpp +++ b/src/panda_qt/main_window.cpp @@ -77,7 +77,6 @@ void MainWindow::emuThreadMainLoop() { } needToLoadROM.store(false, std::memory_order::seq_cst); - emu->dumpRomFS(""); } emu->runFrame(); @@ -105,8 +104,8 @@ void MainWindow::selectROM() { return; } - auto path = QFileDialog::getOpenFileName( - this, tr("Select 3DS ROM to load"), {}, tr("Nintendo 3DS ROMs (*.3ds *.cci *.cxi *.app *.3dsx *.elf *.axf)"), {}); + auto path = + QFileDialog::getOpenFileName(this, tr("Select 3DS ROM to load"), "", tr("Nintendo 3DS ROMs (*.3ds *.cci *.cxi *.app *.3dsx *.elf *.axf)")); if (!path.isEmpty()) { romToLoad = path.toStdU16String(); @@ -195,5 +194,5 @@ void MainWindow::dumpRomFS() { } std::filesystem::path path(folder.toStdU16String()); - //RomFS::DumpingResult res = emu->dumpRomFS(path); + RomFS::DumpingResult res = emu->dumpRomFS(path); } \ No newline at end of file From 6ae8d084b418b6f7863d46e5998e39dc8baccd9d Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Sat, 7 Oct 2023 22:10:10 +0300 Subject: [PATCH 4/5] Use mutex for synchronizing the UI with the emulator thread --- include/panda_qt/main_window.hpp | 7 +++-- src/panda_qt/main_window.cpp | 48 ++++++++++++++++++++++++-------- 2 files changed, 41 insertions(+), 14 deletions(-) diff --git a/include/panda_qt/main_window.hpp b/include/panda_qt/main_window.hpp index e4a454877..3bbabf35d 100644 --- a/include/panda_qt/main_window.hpp +++ b/include/panda_qt/main_window.hpp @@ -6,8 +6,9 @@ #include #include #include -#include #include +#include +#include #include "emulator.hpp" #include "panda_qt/screen.hpp" @@ -27,9 +28,11 @@ class MainWindow : public QMainWindow { std::thread emuThread; std::atomic appRunning = true; // Is the application itself running? - std::atomic needToLoadROM = false; + std::mutex messageQueueMutex; // Used for synchronizing messages between the emulator and UI std::filesystem::path romToLoad = ""; + bool needToLoadROM = false; + ScreenWidget screen; QComboBox* themeSelect = nullptr; QMenuBar* menuBar = nullptr; diff --git a/src/panda_qt/main_window.cpp b/src/panda_qt/main_window.cpp index 8c57e3355..809f56e0a 100644 --- a/src/panda_qt/main_window.cpp +++ b/src/panda_qt/main_window.cpp @@ -70,13 +70,17 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent) void MainWindow::emuThreadMainLoop() { while (appRunning) { - if (needToLoadROM.load()) { - bool success = emu->loadROM(romToLoad); - if (!success) { - printf("Failed to load ROM"); - } + { + std::unique_lock lock(messageQueueMutex); + + if (needToLoadROM) { + needToLoadROM = false; - needToLoadROM.store(false, std::memory_order::seq_cst); + bool success = emu->loadROM(romToLoad); + if (!success) { + printf("Failed to load ROM"); + } + } } emu->runFrame(); @@ -99,17 +103,22 @@ void MainWindow::swapEmuBuffer() { void MainWindow::selectROM() { // Are we already waiting for a ROM to be loaded? Then complain about it! - if (needToLoadROM.load()) { - QMessageBox::warning(this, tr("Already loading ROM"), tr("Panda3DS is already busy loading a ROM, please wait")); - return; + { + std::unique_lock lock(messageQueueMutex); + if (needToLoadROM) { + QMessageBox::warning(this, tr("Already loading ROM"), tr("Panda3DS is already busy loading a ROM, please wait")); + return; + } } auto path = QFileDialog::getOpenFileName(this, tr("Select 3DS ROM to load"), "", tr("Nintendo 3DS ROMs (*.3ds *.cci *.cxi *.app *.3dsx *.elf *.axf)")); if (!path.isEmpty()) { + std::unique_lock lock(messageQueueMutex); + romToLoad = path.toStdU16String(); - needToLoadROM.store(true, std::memory_order_seq_cst); + needToLoadROM = true; } } @@ -184,7 +193,6 @@ void MainWindow::setTheme(Theme theme) { } void MainWindow::dumpRomFS() { - // TODO: LOCK FILE MUTEX HERE auto folder = QFileDialog::getExistingDirectory( this, tr("Select folder to dump RomFS files to"), "", QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks ); @@ -192,7 +200,23 @@ void MainWindow::dumpRomFS() { if (folder.isEmpty()) { return; } - std::filesystem::path path(folder.toStdU16String()); + + // TODO: This might break if the game accesses RomFS while we're dumping, we should move it to the emulator thread when we've got a message queue going + messageQueueMutex.lock(); RomFS::DumpingResult res = emu->dumpRomFS(path); + messageQueueMutex.unlock(); + + switch (res) { + case RomFS::DumpingResult::Success: break; // Yay! + case RomFS::DumpingResult::InvalidFormat: + QMessageBox::warning( + this, tr("Invalid format for RomFS dumping"), tr("The currently loaded app is not in a format that supports RomFS!") + ); + break; + + case RomFS::DumpingResult::NoRomFS: + QMessageBox::warning(this, tr("No RomFS found"), tr("No RomFS partition was found in the loaded app")); + break; + } } \ No newline at end of file From 0421eae7ae769571f19e32a0165832fc6c320f78 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Sat, 7 Oct 2023 23:15:43 +0300 Subject: [PATCH 5/5] Set up icons --- CMakeLists.txt | 8 +++++++- docs/img/rsob_icon.png | Bin 0 -> 14652 bytes src/emulator.cpp | 8 +++++--- src/panda_qt/main_window.cpp | 12 +++++++++--- 4 files changed, 21 insertions(+), 7 deletions(-) create mode 100644 docs/img/rsob_icon.png diff --git a/CMakeLists.txt b/CMakeLists.txt index 10dd66a5c..80c70c1af 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -182,7 +182,7 @@ set(RENDERER_SW_SOURCE_FILES src/core/renderer_sw/renderer_sw.cpp) if(ENABLE_QT_GUI) set(FRONTEND_SOURCE_FILES src/panda_qt/main.cpp src/panda_qt/screen.cpp src/panda_qt/main_window.cpp) set(FRONTEND_HEADER_FILES include/panda_qt/screen.hpp include/panda_qt/main_window.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}) @@ -414,6 +414,12 @@ if(ENABLE_QT_GUI) target_link_libraries(Alber PRIVATE OpenGL::OpenGL OpenGL::EGL OpenGL::GLX) endif() endif() + + qt_add_resources(Alber "app_images" + PREFIX "/" + FILES + docs/img/rsob_icon.png + ) else() target_compile_definitions(Alber PUBLIC "PANDA3DS_FRONTEND_SDL=1") endif() diff --git a/docs/img/rsob_icon.png b/docs/img/rsob_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..4cabd3fb275b8e2cab5cf3473841890ef4a49a7c GIT binary patch literal 14652 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 Dp1T;) literal 0 HcmV?d00001 diff --git a/src/emulator.cpp b/src/emulator.cpp index 7555580b4..43ecc149f 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -1,5 +1,7 @@ #include "emulator.hpp" + #include +#include #ifdef _WIN32 #include @@ -583,7 +585,7 @@ void Emulator::updateDiscord() { void Emulator::updateDiscord() {} #endif -static void printNode(const RomFS::RomFSNode& node, const char* romFSBase, const std::filesystem::path& path) { +static void dumpRomFSNode(const RomFS::RomFSNode& node, const char* romFSBase, const std::filesystem::path& path) { for (auto& file : node.files) { const auto p = path / file->name; std::ofstream outFile(p); @@ -599,7 +601,7 @@ static void printNode(const RomFS::RomFSNode& node, const char* romFSBase, const std::filesystem::create_directories(newPath, ec); if (!ec) { - printNode(*directory, romFSBase, newPath); + dumpRomFSNode(*directory, romFSBase, newPath); } } } @@ -638,7 +640,7 @@ RomFS::DumpingResult Emulator::dumpRomFS(const std::filesystem::path& path) { } std::unique_ptr node = parseRomFSTree((uintptr_t)&romFS[0], size); - printNode(*node, (const char*) &romFS[0], path); + dumpRomFSNode(*node, (const char*)&romFS[0], path); return DumpingResult::Success; } \ No newline at end of file diff --git a/src/panda_qt/main_window.cpp b/src/panda_qt/main_window.cpp index 809f56e0a..2c2cc64f9 100644 --- a/src/panda_qt/main_window.cpp +++ b/src/panda_qt/main_window.cpp @@ -209,11 +209,17 @@ void MainWindow::dumpRomFS() { switch (res) { case RomFS::DumpingResult::Success: break; // Yay! - case RomFS::DumpingResult::InvalidFormat: - QMessageBox::warning( - this, tr("Invalid format for RomFS dumping"), tr("The currently loaded app is not in a format that supports RomFS!") + case RomFS::DumpingResult::InvalidFormat: { + QMessageBox messageBox( + QMessageBox::Icon::Warning, tr("Invalid format for RomFS dumping"), + tr("The currently loaded app is not in a format that supports RomFS") ); + + QAbstractButton* button = messageBox.addButton(tr("OK"), QMessageBox::ButtonRole::YesRole); + button->setIcon(QIcon(":/docs/img/rsob_icon.png")); + messageBox.exec(); break; + } case RomFS::DumpingResult::NoRomFS: QMessageBox::warning(this, tr("No RomFS found"), tr("No RomFS partition was found in the loaded app"));