#include "loader.hpp" #include "dusk/logging.h" #include "dusk/mod_loader.hpp" #include #include #include #include #include #include #include #include #include #include #include #include "../manifest.hpp" #include "depgraph.hpp" #include "dusk/config.hpp" #include "dusk/data.hpp" #include "dusk/io.hpp" #include "dusk/mods/log_buffer.hpp" #include "dusk/mods/path.hpp" #include "dusk/mods/svc/config.hpp" #include "dusk/mods/svc/hook.hpp" #include "dusk/mods/svc/registry.hpp" #include "dusk/ui/mods_window.hpp" #include "dusk/ui/ui.hpp" #include "miniz.h" #include "native_module.hpp" #include "nlohmann/json.hpp" #if DUSK_HAS_PREPATCH #include "prepatch.hpp" #endif using namespace std::string_literals; using namespace std::string_view_literals; namespace fs = std::filesystem; #if defined(_WIN32) #if defined(_M_ARM64) static constexpr std::string_view k_nativePlatform = "windows-arm64"sv; #elif defined(_M_X64) static constexpr std::string_view k_nativePlatform = "windows-amd64"sv; #elif defined(_M_IX86) static constexpr std::string_view k_nativePlatform = "windows-x86"sv; #else static constexpr std::string_view k_nativePlatform = ""sv; #endif static constexpr std::string_view k_nativeLibName = "mod.dll"sv; #elif defined(__ANDROID__) #if defined(__aarch64__) static constexpr std::string_view k_nativePlatform = "android-aarch64"sv; #elif defined(__x86_64__) static constexpr std::string_view k_nativePlatform = "android-x86_64"sv; #else static constexpr std::string_view k_nativePlatform = ""sv; #endif static constexpr std::string_view k_nativeLibName = "mod.so"sv; #elif defined(__APPLE__) #include #if TARGET_OS_IOS static constexpr std::string_view k_nativePlatform = "ios-arm64"sv; #elif TARGET_OS_TV static constexpr std::string_view k_nativePlatform = "tvos-arm64"sv; #elif defined(__aarch64__) static constexpr std::string_view k_nativePlatform = "macos-arm64"sv; #elif defined(__x86_64__) static constexpr std::string_view k_nativePlatform = "macos-x86_64"sv; #else static constexpr std::string_view k_nativePlatform = ""sv; #endif static constexpr std::string_view k_nativeLibName = "mod.so"sv; #elif defined(__linux__) #if defined(__aarch64__) static constexpr std::string_view k_nativePlatform = "linux-aarch64"sv; #elif defined(__x86_64__) static constexpr std::string_view k_nativePlatform = "linux-x86_64"sv; #elif defined(__i386__) static constexpr std::string_view k_nativePlatform = "linux-x86"sv; #else static constexpr std::string_view k_nativePlatform = ""sv; #endif static constexpr std::string_view k_nativeLibName = "mod.so"sv; #else static constexpr std::string_view k_nativePlatform = ""sv; static constexpr std::string_view k_nativeLibName = ""sv; #endif namespace dusk::mods { namespace { constexpr borealis::Log Log{"dusk::mods::loader"}; ModLoader g_modLoader; constexpr std::string_view k_nativeLibDir = "lib/"sv; class DirectoryRollback { public: ~DirectoryRollback() { if (!mPath.empty()) { std::error_code ec; fs::remove_all(mPath, ec); } } void set_path(fs::path path) { mPath = std::move(path); } void release() { mPath.clear(); } private: fs::path mPath; }; std::unique_ptr load_bundle(const fs::path& modPath, bool fromDir) { if (fromDir) { return std::make_unique(modPath); } else { return std::make_unique(modPath); } } struct NativeRuntimeLocation { std::string entry; std::vector runtimeEntries; bool anyLibs = false; }; struct NativeLocateFailure { NativeModStatus status; std::string logMessage; }; using NativeLocateResult = std::variant; bool has_native_library_extension(std::string_view name) { const auto endsWith = [name](std::string_view extension) { if (name.size() < extension.size()) { return false; } const auto suffix = name.substr(name.size() - extension.size()); return std::ranges::equal(suffix, extension, [](char lhs, char rhs) { const auto lower = [](char value) { return value >= 'A' && value <= 'Z' ? static_cast(value + ('a' - 'A')) : value; }; return lower(lhs) == lower(rhs); }); }; return endsWith(".dll"sv) || endsWith(".so"sv) || endsWith(".dylib"sv); } NativeLocateResult locate_native_runtime(ModBundle& bundle) { NativeRuntimeLocation result; const std::string platformPrefix = fmt::format("{}{}/", k_nativeLibDir, k_nativePlatform); const std::string nativeEntry = platformPrefix + std::string{k_nativeLibName}; for (const auto& name : bundle.getFileNames()) { if (name.find('/') == std::string::npos && has_native_library_extension(name)) { return NativeLocateFailure{ NativeModStatus::InvalidBundle, fmt::format( "native library '{}' found at the root (natives go in /lib/{{platform}})", name), }; } if (!name.starts_with(k_nativeLibDir)) { continue; } const std::string_view libPath{ name.data() + k_nativeLibDir.size(), name.size() - k_nativeLibDir.size()}; const auto platformEnd = libPath.find('/'); if (platformEnd != std::string_view::npos) { const auto entryName = libPath.substr(platformEnd + 1); if (entryName.find('/') == std::string_view::npos && (entryName == "mod.dll"sv || entryName == "mod.so"sv)) { result.anyLibs = true; } } if (!k_nativePlatform.empty() && name.starts_with(platformPrefix)) { const std::string_view relativeName{ name.data() + platformPrefix.size(), name.size() - platformPrefix.size()}; if (!is_safe_resource_path(relativeName)) { continue; } result.runtimeEntries.push_back(name); } if (name == nativeEntry) { result.entry = name; } } std::ranges::sort(result.runtimeEntries); result.runtimeEntries.erase( std::unique(result.runtimeEntries.begin(), result.runtimeEntries.end()), result.runtimeEntries.end()); return result; } void complete_operation(const std::shared_ptr& operation, const bool success = true, std::string message = {}) { if (operation == nullptr) { return; } operation->state = success ? ModOperation::State::Succeeded : ModOperation::State::Failed; operation->message = std::move(message); } LoadedMod::FileIdentity file_identity(const fs::path& path) { std::error_code error; const bool isDirectory = fs::is_directory(path, error); if (error) { return {}; } const auto size = isDirectory ? 0 : fs::file_size(path, error); if (error) { return {}; } const auto modified = fs::last_write_time(path, error); if (error) { return {}; } return {.size = size, .modified = modified, .valid = true}; } } // namespace ModLoader& ModLoader::instance() { return g_modLoader; } 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 void validate_mod_id(std::string_view const str) { if (str.empty()) { throw InvalidModDataException("Missing ID value in mod metadata!"); } bool lastWasPeriod = false; for (auto const chr : str) { if (chr == '.') { if (lastWasPeriod) { throw InvalidModDataException("Cannot have two consecutive periods in mod ID!"); } lastWasPeriod = true; continue; } lastWasPeriod = false; if (chr == '_') continue; if (chr >= '0' && chr <= '9') continue; if (chr >= 'a' && chr <= 'z') continue; if (chr >= 'A' && chr <= 'Z') continue; throw InvalidModDataException( fmt::format("Invalid character '{}' in mod ID. Valid characters are period, " "underscore, and alphanumerics.", chr)); } } static bool bundle_has_file(ModBundle& bundle, const std::string& path) { try { bundle.getFileSize(path); return true; } catch (const std::runtime_error&) { return false; } } static std::string resolve_image_path(ModBundle& bundle, const std::string& modId, std::string_view key, const std::string& manifestPath, const std::string& defaultPath) { if (!manifestPath.empty()) { if (!is_safe_resource_path(manifestPath)) { log::write( modId, LOG_LEVEL_WARN, "invalid {} path '{}' in mod.json", key, manifestPath); } else if (!bundle_has_file(bundle, manifestPath)) { log::write( modId, LOG_LEVEL_WARN, "{} path '{}' not found in bundle", key, manifestPath); } else { return manifestPath; } } if (bundle_has_file(bundle, defaultPath)) { return defaultPath; } return {}; } static ModMetadata load_metadata(const fs::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", ""); std::string metaIcon = j.value("icon", ""); std::string metaBanner = j.value("banner", ""); validate_mod_id(metaId); if (metaName.empty()) { metaName = borealis::io::fs_path_to_string(modPath.stem()); } if (metaVersion.empty()) { metaVersion = "?"s; } if (metaAuthor.empty()) { metaAuthor = "unknown"s; } std::string iconPath = resolve_image_path(bundle, metaId, "icon", metaIcon, "res/icon.png"s); std::string bannerPath = resolve_image_path(bundle, metaId, "banner", metaBanner, "res/banner.png"s); return ModMetadata{ std::move(metaId), std::move(metaName), std::move(metaVersion), std::move(metaAuthor), std::move(metaDescription), std::move(iconPath), std::move(bannerPath), }; } bool inspect_mod_bundle( const fs::path& path, ModMetadata& metadata, std::string& error, bool* hasNative) noexcept { try { auto bundle = load_bundle(path, false); metadata = load_metadata(path, *bundle); if (hasNative != nullptr) { *hasNative = std::ranges::any_of(bundle->getFileNames(), [](const auto& name) { return has_native_library_extension(name); }); } error.clear(); return true; } catch (const std::exception& exception) { error = exception.what(); } catch (...) { error = "Unknown bundle validation error"; } return false; } // True if the first `capacity` bytes of `str` contain a NUL. static bool terminated_within(const char* str, size_t capacity) { return std::memchr(str, '\0', capacity) != nullptr; } static bool parse_meta(NativeMod& native, LoadedMod& mod) { const ModMeta* meta = native.meta; if (meta->struct_size < sizeof(ModMeta)) { log::write(mod.metadata.id, LOG_LEVEL_ERROR, "mod_meta descriptor has invalid size {}", meta->struct_size); mod.nativeStatus = NativeModStatus::InvalidMetadata; return false; } const auto* cursor = static_cast(meta->records_begin); const auto* end = static_cast(meta->records_end); if (cursor == nullptr || end == nullptr || cursor > end || (reinterpret_cast(cursor) & 7) != 0) { log::write(mod.metadata.id, LOG_LEVEL_ERROR, "mod_meta section bounds are invalid"); mod.nativeStatus = NativeModStatus::InvalidMetadata; return false; } ModMetaParsed parsed; size_t headerCount = 0; const auto invalid = [&](std::string_view why) { log::write(mod.metadata.id, LOG_LEVEL_ERROR, "invalid metadata record at offset {}: {}", cursor - static_cast(meta->records_begin), why); mod.nativeStatus = NativeModStatus::InvalidMetadata; return false; }; while (cursor < end) { if (end - cursor < 8) { return invalid("trailing bytes"); } uint64_t first = 0; std::memcpy(&first, cursor, sizeof(first)); if (first == 0) { // linker padding / bounds sentinel cursor += 8; continue; } const auto* rec = reinterpret_cast(cursor); const size_t size = rec->size; if (size < 8 || size % 8 != 0 || size > static_cast(end - cursor)) { return invalid("bad record size"); } switch (rec->kind) { case MOD_META_PAD: break; case MOD_META_HEADER: { if (size < sizeof(ModMetaHeader)) { return invalid("truncated header record"); } const auto* header = reinterpret_cast(rec); ++headerCount; parsed.abiVersion = header->abi_version; break; } case MOD_META_IMPORT: { if (size < sizeof(ModMetaImport)) { return invalid("truncated import record"); } auto* record = reinterpret_cast(const_cast(cursor)); if (!terminated_within(record->service_id.chars, sizeof(record->service_id.chars))) { return invalid("unterminated import service id"); } parsed.imports.push_back(record); break; } case MOD_META_EXPORT: { if (size < sizeof(ModMetaExport)) { return invalid("truncated export record"); } auto* record = reinterpret_cast(const_cast(cursor)); if (!terminated_within(record->service_id.chars, sizeof(record->service_id.chars))) { return invalid("unterminated export service id"); } parsed.exports.push_back(record); break; } case MOD_META_HOOK_FN: { if (size < sizeof(ModMetaHookFn)) { return invalid("truncated hook record"); } parsed.hookFns.push_back( reinterpret_cast(const_cast(cursor))); break; } case MOD_META_HOOK_MEM: { if (size <= sizeof(ModMetaHookMem)) { return invalid("truncated hook record"); } auto* record = reinterpret_cast(const_cast(cursor)); const char* strings = reinterpret_cast(cursor) + sizeof(ModMetaHookMem); const size_t capacity = size - sizeof(ModMetaHookMem); if (!terminated_within(strings, capacity)) { return invalid("unterminated hook vtable symbol"); } const size_t vtableLen = std::char_traits::length(strings); if (!terminated_within(strings + vtableLen + 1, capacity - vtableLen - 1)) { return invalid("unterminated hook display name"); } parsed.hookMems.push_back(record); break; } case MOD_META_HOOK_MEM_EXT: { if (size <= sizeof(ModMetaHookMemExt)) { return invalid("truncated extended hook record"); } auto* record = reinterpret_cast(const_cast(cursor)); if (record->pmf_size <= MOD_META_HOOK_MEM_CAPACITY || record->pmf_size > MOD_META_HOOK_MEM_EXT_CAPACITY || record->materialize == nullptr) { return invalid("bad extended hook member-pointer size"); } const char* strings = reinterpret_cast(cursor) + sizeof(ModMetaHookMemExt); const size_t capacity = size - sizeof(ModMetaHookMemExt); if (!terminated_within(strings, capacity)) { return invalid("unterminated extended hook vtable symbol"); } const size_t vtableLen = std::char_traits::length(strings); if (!terminated_within(strings + vtableLen + 1, capacity - vtableLen - 1)) { return invalid("unterminated extended hook display name"); } parsed.hookMemExts.push_back(record); break; } case MOD_META_HOOK_NAME: { if (size <= sizeof(ModMetaHookName)) { return invalid("truncated hook record"); } auto* record = reinterpret_cast(const_cast(cursor)); const char* name = reinterpret_cast(cursor) + sizeof(ModMetaHookName); if (!terminated_within(name, size - sizeof(ModMetaHookName))) { return invalid("unterminated hook symbol name"); } parsed.hookNames.push_back(record); break; } default: // Additive record kinds may appear within a format version; skip them. log::write(mod.metadata.id, LOG_LEVEL_DEBUG, "skipping unknown metadata record kind {}", rec->kind); break; } cursor += size; } if (headerCount != 1) { log::write(mod.metadata.id, LOG_LEVEL_ERROR, "expected 1 metadata header record, found {}", headerCount); mod.nativeStatus = NativeModStatus::InvalidMetadata; return false; } if (parsed.abiVersion != MOD_ABI_VERSION) { log::write(mod.metadata.id, LOG_LEVEL_ERROR, "expects ABI v{} but engine is v{}, skipping", parsed.abiVersion, MOD_ABI_VERSION); mod.nativeStatus = NativeModStatus::ApiVersionMismatch; return false; } native.parsed = std::move(parsed); return true; } static std::string lifecycle_error_message( const char* fnName, const ModResult result, const ModError& error) { if (error.message[0] != '\0') { return error.message; } return fmt::format("{} failed with result {}", fnName, static_cast(result)); } static std::string native_status_message(const NativeModStatus status) { switch (status) { case NativeModStatus::BuildDisabled: return "Code mods are disabled on this Dusklight build"; case NativeModStatus::ModMissingPlatform: return fmt::format("Mod not supported on this platform ({})", k_nativePlatform); case NativeModStatus::ApiVersionMismatch: // TODO: differentiate whether mod or Dusklight is out of date return "Mod ABI version mismatch"; case NativeModStatus::MissingExport: return "Missing required mod API exports"; case NativeModStatus::InvalidMetadata: return "Invalid mod metadata records"; case NativeModStatus::InvalidBundle: return "Invalid mod bundle layout (old mod?)"; case NativeModStatus::Unknown: return "Unknown mod load failure"; case NativeModStatus::None: case NativeModStatus::Loaded: break; } return "native mod failed to load"; } fs::path ModLoader::external_native_lib_path(const LoadedMod& mod) const { if (k_nativeLibName.empty()) { return {}; } const auto& libDir = m_searchDirs[mod.searchDirIndex].nativeLibDir; if (libDir.empty()) { return {}; } const auto filename = fmt::format("{}{}", mod.metadata.id, borealis::io::fs_path_to_string(fs::path(k_nativeLibName).extension())); fs::path path = libDir / fs::path{filename}; std::error_code ec; if (!fs::is_regular_file(path, ec)) { return {}; } return path; } void ModLoader::load_native( LoadedMod& mod, const std::string& dllEntry, const std::vector& runtimeEntries) { if (!EnableCodeMods) { log::write(mod.metadata.id, LOG_LEVEL_ERROR, "Code mods are not available in this build"); mod.nativeStatus = NativeModStatus::BuildDisabled; return; } const fs::path cacheDir = m_cacheDir / mod.metadata.id; const fs::path scratchDir = cacheDir / "data"; std::error_code ec; fs::create_directories(scratchDir, ec); if (ec) { log::write(mod.metadata.id, LOG_LEVEL_ERROR, "failed to create mod directory {}: {}", data::abbreviated_path_string(scratchDir), ec.message()); return; } mod.dir = fs::absolute(scratchDir); mod.dirUtf8 = borealis::io::fs_path_to_string(mod.dir); fs::path libPath; fs::path runtimeDir; DirectoryRollback runtimeDirRollback; if (mod.nativeInPlace) { if (!dllEntry.empty()) { libPath = mod.modPath / dllEntry; } else if (auto external = external_native_lib_path(mod); !external.empty()) { libPath = std::move(external); } else { log::write(mod.metadata.id, LOG_LEVEL_ERROR, "no native library named {} found; skipping", k_nativeLibName); mod.nativeStatus = NativeModStatus::ModMissingPlatform; return; } runtimeDir = libPath.parent_path(); } else { if (dllEntry.empty()) { log::write(mod.metadata.id, LOG_LEVEL_ERROR, "no native library named {} found; skipping", k_nativeLibName); mod.nativeStatus = NativeModStatus::ModMissingPlatform; return; } // Every generation gets a new directory. The main module and all of its runtime // libraries therefore have fresh paths and can coexist with a previous generation // that is still unwinding after a reload. runtimeDir = cacheDir / fmt::format("g{}", ++mod.cacheGeneration); runtimeDirRollback.set_path(runtimeDir); fs::create_directories(runtimeDir, ec); if (ec) { log::write(mod.metadata.id, LOG_LEVEL_ERROR, "failed to create native runtime directory {}: {}", data::abbreviated_path_string(runtimeDir), ec.message()); return; } const std::string platformPrefix = fmt::format("{}{}/", k_nativeLibDir, k_nativePlatform); for (const auto& entry : runtimeEntries) { if (!entry.starts_with(platformPrefix)) { continue; } const std::string_view relativeName{ entry.data() + platformPrefix.size(), entry.size() - platformPrefix.size()}; if (!is_safe_resource_path(relativeName)) { log::write(mod.metadata.id, LOG_LEVEL_ERROR, "unsafe native runtime path '{}'; skipping", entry); return; } const fs::path outputPath = runtimeDir / fs::path{relativeName}; fs::create_directories(outputPath.parent_path(), ec); if (ec) { log::write(mod.metadata.id, LOG_LEVEL_ERROR, "failed to create directory for {}: {}", entry, ec.message()); return; } std::vector data; try { data = mod.bundle->readFile(entry); } catch (const std::exception& e) { log::write( mod.metadata.id, LOG_LEVEL_ERROR, "failed to extract {}: {}", entry, e.what()); return; } std::ofstream out(outputPath, std::ios::binary | std::ios::out); if (!out) { log::write(mod.metadata.id, LOG_LEVEL_ERROR, "failed to write {}", entry); return; } out.write(reinterpret_cast(data.data()), static_cast(data.size())); if (!out) { log::write(mod.metadata.id, LOG_LEVEL_ERROR, "failed to write {}", entry); return; } } libPath = runtimeDir / fs::path{dllEntry}.filename(); } auto nativeMod = std::make_unique(); try { nativeMod->handle = std::make_unique(libPath); } catch (const std::runtime_error& e) { log::write(mod.metadata.id, LOG_LEVEL_ERROR, "failed to open {}: {}", data::abbreviated_path_string(libPath), e.what()); return; } nativeMod->meta = nativeMod->handle->LookupSymbol("mod_meta"); nativeMod->contextSymbol = nativeMod->handle->LookupSymbol("mod_ctx"); nativeMod->fn_initialize = nativeMod->handle->LookupSymbol("mod_initialize"); nativeMod->fn_update = nativeMod->handle->LookupSymbol("mod_update"); nativeMod->fn_shutdown = nativeMod->handle->LookupSymbol("mod_shutdown"); if (!nativeMod->meta || !nativeMod->contextSymbol || !nativeMod->fn_initialize || !nativeMod->fn_update || !nativeMod->fn_shutdown) { log::write(mod.metadata.id, LOG_LEVEL_ERROR, "{} missing required mod API exports; skipping", data::abbreviated_path_string(libPath)); mod.nativeStatus = NativeModStatus::MissingExport; return; } if (!parse_meta(*nativeMod, mod)) { return; } if (nativeMod->contextSymbol == nullptr) { log::write(mod.metadata.id, LOG_LEVEL_ERROR, "missing required mod_ctx export"); mod.nativeStatus = NativeModStatus::MissingExport; return; } *nativeMod->contextSymbol = mod.context.get(); mod.nativePath = fs::absolute(libPath); mod.nativeDir = fs::absolute(runtimeDir); mod.nativeDirUtf8 = borealis::io::fs_path_to_string(mod.nativeDir); mod.native = std::move(nativeMod); mod.nativeStatus = NativeModStatus::Loaded; runtimeDirRollback.release(); } bool ModLoader::load_native_if_present(LoadedMod& mod) { const auto result = locate_native_runtime(*mod.bundle); if (const auto* failure = std::get_if(&result)) { mod.nativeStatus = failure->status; log::write(mod.metadata.id, LOG_LEVEL_ERROR, "{}", failure->logMessage); fail_mod(mod, MOD_ERROR, native_status_message(failure->status)); return false; } const auto& native = std::get(result); if (!native.anyLibs && !(mod.nativeInPlace && !external_native_lib_path(mod).empty())) { mod.nativeStatus = NativeModStatus::None; return true; } mod.nativeStatus = NativeModStatus::Unknown; load_native(mod, native.entry, native.runtimeEntries); if (mod.nativeStatus != NativeModStatus::Loaded) { fail_mod(mod, MOD_ERROR, native_status_message(mod.nativeStatus)); return false; } return true; } void ModLoader::unload_native(LoadedMod& mod) { if (!mod.native || mod.nativeInPlace) { return; } // Deferred dlclose: this mod's code may still be on the stack below the current tick m_retiredNatives.push_back({std::move(mod.native), std::move(mod.nativeDir)}); mod.nativePath.clear(); mod.nativeDir.clear(); mod.nativeDirUtf8.clear(); } void ModLoader::drain_retired_natives() { for (auto& retired : m_retiredNatives) { retired.native.reset(); if (!retired.directory.empty()) { std::error_code ec; fs::remove_all(retired.directory, ec); } } m_retiredNatives.clear(); } static ModManifestInfo build_manifest_info(const ModMetaParsed& parsed) { ModManifestInfo info; info.imports.reserve(parsed.imports.size()); for (const auto* record : parsed.imports) { if (!svc::valid_service_id(record->service_id.chars)) { continue; } info.imports.push_back({ .id = record->service_id.chars, .major = record->major_version, .minMinor = record->min_minor_version, .required = (record->rec.flags & SERVICE_IMPORT_OPTIONAL) == 0, }); } info.exports.reserve(parsed.exports.size()); for (const auto* record : parsed.exports) { if (!svc::valid_service_id(record->service_id.chars)) { continue; } info.exports.push_back({ .id = record->service_id.chars, .major = record->major_version, }); } return info; } std::string escape_mod_id_for_config(std::string_view const id) { std::string buf; // Simple escaping. All characters in mod IDs literal, except for '.' and '_'. // '.' -> '_', '_' -> '__' for (char const chr : id) { if (chr == '.') { buf.push_back('_'); } else if (chr == '_') { buf.push_back('_'); buf.push_back('_'); } else { buf.push_back(chr); } } return buf; } static std::string mod_enabled_cvar_name(std::string_view const id) { return fmt::format("mod.{}.enabled", escape_mod_id_for_config(id)); } static bool required_deps_active(const LoadedMod& mod) { return std::ranges::all_of(mod.dependencies, [](const ModDependencyEdge& edge) { return !edge.required || edge.mod->active; }); } // A deferred export that was not published by the end of the provider's initialization can // never resolve, which is almost certainly a bug in the provider. static void warn_unpublished_deferred_exports(const LoadedMod& mod) { if (!mod.active || !mod.native) { return; } for (const auto* serviceExport : mod.native->parsed.exports) { if ((serviceExport->rec.flags & SERVICE_EXPORT_DEFERRED) == 0) { continue; } const auto* record = svc::find_service_record(serviceExport->service_id.chars, serviceExport->major_version); if (record != nullptr && record->service == nullptr) { log::write(mod.metadata.id, LOG_LEVEL_WARN, "declared deferred service '{}@{}' but never published it during initialization", serviceExport->service_id.chars, serviceExport->major_version); } } } LoadedMod* ModLoader::try_load_mod(const fs::path& modPath, bool fromDir, uint32_t searchDirIndex, std::unique_ptr bundle, std::optional metadata) { if (bundle == nullptr) { try { bundle = load_bundle(modPath, fromDir); } catch (const std::exception& e) { Log.error( "Failed to open {} bundle: {}", data::abbreviated_path_string(modPath), e.what()); return nullptr; } } if (!metadata) { try { metadata = load_metadata(modPath, *bundle); } catch (const std::exception& e) { Log.error("bad mod.json in {}: {}", data::abbreviated_path_string(modPath), e.what()); return nullptr; } } if (const auto* existing = find_mod(metadata->id)) { if (existing->searchDirIndex < searchDirIndex) { log::write(metadata->id, LOG_LEVEL_INFO, "{} shadowed by higher-priority duplicate {}", data::abbreviated_path_string(modPath), data::abbreviated_path_string(existing->modPath)); } else { log::write(metadata->id, LOG_LEVEL_ERROR, "duplicate mod id, not loading {}", data::abbreviated_path_string(modPath)); } return nullptr; } const auto& inserted = m_mods.emplace_back(std::make_unique()); auto& mod = *inserted; mod.active = true; mod.modPath = fs::absolute(modPath); mod.searchDirIndex = searchDirIndex; mod.nativeInPlace = m_searchDirs[searchDirIndex].inPlaceNative && fromDir; mod.fileIdentity = file_identity(modPath); mod.metadata = std::move(*metadata); mod.bundle = std::move(bundle); mod.context = std::make_unique(); mod.context->mod = &mod; mod.cvarIsEnabled = std::make_unique>(mod_enabled_cvar_name(mod.metadata.id), true); if (load_native_if_present(mod) && mod.native) { mod.manifestInfo = build_manifest_info(mod.native->parsed); } log::write(mod.metadata.id, LOG_LEVEL_INFO, "found '{}' v{} by {} ({})", mod.metadata.name, mod.metadata.version, mod.metadata.author, data::abbreviated_path_string(modPath)); return &mod; } bool ModLoader::activate_mod(LoadedMod& mod) { log::write(mod.metadata.id, LOG_LEVEL_INFO, "activating mod"); mod.active = true; // Asset-only mods have no lifecycle beyond their overlay files. if (!mod.native) { mod.enabledApplied = true; return true; } if (!mod.servicesRegistered) { if (!register_static_service_exports(mod)) { log::write(mod.metadata.id, LOG_LEVEL_ERROR, "failed to register service exports"); deactivate_mod(mod); return false; } mod.servicesRegistered = true; } if (!resolve_service_imports(mod)) { log::write(mod.metadata.id, LOG_LEVEL_ERROR, "failed to resolve service imports"); deactivate_mod(mod); return false; } svc::hook_resolve_mod_records(mod); *mod.native->contextSymbol = mod.context.get(); log::write(mod.metadata.id, LOG_LEVEL_TRACE, "calling mod_initialize"); try { ModError error = MOD_ERROR_INIT; const auto result = mod.native->fn_initialize(&error); if (result == MOD_OK && !mod.loadFailed) { mod.initialized = true; log::write(mod.metadata.id, LOG_LEVEL_TRACE, "mod_initialize succeeded"); } else if (result != MOD_OK && !mod.loadFailed) { fail_mod(mod, result, lifecycle_error_message("mod_initialize", result, error)); } } catch (const std::exception& e) { fail_mod(mod, MOD_ERROR, fmt::format("Exception in mod_initialize: {}", e.what())); } catch (...) { fail_mod(mod, MOD_ERROR, "Unknown exception in mod_initialize"); } warn_unpublished_deferred_exports(mod); if (!mod.active) { // Failed initialization may have left hooks or other service state behind deactivate_mod(mod); return false; } mod.enabledApplied = true; return true; } void ModLoader::deactivate_mod(LoadedMod& mod) { svc::modules_mod_deactivating(mod); if (mod.initialized && mod.native && mod.native->fn_shutdown) { log::write(mod.metadata.id, LOG_LEVEL_TRACE, "calling mod_shutdown"); try { ModError error = MOD_ERROR_INIT; const auto result = mod.native->fn_shutdown(&error); if (result == MOD_OK) { log::write(mod.metadata.id, LOG_LEVEL_TRACE, "mod_shutdown succeeded"); } else { log::write(mod.metadata.id, LOG_LEVEL_ERROR, "mod_shutdown failed: {}", lifecycle_error_message("mod_shutdown", result, error)); } } catch (...) { } } mod.initialized = false; if (mod.servicesRegistered) { svc::remove_services_for_provider(mod); mod.servicesRegistered = false; } svc::modules_mod_detached(mod); unload_native(mod); mod.active = false; mod.enabledApplied = false; } void ModLoader::init() { if (m_initialized) { return; } m_initialized = true; manifest::initialize(); #if DUSK_HAS_PREPATCH prepatch::initialize(); #endif if (m_searchDirs.empty()) { Log.warn("no mod search directories configured; mod loading skipped"); return; } if (m_cacheDir.empty()) { m_cacheDir = m_searchDirs.front().path / ".cache"; } std::error_code ec; // Stale libs from previous sessions (see load_native). fs::remove_all(m_cacheDir, ec); // A Windows update can be interrupted between moving the live archive aside and publishing // its replacement. Recover that narrow crash window before scanning the user directory. if (fs::is_directory(m_searchDirs.front().path, ec)) { for (const auto& entry : fs::directory_iterator(m_searchDirs.front().path, ec)) { const auto path = entry.path(); if (!entry.is_regular_file() || path.extension() != ".old" || path.stem().extension() != ".dusk") { continue; } auto primary = path; primary.replace_extension(); if (fs::exists(primary, ec)) { fs::remove(path, ec); } else { fs::rename(path, primary, ec); } if (ec) { Log.warn("failed to recover stale mod archive '{}': {}", data::abbreviated_path_string(path), ec.message()); ec.clear(); } } } for (size_t dirIndex = 0; dirIndex < m_searchDirs.size(); ++dirIndex) { const auto& searchDir = m_searchDirs[dirIndex]; // --mods can point the user dir at the bundled dir; don't scan the same dir twice. bool alreadyScanned = false; for (size_t earlier = 0; earlier < dirIndex && !alreadyScanned; ++earlier) { alreadyScanned = fs::equivalent(m_searchDirs[earlier].path, searchDir.path, ec); } if (alreadyScanned) { continue; } if (!fs::is_directory(searchDir.path)) { if (dirIndex == 0) { Log.info( "mods directory '{}' not found", data::abbreviated_path_string(searchDir.path)); } else { Log.debug( "mods directory '{}' not found", data::abbreviated_path_string(searchDir.path)); } continue; } std::vector entries; for (auto& e : fs::directory_iterator(searchDir.path, ec)) { if (e.is_directory() && fs::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(); }); for (auto& entry : entries) { (void)try_load_mod(entry.path(), entry.is_directory(), static_cast(dirIndex)); } } if (m_mods.empty()) { init_services(); Log.info("no mods found"); svc::modules_lifecycle_applied(); m_startupComplete = true; return; } std::stable_sort(m_mods.begin(), m_mods.end(), [](const auto& a, const auto& b) { return a->searchDirIndex > b->searchDirIndex; }); Log.info("initializing {} mod(s)...", m_mods.size()); for (auto& mod : mods()) { mod.enabledSubscription = Register(*mod.cvarIsEnabled, [this, &mod](const bool&, const bool&) { on_enabled_changed(mod); }); } init_services(); // Providers must initialize (and publish deferred services) before their importers, so // imports are resolved per mod, interleaved with initialization, in dependency order. loader::sort_mods(m_mods); // Decide the startup lifecycle state before publishing exports. Config-disabled mods and // mods blocked by required providers keep dependency edges but must not resolve or provide // services until they can actually initialize. for (auto& mod : mods()) { if (!mod.cvarIsEnabled->getValue()) { log::write(mod.metadata.id, LOG_LEVEL_INFO, "disabled by config"); mod.active = false; mod.suspendedByProvider = false; continue; } if (!mod.loadFailed && !required_deps_active(mod)) { log::write( mod.metadata.id, LOG_LEVEL_INFO, "suspended: a required provider is disabled"); mod.active = false; mod.suspendedByProvider = true; continue; } mod.suspendedByProvider = false; } for (auto& mod : mods()) { if (!mod.active || !mod.native) { continue; } if (register_static_service_exports(mod)) { mod.servicesRegistered = true; } else { log::write(mod.metadata.id, LOG_LEVEL_ERROR, "failed to register service exports"); deactivate_mod(mod); } } for (auto& mod : mods()) { if (mod.active) { activate_mod(mod); } } svc::modules_lifecycle_applied(); auto active = std::ranges::count_if(mods(), [](const LoadedMod& m) { return m.active; }); Log.info("{}/{} mod(s) active", active, m_mods.size()); m_startupComplete = true; } LoadedMod* ModLoader::find_mod(std::string_view id) { for (auto& mod : m_mods) { if (mod->metadata.id == id) { return mod.get(); } } return nullptr; } const LoadedMod* ModLoader::find_mod(std::string_view id) const { for (const auto& mod : m_mods) { if (mod->metadata.id == id) { return mod.get(); } } return nullptr; } void ModLoader::request_enable(std::string_view id) { if (auto* mod = find_mod(id)) { mod->cvarIsEnabled->setValue(true); } } void ModLoader::request_disable(std::string_view id) { if (auto* mod = find_mod(id)) { mod->cvarIsEnabled->setValue(false); } } ModOperationHandle ModLoader::request_reload(std::string_view id) { auto operation = std::make_shared(); if (find_mod(id) != nullptr) { m_pendingRequests.push_back(ReloadRequest{ .modId = std::string{id}, .operation = operation, }); } else { complete_operation(operation, false, "The mod is no longer installed"); } return operation; } ModOperationHandle ModLoader::request_install(fs::path path) { auto operation = std::make_shared(); m_pendingRequests.push_back(InstallRequest{ .stagedPath = std::move(path), .operation = operation, }); return operation; } ModOperationHandle ModLoader::request_uninstall(std::string_view id) { auto operation = std::make_shared(); if (find_mod(id) != nullptr) { m_pendingRequests.push_back(UninstallRequest{ .modId = std::string{id}, .operation = operation, }); } else { complete_operation(operation); } return operation; } ModOperationHandle ModLoader::request_reactivate(std::string_view id) { auto operation = std::make_shared(); m_pendingRequests.push_back(LifecycleRequest{ .modId = std::string{id}, .action = LifecycleAction::Reactivate, .operation = operation, }); return operation; } fs::path ModLoader::user_mods_dir() const { return m_searchDirs.empty() ? fs::path{} : m_searchDirs.front().path; } bool ModLoader::can_uninstall(const LoadedMod& mod) const { return mod.searchDirIndex == 0 && mod.modPath.extension() == ".dusk"; } void ModLoader::notify_mod_failure(LoadedMod& mod, bool firstFailure) { if (firstFailure) { m_pendingFailures.push_back(mod.metadata.name); } // Startup failures are handled inline by activate_mod if (!m_startupComplete) { return; } m_pendingRequests.push_back(LifecycleRequest{ .modId = mod.metadata.id, .action = LifecycleAction::Disable, }); } void ModLoader::flush_toasts() { if (m_pendingFailures.empty()) { return; } const auto names = std::exchange(m_pendingFailures, {}); // Skip displaying toasts if the mods window is currently open if (const auto* window = dynamic_cast(ui::top_document())) { if (window->visible()) { return; } } ui::Toast toast{.type = "warning", .duration = std::chrono::seconds{5}}; if (names.size() == 1) { toast.title = "Mod failed"; toast.content = fmt::format("
{} failed and was disabled.
Check Mods for " "more information.
", ui::escape(names.front())); } else { toast.title = "Mods failed"; toast.content = fmt::format("
{} mods failed and were disabled.
Check " "Mods for more information.
", names.size()); } ui::push_toast(std::move(toast)); } std::vector ModLoader::collect_lifecycle_set(LoadedMod& target) { std::vector included{&target}; std::vector pending{&target}; while (!pending.empty()) { auto* current = pending.back(); pending.pop_back(); for (const auto& edge : current->dependents) { auto* dependent = edge.mod; if (!dependent->active && !dependent->suspendedByProvider) { continue; } if (std::ranges::find(included, dependent) != included.end()) { continue; } included.push_back(dependent); pending.push_back(dependent); } } std::vector ordered; ordered.reserve(included.size()); for (auto& mod : mods()) { if (std::ranges::find(included, &mod) != included.end()) { ordered.push_back(&mod); } } return ordered; } bool ModLoader::ensure_native_loaded(LoadedMod& mod) { if (mod.native || mod.nativeStatus == NativeModStatus::None) { return true; } return load_native_if_present(mod); } bool ModLoader::reload_bundle(LoadedMod& mod) { log::write(mod.metadata.id, LOG_LEVEL_INFO, "reloading from {}", data::abbreviated_path_string(mod.modPath)); std::shared_ptr newBundle; ModMetadata newMetadata; try { std::error_code ec; newBundle = load_bundle(mod.modPath, fs::is_directory(mod.modPath, ec)); newMetadata = load_metadata(mod.modPath, *newBundle); } catch (const std::exception& e) { fail_mod(mod, MOD_ERROR, fmt::format("Reload failed: {}", e.what())); return false; } if (newMetadata.id != mod.metadata.id) { fail_mod(mod, MOD_CONFLICT, fmt::format("Mod ID changed on reload ('{}'); restart required", newMetadata.id)); return false; } mod.metadata = std::move(newMetadata); // In-flight readers of the old bundle keep it alive through their shared_ptr. mod.bundle = std::move(newBundle); mod.fileIdentity = file_identity(mod.modPath); mod.loadFailed = false; mod.failureReason.clear(); ModManifestInfo newInfo; if (!load_native_if_present(mod)) { return false; } if (mod.native) { newInfo = build_manifest_info(mod.native->parsed); } else { ++mod.cacheGeneration; } if (newInfo != mod.manifestInfo) { // The reload changes the mod's imports/exports; rebuild the dependency graph so edges, // init/tick/shutdown order and cascade sets reflect the new manifest. log::write(mod.metadata.id, LOG_LEVEL_INFO, "changed its service imports/exports; rebuilding mod dependency graph"); mod.manifestInfo = std::move(newInfo); loader::sort_mods(m_mods); } return true; } void ModLoader::resume_lifecycle_set(const std::vector& affected) { // Publish every candidate's static exports before any initialize, so optional cycles and // provider changes use the same ordering rules as startup. for (auto* mod : affected) { if (mod->active || mod->loadFailed || !mod->cvarIsEnabled->getValue()) { continue; } if (!ensure_native_loaded(*mod)) { continue; } if (mod->native && !mod->servicesRegistered) { if (register_static_service_exports(*mod)) { mod->servicesRegistered = true; } else { log::write(mod->metadata.id, LOG_LEVEL_ERROR, "failed to register service exports"); deactivate_mod(*mod); } } } for (auto* mod : affected) { if (mod->active || mod->loadFailed || !mod->cvarIsEnabled->getValue()) { continue; } if (!required_deps_active(*mod)) { mod->suspendedByProvider = true; log::write( mod->metadata.id, LOG_LEVEL_INFO, "suspended: a required provider is disabled"); continue; } mod->suspendedByProvider = false; activate_mod(*mod); } for (auto* mod : affected) { if (!mod->active && mod->servicesRegistered) { svc::remove_services_for_provider(*mod); mod->servicesRegistered = false; } } } void ModLoader::apply_lifecycle_change(LoadedMod& target, const bool reload) { auto affected = collect_lifecycle_set(target); // Dependents first (reverse init order), like shutdown. for (auto* mod : affected | std::views::reverse) { const bool needsTeardown = mod->active || (mod == &target && (mod->initialized || (reload && mod->native != nullptr))); if (!needsTeardown) { continue; } const bool wasActive = mod->active; log::write(mod->metadata.id, LOG_LEVEL_INFO, "deactivating mod"); deactivate_mod(*mod); if (mod != &target && wasActive) { // Provisional; cleared below if the mod comes straight back up. mod->suspendedByProvider = true; } } if (reload) { // On failure the target is failed and stays down; dependents get resume attempts below // and suspend against the failed provider where required. reload_bundle(target); // The reload may have rebuilt the dependency graph and reordered m_mods; refresh the // set's iteration order so reactivation still runs providers first. std::vector reordered; reordered.reserve(affected.size()); for (auto& mod : mods()) { if (std::ranges::find(affected, &mod) != affected.end()) { reordered.push_back(&mod); } } affected = std::move(reordered); } resume_lifecycle_set(affected); } void ModLoader::on_enabled_changed(LoadedMod& mod) { svc::config_mark_dirty(); if (mod.loadFailed) { if (!mod.cvarIsEnabled->getValue()) { mod.loadFailed = false; mod.failureReason.clear(); } return; } if (mod.suspendedByProvider) { if (!mod.cvarIsEnabled->getValue()) { // The user disabled a suspended mod; stop waiting for its providers. mod.suspendedByProvider = false; } return; } m_pendingRequests.push_back(LifecycleRequest{ .modId = mod.metadata.id, .action = mod.cvarIsEnabled->getValue() ? LifecycleAction::Enable : LifecycleAction::Disable, }); } void ModLoader::forget_mod(LoadedMod& mod) { auto affected = collect_lifecycle_set(mod); std::vector blocked; std::vector pending{&mod}; while (!pending.empty()) { auto* provider = pending.back(); pending.pop_back(); for (const auto& edge : provider->dependents) { if (!edge.required || edge.mod == nullptr || std::ranges::find(blocked, edge.mod) != blocked.end()) { continue; } blocked.push_back(edge.mod); pending.push_back(edge.mod); } } for (auto* affectedMod : affected | std::views::reverse) { const bool wasActive = affectedMod->active; if (affectedMod->active || affectedMod->initialized || affectedMod->native != nullptr) { log::write(affectedMod->metadata.id, LOG_LEVEL_INFO, "deactivating mod"); deactivate_mod(*affectedMod); } if (affectedMod != &mod && wasActive) { affectedMod->suspendedByProvider = true; } } const std::string modId = mod.metadata.id; auto* removedMod = &mod; if (mod.enabledSubscription != 0) { config::unsubscribe(mod.enabledSubscription); mod.enabledSubscription = 0; } unregister(*mod.cvarIsEnabled); std::vector remaining; remaining.reserve(affected.size()); for (auto* affectedMod : affected) { if (affectedMod != &mod) { remaining.push_back(affectedMod); } } std::erase_if( m_mods, [removedMod](const auto& candidate) { return candidate.get() == removedMod; }); loader::sort_mods(m_mods); std::vector resumable; for (auto* affectedMod : remaining) { const bool needsRemovedProvider = std::ranges::find(blocked, affectedMod) != blocked.end(); affectedMod->suspendedByProvider = needsRemovedProvider; if (needsRemovedProvider) { log::write(affectedMod->metadata.id, LOG_LEVEL_INFO, "suspended: required provider '{}' was removed", modId); } else { resumable.push_back(affectedMod); } } resume_lifecycle_set(resumable); ++m_generation; log::write(modId, LOG_LEVEL_INFO, "forgot removed package"); } ModLoader::OperationResult ModLoader::load_runtime_mod(const fs::path& requestedPath) { const auto path = fs::absolute(requestedPath).lexically_normal(); std::error_code error; const auto status = fs::status(path, error); if (error || !fs::exists(status)) { return { .success = false, .message = error ? fmt::format("Could not inspect the package: {}", error.message()) : "The package was not found", }; } const bool fromDir = fs::is_directory(status); std::unique_ptr bundle; std::optional metadata; try { bundle = load_bundle(path, fromDir); metadata = load_metadata(path, *bundle); } catch (const std::exception& exception) { return { .success = false, .message = fmt::format("Invalid mod package: {}", exception.what()), }; } if (const auto* duplicate = find_mod(metadata->id)) { return { .success = false, .message = fmt::format("A mod with this ID is already loaded from {}", data::abbreviated_path_string(duplicate->modPath)), }; } auto* mod = try_load_mod(path, fromDir, 0, std::move(bundle), std::move(metadata)); if (mod == nullptr) { return { .success = false, .message = "The mod could not be loaded", }; } mod->enabledSubscription = Register( *mod->cvarIsEnabled, [this, mod](const bool&, const bool&) { on_enabled_changed(*mod); }); loader::sort_mods(m_mods); if (!mod->cvarIsEnabled->getValue()) { mod->active = false; mod->suspendedByProvider = false; log::write(mod->metadata.id, LOG_LEVEL_INFO, "installed disabled by config"); } else if (!mod->loadFailed) { mod->active = false; apply_lifecycle_change(*mod, false); } ++m_generation; log::write(mod->metadata.id, LOG_LEVEL_INFO, "installed at runtime"); return runtime_result(*mod); } ModLoader::OperationResult ModLoader::reload_runtime_mod(LoadedMod& mod) { if (mod.nativeInPlace) { return { .success = false, .message = "Built-in mods cannot be updated in-game", .mod = &mod, }; } apply_lifecycle_change(mod, true); ++m_generation; return runtime_result(mod); } ModLoader::OperationResult ModLoader::runtime_result(LoadedMod& mod) { if (mod.loadFailed) { return { .success = false, .message = mod.failureReason.empty() ? "The mod failed to activate" : mod.failureReason, .mod = &mod, }; } if (mod.cvarIsEnabled->getValue() && !mod.active) { return { .success = false, .message = "A required provider is unavailable", .mod = &mod, }; } if (!mod.cvarIsEnabled->getValue()) { return { .message = "Installed, disabled by config", .mod = &mod, }; } return {.mod = &mod}; } ModLoader::OperationResult ModLoader::install_staged(const fs::path& requestedPath) { if (m_searchDirs.empty()) { return { .success = false, .message = "No writable mods directory is configured", }; } std::error_code error; const auto userDir = fs::weakly_canonical(m_searchDirs.front().path, error); if (error) { return { .success = false, .message = fmt::format("Could not resolve the user mods directory: {}", error.message()), }; } const auto stagingDir = userDir / ".staging"; const auto path = fs::weakly_canonical(requestedPath, error); const bool stagedName = path.extension() == ".part" && path.stem().extension() == ".dusk"; if (error || path.parent_path() != stagingDir || !stagedName || !fs::is_regular_file(path, error)) { Log.error("refusing staged install from {}", data::abbreviated_path_string(requestedPath)); return { .success = false, .message = "The package is not in the mod staging directory", }; } ModMetadata metadata; std::string validationError; if (!inspect_mod_bundle(path, metadata, validationError)) { return { .success = false, .message = fmt::format("Invalid mod package: {}", validationError), }; } fs::path destination; auto* installed = find_mod(metadata.id); if (installed != nullptr) { if (!can_uninstall(*installed)) { return { .success = false, .message = "This bundled mod cannot be updated in-game", }; } destination = installed->modPath; } else { destination = userDir / fmt::format("{}.dusk", safe_filename(metadata.id)); } std::string replaceError; #ifdef _WIN32 fs::path aside = destination; aside += ".old"; const bool hadDestination = fs::exists(destination, error); if (hadDestination) { fs::remove(aside, error); error.clear(); fs::rename(destination, aside, error); if (error) { return { .success = false, .message = fmt::format("Failed to prepare the installed package: {}", error.message()), }; } } if (!borealis::io::atomic_replace(path, destination, replaceError)) { if (hadDestination) { std::error_code restoreError; fs::rename(aside, destination, restoreError); } return { .success = false, .message = std::move(replaceError), }; } auto result = installed != nullptr ? reload_runtime_mod(*installed) : load_runtime_mod(destination); if (hadDestination) { fs::remove(aside, error); } return result; #else if (!borealis::io::atomic_replace(path, destination, replaceError)) { return { .success = false, .message = std::move(replaceError), }; } return installed != nullptr ? reload_runtime_mod(*installed) : load_runtime_mod(destination); #endif } void ModLoader::apply_pending_requests() { // Images retired by the previous tick have had a full frame to unwind off the stack. drain_retired_natives(); if (m_pendingRequests.empty()) { return; } // Package mutations retain request order. Enable/disable/reactivate can still coalesce per mod. const auto requests = std::exchange(m_pendingRequests, {}); std::vector coalesced; for (const auto& request : requests) { if (const auto* install = std::get_if(&request)) { auto result = install_staged(install->stagedPath); if (result.success && result.mod != nullptr) { ui::push_toast({ .title = "Mod installed", .content = fmt::format( "{} {}", result.mod->metadata.name, result.mod->metadata.version), .duration = std::chrono::seconds{4}, }); } else if (!result.success && result.mod == nullptr) { ui::push_toast({ .type = "warning", .title = "Mod install failed", .content = result.message.empty() ? "The loader rejected the package" : result.message, .duration = std::chrono::seconds{6}, }); } if (!result.success && !m_searchDirs.empty()) { // request_install owns only files under the configured staging directory. std::error_code error; const auto userDir = fs::weakly_canonical(m_searchDirs.front().path, error); if (!error) { const auto stagedPath = fs::weakly_canonical(install->stagedPath, error); if (!error && stagedPath.parent_path() == userDir / ".staging") { fs::remove(stagedPath, error); } } } complete_operation(install->operation, result.success, std::move(result.message)); continue; } if (const auto* reload = std::get_if(&request)) { auto* mod = find_mod(reload->modId); if (mod == nullptr) { complete_operation(reload->operation, false, "The mod is no longer installed"); continue; } auto result = reload_runtime_mod(*mod); complete_operation(reload->operation, result.success, std::move(result.message)); continue; } if (const auto* uninstall = std::get_if(&request)) { auto* mod = find_mod(uninstall->modId); if (mod == nullptr) { complete_operation(uninstall->operation); continue; } if (!can_uninstall(*mod)) { complete_operation( uninstall->operation, false, "The mod is part of this Dusklight installation"); continue; } const auto removedName = mod->metadata.name; std::error_code error; if (!fs::remove(mod->modPath, error)) { complete_operation(uninstall->operation, false, error ? error.message() : "The package was not found"); continue; } forget_mod(*mod); complete_operation(uninstall->operation); ui::push_toast({ .title = "Mod uninstalled", .content = removedName, .duration = std::chrono::seconds{4}, }); continue; } const auto& lifecycle = std::get(request); const auto existing = std::ranges::find(coalesced, lifecycle.modId, &LifecycleRequest::modId); if (existing != coalesced.end()) { complete_operation( existing->operation, false, "Superseded by a newer lifecycle request"); *existing = lifecycle; } else { coalesced.push_back(lifecycle); } } for (const auto& request : coalesced) { auto* mod = find_mod(request.modId); if (mod == nullptr) { Log.warn("lifecycle request for unknown mod '{}'", request.modId); complete_operation(request.operation, false, "The mod is no longer installed"); continue; } if (request.action == LifecycleAction::Enable && mod->enabledApplied) { continue; } if (request.action == LifecycleAction::Disable && !mod->enabledApplied && !mod->active) { continue; } if (request.action == LifecycleAction::Reactivate) { mod->loadFailed = false; mod->failureReason.clear(); mod->suspendedByProvider = false; if (!mod->cvarIsEnabled->getValue()) { mod->cvarIsEnabled->setValue(true); } } apply_lifecycle_change(*mod, false); if (request.action == LifecycleAction::Reactivate) { std::string error; if (mod->loadFailed) { error = mod->failureReason.empty() ? "The mod failed to activate" : mod->failureReason; } else if (mod->cvarIsEnabled->getValue() && !mod->active) { error = "A required provider is unavailable"; } complete_operation(request.operation, error.empty(), std::move(error)); } } svc::modules_lifecycle_applied(); auto active = std::ranges::count_if(mods(), [](const LoadedMod& m) { return m.active; }); Log.info("{}/{} mod(s) active", active, m_mods.size()); } void ModLoader::tick() { svc::modules_frame_begin(); apply_pending_requests(); for (auto& mod : mods()) { if (!mod.active || !mod.native) { continue; } try { ModError error = MOD_ERROR_INIT; const auto result = mod.native->fn_update(&error); if (result != MOD_OK) { fail_mod(mod, result, lifecycle_error_message("mod_update", result, error)); } } catch (const std::exception& e) { fail_mod(mod, MOD_ERROR, fmt::format("Exception in mod_update: {}", e.what())); } catch (...) { fail_mod(mod, MOD_ERROR, "Unknown exception in mod_update"); } } svc::modules_frame_end(); flush_toasts(); } void ModLoader::shutdown() { // Reverse initialization order, so importers shut down before their service providers. for (auto& mod : mods() | std::views::reverse) { deactivate_mod(mod); if (mod.enabledSubscription != 0) { config::unsubscribe(mod.enabledSubscription); mod.enabledSubscription = 0; } unregister(*mod.cvarIsEnabled); } m_mods.clear(); drain_retired_natives(); svc::modules_shutdown(); Log.info("all mods unloaded"); } } // namespace dusk::mods