Config service

This commit is contained in:
Luke Street
2026-07-07 11:32:13 -06:00
parent 9b75dc5fd2
commit c876ea558f
9 changed files with 609 additions and 30 deletions
+36
View File
@@ -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.<escaped mod id>.<name>` (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
+2
View File
@@ -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
+107
View File
@@ -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.<escaped mod id>.<name>", 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<ConfigService> {
static constexpr const char* id = CONFIG_SERVICE_ID;
static constexpr uint16_t major_version = CONFIG_SERVICE_MAJOR;
};
#endif
+2 -27
View File
@@ -3,7 +3,6 @@
#include "dusk/mod_loader.hpp"
#include <algorithm>
#include <chrono>
#include <filesystem>
#include <fstream>
#include <string>
@@ -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");
}
-3
View File
@@ -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
+451
View File
@@ -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 <fmt/format.h>
#include <algorithm>
#include <chrono>
#include <cstring>
#include <memory>
#include <string>
#include <string_view>
#include <unordered_map>
#include <vector>
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<config::ConfigVarBase> var;
};
struct ModConfigSubscription {
uint64_t handle = 0;
uint64_t varHandle = 0;
config::Subscription coreSubscription = 0;
};
struct ModConfigRecord {
std::vector<ModConfigVarEntry> vars;
std::vector<ModConfigSubscription> subscriptions;
};
std::unordered_map<const LoadedMod*, ModConfigRecord> 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<ConfigVarType>(type);
switch (type) {
case CONFIG_VAR_BOOL:
value.bool_value = *static_cast<const bool*>(previous);
break;
case CONFIG_VAR_INT:
value.int_value = *static_cast<const s64*>(previous);
break;
case CONFIG_VAR_FLOAT:
value.float_value = *static_cast<const f64*>(previous);
break;
case CONFIG_VAR_STRING: {
const auto* str = static_cast<const std::string*>(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<ConfigVarType>(type);
switch (type) {
case CONFIG_VAR_BOOL:
value.bool_value = static_cast<ConfigVar<bool>&>(varBase).getValue();
break;
case CONFIG_VAR_INT:
value.int_value = static_cast<ConfigVar<s64>&>(varBase).getValue();
break;
case CONFIG_VAR_FLOAT:
value.float_value = static_cast<ConfigVar<f64>&>(varBase).getValue();
break;
case CONFIG_VAR_STRING:
stringStorage = static_cast<ConfigVar<std::string>&>(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 <typename T>
ConfigVar<T>* 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<ConfigVar<T>*>(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<config::ConfigVarBase> var;
switch (desc->type) {
case CONFIG_VAR_BOOL:
var = std::make_unique<ConfigVar<bool> >(fullName, desc->default_bool);
break;
case CONFIG_VAR_INT:
var = std::make_unique<ConfigVar<s64> >(fullName, desc->default_int);
break;
case CONFIG_VAR_FLOAT:
var = std::make_unique<ConfigVar<f64> >(fullName, desc->default_float);
break;
case CONFIG_VAR_STRING:
var = std::make_unique<ConfigVar<std::string> >(
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<bool>(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<bool>(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<s64>(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<s64>(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<f64>(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<f64>(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<std::string>(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<std::string>(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, &currentValue, &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
+9
View File
@@ -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
+1
View File
@@ -199,6 +199,7 @@ void ModLoader::init_services() {
{
&svc::g_hostModule,
&svc::g_logModule,
&svc::g_configModule,
})
{
svc::register_module(*module);
+1
View File
@@ -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