From cfc0fbc34248c6c40f4e1219c0b7c9a1cf8af5ac Mon Sep 17 00:00:00 2001 From: PJB3005 Date: Thu, 14 May 2026 20:42:20 +0200 Subject: [PATCH] Allow mods to be loaded from extracted disk files Also just some code cleanup --- files.cmake | 4 +- include/dusk/mod_loader.hpp | 12 +- src/dusk/modding/bundle_disk.cpp | 54 +++++++ src/dusk/modding/bundle_zip.cpp | 54 +++++++ src/dusk/{ => modding}/mod_loader.cpp | 223 ++++++++++++-------------- src/dusk/modding/mod_loader.hpp | 40 +++++ tools/mod_test/CMakeLists.txt | 8 - 7 files changed, 261 insertions(+), 134 deletions(-) create mode 100644 src/dusk/modding/bundle_disk.cpp create mode 100644 src/dusk/modding/bundle_zip.cpp rename src/dusk/{ => modding}/mod_loader.cpp (79%) create mode 100644 src/dusk/modding/mod_loader.hpp diff --git a/files.cmake b/files.cmake index 17105b6f8d..d4460a5f6b 100644 --- a/files.cmake +++ b/files.cmake @@ -1523,7 +1523,9 @@ set(DUSK_FILES src/dusk/OSThread.cpp src/dusk/OSMutex.cpp src/dusk/hook_system.cpp - src/dusk/mod_loader.cpp + src/dusk/modding/mod_loader.cpp + src/dusk/modding/bundle_disk.cpp + src/dusk/modding/bundle_zip.cpp src/dusk/gx_helper.cpp src/dusk/discord.cpp src/dusk/discord.hpp diff --git a/include/dusk/mod_loader.hpp b/include/dusk/mod_loader.hpp index f9c488ce01..b04172048a 100644 --- a/include/dusk/mod_loader.hpp +++ b/include/dusk/mod_loader.hpp @@ -5,7 +5,10 @@ #include #include "dusk/mod_api.h" -#include "miniz.h" + +namespace dusk::modding { +class ModBundle; +} namespace dusk { @@ -40,10 +43,7 @@ struct LoadedMod { FnCleanup fn_cleanup = nullptr; DuskModAPI api{}; - - std::vector zip_data; - mz_zip_archive res_zip{}; - bool res_zip_open = false; + std::unique_ptr bundle; std::vector tab_content; std::vector tab_updates; @@ -65,7 +65,7 @@ private: std::filesystem::path m_modsDir; bool m_initialized = false; - void tryLoadDusk(const std::filesystem::path& modPath); + void tryLoadDusk(const std::filesystem::path& modPath, bool fromDir); void buildAPI(LoadedMod& mod); }; diff --git a/src/dusk/modding/bundle_disk.cpp b/src/dusk/modding/bundle_disk.cpp new file mode 100644 index 0000000000..4bbba938ba --- /dev/null +++ b/src/dusk/modding/bundle_disk.cpp @@ -0,0 +1,54 @@ +#include + +#include "dusk/io.hpp" +#include "mod_loader.hpp" + +namespace fs = std::filesystem; + +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); +} + +std::vector ModBundleDisk::getFileNames() { + std::vector files; + + std::error_code ec; + for (fs::recursive_directory_iterator it(root_path, + fs::directory_options::skip_permission_denied | + fs::directory_options::follow_directory_symlink, + ec); + it != fs::recursive_directory_iterator(); it.increment(ec)) + { + if (ec) { + break; + } + + if (!it->is_regular_file()) { + continue; + } + + const auto& path = it->path(); + const auto relPath = fs::relative(path, root_path); + auto string = io::fs_path_to_string(relPath); + if constexpr (fs::path::preferred_separator != '/') { + // Convert \ to / on Windows + for (auto& chr : string) { + if (chr == fs::path::preferred_separator) { + chr = '/'; + } + } + } + + files.emplace_back(std::move(string)); + } + + return files; +} + +} // 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 new file mode 100644 index 0000000000..b39b7a16f2 --- /dev/null +++ b/src/dusk/modding/bundle_zip.cpp @@ -0,0 +1,54 @@ +#include "fmt/format.h" +#include "mod_loader.hpp" + +#include + +namespace dusk::modding { + +ModBundleZip::ModBundleZip(std::vector&& data) : zip_data(std::move(data)) { + if (!mz_zip_reader_init_mem(&res_zip, zip_data.data(), zip_data.size(), 0)) { + const auto error = mz_zip_get_last_error(&res_zip); + throw std::runtime_error( + fmt::format("Opening zip failed: {}", mz_zip_get_error_string(error))); + } +} + +ModBundleZip::~ModBundleZip() { + mz_zip_reader_end(&res_zip); +} + +std::vector ModBundleZip::readFile(const std::string& fileName) { + size_t size; + const auto ptr = mz_zip_reader_extract_file_to_heap(&res_zip, fileName.c_str(), &size, 0); + + if (!ptr) { + throw std::runtime_error(fmt::format("File does not exist: {}", fileName)); + } + + std::span data(static_cast(ptr), size); + std::vector vec(data.begin(), data.end()); + + mz_free(ptr); + + return vec; +} + +std::vector ModBundleZip::getFileNames() { + std::vector results; + + for (mz_uint i = 0, n = mz_zip_reader_get_num_files(&res_zip); i < n; ++i) { + mz_zip_archive_file_stat stat{}; + if (!mz_zip_reader_file_stat(&res_zip, i, &stat)) { + continue; + } + if (mz_zip_reader_is_file_a_directory(&res_zip, i)) { + continue; + } + + results.emplace_back(stat.m_filename); + } + + return results; +} + +} // namespace dusk::modding diff --git a/src/dusk/mod_loader.cpp b/src/dusk/modding/mod_loader.cpp similarity index 79% rename from src/dusk/mod_loader.cpp rename to src/dusk/modding/mod_loader.cpp index 34384d6366..c7720fc9fc 100644 --- a/src/dusk/mod_loader.cpp +++ b/src/dusk/modding/mod_loader.cpp @@ -1,6 +1,7 @@ #include "dusk/mod_loader.hpp" #include "dusk/hook_system.hpp" #include "dusk/logging.h" +#include "mod_loader.hpp" #include @@ -9,9 +10,11 @@ #include #include #include +#include #include #include +#include "dusk/io.hpp" #include "miniz.h" #include "nlohmann/json.hpp" @@ -20,6 +23,8 @@ #define NOMINMAX #include +static aurora::Module Log("dusk::modLoader"); + static void* pl_dlopen(const std::filesystem::path& p) { return LoadLibraryW(p.wstring().c_str()); } @@ -67,31 +72,26 @@ static constexpr const char* k_libExt = ".so"; #endif #endif -#if defined(_M_ARM64) || defined(__aarch64__) -static constexpr std::string_view k_archSuffix = "_arm64"; -#elif defined(_M_X64) || defined(__x86_64__) -static constexpr std::string_view k_archSuffix = "_x64"; -#elif defined(_M_IX86) || defined(__i386__) -static constexpr std::string_view k_archSuffix = "_x86"; -#else -static constexpr std::string_view k_archSuffix = ""; -#endif +using namespace dusk::modding; +using namespace std::string_view_literals; -static FILE* fs_fopen(const std::filesystem::path& p, const char* mode) { -#if defined(_WIN32) - std::wstring wmode(mode, mode + strlen(mode)); - return _wfopen(p.wstring().c_str(), wmode.c_str()); +#if defined(_M_ARM64) || defined(__aarch64__) +static constexpr std::string_view k_archSuffix = "_arm64"sv; +#elif defined(_M_X64) || defined(__x86_64__) +static constexpr std::string_view k_archSuffix = "_x64"sv; +#elif defined(_M_IX86) || defined(__i386__) +static constexpr std::string_view k_archSuffix = "_x86"sv; #else - return fopen(p.c_str(), mode); +static constexpr std::string_view k_archSuffix = ""sv; #endif -} static thread_local dusk::LoadedMod* g_currentMod = nullptr; static std::unordered_map g_services; namespace dusk { thread_local void* g_dusk_hook_current_mod = nullptr; -} + +} // namespace dusk struct ModGuard { explicit ModGuard(dusk::LoadedMod* m) { @@ -149,26 +149,27 @@ static void* cb_load_resource(const char* relative_path, size_t* out_size) { DuskLog.error("load_resource: called outside mod context or with null path"); return nullptr; } - if (!g_currentMod->res_zip_open) { - DuskLog.error("[{}] load_resource: zip not available", g_currentMod->name); + + std::string entry = std::string("res/") + relative_path; + std::vector data; + try { + data = g_currentMod->bundle->readFile(entry); + } catch (const std::runtime_error& e) { + DuskLog.error("[{}] load_resource: '{}' failed: {}", g_currentMod->name, entry, e.what()); return nullptr; } - std::string entry = std::string("res/") + relative_path; - size_t sz = 0; - void* data = mz_zip_reader_extract_file_to_heap(&g_currentMod->res_zip, entry.c_str(), &sz, 0); - if (!data) { - DuskLog.error("[{}] load_resource: '{}' not found in zip", g_currentMod->name, entry); - return nullptr; - } + const auto retPtr = std::malloc(data.size()); + std::memcpy(retPtr, data.data(), data.size()); + if (out_size) { - *out_size = sz; + *out_size = data.size(); } - return data; + return retPtr; } static void cb_free_resource(void* data) { - mz_free(data); + std::free(data); } namespace { @@ -378,81 +379,74 @@ void ModLoader::buildAPI(LoadedMod& mod) { mod.api.service_get = cb_service_get; } -void ModLoader::tryLoadDusk(const std::filesystem::path& modPath) { +static std::unique_ptr loadBundle(const std::filesystem::path& modPath, bool fromDir) { + if (fromDir) { + return std::make_unique(modPath); + } else { + std::vector data = io::FileStream::ReadAllBytes(modPath); + return std::make_unique(std::move(data)); + } +} + +struct DllLocateResult { + std::string primary; + std::string fallback; +}; + +static std::string_view getFileNameWithoutExtension(const std::string_view fileName) { + return fileName.substr(0, fileName.find_last_of('.')); +} + +static DllLocateResult LocateDllInBundle(ModBundle& bundle) { + std::string dllEntry, dllFallback; + for (const auto name : bundle.getFileNames()) { + if (!name.ends_with(".dll"sv)) { + continue; + } + + if (!k_archSuffix.empty() && getFileNameWithoutExtension(name).ends_with(k_archSuffix)) { + dllEntry = name; + } else if (dllFallback.empty()) { + dllFallback = name; + } + } + + return DllLocateResult{dllEntry, dllFallback}; +} + +void ModLoader::tryLoadDusk(const std::filesystem::path& modPath, bool fromDir) { namespace fs = std::filesystem; - std::vector zipBytes; - { - FILE* f = fs_fopen(modPath, "rb"); - if (!f) { - DuskLog.error("ModLoader: failed to open {}", modPath.filename().string()); - return; - } - fseek(f, 0, SEEK_END); - long fsize = ftell(f); - fseek(f, 0, SEEK_SET); - zipBytes.resize(static_cast(fsize)); - fread(zipBytes.data(), 1, zipBytes.size(), f); - fclose(f); - } - - std::string metaName, metaVersion, metaAuthor, metaDescription; - { - mz_zip_archive zip{}; - if (mz_zip_reader_init_mem(&zip, zipBytes.data(), zipBytes.size(), 0)) { - size_t jsonSize = 0; - void* jsonData = mz_zip_reader_extract_file_to_heap(&zip, "mod.json", &jsonSize, 0); - mz_zip_reader_end(&zip); - if (jsonData) { - try { - std::string jsonStr(static_cast(jsonData), jsonSize); - mz_free(jsonData); - jsonData = nullptr; - auto j = nlohmann::json::parse(jsonStr); - metaName = j.value("name", ""); - metaVersion = j.value("version", ""); - metaAuthor = j.value("author", ""); - metaDescription = j.value("description", ""); - } catch (const std::exception& e) { - mz_free(jsonData); - DuskLog.warn( - "ModLoader: bad mod.json in {}: {}", modPath.filename().string(), e.what()); - } - } - } - } - - mz_zip_archive zip{}; - if (!mz_zip_reader_init_mem(&zip, zipBytes.data(), zipBytes.size(), 0)) { - DuskLog.error("ModLoader: failed to open {}", modPath.filename().string()); + std::unique_ptr bundle; + try { + bundle = loadBundle(modPath, fromDir); + } catch (const std::runtime_error& e) { + Log.error("Failed to open {} bundle: {}", io::fs_path_to_string(modPath.filename()), e.what()); return; } - std::string dllEntry, dllFallback; - for (mz_uint i = 0, n = mz_zip_reader_get_num_files(&zip); i < n; ++i) { - mz_zip_archive_file_stat stat{}; - if (!mz_zip_reader_file_stat(&zip, i, &stat)) { - continue; - } - if (mz_zip_reader_is_file_a_directory(&zip, i)) { - continue; - } - fs::path fname(stat.m_filename); - if (fname.extension() == k_libExt) { - if (!k_archSuffix.empty() && fname.stem().string().ends_with(k_archSuffix)) { - dllEntry = stat.m_filename; - break; - } else if (dllFallback.empty()) { - dllFallback = stat.m_filename; - } - } + std::string metaName, metaVersion, metaAuthor, metaDescription; + try + { + const auto metaJson = bundle->readFile("mod.json"); + auto j = nlohmann::json::parse(metaJson); + metaName = j.value("name", ""); + metaVersion = j.value("version", ""); + metaAuthor = j.value("author", ""); + metaDescription = j.value("description", ""); } + catch (const std::runtime_error& e) { + Log.error( + "ModLoader: bad mod.json in {}: {}", io::fs_path_to_string(modPath.filename()), e.what()); + return; + } + + auto [dllEntry, dllFallback] = LocateDllInBundle(*bundle); if (dllEntry.empty()) { dllEntry = dllFallback; } if (dllEntry.empty()) { - mz_zip_reader_end(&zip); DuskLog.warn( "ModLoader: no *{} found in {} — skipping", k_libExt, modPath.filename().string()); return; @@ -464,27 +458,26 @@ void ModLoader::tryLoadDusk(const std::filesystem::path& modPath) { const fs::path dllCachePath = cacheDir / fs::path(dllEntry).filename(); - size_t dllSize = 0; - void* dllData = mz_zip_reader_extract_file_to_heap(&zip, dllEntry.c_str(), &dllSize, 0); - mz_zip_reader_end(&zip); - - if (!dllData) { + std::vector dllData; + try { + dllData = bundle->readFile(dllEntry); + } catch (const std::runtime_error& e) { DuskLog.error( - "ModLoader: failed to extract {} from {}", dllEntry, modPath.filename().string()); + "ModLoader: failed to extract {} from {}", dllEntry, io::fs_path_to_string(modPath.filename())); return; } + { - FILE* out = fs_fopen(dllCachePath, "wb"); - if (out) { - fwrite(dllData, 1, dllSize, out); - fclose(out); - } else { - mz_free(dllData); + std::ofstream out(dllCachePath, std::ios::binary | std::ios::out); + if (!out) { DuskLog.error("ModLoader: failed to write {}", dllCachePath.string()); return; } + + out.write( + reinterpret_cast(dllData.data()), + static_cast(dllData.size())); } - mz_free(dllData); void* handle = pl_dlopen(dllCachePath); if (!handle) { @@ -520,14 +513,9 @@ void ModLoader::tryLoadDusk(const std::filesystem::path& modPath) { mod.author = metaAuthor.empty() ? "unknown" : metaAuthor; mod.description = metaDescription; - mod.zip_data = std::move(zipBytes); + mod.bundle = std::move(bundle); m_mods.push_back(std::move(mod)); - { - LoadedMod& stored = m_mods.back(); - if (mz_zip_reader_init_mem(&stored.res_zip, stored.zip_data.data(), stored.zip_data.size(), 0)) { - stored.res_zip_open = true; - } - } + DuskLog.info("ModLoader: found '{}' v{} by {} ({})", m_mods.back().name, m_mods.back().version, m_mods.back().author, modPath.filename().string()); } @@ -548,7 +536,9 @@ void ModLoader::init() { std::error_code ec; std::vector entries; for (auto& e : fs::directory_iterator(m_modsDir, ec)) { - if (e.is_regular_file() && e.path().extension() == ".dusk") { + if (e.is_directory() && std::filesystem::exists(e.path() / "mod.json")) { + entries.push_back(e); + } else if (e.is_regular_file() && e.path().extension() == ".dusk") { entries.push_back(e); } } @@ -559,7 +549,7 @@ void ModLoader::init() { m_mods.reserve(entries.size()); for (auto& entry : entries) { - tryLoadDusk(entry.path()); + tryLoadDusk(entry.path(), entry.is_directory()); } if (m_mods.empty()) { @@ -623,11 +613,6 @@ void ModLoader::shutdown() { } catch (...) { } } - if (mod.res_zip_open) { - mz_zip_reader_end(&mod.res_zip); - mod.res_zip_open = false; - } - mod.zip_data.clear(); if (mod.handle) { pl_dlclose(mod.handle); mod.handle = nullptr; diff --git a/src/dusk/modding/mod_loader.hpp b/src/dusk/modding/mod_loader.hpp new file mode 100644 index 0000000000..9fbc34c6a8 --- /dev/null +++ b/src/dusk/modding/mod_loader.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include +#include "miniz.h" + +namespace dusk::modding { + +class ModBundle { +public: + virtual ~ModBundle() = default; + + virtual std::vector readFile(const std::string& fileName) = 0; + virtual std::vector getFileNames() = 0; +}; + +class ModBundleZip final : public ModBundle { +public: + explicit ModBundleZip(std::vector&& data); + ~ModBundleZip() override; + std::vector readFile(const std::string& fileName) override; + std::vector getFileNames() override; + +private: + std::vector zip_data; + mz_zip_archive res_zip{}; + bool res_zip_open = false; +}; + +class ModBundleDisk final : public ModBundle { +public: + explicit ModBundleDisk(std::filesystem::path root); + ~ModBundleDisk() override = default; + std::vector readFile(const std::string& fileName) override; + std::vector getFileNames() override; + +private: + std::filesystem::path root_path; +}; + +} // namespace dusk::modding diff --git a/tools/mod_test/CMakeLists.txt b/tools/mod_test/CMakeLists.txt index 4c1fcf6332..87ea7c60f6 100644 --- a/tools/mod_test/CMakeLists.txt +++ b/tools/mod_test/CMakeLists.txt @@ -1,11 +1,3 @@ -cmake_minimum_required(VERSION 3.25) -project(mod_test CXX) - -set(DUSK_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../.." CACHE PATH "Path to dusk source root") -add_subdirectory("${DUSK_DIR}" dusk EXCLUDE_FROM_ALL) - -set(DUSK_MODS_OUTPUT_DIR "${CMAKE_SOURCE_DIR}/mods" CACHE PATH "Directory to write .dusk packages into") - add_dusk_mod(mod_test SOURCES src/mod.cpp MOD_JSON mod.json