diff --git a/docs/modding.md b/docs/modding.md index c05e736d8b..73457ae7fa 100644 --- a/docs/modding.md +++ b/docs/modding.md @@ -209,8 +209,8 @@ if (svc_resource->load(mod_ctx, "config.txt", &buf) == MOD_OK) { } ``` -Missing files return `MOD_UNAVAILABLE`. Always `free` what you `load`. Note that the bundle is read-only; for writable -storage, use the directory from `svc_host->mod_dir(mod_ctx)`. +Missing files return `MOD_UNAVAILABLE`. Always `free` what you `load`. The bundle is read-only; use +`HostService::data_dir` for persistent storage. ### HostService (`mods/svc/host.h`) @@ -219,9 +219,17 @@ Mod metadata and runtime interaction with the loader: ```cpp IMPORT_SERVICE(HostService, svc_host); -const char* id = svc_host->mod_id(mod_ctx); -const char* dir = svc_host->mod_dir(mod_ctx); // writable per-mod directory -svc_host->fail(mod_ctx, MOD_ERROR, "something unrecoverable happened"); // disables the mod +// Temporary mod data directory, wiped on startup +const char* cacheDir = svc_host->mod_dir(mod_ctx); + +// Persistent mod data directory +const char* dataDir = nullptr; +if (svc_host->data_dir(mod_ctx, &dataDir) == MOD_OK) { + // ... +} + +// Report an error and disable the mod +svc_host->fail(mod_ctx, MOD_ERROR, "something unrecoverable happened"); ``` `get_service`/`publish_service` provide dynamic service lookup; see [Exporting Services](#exporting-services). @@ -927,6 +935,6 @@ const char* nativeDir = svc_host->native_dir(mod_ctx); // read-only ``` Libraries loaded explicitly by the mod remain its responsibility: stop their threads and unload them during -`mod_shutdown`. Do not write into `native_dir`; use `mod_dir` for writable state. Native library namespaces are -process-wide on some platforms, so two mods cannot safely assume that incompatible libraries with the same filename -will remain isolated. +`mod_shutdown`. Do not write into `native_dir`; use `data_dir` for persistent storage or `mod_dir` for temporary +(session) storage. Native library namespaces are process-wide on some platforms, so two mods cannot safely assume that +incompatible libraries with the same filename will remain isolated. diff --git a/sdk/include/mods/svc/host.h b/sdk/include/mods/svc/host.h index 24265cab99..45d89eea3e 100644 --- a/sdk/include/mods/svc/host.h +++ b/sdk/include/mods/svc/host.h @@ -13,7 +13,7 @@ #define HOST_SERVICE_ID "dev.twilitrealm.dusklight.host" #define HOST_SERVICE_MAJOR 2u -#define HOST_SERVICE_MINOR 1u +#define HOST_SERVICE_MINOR 2u /* * Ignore unknown values: later service minors may add events. @@ -96,15 +96,24 @@ typedef struct HostService { ModContext* ctx, ModLifecycleFn fn, void* user_data, uint64_t* out_handle); ModResult (*unwatch_mod_lifecycle)(ModContext* ctx, uint64_t handle); + /* Minor version 1 */ + /* * Read-only directory containing this platform's packaged native runtime: the mod module * and any RUNTIME_LIBRARIES. The path is absolute and remains valid until mod_shutdown * returns. Libraries loaded dynamically from here are owned by the mod and must be unloaded * during mod_shutdown. - * - * Added in minor version 1. */ const char* (*native_dir)(ModContext* ctx); + + /* Minor version 2 */ + + /* + * A persistent writable directory reserved for the calling mod, created on first use. + * + * The returned path remains valid until mod_shutdown returns. *out_path is null on failure. + */ + ModResult (*data_dir)(ModContext* ctx, const char** out_path); } HostService; MOD_DECLARE_SERVICE(HostService, svc_host, HOST_SERVICE_ID, HOST_SERVICE_MAJOR, HOST_SERVICE_MINOR); diff --git a/sdk/include/mods/svc/resource.h b/sdk/include/mods/svc/resource.h index 57fddc9778..60b609d2a2 100644 --- a/sdk/include/mods/svc/resource.h +++ b/sdk/include/mods/svc/resource.h @@ -8,7 +8,8 @@ /* * Read-only access to the res/ tree of the calling mod's own bundle. Reload serves the new - * bundle's contents. For writable storage, use HostService::mod_dir. + * bundle's contents. Use HostService::data_dir for persistent storage or HostService::mod_dir + * for temporary storage. */ #define RESOURCE_SERVICE_ID "dev.twilitrealm.dusklight.resource" diff --git a/src/dusk/mod_loader.hpp b/src/dusk/mod_loader.hpp index fad9ba1501..bf3e93aa53 100644 --- a/src/dusk/mod_loader.hpp +++ b/src/dusk/mod_loader.hpp @@ -166,6 +166,8 @@ struct LoadedMod { 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; // Native lib is dlopen'd in place and stays resident for the session. Reload is unsupported. diff --git a/src/dusk/mods/svc/host.cpp b/src/dusk/mods/svc/host.cpp index 7d6e15b809..a8e975e22f 100644 --- a/src/dusk/mods/svc/host.cpp +++ b/src/dusk/mods/svc/host.cpp @@ -1,13 +1,17 @@ #include "registry.hpp" #include "slot_map.hpp" +#include "dusk/main.h" #include "dusk/mods/loader/loader.hpp" +#include "dusk/mods/log_buffer.hpp" #include "dusk/mods/manifest.hpp" #include "fmt/format.h" #include -#include +#include #include +#include +#include namespace dusk::mods::svc { namespace { @@ -68,6 +72,41 @@ const char* host_native_dir(ModContext* context) { return mod != nullptr ? mod->nativeDirUtf8.c_str() : ""; } +ModResult host_data_dir(ModContext* context, const char** outPath) { + if (outPath == nullptr) { + return MOD_INVALID_ARGUMENT; + } + *outPath = nullptr; + + auto* mod = mod_from_context(context); + if (mod == nullptr) { + return MOD_INVALID_ARGUMENT; + } + + if (mod->dataDirUtf8.empty()) { + namespace fs = std::filesystem; + std::error_code ec; + const fs::path path = fs::absolute(dusk::ConfigPath / "mod_data" / mod->metadata.id, ec); + if (ec) { + log::write(mod->metadata.id, LOG_LEVEL_ERROR, + "failed to resolve persistent mod data directory: {}", ec.message()); + return MOD_ERROR; + } + + fs::create_directories(path, ec); + if (ec) { + log::write(mod->metadata.id, LOG_LEVEL_ERROR, + "failed to create persistent mod data directory {}: {}", + borealis::io::fs_path_to_string(path), ec.message()); + return MOD_ERROR; + } + mod->dataDirUtf8 = borealis::io::fs_path_to_string(path); + } + + *outPath = mod->dataDirUtf8.c_str(); + return MOD_OK; +} + struct LifecycleWatcher { ModLifecycleFn fn = nullptr; void* userData = nullptr; @@ -148,6 +187,7 @@ constinit HostService s_hostService{ .watch_mod_lifecycle = host_watch_mod_lifecycle, .unwatch_mod_lifecycle = host_unwatch_mod_lifecycle, .native_dir = host_native_dir, + .data_dir = host_data_dir, }; } // namespace