From 42d412a06e0e1a76f910d17e11ecea8bdebabf3d Mon Sep 17 00:00:00 2001 From: PJB3005 Date: Fri, 15 May 2026 21:04:42 +0200 Subject: [PATCH] Mod file overlay system Mods can now replace DVD files with contents of their "overlay" folder (I'll update the docs later when I do a full pass and make non-code mods more of a first-class citizen) Fixes https://github.com/TwilitRealm/dusklight/issues/1306 --- files.cmake | 1 + include/dusk/mod_loader.hpp | 1 + src/dusk/modding/bundle_disk.cpp | 13 ++- src/dusk/modding/bundle_zip.cpp | 11 +++ src/dusk/modding/mod_loader.cpp | 3 + src/dusk/modding/mod_loader.hpp | 4 + src/dusk/modding/mod_loader_overlay.cpp | 108 ++++++++++++++++++++++++ 7 files changed, 138 insertions(+), 3 deletions(-) create mode 100644 src/dusk/modding/mod_loader_overlay.cpp diff --git a/files.cmake b/files.cmake index d4460a5f6b..378527a6dd 100644 --- a/files.cmake +++ b/files.cmake @@ -1524,6 +1524,7 @@ set(DUSK_FILES src/dusk/OSMutex.cpp src/dusk/hook_system.cpp src/dusk/modding/mod_loader.cpp + src/dusk/modding/mod_loader_overlay.cpp src/dusk/modding/bundle_disk.cpp src/dusk/modding/bundle_zip.cpp src/dusk/gx_helper.cpp diff --git a/include/dusk/mod_loader.hpp b/include/dusk/mod_loader.hpp index b04172048a..04f896c3ab 100644 --- a/include/dusk/mod_loader.hpp +++ b/include/dusk/mod_loader.hpp @@ -67,6 +67,7 @@ private: void tryLoadDusk(const std::filesystem::path& modPath, bool fromDir); void buildAPI(LoadedMod& mod); + void initOverlayFiles(); }; } // namespace dusk diff --git a/src/dusk/modding/bundle_disk.cpp b/src/dusk/modding/bundle_disk.cpp index 4bbba938ba..c6260a771d 100644 --- a/src/dusk/modding/bundle_disk.cpp +++ b/src/dusk/modding/bundle_disk.cpp @@ -9,10 +9,8 @@ namespace dusk::modding { ModBundleDisk::ModBundleDisk(fs::path root) : root_path(std::move(root)) {} std::vector ModBundleDisk::readFile(const std::string& fileName) { - const fs::path filePath = reinterpret_cast(fileName.c_str()); - const auto finalPath = root_path / fileName; - return io::FileStream::ReadAllBytes(finalPath); + return io::FileStream::ReadAllBytes(toRealPath(fileName)); } std::vector ModBundleDisk::getFileNames() { @@ -51,4 +49,13 @@ std::vector ModBundleDisk::getFileNames() { return files; } +size_t ModBundleDisk::getFileSize(const std::string& fileName) { + return std::filesystem::file_size(toRealPath(fileName)); +} + +std::filesystem::path ModBundleDisk::toRealPath(const std::string& fileName) const { + const fs::path filePath = reinterpret_cast(fileName.c_str()); + return root_path / fileName; +} + } // namespace dusk::modding \ No newline at end of file diff --git a/src/dusk/modding/bundle_zip.cpp b/src/dusk/modding/bundle_zip.cpp index b39b7a16f2..d85f394d32 100644 --- a/src/dusk/modding/bundle_zip.cpp +++ b/src/dusk/modding/bundle_zip.cpp @@ -51,4 +51,15 @@ std::vector ModBundleZip::getFileNames() { return results; } +size_t ModBundleZip::getFileSize(const std::string& fileName) { + const auto idx = mz_zip_reader_locate_file(&res_zip, fileName.c_str(), nullptr, 0); + if (idx < 0) { + throw std::runtime_error(fmt::format("Unable to locate file in zip: {}", fileName)); + } + + mz_zip_archive_file_stat stat{}; + mz_zip_reader_file_stat(&res_zip, idx, &stat); + return stat.m_uncomp_size; +} + } // namespace dusk::modding diff --git a/src/dusk/modding/mod_loader.cpp b/src/dusk/modding/mod_loader.cpp index c7720fc9fc..a726d8850b 100644 --- a/src/dusk/modding/mod_loader.cpp +++ b/src/dusk/modding/mod_loader.cpp @@ -14,6 +14,7 @@ #include #include +#include "aurora/dvd.h" #include "dusk/io.hpp" #include "miniz.h" #include "nlohmann/json.hpp" @@ -557,6 +558,8 @@ void ModLoader::init() { return; } + initOverlayFiles(); + DuskLog.info("ModLoader: initializing {} mod(s)...", m_mods.size()); for (auto& mod : m_mods) { buildAPI(mod); diff --git a/src/dusk/modding/mod_loader.hpp b/src/dusk/modding/mod_loader.hpp index 9fbc34c6a8..b7ca7997a6 100644 --- a/src/dusk/modding/mod_loader.hpp +++ b/src/dusk/modding/mod_loader.hpp @@ -11,6 +11,7 @@ public: virtual std::vector readFile(const std::string& fileName) = 0; virtual std::vector getFileNames() = 0; + virtual size_t getFileSize(const std::string& fileName) = 0; }; class ModBundleZip final : public ModBundle { @@ -19,6 +20,7 @@ public: ~ModBundleZip() override; std::vector readFile(const std::string& fileName) override; std::vector getFileNames() override; + size_t getFileSize(const std::string& fileName) override; private: std::vector zip_data; @@ -32,8 +34,10 @@ public: ~ModBundleDisk() override = default; std::vector readFile(const std::string& fileName) override; std::vector getFileNames() override; + size_t getFileSize(const std::string& fileName) override; private: + [[nodiscard]] std::filesystem::path toRealPath(const std::string& fileName) const; std::filesystem::path root_path; }; diff --git a/src/dusk/modding/mod_loader_overlay.cpp b/src/dusk/modding/mod_loader_overlay.cpp new file mode 100644 index 0000000000..0607473f83 --- /dev/null +++ b/src/dusk/modding/mod_loader_overlay.cpp @@ -0,0 +1,108 @@ +#include "aurora/dvd.h" +#include "aurora/lib/logging.hpp" +#include "dusk/mod_loader.hpp" +#include "mod_loader.hpp" + +#include + +using namespace std::string_literals; + +namespace { + +aurora::Module Log("dusk::modLoader::overlay"); + +struct OverlayFileData { + std::string bundlePath; + dusk::LoadedMod* mod; // TODO: is using a raw pointer a bad idea here? +}; + +std::vector s_overlayFiles; + +void findOverlayFiles(std::vector& files, dusk::LoadedMod& mod) { + for (const auto& file : mod.bundle->getFileNames()) { + if (!file.starts_with("overlay/")) { + continue; + } + + auto overlayPath = file.substr("overlay/"s.size()); + assert(!overlayPath.starts_with('/')); + overlayPath.insert(0, "/"); + + const auto size = mod.bundle->getFileSize(file); + + const auto index = s_overlayFiles.size(); + s_overlayFiles.emplace_back(file, &mod); + files.emplace_back(strdup(overlayPath.c_str()), reinterpret_cast(index), size); + } +} + +struct OpenOverlayFile { + std::vector data; + size_t pos; +}; + +void* cbOpen(void* userdata) { + const auto index = reinterpret_cast(userdata); + const auto& fileData = s_overlayFiles[index]; + auto fileContents = fileData.mod->bundle->readFile(fileData.bundlePath); + + return new OpenOverlayFile(std::move(fileContents), 0); +} + +void cbClose(void* handle) { + const auto openFile = static_cast(handle); + delete openFile; +} + +int64_t cbRead(void* handle, uint8_t *buf, const size_t len) { + auto& openFile = *static_cast(handle); + + const auto remainingSpace = openFile.data.size() - openFile.pos; + const auto toRead = std::min(remainingSpace, len); + std::memcpy(buf, openFile.data.data() + openFile.pos, toRead); + openFile.pos += toRead; + return static_cast(toRead); +} + +int64_t cbSeek(void* handle, int64_t offset, int32_t whence) { + if (whence != 0) { + Log.fatal("Invalid seek mode from aurora: {}", whence); + } + + auto& openFile = *static_cast(handle); + const auto posSigned = std::clamp(offset, static_cast(0), static_cast(openFile.data.size())); + openFile.pos = static_cast(posSigned); + return posSigned; +} + +constexpr AuroraOverlayCallbacks s_overlayCallbacks = { + .open = cbOpen, + .close = cbClose, + .read = cbRead, + .seek = cbSeek, +}; + +} + +namespace dusk { + +void ModLoader::initOverlayFiles() { + Log.debug("Initializing overlay files..."); + + aurora_dvd_overlay_callbacks(&s_overlayCallbacks); + + std::vector files; + + for (auto& mod : m_mods) { + findOverlayFiles(files, mod); + } + + Log.debug("Found {} overlay files.", files.size()); + aurora_dvd_overlay_files(files.data(), files.size()); + + for (const auto& file : files) { + std::free(const_cast(file.fileName)); + } +} + +} // namespace dusk