From c876ea558f2f0e28e5a6045418facbbf0249c8f6 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Tue, 7 Jul 2026 11:32:13 -0600 Subject: [PATCH] Config service --- docs/modding.md | 36 +++ files.cmake | 2 + include/mods/svc/config.h | 107 ++++++++ src/dusk/mods/loader/loader.cpp | 29 +- src/dusk/mods/loader/loader.hpp | 3 - src/dusk/mods/svc/config.cpp | 451 ++++++++++++++++++++++++++++++++ src/dusk/mods/svc/config.hpp | 9 + src/dusk/mods/svc/registry.cpp | 1 + src/dusk/mods/svc/registry.hpp | 1 + 9 files changed, 609 insertions(+), 30 deletions(-) create mode 100644 include/mods/svc/config.h create mode 100644 src/dusk/mods/svc/config.cpp create mode 100644 src/dusk/mods/svc/config.hpp diff --git a/docs/modding.md b/docs/modding.md index f3332da0e9..28fc19af5f 100644 --- a/docs/modding.md +++ b/docs/modding.md @@ -176,6 +176,42 @@ svc_host->watch_mod_lifecycle(mod_ctx, on_mod_lifecycle, nullptr, &watch); `MOD_LIFECYCLE_DETACHED` fires on the game thread at a lifecycle safe point, after the subject's `mod_shutdown` ran and every service dropped its state. For your own mod's teardown, use `mod_shutdown` instead. +### ConfigService (`mods/svc/config.h`) + +Persistent, mod-scoped configuration variables. Each var is stored in the user's `config.json` under +`mod..` (escaping: `.` → `_`, `_` → `__`, so `com.example.my_mod` becomes `com_example_my__mod`), +next to the host's own settings: + +```cpp +IMPORT_SERVICE(ConfigService, svc_config); + +ConfigVarDesc desc = CONFIG_VAR_DESC_INIT; +desc.name = "speedMultiplier"; // 1-64 chars from [A-Za-z0-9_-]; "enabled" is reserved +desc.type = CONFIG_VAR_FLOAT; +desc.default_float = 1.0; +ConfigVarHandle var = 0; +svc_config->register_var(mod_ctx, &desc, &var); + +double speed = 1.0; +svc_config->get_float(mod_ctx, var, &speed); +svc_config->set_float(mod_ctx, var, 2.0); + +// Optional: get notified when the value changes. +void on_speed_changed(ModContext* ctx, ConfigVarHandle var, const ConfigVarValue* value, + const ConfigVarValue* previous, void* user_data) { + /* value->float_value is the new value, previous->float_value the old one */ +} +svc_config->subscribe(mod_ctx, var, on_speed_changed, nullptr, nullptr); +``` + +Types: `CONFIG_VAR_BOOL` (`bool`), `CONFIG_VAR_INT` (`int64_t`), `CONFIG_VAR_FLOAT` (`double`), `CONFIG_VAR_STRING` +(UTF-8; `get_string` copies into a caller buffer, pass a `NULL` buffer with size 0 to query the length). Accessors are +typed and must match the registration. + +Change callbacks fire on the game thread whenever the value changes at runtime (your own `set_*` calls included). +Writes that store the same value are silent. Values applied from `config.json` or `--cvar` at registration do +**not** fire callbacks; read the value after `register_var` for the starting state. + --- ## Runtime Lifecycle diff --git a/files.cmake b/files.cmake index fcf603d360..fb4b8dc25c 100644 --- a/files.cmake +++ b/files.cmake @@ -1550,6 +1550,8 @@ set(DUSK_FILES src/dusk/mods/loader/loader.hpp src/dusk/mods/loader/native_module.cpp src/dusk/mods/loader/native_module.hpp + src/dusk/mods/svc/config.cpp + src/dusk/mods/svc/config.hpp src/dusk/mods/svc/host.cpp src/dusk/mods/svc/log.cpp src/dusk/mods/svc/registry.cpp diff --git a/include/mods/svc/config.h b/include/mods/svc/config.h new file mode 100644 index 0000000000..2a26210295 --- /dev/null +++ b/include/mods/svc/config.h @@ -0,0 +1,107 @@ +#pragma once + +#include "mods/api.h" + +#define CONFIG_SERVICE_ID "dev.twilitrealm.dusklight.config" +#define CONFIG_SERVICE_MAJOR 1u +#define CONFIG_SERVICE_MINOR 0u + +/* Handle for a config var registered by the calling mod. 0 is never a valid handle. */ +typedef uint64_t ConfigVarHandle; +/* Handle for a change subscription. 0 is never a valid handle. */ +typedef uint64_t ConfigSubscriptionHandle; + +typedef enum ConfigVarType { + CONFIG_VAR_BOOL = 0, /* bool */ + CONFIG_VAR_INT = 1, /* int64_t */ + CONFIG_VAR_FLOAT = 2, /* double */ + CONFIG_VAR_STRING = 3, /* UTF-8 */ +} ConfigVarType; + +typedef struct ConfigVarDesc { + uint32_t struct_size; + /* Name fragment: 1-64 characters from [A-Za-z0-9_-]. The full config key is + * "mod..", persisted in config.json alongside host settings. + * "enabled" is reserved by the loader. */ + const char* name; + ConfigVarType type; + /* Default value; only the field matching `type` is read. */ + bool default_bool; + int64_t default_int; + double default_float; + const char* default_string; /* NULL means "" */ +} ConfigVarDesc; + +#define CONFIG_VAR_DESC_INIT {sizeof(ConfigVarDesc), NULL, CONFIG_VAR_BOOL, false, 0, 0.0, NULL} + +/* Snapshot of a var's value; only the field matching `type` is meaningful. */ +typedef struct ConfigVarValue { + uint32_t struct_size; + ConfigVarType type; + bool bool_value; + int64_t int_value; + double float_value; + const char* string_value; /* NUL-terminated; NULL for non-string vars */ + size_t string_length; /* excludes the NUL */ +} ConfigVarValue; + +/* + * Fired on the game thread whenever the var's effective value changes at runtime: the calling mod's + * own set_* calls and any other runtime writer. Writes that leave the value unchanged do not fire, + * and neither do values applied from config.json or --cvar during registration. `value` holds the + * new (current) value and `previous` the one it replaced; both snapshots are valid only for the + * duration of the call (copy string_value if you need to keep it). Setting the same var from inside + * its own callback applies the write but is not re-notified. + */ +typedef void (*ConfigChangedFn)(ModContext* ctx, ConfigVarHandle var, const ConfigVarValue* value, + const ConfigVarValue* previous, void* user_data); + +/* + * Scoped configuration variables. + * + * Registrations are owned by the calling mod and removed automatically (subscriptions included) + * when it is disabled, reloaded, or fails. Values are saved to config.json. Writes are debounced, + * not flushed per set. + */ +typedef struct ConfigService { + ServiceHeader header; + + /* Register a config var. If a value for the full key was saved earlier (or set via --cvar), + * it takes effect immediately; otherwise the var starts at the default. Registering a name + * that is already live is MOD_CONFLICT. */ + ModResult (*register_var)( + ModContext* ctx, const ConfigVarDesc* desc, ConfigVarHandle* out_handle); + /* Unregister a var previously registered by the calling mod. Its persisted value is kept. */ + ModResult (*unregister_var)(ModContext* ctx, ConfigVarHandle var); + + /* Typed accessors; the type must match the registration (MOD_INVALID_ARGUMENT otherwise). */ + ModResult (*get_bool)(ModContext* ctx, ConfigVarHandle var, bool* out_value); + ModResult (*set_bool)(ModContext* ctx, ConfigVarHandle var, bool value); + ModResult (*get_int)(ModContext* ctx, ConfigVarHandle var, int64_t* out_value); + ModResult (*set_int)(ModContext* ctx, ConfigVarHandle var, int64_t value); + ModResult (*get_float)(ModContext* ctx, ConfigVarHandle var, double* out_value); + ModResult (*set_float)(ModContext* ctx, ConfigVarHandle var, double value); + /* Copies the NUL-terminated value into buffer. out_length (optional) receives the full + * length excluding the NUL regardless of buffer size; call with buffer == NULL and + * buffer_size == 0 to query the length. A non-NULL buffer that is too small fails with + * MOD_INVALID_ARGUMENT and writes nothing. */ + ModResult (*get_string)( + ModContext* ctx, ConfigVarHandle var, char* buffer, size_t buffer_size, size_t* out_length); + ModResult (*set_string)(ModContext* ctx, ConfigVarHandle var, const char* value); + + /* Subscribe to changes of a var registered by the calling mod. out_handle may be NULL if + * the subscription is never removed manually (cleanup on mod teardown is automatic). */ + ModResult (*subscribe)(ModContext* ctx, ConfigVarHandle var, ConfigChangedFn callback, + void* user_data, ConfigSubscriptionHandle* out_handle); + ModResult (*unsubscribe)(ModContext* ctx, ConfigSubscriptionHandle handle); +} ConfigService; + +#ifdef __cplusplus +#include "mods/service.hpp" + +template <> +struct dusk::mods::ServiceTraits { + static constexpr const char* id = CONFIG_SERVICE_ID; + static constexpr uint16_t major_version = CONFIG_SERVICE_MAJOR; +}; +#endif diff --git a/src/dusk/mods/loader/loader.cpp b/src/dusk/mods/loader/loader.cpp index a8459717e6..00c2cc69e8 100644 --- a/src/dusk/mods/loader/loader.cpp +++ b/src/dusk/mods/loader/loader.cpp @@ -3,7 +3,6 @@ #include "dusk/mod_loader.hpp" #include -#include #include #include #include @@ -12,6 +11,7 @@ #include "depgraph.hpp" #include "dusk/config.hpp" +#include "dusk/mods/svc/config.hpp" #include "dusk/io.hpp" #include "dusk/mods/svc/registry.hpp" #include "miniz.h" @@ -966,31 +966,8 @@ void ModLoader::apply_lifecycle_change(LoadedMod& target, const bool reload) { } } -namespace { -bool s_configDirty = false; -std::chrono::steady_clock::time_point s_lastConfigSave{}; -constexpr std::chrono::seconds kConfigSaveDebounce{2}; -} // namespace - -void config_mark_dirty() { - s_configDirty = true; -} - -void config_flush_if_dirty(const bool force) { - if (!s_configDirty) { - return; - } - const auto now = std::chrono::steady_clock::now(); - if (!force && now - s_lastConfigSave < kConfigSaveDebounce) { - return; - } - s_configDirty = false; - s_lastConfigSave = now; - config::save(); -} - void ModLoader::on_enabled_changed(LoadedMod& mod) { - config_mark_dirty(); + svc::config_mark_dirty(); if (mod.loadFailed) { return; } @@ -1073,7 +1050,6 @@ void ModLoader::tick() { } svc::modules_frame_end(); - config_flush_if_dirty(false); } void ModLoader::shutdown() { @@ -1091,7 +1067,6 @@ void ModLoader::shutdown() { drain_retired_natives(); svc::modules_shutdown(); clear_services(); - config_flush_if_dirty(true); Log.info("all mods unloaded"); } diff --git a/src/dusk/mods/loader/loader.hpp b/src/dusk/mods/loader/loader.hpp index cb81f1059b..6b9fd39232 100644 --- a/src/dusk/mods/loader/loader.hpp +++ b/src/dusk/mods/loader/loader.hpp @@ -61,7 +61,4 @@ void fail_mod(LoadedMod& mod, ModResult code, std::string_view message); bool is_safe_resource_path(std::string_view path); std::string escape_mod_id_for_config(std::string_view id); -void config_mark_dirty(); -void config_flush_if_dirty(bool force); - } // namespace dusk::mods diff --git a/src/dusk/mods/svc/config.cpp b/src/dusk/mods/svc/config.cpp new file mode 100644 index 0000000000..b7ea9c9e9d --- /dev/null +++ b/src/dusk/mods/svc/config.cpp @@ -0,0 +1,451 @@ +#include "config.hpp" + +#include "registry.hpp" + +#include "aurora/lib/logging.hpp" +#include "dusk/config.hpp" +#include "dusk/mods/loader/loader.hpp" +#include "mods/svc/config.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace dusk::mods::svc { +namespace { + +aurora::Module Log("dusk::mods::config"); + +struct ModConfigVarEntry { + uint64_t handle = 0; + ConfigVarType type = CONFIG_VAR_BOOL; + std::unique_ptr var; +}; + +struct ModConfigSubscription { + uint64_t handle = 0; + uint64_t varHandle = 0; + config::Subscription coreSubscription = 0; +}; + +struct ModConfigRecord { + std::vector vars; + std::vector subscriptions; +}; + +std::unordered_map s_modConfig; +uint64_t s_nextHandle = 1; +bool s_dirty = false; +std::chrono::steady_clock::time_point s_lastSave{}; +constexpr std::chrono::seconds kSaveDebounce{2}; + +void config_flush_if_dirty(const bool force) { + if (!s_dirty) { + return; + } + const auto now = std::chrono::steady_clock::now(); + if (!force && now - s_lastSave < kSaveDebounce) { + return; + } + s_dirty = false; + s_lastSave = now; + config::save(); +} + +// Translate the type-erased previous-value pointer into the C ABI snapshot struct. The string +// pointer aliases the previous std::string, which outlives the notification. +ConfigVarValue translate_previous(const uint32_t type, const void* previous) { + ConfigVarValue value{}; + value.struct_size = sizeof(ConfigVarValue); + value.type = static_cast(type); + switch (type) { + case CONFIG_VAR_BOOL: + value.bool_value = *static_cast(previous); + break; + case CONFIG_VAR_INT: + value.int_value = *static_cast(previous); + break; + case CONFIG_VAR_FLOAT: + value.float_value = *static_cast(previous); + break; + case CONFIG_VAR_STRING: { + const auto* str = static_cast(previous); + value.string_value = str->c_str(); + value.string_length = str->size(); + break; + } + default: + break; + } + return value; +} + +// Snapshot the var's current (new) value for the notification. Strings are copied into +// stringStorage so the snapshot stays valid even if the callback writes the var again. +ConfigVarValue translate_current( + const uint32_t type, config::ConfigVarBase& varBase, std::string& stringStorage) { + ConfigVarValue value{}; + value.struct_size = sizeof(ConfigVarValue); + value.type = static_cast(type); + switch (type) { + case CONFIG_VAR_BOOL: + value.bool_value = static_cast&>(varBase).getValue(); + break; + case CONFIG_VAR_INT: + value.int_value = static_cast&>(varBase).getValue(); + break; + case CONFIG_VAR_FLOAT: + value.float_value = static_cast&>(varBase).getValue(); + break; + case CONFIG_VAR_STRING: + stringStorage = static_cast&>(varBase).getValue(); + value.string_value = stringStorage.c_str(); + value.string_length = stringStorage.size(); + break; + default: + break; + } + return value; +} + +bool valid_var_fragment(const char* name) { + if (name == nullptr) { + return false; + } + const std::string_view fragment{name}; + if (fragment.empty() || fragment.size() > 64) { + return false; + } + return std::ranges::all_of(fragment, [](char ch) { + return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || + ch == '_' || ch == '-'; + }); +} + +config::ConfigVarBase* find_var(LoadedMod& mod, const uint64_t handle, uint32_t expectedType) { + const auto recordIt = s_modConfig.find(&mod); + if (recordIt == s_modConfig.end()) { + return nullptr; + } + const auto& vars = recordIt->second.vars; + const auto entry = + std::ranges::find_if(vars, [&](const auto& e) { return e.handle == handle; }); + if (entry == vars.end() || entry->type != expectedType) { + return nullptr; + } + return entry->var.get(); +} + +template +ConfigVar* find_typed_var(ModContext* context, ConfigVarHandle handle, uint32_t type) { + auto* mod = mod_from_context(context); + if (mod == nullptr || handle == 0) { + return nullptr; + } + // The type tag was checked, so the downcast is safe. + return static_cast*>(find_var(*mod, handle, type)); +} + +void config_remove_mod(LoadedMod& mod) { + const auto it = s_modConfig.find(&mod); + if (it == s_modConfig.end()) { + return; + } + for (const auto& sub : it->second.subscriptions) { + config::unsubscribe(sub.coreSubscription); + } + for (const auto& entry : it->second.vars) { + config::unregister(*entry.var); + } + s_modConfig.erase(it); +} + +ModResult config_register_var( + ModContext* context, const ConfigVarDesc* desc, ConfigVarHandle* outHandle) { + if (outHandle != nullptr) { + *outHandle = 0; + } + auto* mod = mod_from_context(context); + if (mod == nullptr || desc == nullptr || desc->struct_size < sizeof(ConfigVarDesc) || + !valid_var_fragment(desc->name)) + { + return MOD_INVALID_ARGUMENT; + } + + const auto fullName = + fmt::format("mod.{}.{}", escape_mod_id_for_config(mod->metadata.id), desc->name); + if (config::GetConfigVar(fullName) != nullptr) { + Log.error("[{}] config var '{}' conflicts with an existing config key", mod->metadata.id, + fullName); + return MOD_CONFLICT; + } + + std::unique_ptr var; + switch (desc->type) { + case CONFIG_VAR_BOOL: + var = std::make_unique >(fullName, desc->default_bool); + break; + case CONFIG_VAR_INT: + var = std::make_unique >(fullName, desc->default_int); + break; + case CONFIG_VAR_FLOAT: + var = std::make_unique >(fullName, desc->default_float); + break; + case CONFIG_VAR_STRING: + var = std::make_unique >( + fullName, desc->default_string != nullptr ? desc->default_string : ""); + break; + default: + return MOD_INVALID_ARGUMENT; + } + + // Back-fills a stashed/saved value (or a --cvar override) if one exists for this key. + // Loads apply silently: the registering mod cannot have subscribed to this var yet, and it + // reads the value right after registration anyway. + config::Register(*var); + + auto& record = s_modConfig[mod]; + auto& entry = record.vars.emplace_back(); + entry.handle = s_nextHandle++; + entry.type = desc->type; + entry.var = std::move(var); + if (outHandle != nullptr) { + *outHandle = entry.handle; + } + return MOD_OK; +} + +ModResult config_unregister_var(ModContext* context, ConfigVarHandle var) { + auto* mod = mod_from_context(context); + if (mod == nullptr || var == 0) { + return MOD_INVALID_ARGUMENT; + } + const auto recordIt = s_modConfig.find(mod); + if (recordIt != s_modConfig.end()) { + auto& record = recordIt->second; + const auto entry = + std::ranges::find_if(record.vars, [&](const auto& e) { return e.handle == var; }); + if (entry != record.vars.end()) { + std::erase_if(record.subscriptions, [&](const ModConfigSubscription& sub) { + if (sub.varHandle != var) { + return false; + } + config::unsubscribe(sub.coreSubscription); + return true; + }); + + // The persisted value is stashed and restored by a future registration of the + // same name. + config::unregister(*entry->var); + record.vars.erase(entry); + return MOD_OK; + } + } + Log.error("[{}] config unregister failed: unknown handle {}", mod->metadata.id, var); + return MOD_INVALID_ARGUMENT; +} + +ModResult config_get_bool(ModContext* context, ConfigVarHandle var, bool* outValue) { + if (outValue != nullptr) { + *outValue = false; + } + auto* cvar = find_typed_var(context, var, CONFIG_VAR_BOOL); + if (cvar == nullptr || outValue == nullptr) { + return MOD_INVALID_ARGUMENT; + } + *outValue = cvar->getValue(); + return MOD_OK; +} + +ModResult config_set_bool(ModContext* context, ConfigVarHandle var, bool value) { + auto* cvar = find_typed_var(context, var, CONFIG_VAR_BOOL); + if (cvar == nullptr) { + return MOD_INVALID_ARGUMENT; + } + cvar->setValue(value); + config_mark_dirty(); + return MOD_OK; +} + +ModResult config_get_int(ModContext* context, ConfigVarHandle var, int64_t* outValue) { + if (outValue != nullptr) { + *outValue = 0; + } + auto* cvar = find_typed_var(context, var, CONFIG_VAR_INT); + if (cvar == nullptr || outValue == nullptr) { + return MOD_INVALID_ARGUMENT; + } + *outValue = cvar->getValue(); + return MOD_OK; +} + +ModResult config_set_int(ModContext* context, ConfigVarHandle var, int64_t value) { + auto* cvar = find_typed_var(context, var, CONFIG_VAR_INT); + if (cvar == nullptr) { + return MOD_INVALID_ARGUMENT; + } + cvar->setValue(value); + config_mark_dirty(); + return MOD_OK; +} + +ModResult config_get_float(ModContext* context, ConfigVarHandle var, double* outValue) { + if (outValue != nullptr) { + *outValue = 0.0; + } + auto* cvar = find_typed_var(context, var, CONFIG_VAR_FLOAT); + if (cvar == nullptr || outValue == nullptr) { + return MOD_INVALID_ARGUMENT; + } + *outValue = cvar->getValue(); + return MOD_OK; +} + +ModResult config_set_float(ModContext* context, ConfigVarHandle var, double value) { + auto* cvar = find_typed_var(context, var, CONFIG_VAR_FLOAT); + if (cvar == nullptr) { + return MOD_INVALID_ARGUMENT; + } + cvar->setValue(value); + config_mark_dirty(); + return MOD_OK; +} + +ModResult config_get_string( + ModContext* context, ConfigVarHandle var, char* buffer, size_t bufferSize, size_t* outLength) { + if (outLength != nullptr) { + *outLength = 0; + } + auto* cvar = find_typed_var(context, var, CONFIG_VAR_STRING); + if (cvar == nullptr) { + return MOD_INVALID_ARGUMENT; + } + const auto& value = cvar->getValue(); + if (outLength != nullptr) { + *outLength = value.size(); + } + if (buffer == nullptr) { + // Length query; any other use of a null buffer is a caller bug. + return bufferSize == 0 ? MOD_OK : MOD_INVALID_ARGUMENT; + } + if (bufferSize < value.size() + 1) { + return MOD_INVALID_ARGUMENT; + } + std::memcpy(buffer, value.c_str(), value.size() + 1); + return MOD_OK; +} + +ModResult config_set_string(ModContext* context, ConfigVarHandle var, const char* value) { + auto* cvar = find_typed_var(context, var, CONFIG_VAR_STRING); + if (cvar == nullptr || value == nullptr) { + return MOD_INVALID_ARGUMENT; + } + cvar->setValue(std::string{value}); + config_mark_dirty(); + return MOD_OK; +} + +ModResult config_subscribe(ModContext* context, ConfigVarHandle var, ConfigChangedFn callback, + void* userData, ConfigSubscriptionHandle* outHandle) { + if (outHandle != nullptr) { + *outHandle = 0; + } + auto* mod = mod_from_context(context); + if (mod == nullptr || var == 0 || callback == nullptr) { + return MOD_INVALID_ARGUMENT; + } + const auto recordIt = s_modConfig.find(mod); + if (recordIt == s_modConfig.end()) { + return MOD_INVALID_ARGUMENT; + } + auto& record = recordIt->second; + const auto entry = + std::ranges::find_if(record.vars, [&](const auto& e) { return e.handle == var; }); + if (entry == record.vars.end()) { + return MOD_INVALID_ARGUMENT; + } + + auto& sub = record.subscriptions.emplace_back(); + sub.handle = s_nextHandle++; + sub.varHandle = var; + sub.coreSubscription = config::subscribe(entry->var->getName(), + [modPtr = mod, callback, userData, varHandle = var, type = entry->type]( + config::ConfigVarBase& varBase, const void* previous) { + const ConfigVarValue previousValue = translate_previous(type, previous); + std::string stringStorage; + const ConfigVarValue currentValue = translate_current(type, varBase, stringStorage); + try { + callback(modPtr->context.get(), varHandle, ¤tValue, &previousValue, userData); + } catch (const std::exception& e) { + fail_mod(*modPtr, MOD_ERROR, + fmt::format("exception in config change callback: {}", e.what())); + } catch (...) { + fail_mod(*modPtr, MOD_ERROR, "unknown exception in config change callback"); + } + }); + if (outHandle != nullptr) { + *outHandle = sub.handle; + } + return MOD_OK; +} + +ModResult config_unsubscribe(ModContext* context, ConfigSubscriptionHandle handle) { + auto* mod = mod_from_context(context); + if (mod == nullptr || handle == 0) { + return MOD_INVALID_ARGUMENT; + } + const auto recordIt = s_modConfig.find(mod); + if (recordIt != s_modConfig.end()) { + auto& subscriptions = recordIt->second.subscriptions; + const auto sub = + std::ranges::find_if(subscriptions, [&](const auto& s) { return s.handle == handle; }); + if (sub != subscriptions.end()) { + config::unsubscribe(sub->coreSubscription); + subscriptions.erase(sub); + return MOD_OK; + } + } + Log.error("[{}] config unsubscribe failed: unknown handle {}", mod->metadata.id, handle); + return MOD_INVALID_ARGUMENT; +} + +constexpr ConfigService s_configService{ + .header = SERVICE_HEADER(ConfigService, CONFIG_SERVICE_MAJOR, CONFIG_SERVICE_MINOR), + .register_var = config_register_var, + .unregister_var = config_unregister_var, + .get_bool = config_get_bool, + .set_bool = config_set_bool, + .get_int = config_get_int, + .set_int = config_set_int, + .get_float = config_get_float, + .set_float = config_set_float, + .get_string = config_get_string, + .set_string = config_set_string, + .subscribe = config_subscribe, + .unsubscribe = config_unsubscribe, +}; + +} // namespace + +void config_mark_dirty() { + s_dirty = true; +} + +constinit const ServiceModule g_configModule{ + .id = CONFIG_SERVICE_ID, + .majorVersion = CONFIG_SERVICE_MAJOR, + .minorVersion = CONFIG_SERVICE_MINOR, + .service = &s_configService, + .modDetached = config_remove_mod, + .frameEnd = [] { config_flush_if_dirty(false); }, + .shutdown = [] { config_flush_if_dirty(true); }, +}; + +} // namespace dusk::mods::svc diff --git a/src/dusk/mods/svc/config.hpp b/src/dusk/mods/svc/config.hpp new file mode 100644 index 0000000000..657c1cbf14 --- /dev/null +++ b/src/dusk/mods/svc/config.hpp @@ -0,0 +1,9 @@ +#pragma once + +namespace dusk::mods::svc { + +// Marks the mods' config keys dirty for the config service's debounced save. The loader +// calls this for the enabled cvars it owns. +void config_mark_dirty(); + +} // namespace dusk::mods::svc diff --git a/src/dusk/mods/svc/registry.cpp b/src/dusk/mods/svc/registry.cpp index 3951822bad..fd5909d996 100644 --- a/src/dusk/mods/svc/registry.cpp +++ b/src/dusk/mods/svc/registry.cpp @@ -199,6 +199,7 @@ void ModLoader::init_services() { { &svc::g_hostModule, &svc::g_logModule, + &svc::g_configModule, }) { svc::register_module(*module); diff --git a/src/dusk/mods/svc/registry.hpp b/src/dusk/mods/svc/registry.hpp index 4f59aabff1..4954c97077 100644 --- a/src/dusk/mods/svc/registry.hpp +++ b/src/dusk/mods/svc/registry.hpp @@ -64,5 +64,6 @@ void modules_shutdown(); extern const ServiceModule g_hostModule; extern const ServiceModule g_logModule; +extern const ServiceModule g_configModule; } // namespace dusk::mods::svc