#pragma once #include "dusk/config.hpp" #include "dusk/config_var.hpp" #include "mods/api.h" #include "mods/runtime.h" #include #include #include #include #include #include #include #include namespace dusk::mods { struct LoadedMod; class ModBundle; } // namespace dusk::mods struct ModContext { dusk::mods::LoadedMod* mod = nullptr; }; namespace dusk::mods::loader { class NativeModule; } // namespace dusk::mods::loader namespace dusk::mods { struct ModDependencyEdge { LoadedMod* mod = nullptr; bool required = false; }; struct ModManifestInfo { struct Import { std::string id; uint16_t major = 0; uint16_t minMinor = 0; bool required = false; bool operator==(const Import&) const = default; }; struct Export { std::string id; uint16_t major = 0; bool operator==(const Export&) const = default; }; std::vector imports; std::vector exports; bool operator==(const ModManifestInfo&) const = default; }; struct DelegatedModRuntime { std::string id; uint16_t major = 0; uint16_t minMinor = 0; const ModRuntimeService* service = nullptr; ModContext* providerContext = nullptr; bool operator==(const DelegatedModRuntime& other) const { return id == other.id && major == other.major && minMinor == other.minMinor; } }; struct ModMetadata { std::string id; std::string name; std::string version; std::string author; std::string description; std::string iconPath; std::string bannerPath; }; struct ModSearchDir { std::filesystem::path path; // Directory bundles dlopen their native lib in place instead of extracting it to the cache. // Required where extracted code cannot run (iOS), desirable for signed/read-only installs. bool inPlaceNative = false; // Native library location for platforms that restrict placement (e.g. iOS/tvOS Frameworks/) std::filesystem::path nativeLibDir; }; struct ModOperation { enum class State : u8 { Pending, Succeeded, Failed, }; State state = State::Pending; std::string message; }; using ModOperationHandle = std::shared_ptr; struct ModMetaParsed { uint32_t abiVersion = 0; std::vector imports; std::vector exports; std::vector hookFns; std::vector hookMems; std::vector hookMemExts; std::vector hookNames; }; inline const char* hook_mem_vtable_symbol(const ModMetaHookMem& rec) { return reinterpret_cast(&rec) + sizeof(ModMetaHookMem); } inline const char* hook_mem_display_name(const ModMetaHookMem& rec) { const char* vtable = hook_mem_vtable_symbol(rec); return vtable + std::char_traits::length(vtable) + 1; } inline const char* hook_mem_vtable_symbol(const ModMetaHookMemExt& rec) { return reinterpret_cast(&rec) + sizeof(ModMetaHookMemExt); } inline const char* hook_mem_display_name(const ModMetaHookMemExt& rec) { const char* vtable = hook_mem_vtable_symbol(rec); return vtable + std::char_traits::length(vtable) + 1; } inline const char* hook_name_symbol(const ModMetaHookName& rec) { return reinterpret_cast(&rec) + sizeof(ModMetaHookName); } struct NativeMod { std::unique_ptr handle; const ModMeta* meta = nullptr; ModMetaParsed parsed; ModContext** contextSymbol = nullptr; ModInitializeFn fn_initialize = nullptr; ModUpdateFn fn_update = nullptr; ModShutdownFn fn_shutdown = nullptr; }; enum class NativeModStatus : u8 { /** * Mod does not have native code included. */ None, /** * Native code mod loaded successfully. * * Note that this only indicates load status of the native library. If the native lib throws in * its init function, it will still be disabled! */ Loaded, /** * This build was compiled without native mod support! */ BuildDisabled, /** * Mod ships native libraries, but none matches this build's platform and architecture. */ ModMissingPlatform, /** * Mod is built for a different ABI version than this build of the game. */ ApiVersionMismatch, /** * Mod is missing a required native API export. */ MissingExport, /** * Mod's metadata record section is malformed. */ InvalidMetadata, /** * Mod bundle contains files in an invalid location. */ InvalidBundle, /** * Unknown error loading the native mod. */ Unknown, }; struct LoadedMod { struct FileIdentity { std::uintmax_t size = 0; std::filesystem::file_time_type modified{}; bool valid = false; bool operator==(const FileIdentity&) const = default; }; ModMetadata metadata; std::filesystem::path modPath; std::filesystem::path dir; // Stable UTF-8 storage for HostService::mod_dir. std::string dirUtf8; // Stable UTF-8 storage for HostService::data_dir. std::string dataDirUtf8; uint32_t searchDirIndex = 0; bool fromDirectory = false; // Native lib is dlopen'd in place. bool nativeInPlace = false; bool hasUserPackage = false; bool hasBundledCopy = false; FileIdentity fileIdentity; std::unique_ptr> cvarIsEnabled; config::Subscription enabledSubscription = 0; bool active = false; bool loadFailed = false; std::string failureReason; // Initialization succeeded; shutdown is owed on deactivation. bool initialized = false; // Static service exports are currently present in the registry. bool servicesRegistered = false; // Lifecycle state last applied by the loader; diffed against cvarIsEnabled to pick up // runtime enable/disable requests. bool enabledApplied = false; // Deactivated because a provider it imports from was disabled, not by its own cvar. bool suspendedByProvider = false; // Bumped per native lib extraction so every dlopen sees a fresh path (and thus a fresh // image with fresh statics; a previous dlclose may not fully unmap). Also bumped by // asset-only reloads, so it doubles as a generation for anything caching per-mod content. uint32_t cacheGeneration = 0; // Currently extracted native library, empty if none. std::filesystem::path nativePath; // Read-only directory containing the current platform's main module and runtime libraries. std::filesystem::path nativeDir; // Stable UTF-8 storage for HostService::native_dir. std::string nativeDirUtf8; NativeModStatus nativeStatus = NativeModStatus::None; std::unique_ptr native; std::optional runtime; std::unique_ptr context; // Shared with overlay file registrations so in-flight DVD reads survive disable/reload. std::shared_ptr bundle; ModManifestInfo manifestInfo; // Mods this mod imports services from, and mods importing services from this mod. std::vector dependencies; std::vector dependents; [[nodiscard]] bool is_enabled() const { return cvarIsEnabled != nullptr && cvarIsEnabled->getValue(); } [[nodiscard]] bool activation_failed() const { return loadFailed || (is_enabled() && !active); } }; struct PackageCandidate; class ModLoader { public: static ModLoader& instance(); void set_search_dirs(std::vector dirs) { m_searchDirs = std::move(dirs); } void set_cache_dir(std::filesystem::path dir) { m_cacheDir = std::move(dir); } void init(); void tick(); void shutdown(); void request_enable(std::string_view id); void request_disable(std::string_view id); ModOperationHandle request_reload(std::string_view id); ModOperationHandle request_install(std::filesystem::path path); ModOperationHandle request_uninstall(std::string_view id); ModOperationHandle request_reactivate(std::string_view id); void notify_mod_failure(LoadedMod& mod, bool firstFailure); [[nodiscard]] std::filesystem::path user_mods_dir() const; [[nodiscard]] bool can_uninstall(const LoadedMod& mod) const; [[nodiscard]] bool can_update(const LoadedMod& mod) const; [[nodiscard]] LoadedMod* find_mod(std::string_view id); [[nodiscard]] const LoadedMod* find_mod(std::string_view id) const; [[nodiscard]] uint64_t generation() const noexcept { return m_generation; } [[nodiscard]] auto mods() const { return m_mods | std::views::transform([](const auto& m) -> LoadedMod& { return *m; }); } [[nodiscard]] auto active_mods() const { return mods() | std::views::filter([](const auto& m) { return m.active; }); } private: enum class LifecycleAction : u8 { Enable, Disable, Reactivate }; struct LifecycleRequest { std::string modId; LifecycleAction action; std::shared_ptr operation; }; struct InstallRequest { std::filesystem::path stagedPath; std::shared_ptr operation; }; struct ReloadRequest { std::string modId; std::shared_ptr operation; }; struct UninstallRequest { std::string modId; std::shared_ptr operation; }; using Request = std::variant; struct OperationResult { bool success = true; std::string message; LoadedMod* mod = nullptr; }; // ModLoader::tick runs inside fapGm_Execute, so code from an unloading mod can still be // live on the stack (its frame unwinds after the tick). dlclose is therefore deferred to // the next tick, by which point every per-frame entry into the mod should have returned. struct RetiredNative { std::unique_ptr native; std::filesystem::path directory; }; std::vector> m_mods; std::vector m_searchDirs; std::filesystem::path m_cacheDir; std::vector m_pendingRequests; std::vector m_pendingFailures; std::vector m_retiredNatives; uint64_t m_generation = 0; bool m_initialized = false; bool m_startupComplete = false; LoadedMod* try_load_mod(const std::filesystem::path& modPath, bool fromDir, uint32_t searchDirIndex, std::unique_ptr bundle = {}); void load_native(LoadedMod& mod, const std::string& dllEntry, const std::vector& runtimeEntries); bool load_native_if_present(LoadedMod& mod); // Resolved / if it exists on disk, empty otherwise. [[nodiscard]] std::filesystem::path external_native_lib_path(const LoadedMod& mod) const; void unload_native(LoadedMod& mod); // Registers exports (if needed), resolves imports and runs mod_initialize. // Returns whether the mod ended up active; failures go through fail_mod. bool activate_mod(LoadedMod& mod); // Runs mod_shutdown (if needed), detaches the mod from every service, and unloads the // native lib. Must only run with no mod code on the stack (startup, shutdown, or top of tick). void deactivate_mod(LoadedMod& mod); void init_services(); bool register_static_service_exports(LoadedMod& mod); bool resolve_service_imports(LoadedMod& mod); [[nodiscard]] std::string describe_missing_import( const char* serviceId, uint16_t majorVersion, uint16_t minMinorVersion) const; void drain_retired_natives(); void apply_pending_requests(); [[nodiscard]] OperationResult install_staged(const std::filesystem::path& path); [[nodiscard]] OperationResult load_runtime_mod(const std::filesystem::path& path); [[nodiscard]] OperationResult reload_runtime_mod( LoadedMod& mod, const PackageCandidate* replacement = nullptr); [[nodiscard]] OperationResult uninstall_runtime_mod(LoadedMod& mod); [[nodiscard]] OperationResult runtime_result(LoadedMod& mod); void forget_mod(LoadedMod& mod); void flush_toasts(); void on_enabled_changed(LoadedMod& mod); // Deactivates `target` (if needed) and its transitive dependents, optionally re-reads the // bundle from disk, then reactivates whatever the current cvar/provider state allows. void apply_lifecycle_change( LoadedMod& target, bool reload, const PackageCandidate* replacement = nullptr); // `target` plus transitive active/suspended dependents, in m_mods (init) order. std::vector collect_lifecycle_set(LoadedMod& target) const; void resume_lifecycle_set(const std::vector& mods); bool reload_bundle(LoadedMod& mod); bool ensure_native_loaded(LoadedMod& mod); }; bool inspect_mod_bundle(const std::filesystem::path& path, ModMetadata& metadata, std::string& error, bool* hasNative = nullptr) noexcept; using ModIndex = std::ranges::range_difference_t().mods())>; } // namespace dusk::mods