mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-05-24 06:50:43 -04:00
643 lines
20 KiB
C++
643 lines
20 KiB
C++
#include "dusk/mod_loader.hpp"
|
|
#include "dusk/hook_system.hpp"
|
|
#include "dusk/logging.h"
|
|
#include "mod_loader.hpp"
|
|
|
|
#include <RmlUi/Core.h>
|
|
|
|
|
|
#include <algorithm>
|
|
#include <cstdarg>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <string>
|
|
#include <unordered_map>
|
|
|
|
#include "aurora/dvd.h"
|
|
#include "dusk/io.hpp"
|
|
#include "miniz.h"
|
|
#include "native_module.hpp"
|
|
#include "nlohmann/json.hpp"
|
|
|
|
static aurora::Module Log("dusk::modLoader");
|
|
|
|
using namespace dusk::modding;
|
|
using namespace std::string_literals;
|
|
using namespace std::string_view_literals;
|
|
|
|
#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
|
|
static constexpr std::string_view k_archSuffix = ""sv;
|
|
#endif
|
|
|
|
static thread_local dusk::LoadedMod* g_currentMod = nullptr;
|
|
static std::unordered_map<std::string, void*> g_services;
|
|
|
|
namespace dusk {
|
|
thread_local void* g_dusk_hook_current_mod = nullptr;
|
|
|
|
} // namespace dusk
|
|
|
|
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->metadata.id.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) {
|
|
DuskLog.error("load_resource: called outside mod context or with null path");
|
|
return nullptr;
|
|
}
|
|
|
|
std::string entry = std::string("res/") + relative_path;
|
|
std::vector<u8> data;
|
|
try {
|
|
data = g_currentMod->bundle->readFile(entry);
|
|
} catch (const std::runtime_error& e) {
|
|
DuskLog.error("[{}] load_resource: '{}' failed: {}", g_currentMod->metadata.id, entry, e.what());
|
|
return nullptr;
|
|
}
|
|
|
|
const auto retPtr = std::malloc(data.size());
|
|
std::memcpy(retPtr, data.data(), data.size());
|
|
|
|
if (out_size) {
|
|
*out_size = data.size();
|
|
}
|
|
return retPtr;
|
|
}
|
|
|
|
static void cb_free_resource(void* data) {
|
|
std::free(data);
|
|
}
|
|
|
|
namespace {
|
|
|
|
class ModClickListener : public Rml::EventListener {
|
|
public:
|
|
ModClickListener(void (*cb)(void*), void* ud) : m_cb(cb), m_ud(ud) {}
|
|
void ProcessEvent(Rml::Event&) override { m_cb(m_ud); }
|
|
void OnDetach(Rml::Element*) override { delete this; }
|
|
private:
|
|
void (*m_cb)(void*);
|
|
void* m_ud;
|
|
};
|
|
|
|
static std::string escape_rml(const char* text) {
|
|
std::string out;
|
|
for (const char* p = text; *p; ++p) {
|
|
switch (*p) {
|
|
case '&': out += "&"; break;
|
|
case '<': out += "<"; break;
|
|
case '>': out += ">"; break;
|
|
default: out += *p; break;
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
}
|
|
|
|
static void cb_panel_add_section(DuskPanelHandle panel, const char* text) {
|
|
auto* pane = static_cast<Rml::Element*>(panel);
|
|
if (!pane || !text) {
|
|
return;
|
|
}
|
|
auto el = pane->GetOwnerDocument()->CreateElement("div");
|
|
el->SetClass("section-heading", true);
|
|
el->SetInnerRML(escape_rml(text));
|
|
pane->AppendChild(std::move(el));
|
|
}
|
|
|
|
static void cb_panel_add_button(DuskPanelHandle panel, const char* label,
|
|
void (*cb)(void*), void* userdata) {
|
|
auto* pane = static_cast<Rml::Element*>(panel);
|
|
if (!pane || !label || !cb) {
|
|
return;
|
|
}
|
|
auto btn = pane->GetOwnerDocument()->CreateElement("button");
|
|
btn->SetInnerRML(escape_rml(label));
|
|
btn->AddEventListener(Rml::EventId::Click, new ModClickListener(cb, userdata));
|
|
pane->AppendChild(std::move(btn));
|
|
}
|
|
|
|
static DuskElemHandle cb_panel_add_badge_row(DuskPanelHandle panel, const char* label, int ok) {
|
|
auto* pane = static_cast<Rml::Element*>(panel);
|
|
if (!pane || !label) {
|
|
return nullptr;
|
|
}
|
|
auto* doc = pane->GetOwnerDocument();
|
|
|
|
auto row = doc->CreateElement("div");
|
|
row->SetClass("mod-info-row", true);
|
|
|
|
auto badge = doc->CreateElement("span");
|
|
badge->SetClass("achievement-badge", true);
|
|
badge->SetClass(ok ? "unlocked" : "locked", true);
|
|
badge->SetInnerRML(ok ? "PASS" : "WAIT");
|
|
Rml::Element* badgePtr = row->AppendChild(std::move(badge));
|
|
|
|
auto lbl = doc->CreateElement("span");
|
|
lbl->SetClass("mod-info-value", true);
|
|
lbl->SetInnerRML(escape_rml(label));
|
|
row->AppendChild(std::move(lbl));
|
|
|
|
pane->AppendChild(std::move(row));
|
|
return static_cast<DuskElemHandle>(badgePtr);
|
|
}
|
|
|
|
static DuskElemHandle cb_panel_add_dyn_text(DuskPanelHandle panel, const char* text) {
|
|
auto* pane = static_cast<Rml::Element*>(panel);
|
|
if (!pane) {
|
|
return nullptr;
|
|
}
|
|
auto el = pane->GetOwnerDocument()->CreateElement("div");
|
|
el->SetInnerRML(text ? escape_rml(text) : std::string{});
|
|
Rml::Element* ptr = pane->AppendChild(std::move(el));
|
|
return static_cast<DuskElemHandle>(ptr);
|
|
}
|
|
|
|
static void cb_elem_set_badge(DuskElemHandle elem, int ok) {
|
|
auto* el = static_cast<Rml::Element*>(elem);
|
|
if (!el) {
|
|
return;
|
|
}
|
|
el->SetClass("unlocked", ok != 0);
|
|
el->SetClass("locked", ok == 0);
|
|
el->SetInnerRML(ok ? "PASS" : "WAIT");
|
|
}
|
|
|
|
static void cb_elem_set_text(DuskElemHandle elem, const char* text) {
|
|
auto* el = static_cast<Rml::Element*>(elem);
|
|
if (!el || !text) {
|
|
return;
|
|
}
|
|
el->SetInnerRML(escape_rml(text));
|
|
}
|
|
|
|
static DuskElemHandle cb_panel_add_progress(DuskPanelHandle panel, float value) {
|
|
auto* pane = static_cast<Rml::Element*>(panel);
|
|
if (!pane) {
|
|
return nullptr;
|
|
}
|
|
auto el = pane->GetOwnerDocument()->CreateElement("progress");
|
|
el->SetClass("progress-health", true);
|
|
el->SetAttribute("value", value);
|
|
Rml::Element* ptr = pane->AppendChild(std::move(el));
|
|
return static_cast<DuskElemHandle>(ptr);
|
|
}
|
|
|
|
static void cb_elem_set_progress(DuskElemHandle elem, float value) {
|
|
auto* el = static_cast<Rml::Element*>(elem);
|
|
if (!el) {
|
|
return;
|
|
}
|
|
el->SetAttribute("value", value);
|
|
}
|
|
|
|
static void cb_register_tab_content(void (*build_fn)(void*, void*), void* userdata) {
|
|
if (g_currentMod && build_fn) {
|
|
g_currentMod->tab_content.push_back({build_fn, userdata});
|
|
}
|
|
}
|
|
|
|
static void cb_register_tab_update(void (*update_fn)(void*), void* userdata) {
|
|
if (g_currentMod && update_fn) {
|
|
g_currentMod->tab_updates.push_back({update_fn, userdata});
|
|
}
|
|
}
|
|
|
|
static void cb_service_publish(const char* name, void* ptr) {
|
|
if (!name) {
|
|
return;
|
|
}
|
|
if (g_services.count(name)) {
|
|
DuskLog.error(
|
|
"[{}] service_publish: '{}' already published by another mod", modName(), name);
|
|
}
|
|
g_services[name] = ptr;
|
|
}
|
|
|
|
static void* cb_service_get(const char* name) {
|
|
if (!name) {
|
|
return nullptr;
|
|
}
|
|
auto it = g_services.find(name);
|
|
return it != g_services.end() ? it->second : nullptr;
|
|
}
|
|
|
|
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, void* retval)) {
|
|
dusk::hookRegisterPost(addr, g_currentMod, modName(), fn);
|
|
}
|
|
|
|
static void api_hook_replace(void* addr, void (*fn)(void* args, void* retval)) {
|
|
if (!dusk::hookSetReplace(addr, g_currentMod, modName(), fn)) {
|
|
if (g_currentMod) {
|
|
g_currentMod->load_failed = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
static dusk::ModLoader g_modLoader;
|
|
|
|
namespace dusk {
|
|
|
|
ModLoader& ModLoader::instance() {
|
|
return g_modLoader;
|
|
}
|
|
|
|
void ModLoader::buildAPI(LoadedMod& mod) {
|
|
auto& native = *mod.native;
|
|
native.api.api_version = DUSK_MOD_API_VERSION;
|
|
native.api.mod_dir = mod.dir.c_str();
|
|
native.api.log_info = cb_log_info;
|
|
native.api.log_warn = cb_log_warn;
|
|
native.api.log_error = cb_log_error;
|
|
native.api.load_resource = cb_load_resource;
|
|
native.api.free_resource = cb_free_resource;
|
|
native.api.register_tab_content = cb_register_tab_content;
|
|
native.api.register_tab_update = cb_register_tab_update;
|
|
native.api.panel_add_section = cb_panel_add_section;
|
|
native.api.panel_add_button = cb_panel_add_button;
|
|
native.api.panel_add_badge_row = cb_panel_add_badge_row;
|
|
native.api.panel_add_dyn_text = cb_panel_add_dyn_text;
|
|
native.api.elem_set_badge = cb_elem_set_badge;
|
|
native.api.elem_set_text = cb_elem_set_text;
|
|
native.api.panel_add_progress = cb_panel_add_progress;
|
|
native.api.elem_set_progress = cb_elem_set_progress;
|
|
native.api.hook_install = hookInstallByAddr;
|
|
native.api.hook_pre = api_hook_pre;
|
|
native.api.hook_post = api_hook_post;
|
|
native.api.hook_replace = api_hook_replace;
|
|
native.api.hook_dispatch_pre = hookDispatchPre;
|
|
native.api.hook_dispatch_post = hookDispatchPost;
|
|
native.api.service_publish = cb_service_publish;
|
|
native.api.service_get = cb_service_get;
|
|
}
|
|
|
|
static std::unique_ptr<ModBundle> loadBundle(const std::filesystem::path& modPath, bool fromDir) {
|
|
if (fromDir) {
|
|
return std::make_unique<ModBundleDisk>(modPath);
|
|
} else {
|
|
std::vector<u8> data = io::FileStream::ReadAllBytes(modPath);
|
|
return std::make_unique<ModBundleZip>(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};
|
|
}
|
|
|
|
class InvalidModDataException : public std::runtime_error {
|
|
public:
|
|
explicit InvalidModDataException(const std::string& msg) : runtime_error(msg) {}
|
|
explicit InvalidModDataException(const char* msg) : runtime_error(msg) {}
|
|
};
|
|
|
|
static ModMetadata loadMetadata(const std::filesystem::path& modPath, ModBundle& bundle) {
|
|
const auto metaJson = bundle.readFile("mod.json");
|
|
auto j = nlohmann::json::parse(metaJson);
|
|
|
|
std::string metaId = j.value("id", "");
|
|
std::string metaName = j.value("name", "");
|
|
std::string metaVersion = j.value("version", "");
|
|
std::string metaAuthor = j.value("author", "");
|
|
std::string metaDescription = j.value("description", "");
|
|
const bool hasCode = j.value("has_code", false);
|
|
|
|
if (metaId.empty()) {
|
|
throw InvalidModDataException("Missing ID value in mod metadata!");
|
|
}
|
|
if (metaName.empty()) {
|
|
metaName = io::fs_path_to_string(modPath.stem());
|
|
}
|
|
if (metaVersion.empty()) {
|
|
metaVersion = "?"s;
|
|
}
|
|
if (metaAuthor.empty()) {
|
|
metaAuthor = "unknown"s;
|
|
}
|
|
|
|
return ModMetadata{
|
|
std::move(metaId),
|
|
std::move(metaName),
|
|
std::move(metaVersion),
|
|
std::move(metaAuthor),
|
|
std::move(metaDescription),
|
|
hasCode,
|
|
};
|
|
}
|
|
|
|
static bool checkDuplicateMod(const ModMetadata& metadata, const std::vector<LoadedMod>& mods) {
|
|
return std::ranges::any_of(mods, [&](const LoadedMod& mod) {
|
|
return mod.metadata.id == metadata.id;
|
|
});
|
|
}
|
|
|
|
bool ModLoader::tryLoadNativeMod(LoadedMod& mod) {
|
|
namespace fs = std::filesystem;
|
|
|
|
auto [dllEntry, dllFallback] = LocateDllInBundle(*mod.bundle);
|
|
if (dllEntry.empty()) {
|
|
dllEntry = dllFallback;
|
|
}
|
|
|
|
if (dllEntry.empty()) {
|
|
DuskLog.error(
|
|
"ModLoader: no *{} found in {} — skipping", NativeModule::LibraryExtension, mod.metadata.id);
|
|
return false;
|
|
}
|
|
|
|
const fs::path cacheDir = m_modsDir / ".cache" / mod.metadata.id;
|
|
std::error_code ec;
|
|
fs::create_directories(cacheDir, ec);
|
|
|
|
const fs::path dllCachePath = cacheDir / fs::path(dllEntry).filename();
|
|
|
|
std::vector<u8> dllData;
|
|
try {
|
|
dllData = mod.bundle->readFile(dllEntry);
|
|
} catch (const std::runtime_error& e) {
|
|
DuskLog.error(
|
|
"ModLoader: failed to extract {} from {}", dllEntry, mod.metadata.id);
|
|
return false;
|
|
}
|
|
|
|
{
|
|
std::ofstream out(dllCachePath, std::ios::binary | std::ios::out);
|
|
if (!out) {
|
|
DuskLog.error("ModLoader: failed to write {}", io::fs_path_to_string(dllCachePath));
|
|
return false;
|
|
}
|
|
|
|
out.write(
|
|
reinterpret_cast<const char*>(dllData.data()),
|
|
static_cast<std::streamsize>(dllData.size()));
|
|
}
|
|
|
|
auto nativeMod = std::make_unique<NativeMod>();
|
|
try {
|
|
nativeMod->handle = std::make_unique<NativeModule>(dllCachePath);
|
|
} catch (const std::runtime_error& e) {
|
|
DuskLog.error("ModLoader: failed to open {}: {}", io::fs_path_to_string(dllCachePath), e.what());
|
|
return false;
|
|
}
|
|
|
|
const auto mod_api_ver = nativeMod->handle->LookupSymbol<uint32_t*>("mod_api_version");
|
|
if (mod_api_ver && *mod_api_ver != DUSK_MOD_API_VERSION) {
|
|
DuskLog.error("ModLoader: {} expects API v{} but engine is v{}, skipping",
|
|
io::fs_path_to_string(fs::path(dllEntry).filename()), *mod_api_ver, DUSK_MOD_API_VERSION);
|
|
return false;
|
|
}
|
|
|
|
nativeMod->fn_init = nativeMod->handle->LookupSymbol<NativeMod::FnInit>("mod_init");
|
|
nativeMod->fn_tick = nativeMod->handle->LookupSymbol<NativeMod::FnTick>("mod_tick");
|
|
nativeMod->fn_cleanup = nativeMod->handle->LookupSymbol<NativeMod::FnCleanup>("mod_cleanup");
|
|
|
|
if (!nativeMod->fn_init || !nativeMod->fn_tick) {
|
|
DuskLog.error("ModLoader: {} missing mod_init or mod_tick — skipping",
|
|
io::fs_path_to_string(fs::path(dllEntry).filename()));
|
|
return false;
|
|
}
|
|
|
|
mod.dir = io::fs_path_to_string(fs::absolute(cacheDir));
|
|
mod.native = std::move(nativeMod);
|
|
return true;
|
|
}
|
|
|
|
void ModLoader::tryLoadDusk(const std::filesystem::path& modPath, bool fromDir) {
|
|
namespace fs = std::filesystem;
|
|
|
|
std::unique_ptr<ModBundle> 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;
|
|
}
|
|
|
|
ModMetadata metadata;
|
|
try
|
|
{
|
|
metadata = loadMetadata(modPath, *bundle);
|
|
}
|
|
catch (const std::runtime_error& e) {
|
|
Log.error(
|
|
"ModLoader: bad mod.json in {}: {}", io::fs_path_to_string(modPath.filename()), e.what());
|
|
return;
|
|
}
|
|
|
|
if (checkDuplicateMod(metadata, m_mods)) {
|
|
Log.error(
|
|
"ModLoader: mod with id '{}' already exists, not loading {}",
|
|
metadata.id,
|
|
io::fs_path_to_string(modPath.filename()));
|
|
return;
|
|
}
|
|
|
|
LoadedMod mod;
|
|
mod.mod_path = io::fs_path_to_string(fs::absolute(modPath));
|
|
mod.metadata = std::move(metadata);
|
|
mod.bundle = std::move(bundle);
|
|
|
|
if (mod.metadata.hasCode && !tryLoadNativeMod(mod)) {
|
|
return;
|
|
}
|
|
|
|
const auto& inserted = m_mods.emplace_back(std::move(mod));
|
|
|
|
DuskLog.info(
|
|
"ModLoader: found '{}' ('{}') v{} by {} ({})",
|
|
inserted.metadata.name,
|
|
inserted.metadata.id,
|
|
inserted.metadata.version,
|
|
inserted.metadata.author,
|
|
io::fs_path_to_string(modPath.filename()));
|
|
}
|
|
|
|
void ModLoader::init() {
|
|
if (m_initialized) {
|
|
return;
|
|
}
|
|
m_initialized = true;
|
|
|
|
namespace fs = std::filesystem;
|
|
if (!fs::is_directory(m_modsDir)) {
|
|
DuskLog.info(
|
|
"ModLoader: mods directory '{}' not found — mod loading skipped", io::fs_path_to_string(m_modsDir));
|
|
return;
|
|
}
|
|
|
|
std::error_code ec;
|
|
std::vector<fs::directory_entry> entries;
|
|
for (auto& e : fs::directory_iterator(m_modsDir, ec)) {
|
|
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);
|
|
}
|
|
}
|
|
std::sort(entries.begin(), entries.end(),
|
|
[](const fs::directory_entry& a, const fs::directory_entry& b) {
|
|
return a.path().filename() < b.path().filename();
|
|
});
|
|
|
|
m_mods.reserve(entries.size());
|
|
for (auto& entry : entries) {
|
|
tryLoadDusk(entry.path(), entry.is_directory());
|
|
}
|
|
|
|
if (m_mods.empty()) {
|
|
DuskLog.info("ModLoader: no mods found");
|
|
return;
|
|
}
|
|
|
|
initOverlayFiles();
|
|
|
|
DuskLog.info("ModLoader: initializing {} mod(s)...", m_mods.size());
|
|
for (auto& mod : m_mods) {
|
|
if (mod.native) {
|
|
buildAPI(mod);
|
|
}
|
|
}
|
|
|
|
for (auto& mod : m_mods) {
|
|
if (!mod.native) {
|
|
continue;
|
|
}
|
|
|
|
ModGuard guard(&mod);
|
|
try {
|
|
mod.native->fn_init(&mod.native->api);
|
|
if (!mod.load_failed) {
|
|
mod.active = true;
|
|
DuskLog.info("ModLoader: '{}' initialized", mod.metadata.id);
|
|
} else {
|
|
DuskLog.error("ModLoader: '{}' failed to load due to hook conflicts", mod.metadata.id);
|
|
}
|
|
} catch (const std::exception& e) {
|
|
DuskLog.error("ModLoader: exception in {}.mod_init(): {}", mod.metadata.id, e.what());
|
|
} catch (...) {
|
|
DuskLog.error("ModLoader: unknown exception in {}.mod_init()", mod.metadata.id);
|
|
}
|
|
}
|
|
|
|
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 || !mod.native) {
|
|
continue;
|
|
}
|
|
ModGuard guard(&mod);
|
|
try {
|
|
mod.native->fn_tick(&mod.native->api);
|
|
} catch (const std::exception& e) {
|
|
DuskLog.error(
|
|
"ModLoader: exception in {}.mod_tick(): {} — disabling", mod.metadata.id, e.what());
|
|
mod.active = false;
|
|
} catch (...) {
|
|
DuskLog.error("ModLoader: unknown exception in {}.mod_tick() — disabling", mod.metadata.id);
|
|
mod.active = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
void ModLoader::shutdown() {
|
|
for (auto& mod : m_mods) {
|
|
hookClearMod(&mod);
|
|
if (mod.native && mod.native->fn_cleanup) {
|
|
ModGuard guard(&mod);
|
|
try {
|
|
mod.native->fn_cleanup(&mod.native->api);
|
|
} catch (...) {
|
|
}
|
|
}
|
|
}
|
|
m_mods.clear();
|
|
g_services.clear();
|
|
DuskLog.info("ModLoader: all mods unloaded");
|
|
}
|
|
|
|
} // namespace dusk
|