diff --git a/include/dusk/config.hpp b/include/dusk/config.hpp index 382c4c24c6..b56a4cbd43 100644 --- a/include/dusk/config.hpp +++ b/include/dusk/config.hpp @@ -1,9 +1,11 @@ #ifndef DUSK_CONFIG_HPP #define DUSK_CONFIG_HPP +#include #include +#include #include -#include "nlohmann/json.hpp" + #include "config_var.hpp" namespace dusk::config { @@ -40,7 +42,7 @@ public: [[nodiscard]] virtual nlohmann::json dumpToJson(const ConfigVarBase& cVar) const = 0; }; -template +template class ConfigImpl : public ConfigImplBase { // Just downcasting the references... void loadFromJson(ConfigVarBase& cVar, const nlohmann::json& jsonValue) const final { @@ -90,20 +92,28 @@ public: void Register(ConfigVarBase& configVar); /** - * \brief Indicate that all registrations have happened and everything should lock in. + * \brief Unregister a CVar, detaching it from the config system. + * + * If the CVar carries a user-set value (Value or Speedrun layer), it is stashed as an + * unregistered key: Save() keeps writing it, and a later Register() of the same name restores + * it through the normal back-fill path. The CVar may be destroyed after this returns. */ -void FinishRegistration(); +void unregister(ConfigVarBase& configVar); /** * \brief Load config from the standard user preferences location. */ -void LoadFromUserPreferences(); -void LoadFromFileName(const char* path); +void load_from_user_preferences(); +void load_from_file_name(const char* path); + +void load_arg_override(std::string_view name, std::string_view value); + +void shutdown(); /** * \brief Save the config to file. */ -void Save(); +void save(); /** * \brief Get a registered CVar by name. @@ -124,6 +134,59 @@ void ClearAllActionBindings(int port); */ void EnumerateRegistered(std::function callback); +/** + * \brief Type-erased change callback. previousValue points at the value before the mutation + * (a `const T*` for a `ConfigVar`) and is valid only for the duration of the call. + */ +using ChangeCallback = std::function; + +/** + * \brief Token identifying a change subscription. 0 is never a valid token. + */ +using Subscription = u64; + +/** + * \brief Subscribe to changes of the named CVar (registered or not yet registered). + * + * Fired synchronously on the mutating thread (in practice the game thread) whenever the CVar's + * effective value changes at runtime: setValue, override/speedrun setters and clears. Values + * applied by config load or launch arguments do *not* notify: loads happen during startup + * before the subsystems callbacks push values into are initialized, and each subsystem reads + * its initial value itself at its own init. Callbacks may mutate other CVars; a nested + * mutation of the same CVar applies but does not re-notify. + */ +Subscription subscribe(std::string_view name, ChangeCallback callback); + +/** + * \brief Typed convenience overload: the callback receives the current and previous values. + */ +template +requires std::invocable Subscription subscribe( + ConfigVar& cVar, Callback&& callback) { + return subscribe(cVar.getName(), + [&cVar, cb = std::forward(callback)](ConfigVarBase&, const void* previousValue) { + cb(cVar.getValue(), *static_cast(previousValue)); + }); +} + +void unsubscribe(Subscription token); + +/** + * \brief Register a CVar and attach a change callback in one step. + * + * Useful for pushing settings into external systems (e.g. aurora) from one place instead of + * every UI setter. The callback fires only for runtime changes (see subscribe); the value + * loaded from config or launch arguments does not fire it — the external system reads its + * initial value itself at its own initialization. + */ +template +requires std::invocable Subscription Register( + ConfigVar& cVar, Callback&& onChange) { + auto subscription = subscribe(cVar, std::forward(onChange)); + Register(static_cast(cVar)); + return subscription; +} + template const ConfigImplBase* GetConfigImpl() { static ConfigImpl config; diff --git a/include/dusk/config_var.hpp b/include/dusk/config_var.hpp index 258bf4143c..52e62172cb 100644 --- a/include/dusk/config_var.hpp +++ b/include/dusk/config_var.hpp @@ -2,10 +2,12 @@ #define DUSK_CONFIG_VAR_HPP #include "dolphin/types.h" -#include +#include #include #include +#include #include +#include /** * The configuration system. @@ -69,7 +71,7 @@ protected: /** * The name of this CVar, used in the configuration file. */ - const char* name; + std::string name; /** * Whether this CVar has been registered with the global managing logic. @@ -87,8 +89,8 @@ protected: */ const ConfigImplBase* impl; - ConfigVarBase(const char* name, const ConfigImplBase* impl); - virtual ~ConfigVarBase() = default; + ConfigVarBase(const ConfigVarBase&) = delete; + ConfigVarBase(std::string name, const ConfigImplBase* impl); /** * Check that the CVar is registered, aborting if this is not the case. @@ -98,7 +100,22 @@ protected: abort(); } + /** + * Whether any change subscriber (see config::subscribe) is attached to this CVar's name. + */ + [[nodiscard]] bool has_subscribers() const; + + /** + * Notify change subscribers (see config::subscribe) that the effective value of this CVar + * changed. Called by mutators after the change has been applied; previousValue points at + * the old value (a `const T*` for a `ConfigVar`), valid only for the duration of the + * call. + */ + void notify_changed(const void* previousValue); + public: + virtual ~ConfigVarBase(); + /** * Get the name of this CVar, used in the configuration file. */ @@ -121,6 +138,7 @@ public: * This is necessary to make it legal to access. */ void markRegistered(); + void unmarkRegistered(); /** * Clear a speedrun-mode override if one is active on this CVar. @@ -155,6 +173,7 @@ template concept ConfigValue = !std::is_const_v && !std::is_volatile_v + && std::equality_comparable && (std::is_same_v || ConfigValueInteger || std::is_same_v @@ -166,6 +185,9 @@ concept ConfigValue = template const ConfigImplBase* GetConfigImpl(); +template +class ConfigImpl; + template struct ConfigEnumRange { static constexpr auto min = std::numeric_limits>::min(); @@ -192,10 +214,12 @@ public: * @param arg Arguments to forward to construct the default value. */ template - ConfigVar(const char* name, Args&&... arg) - : ConfigVarBase(name, GetConfigImpl()), defaultValue(std::forward(arg)...), + ConfigVar(std::string name, Args&&... arg) + : ConfigVarBase(std::move(name), GetConfigImpl()), defaultValue(std::forward(arg)...), value(), overrideValue() {} + ConfigVar(ConfigVar const&) = delete; + /** * \brief Get the current value of the CVar. * @@ -234,6 +258,7 @@ public: */ void setValue(T newValue, bool replaceOverride = true) { checkRegistered(); + const auto previous = previous_for_notify(); value = std::move(newValue); if (replaceOverride) { @@ -242,6 +267,7 @@ public: } else if (layer != ConfigVarLayer::Override) { layer = ConfigVarLayer::Value; } + notify_if_changed(previous); } operator const T&() { @@ -258,8 +284,10 @@ public: */ void setOverrideValue(T newValue) { checkRegistered(); + const auto previous = previous_for_notify(); overrideValue = std::move(newValue); layer = ConfigVarLayer::Override; + notify_if_changed(previous); } /** @@ -273,25 +301,31 @@ public: void setSpeedrunValue(T newValue) { checkRegistered(); if (layer != ConfigVarLayer::Override) { + const auto previous = previous_for_notify(); priorLayer = layer; overrideValue = std::move(newValue); layer = ConfigVarLayer::Speedrun; + notify_if_changed(previous); } } void clearOverride() { checkRegistered(); if (layer == ConfigVarLayer::Override) { + const auto previous = previous_for_notify(); overrideValue = {}; layer = ConfigVarLayer::Value; + notify_if_changed(previous); } } void clearSpeedrunOverride() override { checkRegistered(); if (layer == ConfigVarLayer::Speedrun) { + const auto previous = previous_for_notify(); overrideValue = {}; layer = priorLayer; + notify_if_changed(previous); } } @@ -305,6 +339,48 @@ public: const ConfigVarLayer effectiveLayer = (layer == ConfigVarLayer::Speedrun) ? priorLayer : layer; return effectiveLayer == ConfigVarLayer::Default ? defaultValue : value; } + +private: + // The config loader applies values through the silent load_* methods below. + friend class ConfigImpl; + + /** + * Copy of the effective value before a mutation, taken only when someone is subscribed. + */ + [[nodiscard]] std::optional previous_for_notify() const { + return has_subscribers() ? std::optional{getValue()} : std::nullopt; + } + + /** + * Notify subscribers if the effective value actually changed across a mutation. + */ + void notify_if_changed(const std::optional& previous) { + if (previous.has_value() && !(getValue() == *previous)) { + notify_changed(&*previous); + } + } + + /** + * setValue(newValue, false) without notifying change subscribers. Used when loading config: + * loads happen during startup before the subsystems change callbacks push values into are + * initialized, and each subsystem applies the loaded value itself at its own init. + */ + void load_value(T newValue) { + checkRegistered(); + value = std::move(newValue); + if (layer != ConfigVarLayer::Override) { + layer = ConfigVarLayer::Value; + } + } + + /** + * setOverrideValue without notifying change subscribers (see load_value). + */ + void load_override_value(T newValue) { + checkRegistered(); + overrideValue = std::move(newValue); + layer = ConfigVarLayer::Override; + } }; using ActionBindConfigVar = ConfigVar; diff --git a/include/dusk/settings.h b/include/dusk/settings.h index fdff069226..326f7f321c 100644 --- a/include/dusk/settings.h +++ b/include/dusk/settings.h @@ -7,7 +7,8 @@ namespace dusk { -using namespace config; +using config::ConfigVar; +using config::ActionBindConfigVar; enum class BloomMode : int { Off = 0, diff --git a/src/dusk/config.cpp b/src/dusk/config.cpp index 43d3f19dad..698dceaaac 100644 --- a/src/dusk/config.cpp +++ b/src/dusk/config.cpp @@ -7,6 +7,7 @@ #include "dusk/io.hpp" #include "dusk/settings.h" +#include #include #include #include @@ -15,77 +16,90 @@ #include #include #include +#include #include "dusk/action_bindings.h" +#include "dusk/logging.h" #include "dusk/main.h" -using namespace dusk::config; - +namespace dusk::config { +namespace { constexpr auto ConfigFileName = "config.json"; using json = nlohmann::json; aurora::Module DuskConfigLog("dusk::config"); -static absl::flat_hash_map RegisteredConfigVars; -static bool RegistrationDone = false; +absl::flat_hash_map RegisteredConfigVars; +absl::flat_hash_map UnregisteredConfigVars; +absl::flat_hash_map UnregisteredConfigVarOverrides; -static std::optional parse_control_anchor(std::string_view value) { +struct ChangeSubscription { + Subscription token; + ChangeCallback callback; +}; +absl::flat_hash_map > s_changeSubscriptions; +absl::flat_hash_map s_changeTokenNames; +Subscription s_nextChangeToken = 1; +// Names currently being notified; guards against a callback re-notifying its own CVar. +std::vector s_activeChangeNotifications; + +std::optional parse_control_anchor(std::string_view value) { if (value == "none") { - return dusk::ui::ControlAnchor::None; + return ui::ControlAnchor::None; } if (value == "top") { - return dusk::ui::ControlAnchor::Top; + return ui::ControlAnchor::Top; } if (value == "left") { - return dusk::ui::ControlAnchor::Left; + return ui::ControlAnchor::Left; } if (value == "bottom") { - return dusk::ui::ControlAnchor::Bottom; + return ui::ControlAnchor::Bottom; } if (value == "right") { - return dusk::ui::ControlAnchor::Right; + return ui::ControlAnchor::Right; } if (value == "topLeft") { - return dusk::ui::ControlAnchor::TopLeft; + return ui::ControlAnchor::TopLeft; } if (value == "topRight") { - return dusk::ui::ControlAnchor::TopRight; + return ui::ControlAnchor::TopRight; } if (value == "bottomLeft") { - return dusk::ui::ControlAnchor::BottomLeft; + return ui::ControlAnchor::BottomLeft; } if (value == "bottomRight") { - return dusk::ui::ControlAnchor::BottomRight; + return ui::ControlAnchor::BottomRight; } return std::nullopt; } -static const char* control_anchor_value(dusk::ui::ControlAnchor anchor) { +const char* control_anchor_value(ui::ControlAnchor anchor) { switch (anchor) { - case dusk::ui::ControlAnchor::None: + case ui::ControlAnchor::None: return "none"; - case dusk::ui::ControlAnchor::Top: + case ui::ControlAnchor::Top: return "top"; - case dusk::ui::ControlAnchor::Left: + case ui::ControlAnchor::Left: return "left"; - case dusk::ui::ControlAnchor::Bottom: + case ui::ControlAnchor::Bottom: return "bottom"; - case dusk::ui::ControlAnchor::Right: + case ui::ControlAnchor::Right: return "right"; - case dusk::ui::ControlAnchor::TopLeft: + case ui::ControlAnchor::TopLeft: return "topLeft"; - case dusk::ui::ControlAnchor::TopRight: + case ui::ControlAnchor::TopRight: return "topRight"; - case dusk::ui::ControlAnchor::BottomLeft: + case ui::ControlAnchor::BottomLeft: return "bottomLeft"; - case dusk::ui::ControlAnchor::BottomRight: + case ui::ControlAnchor::BottomRight: return "bottomRight"; } return "none"; } -static std::optional json_finite_float(const json& object, const char* key) { +std::optional json_finite_float(const json& object, const char* key) { const auto iter = object.find(key); if (iter == object.end() || !iter->is_number()) { return std::nullopt; @@ -99,7 +113,7 @@ static std::optional json_finite_float(const json& object, const char* ke return value; } -static std::optional parse_control_props(const json& value) { +std::optional parse_control_props(const json& value) { if (!value.is_object()) { return std::nullopt; } @@ -118,7 +132,7 @@ static std::optional parse_control_props(const json& val if (!anchor || *w <= 0.0f || *h <= 0.0f || *scale <= 0.0f) { return std::nullopt; } - return dusk::ui::ControlProps{ + return ui::ControlProps{ .x = *x, .y = *y, .w = *w, @@ -128,17 +142,17 @@ static std::optional parse_control_props(const json& val }; } -static std::filesystem::path GetConfigJsonPath() { - return dusk::ConfigPath / ConfigFileName; +std::filesystem::path GetConfigJsonPath() { + return ConfigPath / ConfigFileName; } -static std::filesystem::path GetTempConfigJsonPath(const std::filesystem::path& configJsonPath) { +std::filesystem::path GetTempConfigJsonPath(const std::filesystem::path& configJsonPath) { auto tempPath = configJsonPath; tempPath.replace_filename(fmt::format(".{}.tmp", configJsonPath.filename().string())); return tempPath; } -static void ReplaceFile(const std::filesystem::path& source, const std::filesystem::path& target) { +void ReplaceFile(const std::filesystem::path& source, const std::filesystem::path& target) { std::error_code ec; std::filesystem::rename(source, target, ec); if (ec) { @@ -148,19 +162,8 @@ static void ReplaceFile(const std::filesystem::path& source, const std::filesyst } } -ConfigVarBase::ConfigVarBase(const char* name, const ConfigImplBase* impl) - : name(name), registered(false), layer(ConfigVarLayer::Default), impl(impl) {} - -const char* ConfigVarBase::getName() const noexcept { - return name; -} - -const ConfigImplBase* ConfigVarBase::getImpl() const noexcept { - return impl; -} - template -static T sanitizeEnumValue(const ConfigVar& cVar, T value) { +T sanitizeEnumValue(const ConfigVar& cVar, T value) { if constexpr (std::is_enum_v) { using Underlying = std::underlying_type_t; const Underlying raw = static_cast(value); @@ -174,6 +177,83 @@ static T sanitizeEnumValue(const ConfigVar& cVar, T value) { return value; } +template +requires std::is_integral_v&& std::is_signed_v T parse_arg_value( + const ConfigVar&, const std::string_view stringValue) { + const std::string str(stringValue); + const auto result = std::stoll(str); + if (result >= std::numeric_limits::min() && result <= std::numeric_limits::max()) { + return static_cast(result); + } + throw std::out_of_range("Value is too large"); +} + +template +requires std::is_integral_v&& std::is_unsigned_v T parse_arg_value( + const ConfigVar&, const std::string_view stringValue) { + const std::string str(stringValue); + const auto result = std::stoull(str); + if (result <= std::numeric_limits::max()) { + return static_cast(result); + } + throw std::out_of_range("Value is too large"); +} + +f32 parse_arg_value(const ConfigVar&, const std::string_view stringValue) { + const std::string str(stringValue); + return std::stof(str); +} + +f64 parse_arg_value(const ConfigVar&, const std::string_view stringValue) { + const std::string str(stringValue); + return std::stod(str); +} + +std::string parse_arg_value(const ConfigVar&, const std::string_view stringValue) { + return std::string(stringValue); +} + +template +requires std::is_enum_v T parse_arg_value( + const ConfigVar& cVar, const std::string_view stringValue) { + using Underlying = std::underlying_type_t; + const std::string str(stringValue); + + if constexpr (std::is_signed_v) { + const auto result = std::stoll(str); + if (result >= std::numeric_limits::min() && + result <= std::numeric_limits::max()) + { + return sanitizeEnumValue(cVar, static_cast(result)); + } + throw std::out_of_range("Value is too large"); + } else { + const auto result = std::stoull(str); + if (result <= std::numeric_limits::max()) { + return sanitizeEnumValue(cVar, static_cast(result)); + } + throw std::out_of_range("Value is too large"); + } +} +} // namespace + +ConfigVarBase::ConfigVarBase(std::string name, const ConfigImplBase* impl) + : name(std::move(name)), registered(false), layer(ConfigVarLayer::Default), impl(impl) {} + +const char* ConfigVarBase::getName() const noexcept { + return name.c_str(); +} + +const ConfigImplBase* ConfigVarBase::getImpl() const noexcept { + return impl; +} + +ConfigVarBase::~ConfigVarBase() { + if (registered) { + DuskLog.fatal("CVar '{}' was destroyed while still registered!", name); + } +} + template void ConfigImpl::loadFromJson(ConfigVar& cVar, const json& jsonValue) { if constexpr (std::is_enum_v) { @@ -187,12 +267,12 @@ void ConfigImpl::loadFromJson(ConfigVar& cVar, const json& jsonValue) { const Underlying raw = b ? static_cast(1) : static_cast(0); - cVar.setValue(sanitizeEnumValue(cVar, static_cast(raw)), false); + cVar.load_value(sanitizeEnumValue(cVar, static_cast(raw))); return; } } - cVar.setValue(sanitizeEnumValue(cVar, jsonValue.get()), false); + cVar.load_value(sanitizeEnumValue(cVar, jsonValue.get())); } template @@ -200,74 +280,9 @@ nlohmann::json ConfigImpl::dumpToJson(const ConfigVar& cVar) { return cVar.getValueForSave(); } -template -requires std::is_integral_v&& std::is_signed_v static void loadFromArgImpl( - ConfigVar& cVar, const std::string_view stringValue) { - const std::string str(stringValue); - const auto result = std::stoll(str); - if (result >= std::numeric_limits::min() && result <= std::numeric_limits::max()) { - cVar.setOverrideValue(result); - } else { - throw std::out_of_range("Value is too large"); - } -} - -template -requires std::is_integral_v&& std::is_unsigned_v static void loadFromArgImpl( - ConfigVar& cVar, const std::string_view stringValue) { - const std::string str(stringValue); - const auto result = std::stoull(str); - if (result <= std::numeric_limits::max()) { - cVar.setOverrideValue(result); - } else { - throw std::out_of_range("Value is too large"); - } -} - -static void loadFromArgImpl(ConfigVar& cVar, const std::string_view stringValue) { - const std::string str(stringValue); - const auto result = std::stof(str); - cVar.setOverrideValue(result); -} - -static void loadFromArgImpl(ConfigVar& cVar, const std::string_view stringValue) { - const std::string str(stringValue); - const auto result = std::stod(str); - cVar.setOverrideValue(result); -} - -static void loadFromArgImpl(ConfigVar& cVar, const std::string_view stringValue) { - cVar.setOverrideValue(std::string(stringValue)); -} - -template -requires std::is_enum_v static void loadFromArgImpl( - ConfigVar& cVar, const std::string_view stringValue) { - using Underlying = std::underlying_type_t; - const std::string str(stringValue); - - if constexpr (std::is_signed_v) { - const auto result = std::stoll(str); - if (result >= std::numeric_limits::min() && - result <= std::numeric_limits::max()) - { - cVar.setOverrideValue(sanitizeEnumValue(cVar, static_cast(result))); - } else { - throw std::out_of_range("Value is too large"); - } - } else { - const auto result = std::stoull(str); - if (result <= std::numeric_limits::max()) { - cVar.setOverrideValue(sanitizeEnumValue(cVar, static_cast(result))); - } else { - throw std::out_of_range("Value is too large"); - } - } -} - template void ConfigImpl::loadFromArg(ConfigVar& cVar, const std::string_view stringValue) { - loadFromArgImpl(cVar, stringValue); + cVar.load_override_value(parse_arg_value(cVar, stringValue)); } template <> @@ -275,18 +290,16 @@ void ConfigImpl::loadFromArg(ConfigVar& cVar, const std::string_view if (stringValue == "1" || stringValue == "TRUE" || stringValue == "true" || stringValue == "True") { - cVar.setOverrideValue(true); + cVar.load_override_value(true); } else if (stringValue == "0" || stringValue == "FALSE" || stringValue == "false" || stringValue == "False") { - cVar.setOverrideValue(false); + cVar.load_override_value(false); } else { throw InvalidConfigError("Value cannot be parsed as boolean"); } } -// My IDE is convinced this namespace is necessary. It shouldn't be AFAICT? -namespace dusk::config { template class ConfigImpl; template class ConfigImpl; template class ConfigImpl; @@ -299,10 +312,10 @@ template class ConfigImpl; template class ConfigImpl; template class ConfigImpl; template class ConfigImpl; -template class ConfigImpl; -template class ConfigImpl; -template class ConfigImpl; -template class ConfigImpl; +template class ConfigImpl; +template class ConfigImpl; +template class ConfigImpl; +template class ConfigImpl; template <> void ConfigImpl::loadFromJson( @@ -312,11 +325,11 @@ void ConfigImpl::loadFromJson( const FrameInterpMode mode = b ? FrameInterpMode::Unlimited : FrameInterpMode::Off; - cVar.setValue(sanitizeEnumValue(cVar, mode), false); + cVar.load_value(sanitizeEnumValue(cVar, mode)); return; } - cVar.setValue(sanitizeEnumValue(cVar, jsonValue.get()), false); + cVar.load_value(sanitizeEnumValue(cVar, jsonValue.get())); } template <> @@ -347,7 +360,7 @@ void ConfigImpl::loadFromJson( } } - cVar.setValue(std::move(layout), false); + cVar.load_value(std::move(layout)); } template <> @@ -377,26 +390,59 @@ nlohmann::json ConfigImpl::dumpToJson(const ConfigVar; -template class ConfigImpl; -template class ConfigImpl; -template class ConfigImpl; -template class ConfigImpl; -template class ConfigImpl; -} // namespace dusk::config - -void dusk::config::Register(ConfigVarBase& configVar) { - const auto& name = configVar.getName(); - if (RegistrationDone) { - DuskConfigLog.fatal("Tried to register CVar {} after registrations closed!", name); - } +template class ConfigImpl; +template class ConfigImpl; +template class ConfigImpl; +template class ConfigImpl; +template class ConfigImpl; +template class ConfigImpl; +void Register(ConfigVarBase& configVar) { + const std::string_view name = configVar.getName(); if (RegisteredConfigVars.contains(name)) { DuskConfigLog.fatal("Tried to register CVar {} twice!", name); } RegisteredConfigVars[name] = &configVar; configVar.markRegistered(); + + const auto unregPair = UnregisteredConfigVars.find(name); + if (unregPair != UnregisteredConfigVars.end()) { + const auto value = std::move(unregPair->second); + UnregisteredConfigVars.erase(name); + + try { + configVar.getImpl()->loadFromJson(configVar, value); + } catch (std::exception& e) { + DuskConfigLog.error("Failed to load key '{}' from config value: {}", name, e.what()); + } + } + + const auto overridePair = UnregisteredConfigVarOverrides.find(name); + if (overridePair != UnregisteredConfigVarOverrides.end()) { + try { + configVar.getImpl()->loadFromArg(configVar, overridePair->second); + } catch (std::exception& e) { + DuskConfigLog.error("Failed to load key '{}' from override arg: {}", name, e.what()); + } + } +} + +void unregister(ConfigVarBase& configVar) { + const std::string_view name = configVar.getName(); + const auto it = RegisteredConfigVars.find(name); + if (it == RegisteredConfigVars.end() || it->second != &configVar) { + DuskConfigLog.fatal("Tried to unregister CVar '{}' that is not registered!", name); + } + + const auto layer = configVar.getLayer(); + if (layer == ConfigVarLayer::Value || layer == ConfigVarLayer::Speedrun) { + UnregisteredConfigVars.insert_or_assign( + std::string{name}, configVar.getImpl()->dumpToJson(configVar)); + } + + RegisteredConfigVars.erase(it); + configVar.unmarkRegistered(); } void ConfigVarBase::markRegistered() { @@ -406,21 +452,24 @@ void ConfigVarBase::markRegistered() { registered = true; } -void dusk::config::FinishRegistration() { - RegistrationDone = true; +void ConfigVarBase::unmarkRegistered() { + if (!registered) + abort(); + + registered = false; } -void dusk::config::LoadFromUserPreferences() { +void load_from_user_preferences() { const auto configJsonPath = GetConfigJsonPath(); if (configJsonPath.empty()) { return; } const auto configPathString = io::fs_path_to_string(configJsonPath); - LoadFromFileName(configPathString.c_str()); + load_from_file_name(configPathString.c_str()); } static void LoadFromPath(const char* path) { - auto data = dusk::io::FileStream::ReadAllBytes(path); + auto data = io::FileStream::ReadAllBytes(path); json j = json::parse(data); if (!j.is_object()) { @@ -428,11 +477,16 @@ static void LoadFromPath(const char* path) { return; } + UnregisteredConfigVars.clear(); + for (const auto& el : j.items()) { const auto& key = el.key(); auto configVar = RegisteredConfigVars.find(key); if (configVar == RegisteredConfigVars.end()) { - DuskConfigLog.error("Unknown key '{}' found in config!", key); + DuskConfigLog.debug("Unknown key '{}' found in config! If this gets registered later, " + "that's acceptable!", + key); + UnregisteredConfigVars.emplace(key, el.value()); continue; } @@ -444,11 +498,7 @@ static void LoadFromPath(const char* path) { } } -void dusk::config::LoadFromFileName(const char* path) { - if (!RegistrationDone) { - DuskConfigLog.fatal("Registration not finished yet!"); - } - +void load_from_file_name(const char* path) { DuskConfigLog.info("Loading config from '{}'", path); try { @@ -466,7 +516,21 @@ void dusk::config::LoadFromFileName(const char* path) { } } -void dusk::config::Save() { +void load_arg_override(std::string_view name, std::string_view value) { + const auto cVar = GetConfigVar(name); + if (!cVar) { + UnregisteredConfigVarOverrides.emplace(name, value); + return; + } + + try { + cVar->getImpl()->loadFromArg(*cVar, value); + } catch (const std::exception& e) { + DuskLog.fatal("Unable to parse: '{}': {}", value, e.what()); + } +} + +void save() { const auto configJsonPath = GetConfigJsonPath(); if (configJsonPath.empty()) { return; @@ -484,6 +548,10 @@ void dusk::config::Save() { } } + for (const auto& pair : UnregisteredConfigVars) { + j[pair.first] = pair.second; + } + try { const auto tempConfigJsonPath = GetTempConfigJsonPath(configJsonPath); io::FileStream::WriteAllText(tempConfigJsonPath, j.dump(4)); @@ -493,14 +561,14 @@ void dusk::config::Save() { } } -void dusk::config::ClearAllActionBindings(int port) { +void ClearAllActionBindings(int port) { for (auto& actionBinding : getActionBinds() | std::views::values) { actionBinding.configVars->at(port).setValue(PAD_NATIVE_BUTTON_INVALID); } - Save(); + save(); } -ConfigVarBase* dusk::config::GetConfigVar(std::string_view name) { +ConfigVarBase* GetConfigVar(std::string_view name) { const auto configVar = RegisteredConfigVars.find(name); if (configVar != RegisteredConfigVars.end()) { return configVar->second; @@ -509,8 +577,69 @@ ConfigVarBase* dusk::config::GetConfigVar(std::string_view name) { return nullptr; } -void dusk::config::EnumerateRegistered(std::function callback) { +void EnumerateRegistered(std::function callback) { for (auto& pair : RegisteredConfigVars) { callback(*pair.second); } } + +Subscription subscribe(std::string_view name, ChangeCallback callback) { + const auto token = s_nextChangeToken++; + s_changeSubscriptions[std::string{name}].push_back({token, std::move(callback)}); + s_changeTokenNames.emplace(token, std::string{name}); + return token; +} + +void unsubscribe(Subscription token) { + const auto nameIt = s_changeTokenNames.find(token); + if (nameIt == s_changeTokenNames.end()) { + DuskConfigLog.fatal("Tried to unsubscribe unknown change token {}!", token); + } + + const auto subsIt = s_changeSubscriptions.find(nameIt->second); + auto& subscriptions = subsIt->second; + std::erase_if( + subscriptions, [token](const ChangeSubscription& sub) { return sub.token == token; }); + if (subscriptions.empty()) { + s_changeSubscriptions.erase(subsIt); + } + s_changeTokenNames.erase(nameIt); +} + +bool ConfigVarBase::has_subscribers() const { + return s_changeSubscriptions.contains(name); +} + +void ConfigVarBase::notify_changed(const void* previousValue) { + const auto subsIt = s_changeSubscriptions.find(name); + if (subsIt == s_changeSubscriptions.end()) { + return; + } + if (std::ranges::find(s_activeChangeNotifications, name) != s_activeChangeNotifications.end()) { + DuskConfigLog.error("Recursive change notification for CVar '{}' suppressed", name); + return; + } + + s_activeChangeNotifications.push_back(name); + // Copied so callbacks can subscribe/unsubscribe safely. + const auto subscriptions = subsIt->second; + for (const auto& sub : subscriptions) { + sub.callback(*this, previousValue); + } + s_activeChangeNotifications.pop_back(); +} + +void shutdown() { + for (auto& pair : RegisteredConfigVars) { + pair.second->unmarkRegistered(); + } + + RegisteredConfigVars.clear(); + UnregisteredConfigVars.clear(); + UnregisteredConfigVarOverrides.clear(); + s_changeSubscriptions.clear(); + s_changeTokenNames.clear(); + s_activeChangeNotifications.clear(); +} + +} // namespace dusk::config \ No newline at end of file diff --git a/src/dusk/imgui/ImGuiConfig.hpp b/src/dusk/imgui/ImGuiConfig.hpp index e3e80b9920..4bceb36a38 100644 --- a/src/dusk/imgui/ImGuiConfig.hpp +++ b/src/dusk/imgui/ImGuiConfig.hpp @@ -9,7 +9,7 @@ namespace dusk::config { bool copy = var.getValue(); if (ImGui::Checkbox(title, ©)) { var.setValue(copy); - Save(); + save(); return true; } @@ -20,7 +20,7 @@ namespace dusk::config { float val = var; if (ImGui::SliderFloat(label, &val, v_min, v_max, format, flags)) { var.setValue(val); - Save(); + save(); return true; } @@ -31,7 +31,7 @@ namespace dusk::config { int val = var; if (ImGui::SliderInt(label, &val, v_min, v_max, format, flags)) { var.setValue(val); - Save(); + save(); return true; } @@ -42,7 +42,7 @@ namespace dusk::config { bool copy = p_selected.getValue(); if (ImGui::MenuItem(label, shortcut, ©, enabled)) { p_selected.setValue(copy); - Save(); + save(); return true; } diff --git a/src/dusk/imgui/ImGuiConsole.cpp b/src/dusk/imgui/ImGuiConsole.cpp index 4c0f2e6de6..3c0131c729 100644 --- a/src/dusk/imgui/ImGuiConsole.cpp +++ b/src/dusk/imgui/ImGuiConsole.cpp @@ -254,7 +254,7 @@ namespace dusk { if (ImGui::IsKeyPressed(ImGuiKey_F11)) { getSettings().video.enableFullscreen.setValue(!getSettings().video.enableFullscreen); VISetWindowFullscreen(getSettings().video.enableFullscreen); - config::Save(); + config::save(); } if (getSettings().game.enableResetKeybind && ImGui::GetIO().KeyCtrl && @@ -336,7 +336,7 @@ namespace dusk { if constexpr (SupportsProcessRestart) { if (ImGui::Button("Retry (Auto backend)")) { getSettings().backend.graphicsBackend.setValue("auto"); - config::Save(); + config::save(); RestartRequested = true; IsRunning = false; } diff --git a/src/dusk/imgui/ImGuiMenuTools.cpp b/src/dusk/imgui/ImGuiMenuTools.cpp index 02326eceea..cf5b22a270 100644 --- a/src/dusk/imgui/ImGuiMenuTools.cpp +++ b/src/dusk/imgui/ImGuiMenuTools.cpp @@ -73,7 +73,7 @@ namespace dusk { bool disableWaterRefraction = getSettings().game.disableWaterRefraction; if (ImGui::Checkbox("Disable Water Refraction", &disableWaterRefraction)) { getSettings().game.disableWaterRefraction.setValue(disableWaterRefraction); - config::Save(); + config::save(); } ImGui::Checkbox("Enable LOD Bias", &aurora::gx::enableLodBias); ImGui::EndMenu(); diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index 7831c5c41f..e2a2169485 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -1,5 +1,6 @@ #include "dusk/settings.h" #include "dusk/config.hpp" +#include namespace dusk { @@ -267,7 +268,8 @@ void registerSettings() { Register(g_userSettings.game.touchCameraYSensitivity); Register(g_userSettings.game.minimalHUD); Register(g_userSettings.game.hudScale); - Register(g_userSettings.game.pauseOnFocusLost); + Register(g_userSettings.game.pauseOnFocusLost, + [](const bool& value, const bool&) { aurora_set_pause_on_focus_lost(value); }); Register(g_userSettings.game.enableDiscordPresence); Register(g_userSettings.game.bloomMode); Register(g_userSettings.game.bloomMultiplier); diff --git a/src/dusk/speedrun.cpp b/src/dusk/speedrun.cpp index 0a70125da2..e396197040 100644 --- a/src/dusk/speedrun.cpp +++ b/src/dusk/speedrun.cpp @@ -37,7 +37,6 @@ void resetForSpeedrunMode() { getSettings().game.invincibleEnemies.setSpeedrunValue(false); getSettings().game.pauseOnFocusLost.setSpeedrunValue(false); - aurora_set_pause_on_focus_lost(false); getSettings().backend.enableAdvancedSettings.setSpeedrunValue(false); getSettings().game.recordingMode.setSpeedrunValue(false); diff --git a/src/dusk/ui/controller_config.cpp b/src/dusk/ui/controller_config.cpp index a506a593a6..7c8e720ecd 100644 --- a/src/dusk/ui/controller_config.cpp +++ b/src/dusk/ui/controller_config.cpp @@ -278,7 +278,7 @@ ControllerConfigWindow::ControllerConfigWindow(bool prelaunch) { void ControllerConfigWindow::hide(bool close) { stop_rumble_test(); cancel_pending_binding(); - config::Save(); + config::save(); Window::hide(close); } @@ -403,7 +403,7 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) { PADClearPort(port); PADSetKeyboardActive(static_cast(port), FALSE); PADSerializeMappings(); - ClearAllActionBindings(port); + config::ClearAllActionBindings(port); refresh_controller_page(); }); @@ -417,7 +417,7 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) { PADClearPort(port); PADSetKeyboardActive(static_cast(port), TRUE); PADSerializeMappings(); - ClearAllActionBindings(port); + config::ClearAllActionBindings(port); }); const u32 controllerCount = PADCount(); @@ -439,7 +439,7 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) { PADSetKeyboardActive(static_cast(port), FALSE); PADSetPortForIndex(i, port); PADSerializeMappings(); - ClearAllActionBindings(port); + config::ClearAllActionBindings(port); }); } break; @@ -1125,7 +1125,7 @@ void ControllerConfigWindow::poll_pending_binding() { return; } mPendingActionBinding->setValue(button); - config::Save(); + config::save(); finish_pending_binding(completedPort); } return; diff --git a/src/dusk/ui/controls.hpp b/src/dusk/ui/controls.hpp index 2a83d6127f..5b03bf179a 100644 --- a/src/dusk/ui/controls.hpp +++ b/src/dusk/ui/controls.hpp @@ -51,6 +51,8 @@ struct ControlProps { float h = 0.0f; float scale = 1.0f; ControlAnchor anchor = ControlAnchor::None; + + bool operator==(const ControlProps&) const = default; }; struct ControlRect { @@ -76,6 +78,8 @@ struct ControlLayout { int version = Version; std::map > controls; + + bool operator==(const ControlLayout&) const = default; }; constexpr std::array kControlLayoutIds = { diff --git a/src/dusk/ui/graphics_tuner.cpp b/src/dusk/ui/graphics_tuner.cpp index 57fa3ee329..054c0b302c 100644 --- a/src/dusk/ui/graphics_tuner.cpp +++ b/src/dusk/ui/graphics_tuner.cpp @@ -300,7 +300,7 @@ void GraphicsTuner::show() { } void GraphicsTuner::hide(bool close) { - config::Save(); + config::save(); mRoot->RemoveAttribute("open"); if (close) { mPendingClose = true; diff --git a/src/dusk/ui/prelaunch.cpp b/src/dusk/ui/prelaunch.cpp index 998cdb87ea..cef030f811 100644 --- a/src/dusk/ui/prelaunch.cpp +++ b/src/dusk/ui/prelaunch.cpp @@ -309,7 +309,7 @@ void persist_disc_choice(const std::string& path, iso::ValidationError validatio getSettings().backend.isoPath.setValue(path); getSettings().backend.isoVerification.setValue(verification); - config::Save(); + config::save(); if (previousPath != path || previousVerification != verification) { iso::log_verification_state(path, verification); diff --git a/src/dusk/ui/preset.cpp b/src/dusk/ui/preset.cpp index 1a75a27e6b..7fc7d956b4 100644 --- a/src/dusk/ui/preset.cpp +++ b/src/dusk/ui/preset.cpp @@ -106,7 +106,7 @@ PresetWindow::PresetWindow() : WindowSmall("modal", "modal-dialog") { if (cmd == NavCommand::Confirm) { apply(); getSettings().backend.wasPresetChosen.setValue(true); - config::Save(); + config::save(); hide(true); return true; } diff --git a/src/dusk/ui/settings.cpp b/src/dusk/ui/settings.cpp index 3c89f34136..401d2aee89 100644 --- a/src/dusk/ui/settings.cpp +++ b/src/dusk/ui/settings.cpp @@ -248,7 +248,6 @@ void reset_for_speedrun_mode() { getSettings().game.invincibleEnemies.setSpeedrunValue(false); getSettings().game.pauseOnFocusLost.setSpeedrunValue(false); - aurora_set_pause_on_focus_lost(false); getSettings().backend.enableAdvancedSettings.setSpeedrunValue(false); getSettings().game.recordingMode.setSpeedrunValue(false); @@ -263,7 +262,6 @@ void clear_speedrun_overrides() { void restore_from_speedrun_mode() { clear_speedrun_overrides(); - aurora_set_pause_on_focus_lost(getSettings().game.pauseOnFocusLost.getValue()); } std::filesystem::path normalized_display_path(const std::filesystem::path& path) { @@ -447,7 +445,7 @@ SelectButton& config_bool_select( return; } var.setValue(value); - config::Save(); + config::save(); if (callback) { callback(value); } @@ -468,7 +466,7 @@ void add_speedrun_disabled_option(Pane& leftPane, Pane& rightPane, ConfigVar& std::string suffix = "") { auto& button = leftPane.add_child(NumberButton::Props{ .key = std::move(key), - .getValue = [&var] { return var; }, + .getValue = [&var] { return var.getValue(); }, .setValue = [&var, min, max, callback = std::move(onChange)](int value) { const int clampedValue = std::clamp(value, min, max); var.setValue(clampedValue); - config::Save(); + config::save(); if (callback) { callback(clampedValue); } @@ -680,7 +678,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { .on_pressed([i] { mDoAud_seStartMenu(kSoundItemChange); getSettings().game.language.setValue(static_cast(i)); - config::Save(); + config::save(); }); } pane.add_rml("
Changes require a restart."); @@ -707,7 +705,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { mDoAud_seStartMenu(kSoundItemChange); getSettings().backend.graphicsBackend.setValue( std::string{backend_id(backend)}); - config::Save(); + config::save(); }); } pane.add_rml("
Changes require a restart."); @@ -738,7 +736,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { .on_pressed([i] { mDoAud_seStartMenu(kSoundItemChange); getSettings().backend.cardFileType.setValue(i); - config::Save(); + config::save(); }); } }); @@ -755,7 +753,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { mDoAud_seStartMenu(kSoundItemChange); getSettings().video.enableFullscreen.setValue(!getSettings().video.enableFullscreen); VISetWindowFullscreen(getSettings().video.enableFullscreen); - config::Save(); + config::save(); }), rightPane, [](Pane& pane) { pane.clear(); }); leftPane.register_control(leftPane.add_button("Restore Default Window Size").on_pressed([] { @@ -786,7 +784,6 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { { .key = "Pause on Focus Lost", .helpText = "Pause the game when window focus is lost.", - .onChange = [](bool value) { aurora_set_pause_on_focus_lost(value); }, .isDisabled = [] { return IsMobile || getSettings().game.speedrunMode; }, }); leftPane.register_control( @@ -818,7 +815,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { .on_pressed([] { mDoAud_seStartMenu(kSoundItemChange); getSettings().video.enableFpsOverlay.setValue(false); - config::Save(); + config::save(); }); for (int i = 0; i < static_cast(kFpsOverlayCornerNames.size()); ++i) { pane.add_button( @@ -834,7 +831,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { mDoAud_seStartMenu(kSoundItemChange); getSettings().video.enableFpsOverlay.setValue(true); getSettings().video.fpsOverlayCorner.setValue(i); - config::Save(); + config::save(); }); } pane.add_rml( @@ -850,7 +847,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { const auto windowSize = aurora::window::get_window_size(); dusk::getSettings().video.lastWindowWidth.setValue(windowSize.width); dusk::getSettings().video.lastWindowHeight.setValue(windowSize.height); - dusk::config::Save(); + dusk::config::save(); } }, .isDisabled = [] { return IsMobile; }, @@ -956,7 +953,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { mDoAud_seStartMenu(kSoundItemChange); getSettings().game.enableFrameInterpolation.setValue(static_cast(i)); android::update_surface_frame_rate(); - config::Save(); + config::save(); }); } pane.add_rml(kUnlockFramerateHelpText); @@ -1151,7 +1148,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { leftPane.add_section("Tools"); addOption("Turbo Key", getSettings().game.enableTurboKeybind, "Hold Tab to increase game speed by up to 4x.", - [] { return getSettings().game.speedrunMode; }); + [] { return getSettings().game.speedrunMode.getValue(); }); addOption("Reset Key (" + Rml::String{hotkeys::DO_RESET} + ")", getSettings().game.enableResetKeybind, "Press " + Rml::String{hotkeys::DO_RESET} + " to reset the game."); @@ -1170,7 +1167,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { .setValue = [](int value) { getSettings().audio.masterVolume.setValue(value); - config::Save(); + config::save(); audio::SetMasterVolume(audio::MasterVolumeToLinear(value / 100.0f)); }, .isModified = @@ -1262,9 +1259,9 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { .setValue = [](int value) { getSettings().game.damageMultiplier.setValue(value); - config::Save(); + config::save(); }, - .isDisabled = [] { return getSettings().game.speedrunMode; }, + .isDisabled = [] { return getSettings().game.speedrunMode.getValue(); }, .isModified = [] { return getSettings().game.damageMultiplier.getValue() != @@ -1408,7 +1405,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { [] { return kMagicArmorModes[static_cast(getSettings().game.armorRupeeDrain.getValue())]; }, - .isDisabled = [] { return getSettings().game.speedrunMode; }, + .isDisabled = [] { return getSettings().game.speedrunMode.getValue(); }, .isModified = [] { return getSettings().game.armorRupeeDrain.getValue() != @@ -1427,7 +1424,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { .on_pressed([i] { mDoAud_seStartMenu(kSoundItemChange); getSettings().game.armorRupeeDrain.setValue(static_cast(i)); - config::Save(); + config::save(); }); } pane.add_rml( @@ -1480,13 +1477,13 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { mDoAud_seStartMenu(kSoundItemChange); getSettings().game.enableAchievementToasts.setValue(true); getSettings().game.enableControllerToasts.setValue(true); - config::Save(); + config::save(); }); pane.add_button("Select None").on_pressed([] { mDoAud_seStartMenu(kSoundItemChange); getSettings().game.enableAchievementToasts.setValue(false); getSettings().game.enableControllerToasts.setValue(false); - config::Save(); + config::save(); }); pane.add_section("Types"); @@ -1502,7 +1499,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { mDoAud_seStartMenu(kSoundItemChange); auto& v = getSettings().game.enableAchievementToasts; v.setValue(!v.getValue()); - config::Save(); + config::save(); }); pane.add_button( { @@ -1514,7 +1511,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { mDoAud_seStartMenu(kSoundItemChange); auto& v = getSettings().game.enableControllerToasts; v.setValue(!v.getValue()); - config::Save(); + config::save(); }); pane.add_rml("
Choose which notifications can be displayed."); }); @@ -1580,7 +1577,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { } } }, - .isDisabled = [] { return getSettings().game.speedrunMode; }, + .isDisabled = [] { return getSettings().game.speedrunMode.getValue(); }, }); config_bool_select(leftPane, rightPane, getSettings().game.showInputViewer, { @@ -1617,15 +1614,13 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { [i] { return getSettings().game.menuScalingMode.getValue() == static_cast(i); - ; }, }) .on_pressed([i] { mDoAud_seStartMenu(kSoundItemChange); getSettings().game.menuScalingMode.setValue( static_cast(i)); - ; - config::Save(); + config::save(); }); } pane.add_rml("
Changes how the Collection and File Select menus scale to your " @@ -1651,7 +1646,7 @@ void SettingsWindow::update() { } void SettingsWindow::hide(bool close) { - config::Save(); + config::save(); Window::hide(close); } diff --git a/src/dusk/ui/touch_controls_editor.cpp b/src/dusk/ui/touch_controls_editor.cpp index 539494d42f..ca7a4fb218 100644 --- a/src/dusk/ui/touch_controls_editor.cpp +++ b/src/dusk/ui/touch_controls_editor.cpp @@ -585,7 +585,7 @@ bool TouchControlsEditor::handle_nav_command(Rml::Event& event, NavCommand cmd) void TouchControlsEditor::save_layout() { mWorkingLayout.version = ControlLayout::Version; getSettings().game.touchControlsLayout.setValue(mWorkingLayout); - config::Save(); + config::save(); mDoAud_seStartMenu(kSoundItemChange); pop(); } diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp index 0a190ed3dd..8ed11c54c4 100644 --- a/src/m_Do/m_Do_main.cpp +++ b/src/m_Do/m_Do_main.cpp @@ -265,7 +265,7 @@ void main01(void) { if (dusk::getSettings().video.rememberWindowSize && !dusk::getSettings().video.enableFullscreen) { dusk::getSettings().video.lastWindowWidth.setValue(event->windowSize.width); dusk::getSettings().video.lastWindowHeight.setValue(event->windowSize.height); - dusk::config::Save(); + dusk::config::save(); } break; case AURORA_DISPLAY_SCALE_CHANGED: @@ -430,16 +430,7 @@ static void ApplyCVarOverrides(const cxxopts::OptionValue& option) { const auto name = std::string_view(cvarArg).substr(0, sep); const auto value = std::string_view(cvarArg).substr(sep + 1); - const auto cVar = dusk::config::GetConfigVar(name); - if (!cVar) { - DuskLog.fatal("Unknown --cvar name: '{}'", name); - } - - try { - cVar->getImpl()->loadFromArg(*cVar, value); - } catch (const std::exception& e) { - DuskLog.fatal("Unable to parse: '{}': {}", value, e.what()); - } + dusk::config::load_arg_override(name, value); } } @@ -510,7 +501,6 @@ int game_main(int argc, char* argv[]) { mainCalled = true; dusk::registerSettings(); - dusk::config::FinishRegistration(); cxxopts::ParseResult parsed_arg_options; @@ -551,10 +541,7 @@ int game_main(int argc, char* argv[]) { log_build_info(); - dusk::config::LoadFromUserPreferences(); - if (dusk::getSettings().game.speedrunMode) { - dusk::resetForSpeedrunMode(); - } + dusk::config::load_from_user_preferences(); ApplyCVarOverrides(parsed_arg_options["cvar"]); dusk::android::update_surface_frame_rate(); dusk::crash_reporting::initialize(); @@ -618,6 +605,12 @@ int game_main(int argc, char* argv[]) { auroraInfo = aurora_initialize(argc, argv, &config); } + // Apply after aurora_initialize: speedrun mode mutates cvars whose change callbacks push + // values into aurora. + if (dusk::getSettings().game.speedrunMode) { + dusk::resetForSpeedrunMode(); + } + #ifdef DUSK_DISCORD if (dusk::getSettings().game.enableDiscordPresence) { dusk::discord::initialize(); @@ -700,7 +693,7 @@ int game_main(int argc, char* argv[]) { dusk::getSettings().backend.isoPath.setValue(dvd_path); dusk::getSettings().backend.isoVerification.setValue( dusk::DiscVerificationState::Unknown); - dusk::config::Save(); + dusk::config::save(); dusk::IsGameLaunched = true; } } else { @@ -723,7 +716,7 @@ int game_main(int argc, char* argv[]) { saveConfigBeforePrelaunch = true; } if (saveConfigBeforePrelaunch) { - dusk::config::Save(); + dusk::config::save(); } if (!dusk::getSettings().backend.skipPreLaunchUI) { @@ -814,6 +807,7 @@ int game_main(int argc, char* argv[]) { #endif dusk::ui::shutdown(); dusk::texture_replacements::shutdown(); + dusk::config::shutdown(); aurora_shutdown(); return 0;