mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-07-09 20:41:29 -04:00
Config service
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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, ¤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
|
||||
@@ -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
|
||||
@@ -199,6 +199,7 @@ void ModLoader::init_services() {
|
||||
{
|
||||
&svc::g_hostModule,
|
||||
&svc::g_logModule,
|
||||
&svc::g_configModule,
|
||||
})
|
||||
{
|
||||
svc::register_module(*module);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user