mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-08-04 00:38:59 -04:00
mod loader
This commit is contained in:
@@ -142,6 +142,12 @@ DynamicModuleControl::DynamicModuleControl(char const* name) {
|
||||
}
|
||||
#endif
|
||||
|
||||
#if TARGET_PC
|
||||
// dump() is declared but its definition is inside #if !TARGET_PC above; stub it out.
|
||||
void DynamicModuleControlBase::dump() {}
|
||||
void DynamicModuleControlBase::dump(char*) {}
|
||||
#endif
|
||||
|
||||
u32 DynamicModuleControl::sAllocBytes;
|
||||
|
||||
JKRArchive* DynamicModuleControl::sArchive;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
#include "dusk/gx_helper.h"
|
||||
|
||||
#ifdef TARGET_PC
|
||||
GXTexObjRAII::~GXTexObjRAII() { GXDestroyTexObj(this); }
|
||||
void GXTexObjRAII::reset() { GXDestroyTexObj(this); }
|
||||
#endif
|
||||
|
||||
GXScopedDebugGroup::GXScopedDebugGroup(const char* text) {
|
||||
GXPushDebugGroup(text);
|
||||
}
|
||||
GXScopedDebugGroup::~GXScopedDebugGroup() {
|
||||
GXPopDebugGroup();
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
#include "dusk/hook_system.hpp"
|
||||
#include "dusk/logging.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <funchook.h>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace dusk {
|
||||
|
||||
extern void* g_dusk_hook_current_mod;
|
||||
|
||||
struct PreHookFn {
|
||||
void* mod;
|
||||
int32_t (*fn)(void* args);
|
||||
};
|
||||
struct VoidHookFn {
|
||||
void* mod;
|
||||
void (*fn)(void* args);
|
||||
};
|
||||
|
||||
struct HookSlot {
|
||||
std::vector<PreHookFn> pre;
|
||||
VoidHookFn replace = {};
|
||||
std::vector<VoidHookFn> post;
|
||||
};
|
||||
|
||||
static std::unordered_map<uintptr_t, HookSlot>& registry() {
|
||||
static std::unordered_map<uintptr_t, HookSlot> s;
|
||||
return s;
|
||||
}
|
||||
static std::unordered_map<uintptr_t, void*>& installed() {
|
||||
static std::unordered_map<uintptr_t, void*> s;
|
||||
return s;
|
||||
}
|
||||
|
||||
// Follow E9/FF25 chains to skip MSVC incremental-link and import stubs
|
||||
static void* resolveImportThunk(void* addr) {
|
||||
#if _WIN32
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
const auto* p = static_cast<const uint8_t*>(addr);
|
||||
if (p[0] == 0xFF && p[1] == 0x25) {
|
||||
int32_t offset;
|
||||
std::memcpy(&offset, p + 2, 4);
|
||||
addr = const_cast<void*>(*reinterpret_cast<const void* const*>(p + 6 + offset));
|
||||
break;
|
||||
} else if (p[0] == 0xE9) {
|
||||
int32_t offset;
|
||||
std::memcpy(&offset, p + 1, 4);
|
||||
addr = const_cast<uint8_t*>(p) + 5 + offset;
|
||||
} else
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
return addr;
|
||||
}
|
||||
|
||||
struct ModGuard {
|
||||
void* prev;
|
||||
explicit ModGuard(void* mod) : prev(g_dusk_hook_current_mod) { g_dusk_hook_current_mod = mod; }
|
||||
~ModGuard() { g_dusk_hook_current_mod = prev; }
|
||||
};
|
||||
|
||||
void hookInstallByAddr(void* fn_addr, void* tramp_fn, void** orig_store) {
|
||||
fn_addr = resolveImportThunk(fn_addr);
|
||||
auto key = reinterpret_cast<uintptr_t>(fn_addr);
|
||||
auto it = installed().find(key);
|
||||
if (it != installed().end()) {
|
||||
*orig_store = it->second;
|
||||
return;
|
||||
}
|
||||
|
||||
funchook_t* fh = funchook_create();
|
||||
void* fn = fn_addr;
|
||||
int prep = funchook_prepare(fh, &fn, tramp_fn);
|
||||
int inst = (prep == 0) ? funchook_install(fh, 0) : -1;
|
||||
if (prep != 0 || inst != 0) {
|
||||
DuskLog.warn("HookSystem: funchook failed for {:p} (prepare={} install={})", fn_addr, prep,
|
||||
inst);
|
||||
funchook_destroy(fh);
|
||||
return;
|
||||
}
|
||||
|
||||
funchook_destroy(fh);
|
||||
installed()[key] = fn;
|
||||
*orig_store = fn;
|
||||
}
|
||||
|
||||
bool hookDispatchPre(void* fn_addr, void* args) {
|
||||
auto it = registry().find(reinterpret_cast<uintptr_t>(fn_addr));
|
||||
if (it == registry().end())
|
||||
return false;
|
||||
auto& slot = it->second;
|
||||
for (auto& h : slot.pre) {
|
||||
ModGuard g(h.mod);
|
||||
if (h.fn(args) != 0)
|
||||
return true;
|
||||
}
|
||||
if (slot.replace.fn) {
|
||||
ModGuard g(slot.replace.mod);
|
||||
slot.replace.fn(args);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void hookDispatchPost(void* fn_addr, void* args) {
|
||||
auto it = registry().find(reinterpret_cast<uintptr_t>(fn_addr));
|
||||
if (it == registry().end())
|
||||
return;
|
||||
for (auto& h : it->second.post) {
|
||||
if (h.fn) {
|
||||
ModGuard g(h.mod);
|
||||
h.fn(args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void hookRegisterPre(void* fn_addr, void* mod, int32_t (*fn)(void* args)) {
|
||||
registry()[reinterpret_cast<uintptr_t>(fn_addr)].pre.push_back({mod, fn});
|
||||
}
|
||||
|
||||
void hookRegisterPost(void* fn_addr, void* mod, void (*fn)(void* args)) {
|
||||
registry()[reinterpret_cast<uintptr_t>(fn_addr)].post.push_back({mod, fn});
|
||||
}
|
||||
|
||||
void hookSetReplace(void* fn_addr, void* mod, void (*fn)(void* args)) {
|
||||
auto& slot = registry()[reinterpret_cast<uintptr_t>(fn_addr)];
|
||||
if (slot.replace.fn)
|
||||
DuskLog.warn("HookSystem: replace hook for {} already set — overwriting", fn_addr);
|
||||
slot.replace = {mod, fn};
|
||||
}
|
||||
|
||||
void hookClearMod(void* mod) {
|
||||
for (auto& [addr, slot] : registry()) {
|
||||
auto erase = [&](auto& v) {
|
||||
v.erase(
|
||||
std::remove_if(v.begin(), v.end(), [mod](const auto& h) { return h.mod == mod; }),
|
||||
v.end());
|
||||
};
|
||||
erase(slot.pre);
|
||||
erase(slot.post);
|
||||
if (slot.replace.mod == mod)
|
||||
slot.replace = {};
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace dusk
|
||||
@@ -322,6 +322,7 @@ namespace dusk {
|
||||
if (showMenu && ImGui::BeginMainMenuBar()) {
|
||||
m_menuGame.draw();
|
||||
m_menuEnhancements.draw();
|
||||
m_menuMods.draw();
|
||||
m_menuTools.draw();
|
||||
|
||||
const auto fpsLabel =
|
||||
@@ -365,6 +366,7 @@ namespace dusk {
|
||||
m_menuTools.ShowPlayerInfo();
|
||||
m_menuTools.ShowAudioDebug();
|
||||
m_menuTools.ShowSaveEditor();
|
||||
m_menuMods.showModsWindow();
|
||||
}
|
||||
m_menuTools.ShowStateShare();
|
||||
DuskDebugPad(); // temporary, remove later
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "ImGuiFirstRunPreset.hpp"
|
||||
#include "ImGuiMenuEnhancements.hpp"
|
||||
#include "ImGuiMenuGame.hpp"
|
||||
#include "ImGuiMenuMods.hpp"
|
||||
#include "ImGuiMenuTools.hpp"
|
||||
#include "ImGuiPreLaunchWindow.hpp"
|
||||
#include "imgui.h"
|
||||
@@ -53,6 +54,7 @@ private:
|
||||
ImGuiFirstRunPreset m_firstRunPreset;
|
||||
ImGuiMenuGame m_menuGame;
|
||||
ImGuiMenuEnhancements m_menuEnhancements;
|
||||
ImGuiMenuMods m_menuMods;
|
||||
ImGuiPreLaunchWindow m_preLaunchWindow;
|
||||
|
||||
// Keep always last
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
#include "ImGuiMenuMods.hpp"
|
||||
|
||||
#include "ImGuiConsole.hpp"
|
||||
#include "dusk/mod_loader.hpp"
|
||||
#include "imgui.h"
|
||||
|
||||
namespace dusk {
|
||||
|
||||
void ImGuiMenuMods::draw() {
|
||||
const auto& mods = ModLoader::instance().mods();
|
||||
if (mods.empty()) return;
|
||||
|
||||
if (ImGui::BeginMenu("Mods")) {
|
||||
if (ImGui::MenuItem("Mod Manager", nullptr, m_showWindow)) {
|
||||
m_showWindow = !m_showWindow;
|
||||
}
|
||||
|
||||
for (const auto& mod : mods) {
|
||||
if (mod.menu_items.empty()) continue;
|
||||
ImGui::Separator();
|
||||
if (ImGui::BeginMenu(mod.name.c_str())) {
|
||||
for (const auto& item : mod.menu_items) {
|
||||
ModLoader::callDrawCallback(mod, item);
|
||||
}
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
}
|
||||
|
||||
void ImGuiMenuMods::showModsWindow() {
|
||||
if (!m_showWindow) return;
|
||||
|
||||
ImGui::SetNextWindowSize(ImVec2(520, 420), ImGuiCond_FirstUseEver);
|
||||
if (!ImGui::Begin("Mod Manager", &m_showWindow)) {
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& mods = ModLoader::instance().mods();
|
||||
if (mods.empty()) {
|
||||
ImGuiTextCenter("No mods loaded.");
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
if (ImGui::BeginTabBar("##ModsOuter")) {
|
||||
for (const auto& mod : mods) {
|
||||
const std::string tabLabel = mod.name + (mod.active ? "" : " [disabled]");
|
||||
|
||||
if (ImGui::BeginTabItem(tabLabel.c_str())) {
|
||||
ImGui::Text("Version: %s", mod.version.c_str());
|
||||
ImGui::Text("Author: %s", mod.author.c_str());
|
||||
ImGui::Text("Status: %s", mod.active ? "Active" : "Disabled");
|
||||
ImGui::Text("Path: %s", mod.mod_path.c_str());
|
||||
|
||||
if (!mod.description.empty()) {
|
||||
ImGui::Separator();
|
||||
ImGui::TextWrapped("%s", mod.description.c_str());
|
||||
}
|
||||
|
||||
for (const auto& cb : mod.tab_content) {
|
||||
ImGui::Separator();
|
||||
ModLoader::callDrawCallback(mod, cb);
|
||||
}
|
||||
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
}
|
||||
ImGui::EndTabBar();
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
} // namespace dusk
|
||||
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
namespace dusk {
|
||||
|
||||
class ImGuiMenuMods {
|
||||
public:
|
||||
void draw();
|
||||
|
||||
void showModsWindow();
|
||||
|
||||
private:
|
||||
bool m_showWindow = false;
|
||||
};
|
||||
|
||||
} // namespace dusk
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Thin Windows launcher EXE. The game lives in dusk.dll, this just forwards
|
||||
* the Windows entry point to it. Keeping the game as a DLL lets mod .dll
|
||||
* files link against dusk.lib and resolve all game symbols at load time.
|
||||
*/
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <Windows.h>
|
||||
|
||||
// see src/dusk/main.cpp
|
||||
extern "C" int WINAPI dusk_WinMain(HINSTANCE hInst, HINSTANCE hPrev, PWSTR cmd, int show);
|
||||
|
||||
int WINAPI wWinMain(HINSTANCE hInst, HINSTANCE hPrev, PWSTR cmd, int show) {
|
||||
return dusk_WinMain(hInst, hPrev, cmd, show);
|
||||
}
|
||||
+3
-2
@@ -1,5 +1,5 @@
|
||||
#if _WIN32
|
||||
#define WINDOWS_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <Windows.h>
|
||||
#include <shellapi.h>
|
||||
#endif
|
||||
@@ -120,7 +120,8 @@ int main(int argc, char* argv[]) {
|
||||
}
|
||||
|
||||
#if _WIN32
|
||||
int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int) {
|
||||
// Entry point called by the launcher executable.
|
||||
extern "C" int WINAPI dusk_WinMain(HINSTANCE, HINSTANCE, PWSTR, int) {
|
||||
return RunWindowsGuiEntryPoint();
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,377 @@
|
||||
#include "dusk/mod_loader.hpp"
|
||||
#include "dusk/hook_system.hpp"
|
||||
#include "dusk/logging.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdarg>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "miniz.h"
|
||||
#include "nlohmann/json.hpp"
|
||||
|
||||
#if defined(_WIN32)
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#define NOMINMAX
|
||||
#include <Windows.h>
|
||||
|
||||
static void* pl_dlopen(const std::filesystem::path& p) {
|
||||
return LoadLibraryW(p.wstring().c_str());
|
||||
}
|
||||
static void* pl_dlsym(void* h, const char* name) {
|
||||
return reinterpret_cast<void*>(GetProcAddress(static_cast<HMODULE>(h), name));
|
||||
}
|
||||
static void pl_dlclose(void* h) {
|
||||
FreeLibrary(static_cast<HMODULE>(h));
|
||||
}
|
||||
static std::string pl_dlerror() {
|
||||
char buf[256]{};
|
||||
FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, nullptr,
|
||||
GetLastError(), 0, buf, sizeof(buf), nullptr);
|
||||
std::string s = buf;
|
||||
while (!s.empty() && (s.back() == '\r' || s.back() == '\n'))
|
||||
s.pop_back();
|
||||
return s;
|
||||
}
|
||||
static constexpr const char* k_libExt = ".dll";
|
||||
|
||||
#else
|
||||
#include <dlfcn.h>
|
||||
static void* pl_dlopen(const std::filesystem::path& p) {
|
||||
return dlopen(p.c_str(), RTLD_LAZY | RTLD_LOCAL);
|
||||
}
|
||||
static void* pl_dlsym(void* h, const char* name) {
|
||||
return dlsym(h, name);
|
||||
}
|
||||
static void pl_dlclose(void* h) {
|
||||
dlclose(h);
|
||||
}
|
||||
static std::string pl_dlerror() {
|
||||
const char* e = dlerror();
|
||||
return e ? e : "(unknown error)";
|
||||
}
|
||||
#if defined(__APPLE__)
|
||||
static constexpr const char* k_libExt = ".dylib";
|
||||
#else
|
||||
static constexpr const char* k_libExt = ".so";
|
||||
#endif
|
||||
#endif
|
||||
|
||||
static dusk::LoadedMod* g_currentMod = nullptr;
|
||||
|
||||
namespace dusk {
|
||||
void* g_dusk_hook_current_mod = nullptr;
|
||||
}
|
||||
|
||||
struct ModGuard {
|
||||
explicit ModGuard(dusk::LoadedMod* m) {
|
||||
g_currentMod = m;
|
||||
dusk::g_dusk_hook_current_mod = m;
|
||||
}
|
||||
~ModGuard() {
|
||||
g_currentMod = nullptr;
|
||||
dusk::g_dusk_hook_current_mod = nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
static const char* modName() {
|
||||
return g_currentMod ? g_currentMod->name.c_str() : "mod";
|
||||
}
|
||||
|
||||
static void cb_log_info(const char* fmt, ...) {
|
||||
va_list ap, ap2; va_start(ap, fmt); va_copy(ap2, ap);
|
||||
std::string s(vsnprintf(nullptr, 0, fmt, ap2), '\0'); va_end(ap2);
|
||||
vsnprintf(s.data(), s.size() + 1, fmt, ap); va_end(ap);
|
||||
DuskLog.info("[{}] {}", modName(), s);
|
||||
}
|
||||
|
||||
static void cb_log_warn(const char* fmt, ...) {
|
||||
va_list ap, ap2; va_start(ap, fmt); va_copy(ap2, ap);
|
||||
std::string s(vsnprintf(nullptr, 0, fmt, ap2), '\0'); va_end(ap2);
|
||||
vsnprintf(s.data(), s.size() + 1, fmt, ap); va_end(ap);
|
||||
DuskLog.warn("[{}] {}", modName(), s);
|
||||
}
|
||||
|
||||
static void cb_log_error(const char* fmt, ...) {
|
||||
va_list ap, ap2; va_start(ap, fmt); va_copy(ap2, ap);
|
||||
std::string s(vsnprintf(nullptr, 0, fmt, ap2), '\0'); va_end(ap2);
|
||||
vsnprintf(s.data(), s.size() + 1, fmt, ap); va_end(ap);
|
||||
DuskLog.error("[{}] {}", modName(), s);
|
||||
}
|
||||
|
||||
static void* cb_load_resource(const char* relative_path, size_t* out_size) {
|
||||
if (out_size)
|
||||
*out_size = 0;
|
||||
if (!g_currentMod || !relative_path)
|
||||
return nullptr;
|
||||
|
||||
mz_zip_archive zip{};
|
||||
if (!mz_zip_reader_init_file(&zip, g_currentMod->mod_path.c_str(), 0)) {
|
||||
DuskLog.warn("[{}] load_resource: could not open {}", g_currentMod->name,
|
||||
g_currentMod->mod_path);
|
||||
return nullptr;
|
||||
}
|
||||
std::string entry = std::string("res/") + relative_path;
|
||||
size_t sz = 0;
|
||||
void* data = mz_zip_reader_extract_file_to_heap(&zip, entry.c_str(), &sz, 0);
|
||||
mz_zip_reader_end(&zip);
|
||||
if (!data) {
|
||||
DuskLog.warn("[{}] load_resource: '{}' not found in zip", g_currentMod->name, entry);
|
||||
return nullptr;
|
||||
}
|
||||
if (out_size)
|
||||
*out_size = sz;
|
||||
return data;
|
||||
}
|
||||
|
||||
static void cb_free_resource(void* data) {
|
||||
mz_free(data);
|
||||
}
|
||||
|
||||
static void cb_register_tab_content(void (*draw_fn)(void*), void* userdata) {
|
||||
if (g_currentMod && draw_fn)
|
||||
g_currentMod->tab_content.push_back({draw_fn, userdata});
|
||||
}
|
||||
|
||||
static void cb_register_menu_item(void (*draw_fn)(void*), void* userdata) {
|
||||
if (g_currentMod && draw_fn)
|
||||
g_currentMod->menu_items.push_back({draw_fn, userdata});
|
||||
}
|
||||
|
||||
static void api_hook_pre(void* addr, int32_t (*fn)(void* args)) {
|
||||
dusk::hookRegisterPre(addr, g_currentMod, fn);
|
||||
}
|
||||
|
||||
static void api_hook_post(void* addr, void (*fn)(void* args)) {
|
||||
dusk::hookRegisterPost(addr, g_currentMod, fn);
|
||||
}
|
||||
|
||||
static void api_hook_replace(void* addr, void (*fn)(void* args)) {
|
||||
dusk::hookSetReplace(addr, g_currentMod, fn);
|
||||
}
|
||||
|
||||
namespace dusk {
|
||||
|
||||
ModLoader& ModLoader::instance() {
|
||||
static ModLoader inst;
|
||||
return inst;
|
||||
}
|
||||
|
||||
void ModLoader::buildAPI(LoadedMod& mod) {
|
||||
mod.api.api_version = DUSK_MOD_API_VERSION;
|
||||
mod.api.mod_dir = mod.dir.c_str();
|
||||
mod.api.log_info = cb_log_info;
|
||||
mod.api.log_warn = cb_log_warn;
|
||||
mod.api.log_error = cb_log_error;
|
||||
mod.api.load_resource = cb_load_resource;
|
||||
mod.api.free_resource = cb_free_resource;
|
||||
mod.api.register_tab_content = cb_register_tab_content;
|
||||
mod.api.register_menu_item = cb_register_menu_item;
|
||||
mod.api.hook_install = hookInstallByAddr;
|
||||
mod.api.hook_pre = api_hook_pre;
|
||||
mod.api.hook_post = api_hook_post;
|
||||
mod.api.hook_replace = api_hook_replace;
|
||||
mod.api.hook_dispatch_pre = hookDispatchPre;
|
||||
mod.api.hook_dispatch_post = hookDispatchPost;
|
||||
}
|
||||
|
||||
void ModLoader::tryLoadDusk(const std::filesystem::path& modPath) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
std::string metaName, metaVersion, metaAuthor, metaDescription;
|
||||
{
|
||||
mz_zip_archive zip{};
|
||||
if (mz_zip_reader_init_file(&zip, modPath.string().c_str(), 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<char*>(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_file(&zip, modPath.string().c_str(), 0)) {
|
||||
DuskLog.error("ModLoader: failed to open {}", modPath.filename().string());
|
||||
return;
|
||||
}
|
||||
|
||||
std::string dllEntry;
|
||||
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;
|
||||
if (fs::path(stat.m_filename).extension() == k_libExt) {
|
||||
dllEntry = stat.m_filename;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (dllEntry.empty()) {
|
||||
mz_zip_reader_end(&zip);
|
||||
DuskLog.warn("ModLoader: no *{} found in {} — skipping", k_libExt,
|
||||
modPath.filename().string());
|
||||
return;
|
||||
}
|
||||
|
||||
const fs::path cacheDir = fs::path("mods") / ".cache" / modPath.stem();
|
||||
std::error_code ec;
|
||||
fs::create_directories(cacheDir, ec);
|
||||
|
||||
const fs::path dllCachePath = cacheDir / fs::path(dllEntry).filename();
|
||||
if (!mz_zip_reader_extract_file_to_file(&zip, dllEntry.c_str(), dllCachePath.string().c_str(),
|
||||
0))
|
||||
{
|
||||
mz_zip_reader_end(&zip);
|
||||
DuskLog.error("ModLoader: failed to extract {} from {}", dllEntry,
|
||||
modPath.filename().string());
|
||||
return;
|
||||
}
|
||||
mz_zip_reader_end(&zip);
|
||||
|
||||
void* handle = pl_dlopen(dllCachePath);
|
||||
if (!handle) {
|
||||
DuskLog.error("ModLoader: failed to open {}: {}", dllCachePath.string(), pl_dlerror());
|
||||
return;
|
||||
}
|
||||
|
||||
LoadedMod mod;
|
||||
mod.mod_path = fs::absolute(modPath).string();
|
||||
mod.dir = fs::absolute(cacheDir).string();
|
||||
mod.handle = handle;
|
||||
mod.fn_init = reinterpret_cast<LoadedMod::FnInit>(pl_dlsym(handle, "mod_init"));
|
||||
mod.fn_tick = reinterpret_cast<LoadedMod::FnTick>(pl_dlsym(handle, "mod_tick"));
|
||||
mod.fn_cleanup = reinterpret_cast<LoadedMod::FnCleanup>(pl_dlsym(handle, "mod_cleanup"));
|
||||
mod.fn_set_imgui_ctx =
|
||||
reinterpret_cast<LoadedMod::FnSetImguiCtx>(pl_dlsym(handle, "dusk_mod_set_imgui_ctx"));
|
||||
|
||||
if (!mod.fn_init || !mod.fn_tick) {
|
||||
DuskLog.error("ModLoader: {} missing mod_init or mod_tick — skipping",
|
||||
fs::path(dllEntry).filename().string());
|
||||
pl_dlclose(handle);
|
||||
return;
|
||||
}
|
||||
|
||||
mod.name = metaName.empty() ? modPath.stem().string() : metaName;
|
||||
mod.version = metaVersion.empty() ? "?" : metaVersion;
|
||||
mod.author = metaAuthor.empty() ? "unknown" : metaAuthor;
|
||||
mod.description = metaDescription;
|
||||
|
||||
m_mods.push_back(std::move(mod));
|
||||
DuskLog.info("ModLoader: found '{}' v{} by {} ({})", m_mods.back().name, m_mods.back().version,
|
||||
m_mods.back().author, modPath.filename().string());
|
||||
}
|
||||
|
||||
void ModLoader::init() {
|
||||
if (m_initialized)
|
||||
return;
|
||||
m_initialized = true;
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
if (!fs::exists(m_modsDir)) {
|
||||
DuskLog.info("ModLoader: mods directory '{}' not found — mod loading skipped",
|
||||
m_modsDir.string());
|
||||
return;
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
std::vector<fs::directory_entry> entries;
|
||||
for (auto& e : fs::directory_iterator(m_modsDir, ec))
|
||||
if (e.is_regular_file() && e.path().extension() == ".dusk")
|
||||
entries.push_back(e);
|
||||
std::sort(entries.begin(), entries.end(),
|
||||
[](const fs::directory_entry& a, const fs::directory_entry& b) {
|
||||
return a.path().filename() < b.path().filename();
|
||||
});
|
||||
|
||||
for (auto& entry : entries)
|
||||
tryLoadDusk(entry.path());
|
||||
|
||||
if (m_mods.empty()) {
|
||||
DuskLog.info("ModLoader: no mods found");
|
||||
return;
|
||||
}
|
||||
|
||||
DuskLog.info("ModLoader: initializing {} mod(s)...", m_mods.size());
|
||||
for (auto& mod : m_mods)
|
||||
buildAPI(mod);
|
||||
|
||||
for (auto& mod : m_mods) {
|
||||
ModGuard guard(&mod);
|
||||
try {
|
||||
mod.fn_init(&mod.api);
|
||||
mod.active = true;
|
||||
DuskLog.info("ModLoader: '{}' initialized", mod.name);
|
||||
} catch (const std::exception& e) {
|
||||
DuskLog.error("ModLoader: exception in {}.mod_init(): {}", mod.name, e.what());
|
||||
} catch (...) {
|
||||
DuskLog.error("ModLoader: unknown exception in {}.mod_init()", mod.name);
|
||||
}
|
||||
}
|
||||
|
||||
auto active =
|
||||
std::count_if(m_mods.begin(), m_mods.end(), [](const LoadedMod& m) { return m.active; });
|
||||
DuskLog.info("ModLoader: {}/{} mod(s) active", active, m_mods.size());
|
||||
}
|
||||
|
||||
void ModLoader::tick() {
|
||||
for (auto& mod : m_mods) {
|
||||
if (!mod.active)
|
||||
continue;
|
||||
ModGuard guard(&mod);
|
||||
try {
|
||||
mod.fn_tick(&mod.api);
|
||||
} catch (const std::exception& e) {
|
||||
DuskLog.error("ModLoader: exception in {}.mod_tick(): {} — disabling", mod.name,
|
||||
e.what());
|
||||
mod.active = false;
|
||||
} catch (...) {
|
||||
DuskLog.error("ModLoader: unknown exception in {}.mod_tick() — disabling", mod.name);
|
||||
mod.active = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ModLoader::shutdown() {
|
||||
for (auto& mod : m_mods) {
|
||||
hookClearMod(&mod);
|
||||
if (mod.fn_cleanup) {
|
||||
ModGuard guard(&mod);
|
||||
try {
|
||||
mod.fn_cleanup(&mod.api);
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
if (mod.handle) {
|
||||
pl_dlclose(mod.handle);
|
||||
mod.handle = nullptr;
|
||||
}
|
||||
}
|
||||
m_mods.clear();
|
||||
DuskLog.info("ModLoader: all mods unloaded");
|
||||
}
|
||||
|
||||
void ModLoader::callDrawCallback(const LoadedMod& mod, const ModDrawCallback& cb) {
|
||||
if (mod.fn_set_imgui_ctx)
|
||||
mod.fn_set_imgui_ctx(ImGui::GetCurrentContext());
|
||||
cb.draw_fn(cb.userdata);
|
||||
}
|
||||
|
||||
} // namespace dusk
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "d/d_tresure.h"
|
||||
#include "dusk/frame_interpolation.h"
|
||||
#include "dusk/logging.h"
|
||||
#include "dusk/mod_loader.hpp"
|
||||
#include "f_op/f_op_camera_mng.h"
|
||||
#include "f_op/f_op_draw_tag.h"
|
||||
#include "f_op/f_op_overlap_mng.h"
|
||||
@@ -812,6 +813,7 @@ void fapGm_Execute() {
|
||||
fpcM_ManagementFunc(NULL, fapGm_After);
|
||||
#endif
|
||||
cCt_Counter(0);
|
||||
dusk::ModLoader::instance().tick();
|
||||
}
|
||||
|
||||
fapGm_HIO_c g_HIO;
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
#include "dusk/game_clock.h"
|
||||
#include "dusk/gyro.h"
|
||||
#include "dusk/imgui/ImGuiEngine.hpp"
|
||||
#include "dusk/mod_loader.hpp"
|
||||
#include "dusk/logging.h"
|
||||
#include "dusk/main.h"
|
||||
#include "dusk/imgui/ImGuiConsole.hpp"
|
||||
@@ -190,6 +191,8 @@ void main01(void) {
|
||||
OSReport("Calling cDyl_InitAsync()...\n");
|
||||
cDyl_InitAsync();
|
||||
|
||||
dusk::ModLoader::instance().init();
|
||||
|
||||
g_mDoAud_audioHeap = JKRCreateSolidHeap(audioHeapSize, JKRGetCurrentHeap(), false);
|
||||
JKRHEAP_NAME(g_mDoAud_audioHeap, "g_mDoAud_audioHeap");
|
||||
|
||||
@@ -292,6 +295,7 @@ void main01(void) {
|
||||
} while (dusk::IsRunning);
|
||||
|
||||
exit:;
|
||||
dusk::ModLoader::instance().shutdown();
|
||||
}
|
||||
|
||||
static bool IsBackendAvailable(AuroraBackend backend) {
|
||||
@@ -479,6 +483,7 @@ int game_main(int argc, char* argv[]) {
|
||||
("h,help", "Print usage")
|
||||
("console", "Show the Windows console window for logs", cxxopts::value<bool>()->default_value("false")->implicit_value("true"))
|
||||
("dvd", "Path to DVD image file", cxxopts::value<std::string>())
|
||||
("mods", "Path to mods directory", cxxopts::value<std::string>()->default_value("mods"))
|
||||
("backend", "Graphics API backend to use (auto, d3d12, metal, vulkan, null)", cxxopts::value<std::string>())
|
||||
("cvar", "Override configuration variables without modifying config", cxxopts::value<std::vector<std::string>>());
|
||||
|
||||
@@ -596,9 +601,8 @@ int game_main(int argc, char* argv[]) {
|
||||
mDoMain::developmentMode = 1; // Force Dev Mode for Debugging
|
||||
mDoDvdThd::SyncWidthSound = false;
|
||||
|
||||
dusk::ModLoader::instance().setModsDir(parsed_arg_options["mods"].as<std::string>());
|
||||
OSReport("Starting main01 (Game Loop)...\n");
|
||||
|
||||
|
||||
main01();
|
||||
|
||||
dusk::ShutdownCrashReporting();
|
||||
|
||||
Reference in New Issue
Block a user