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 c884b29d..462d6726 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 00000000..8b6b7d87 --- /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 2c13d634..10dd66a5 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 4a5fab75..1901e425 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 20213761..114b1c1e 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 7f0ff37f..5e2ad1d8 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 00000000..e8314155 --- /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 9f9f1014..e4a45487 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 d3d05839..2546aa01 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 5d87fccd..e0de4a29 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 00000000..e62b4636 --- /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 b4887454..977a7f80 100644 --- a/src/panda_qt/main_window.cpp +++ b/src/panda_qt/main_window.cpp @@ -16,13 +16,19 @@ 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 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 emulationMenu = menuBar->addMenu(tr("Emulation")); - auto helpMenu = menuBar->addMenu(tr("Help")); - auto aboutMenu = menuBar->addMenu(tr("About")); + auto dumpRomFSAction = toolsMenu->addAction(tr("Dump RomFS")); + connect(dumpRomFSAction, &QAction::triggered, this, &MainWindow::dumpRomFS); // Set up theme selection setTheme(Theme::Dark); @@ -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 e0de4a29..7555580b 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 977a7f80..8c57e335 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 e4a45487..3bbabf35 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 8c57e335..809f56e0 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); - needToLoadROM.store(false, std::memory_order::seq_cst); + if (needToLoadROM) { + needToLoadROM = false; + + 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 10dd66a5..80c70c1a 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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 SSQ!`?7(8A5T-G@yGywpfyBN^` literal 0 HcmV?d00001 diff --git a/src/emulator.cpp b/src/emulator.cpp index 7555580b..43ecc149 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 809f56e0..2c2cc64f 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"));