include/DetourModKit/config.hpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #ifndef DETOURMODKIT_CONFIG_HPP | ||
| 2 | #define DETOURMODKIT_CONFIG_HPP | ||
| 3 | |||
| 4 | /** | ||
| 5 | * @file config.hpp | ||
| 6 | * @brief INI-backed configuration binding, hot-reload, and the INI-to-input combo fusion. | ||
| 7 | * @details Binds INI keys to atomics, callbacks, and the logger, loads and hot-reloads an INI file, and fuses an INI | ||
| 8 | * key to a live input combo binding. The filesystem watcher that drives auto-reload is folded in behind | ||
| 9 | * enable_auto_reload / disable_auto_reload. | ||
| 10 | * | ||
| 11 | * Config is fail-soft: a missing or malformed INI key falls back to the registered default and is logged, | ||
| 12 | * never reported as an error, so the surface speaks void / bool / AutoReloadStatus rather than Result. Only | ||
| 13 | * the input combo fusion returns an input::BindingGuard. | ||
| 14 | * | ||
| 15 | * @note Thread safety: bind_* / getters / log_all use a deferred-callback pattern: registry state is read and written | ||
| 16 | * under the config mutex, but setter callbacks run after the mutex is released, so a setter may re-enter those | ||
| 17 | * data-plane calls without deadlocking. load() and reload() hold an outer, non-reentrant pass lock across the | ||
| 18 | * whole read + content-hash + setter phase. load(), reload(), and disable_auto_reload() called by a bound | ||
| 19 | * setter are refused rather than allowed to deadlock. clear() is also refused except on the reload-servicer | ||
| 20 | * thread. | ||
| 21 | * @warning `[B-100]` Run load(), reload(), registration, and enable_auto_reload() outside the loader lock. These | ||
| 22 | * routes allocate, and enable_auto_reload() creates the watcher thread. The loader-lock teardown path | ||
| 23 | * detaches the watcher without a wait. `ConfigWatcherLoaderLockTest.*` pins the boundary. | ||
| 24 | */ | ||
| 25 | |||
| 26 | #include "DetourModKit/input.hpp" | ||
| 27 | |||
| 28 | #include <atomic> | ||
| 29 | #include <chrono> | ||
| 30 | #include <concepts> | ||
| 31 | #include <cstdint> | ||
| 32 | #include <functional> | ||
| 33 | #include <optional> | ||
| 34 | #include <string> | ||
| 35 | #include <string_view> | ||
| 36 | |||
| 37 | namespace DetourModKit | ||
| 38 | { | ||
| 39 | namespace config | ||
| 40 | { | ||
| 41 | /** | ||
| 42 | * @enum AutoReloadStatus | ||
| 43 | * @brief Outcome of a call to enable_auto_reload(). | ||
| 44 | */ | ||
| 45 | enum class AutoReloadStatus : std::uint8_t | ||
| 46 | { | ||
| 47 | /// The watcher is now running. | ||
| 48 | Started, | ||
| 49 | /// Called while a watcher was already installed; the existing one was kept. | ||
| 50 | AlreadyRunning, | ||
| 51 | /// load() was never called, so there is no path to watch. | ||
| 52 | NoPriorLoad, | ||
| 53 | /// The parent-directory open failed or the start handshake failed. | ||
| 54 | StartFailed | ||
| 55 | }; | ||
| 56 | |||
| 57 | /// Constrains the atomic-backed bind to the scalar types the INI pipeline parses directly. | ||
| 58 | template <typename T> | ||
| 59 | concept BindableScalar = std::same_as<T, int> || std::same_as<T, bool> || std::same_as<T, float>; | ||
| 60 | |||
| 61 | /** | ||
| 62 | * @brief Binds an integer INI key to a callback. | ||
| 63 | * @details The setter is invoked immediately with @p default_value and again on every load() / reload() with | ||
| 64 | * the parsed INI value. Integers are parsed with a hardened decimal/hex parser (range-checked) rather | ||
| 65 | * than the underlying INI library's saturating conversion. | ||
| 66 | * @param section INI section name. | ||
| 67 | * @param key INI key name. | ||
| 68 | * @param display_name Human-readable name shown in log output. | ||
| 69 | * @param setter Callback applied with the resolved value. Must be reentrant and thread-safe. | ||
| 70 | * @param default_value Value used when the key is absent or unparsable. | ||
| 71 | * @note Setup/control-plane only: registration may allocate and updates the config registry. | ||
| 72 | */ | ||
| 73 | void bind_int( | ||
| 74 | std::string_view section, | ||
| 75 | std::string_view key, | ||
| 76 | std::string_view display_name, | ||
| 77 | std::function<void(int)> setter, | ||
| 78 | int default_value | ||
| 79 | ); | ||
| 80 | |||
| 81 | /// Binds a floating-point INI key to a callback. See bind_int for the invocation contract. | ||
| 82 | void bind_float( | ||
| 83 | std::string_view section, | ||
| 84 | std::string_view key, | ||
| 85 | std::string_view display_name, | ||
| 86 | std::function<void(float)> setter, | ||
| 87 | float default_value | ||
| 88 | ); | ||
| 89 | |||
| 90 | /// Binds a boolean INI key to a callback. See bind_int for the invocation contract. | ||
| 91 | void bind_bool( | ||
| 92 | std::string_view section, | ||
| 93 | std::string_view key, | ||
| 94 | std::string_view display_name, | ||
| 95 | std::function<void(bool)> setter, | ||
| 96 | bool default_value | ||
| 97 | ); | ||
| 98 | |||
| 99 | /** | ||
| 100 | * @brief Binds a string INI key to a callback (the general parse-into-anything form). | ||
| 101 | * @details The setter receives the raw INI value verbatim (narrow bytes; ASCII passes through unchanged, a | ||
| 102 | * non-ASCII value arrives as raw bytes for the consumer to interpret). Use it to parse a value into a | ||
| 103 | * mask, an enum, or a non-atomic field. The value is delivered as a string_view that is valid only for | ||
| 104 | * the duration of the call and is not guaranteed to be NUL-terminated. | ||
| 105 | * @param section INI section name. | ||
| 106 | * @param key INI key name. | ||
| 107 | * @param display_name Human-readable name shown in log output. | ||
| 108 | * @param setter Callback applied with the resolved value. Must be reentrant and thread-safe. | ||
| 109 | * @param default_value Value used when the key is absent. | ||
| 110 | * @note Setup/control-plane only: registration may allocate and updates the config registry. | ||
| 111 | */ | ||
| 112 | void bind_string( | ||
| 113 | std::string_view section, | ||
| 114 | std::string_view key, | ||
| 115 | std::string_view display_name, | ||
| 116 | std::function<void(std::string_view)> setter, | ||
| 117 | std::string_view default_value | ||
| 118 | ); | ||
| 119 | |||
| 120 | /** | ||
| 121 | * @brief Binds an INI combo string to a callback receiving the parsed combo list (no input binding). | ||
| 122 | * @details Parses the INI value into an input::KeyComboList (see press_combo for the combo grammar and the | ||
| 123 | * "NONE"/empty opt-out) and delivers it to @p setter at registration and on each load() / reload(). | ||
| 124 | * Unlike press_combo this registers no input binding; the consumer owns the parsed combos and uses | ||
| 125 | * them however it likes. | ||
| 126 | * @param section INI section name. | ||
| 127 | * @param key INI key name. | ||
| 128 | * @param display_name Human-readable name shown in log output and in the typo Warning. | ||
| 129 | * @param setter Callback applied with the parsed combo list. | ||
| 130 | * @param default_value Default combo string when the key is absent. | ||
| 131 | * @note Setup/control-plane only: registration may allocate and updates the config registry. | ||
| 132 | */ | ||
| 133 | void bind_combos( | ||
| 134 | std::string_view section, | ||
| 135 | std::string_view key, | ||
| 136 | std::string_view display_name, | ||
| 137 | std::function<void(const input::KeyComboList &)> setter, | ||
| 138 | std::string_view default_value | ||
| 139 | ); | ||
| 140 | |||
| 141 | /** | ||
| 142 | * @brief Binds an INI key to a caller-supplied atomic (the most common form). | ||
| 143 | * @details Convenience over the matching bind_<T> callback that stores the parsed value with | ||
| 144 | * std::memory_order_relaxed. The atomic must outlive every load() / reload(): the setter captures | ||
| 145 | * @p out by reference. | ||
| 146 | * @tparam T One of int, bool, float. | ||
| 147 | * @param section INI section name. | ||
| 148 | * @param key INI key name. | ||
| 149 | * @param display_name Human-readable name shown in log output. | ||
| 150 | * @param out Atomic destination updated on every successful parse. | ||
| 151 | * @param default_value Value applied when the INI key is missing. | ||
| 152 | * @note Setup/control-plane only: registration may allocate and updates the config registry. | ||
| 153 | */ | ||
| 154 | template <BindableScalar T> | ||
| 155 | 16 | void bind( | |
| 156 | std::string_view section, | ||
| 157 | std::string_view key, | ||
| 158 | std::string_view display_name, | ||
| 159 | std::atomic<T> &out, | ||
| 160 | T default_value | ||
| 161 | ) | ||
| 162 | { | ||
| 163 | if constexpr (std::same_as<T, int>) | ||
| 164 | { | ||
| 165 |
1/2✓ Branch 3 → 4 taken 11 times.
✗ Branch 3 → 6 not taken.
|
11 | bind_int( |
| 166 | section, | ||
| 167 | key, | ||
| 168 | display_name, | ||
| 169 | 52 | [&out](int v) { out.store(v, std::memory_order_relaxed); }, | |
| 170 | default_value | ||
| 171 | ); | ||
| 172 | } | ||
| 173 | else if constexpr (std::same_as<T, bool>) | ||
| 174 | { | ||
| 175 |
1/2✓ Branch 3 → 4 taken 3 times.
✗ Branch 3 → 6 not taken.
|
3 | bind_bool( |
| 176 | section, | ||
| 177 | key, | ||
| 178 | display_name, | ||
| 179 | 12 | [&out](bool v) { out.store(v, std::memory_order_relaxed); }, | |
| 180 | default_value | ||
| 181 | ); | ||
| 182 | } | ||
| 183 | else | ||
| 184 | { | ||
| 185 |
1/2✓ Branch 3 → 4 taken 2 times.
✗ Branch 3 → 6 not taken.
|
2 | bind_float( |
| 186 | section, | ||
| 187 | key, | ||
| 188 | display_name, | ||
| 189 | 8 | [&out](float v) { out.store(v, std::memory_order_relaxed); }, | |
| 190 | default_value | ||
| 191 | ); | ||
| 192 | } | ||
| 193 | 16 | } | |
| 194 | |||
| 195 | /** | ||
| 196 | * @brief Binds an INI key to an atomic, using the atomic's current value as the default. | ||
| 197 | * @details Samples @p out once with std::memory_order_relaxed at registration time to use as the INI fallback, | ||
| 198 | * then stores parsed values back to @p out on every load() / reload(). Initialize the atomic | ||
| 199 | * deliberately before calling this overload. | ||
| 200 | * @tparam T One of int, bool, float. | ||
| 201 | * @note Setup/control-plane only (see the default-taking overload). | ||
| 202 | */ | ||
| 203 | template <BindableScalar T> | ||
| 204 | 6 | void bind(std::string_view section, std::string_view key, std::string_view display_name, std::atomic<T> &out) | |
| 205 | { | ||
| 206 | 8 | bind<T>(section, key, display_name, out, out.load(std::memory_order_relaxed)); | |
| 207 | 6 | } | |
| 208 | |||
| 209 | /** | ||
| 210 | * @brief Binds an INI key to an atomic uint32 through a user parse function. | ||
| 211 | * @details The parse function turns the raw INI string into a uint32 (for example a bitmask) that is stored | ||
| 212 | * with std::memory_order_relaxed. Applied at registration with @p default_value and again on each | ||
| 213 | * load() / reload(). | ||
| 214 | * @param section INI section name. | ||
| 215 | * @param key INI key name. | ||
| 216 | * @param display_name Human-readable name shown in log output. | ||
| 217 | * @param out Atomic destination for the parsed value. | ||
| 218 | * @param parse Pure function turning the raw INI string into the stored value. | ||
| 219 | * @param default_value Default INI string parsed when the key is absent. | ||
| 220 | * @note Setup/control-plane only: registration may allocate and updates the config registry. | ||
| 221 | */ | ||
| 222 | void bind_parsed( | ||
| 223 | std::string_view section, | ||
| 224 | std::string_view key, | ||
| 225 | std::string_view display_name, | ||
| 226 | std::atomic<std::uint32_t> &out, | ||
| 227 | std::function<std::uint32_t(std::string_view)> parse, | ||
| 228 | std::string_view default_value | ||
| 229 | ); | ||
| 230 | |||
| 231 | /** | ||
| 232 | * @brief Binds a log-level INI key that applies directly to the logger. | ||
| 233 | * @details Parses @p default_value via the logger's string-to-level mapping and applies it both at registration | ||
| 234 | * and on each load() / reload(). Unrecognized values fall back to the Info level. | ||
| 235 | * @param section INI section name. | ||
| 236 | * @param key INI key name. | ||
| 237 | * @param default_value Default level string (e.g. "INFO", "DEBUG"). | ||
| 238 | * @note Setup/control-plane only: registration may allocate and updates the config registry. | ||
| 239 | */ | ||
| 240 | void bind_log_level(std::string_view section, std::string_view key, std::string_view default_value = "INFO"); | ||
| 241 | |||
| 242 | /** | ||
| 243 | * @brief Binds an INI combo string to a press-mode input binding and returns its guard. | ||
| 244 | * @details Parses the INI value as one or more key combinations (commas separate independent combos under OR | ||
| 245 | * logic; '+' separates modifiers from the trailing trigger; tokens are key names or hex VK codes), | ||
| 246 | * registers a press binding under @p binding_name via input::register_combo, and rebinds it on every | ||
| 247 | * load() / reload() so the bound keys track the INI without re-registering. | ||
| 248 | * | ||
| 249 | * Two opt-out sentinels yield an unbound but addressable binding silently: an empty value and the | ||
| 250 | * literal "NONE" (case-insensitive, whole trimmed value only). A non-empty value whose every token | ||
| 251 | * fails to parse is logged once at Warning level naming @p log_name and the offending string. | ||
| 252 | * @param section INI section name. | ||
| 253 | * @param ini_key INI key holding the combo string. | ||
| 254 | * @param log_name Human-readable name echoed by the config logger and in the typo Warning. | ||
| 255 | * @param binding_name input binding name (must be unique). | ||
| 256 | * @param on_press Callback fired on the key-down edge. | ||
| 257 | * @param default_combo Default combo string when the key is absent. | ||
| 258 | * @param consume Optional per-binding suppression facet. std::nullopt registers no extra key; a value registers | ||
| 259 | * a bool item named "<ini_key>.Consume" wired to input suppression for this binding (honored for | ||
| 260 | * digital gamepad buttons and the mouse wheel only). | ||
| 261 | * @return An input::BindingGuard owning the callback's lifetime. Store it (e.g. in an input::Scope); letting it | ||
| 262 | * drop immediately disables the binding. Fail-soft: if the underlying input::register_combo cannot | ||
| 263 | * allocate, an inert guard whose name() is empty is returned and the failure is logged (the binding is | ||
| 264 | * simply not installed). | ||
| 265 | * @note Setup/control-plane only: the bind registers an input binding and updates the config registry. | ||
| 266 | */ | ||
| 267 | [[nodiscard]] input::BindingGuard press_combo( | ||
| 268 | std::string_view section, | ||
| 269 | std::string_view ini_key, | ||
| 270 | std::string_view log_name, | ||
| 271 | std::string_view binding_name, | ||
| 272 | std::function<void()> on_press, | ||
| 273 | std::string_view default_combo, | ||
| 274 | std::optional<bool> consume = std::nullopt | ||
| 275 | ); | ||
| 276 | |||
| 277 | /** | ||
| 278 | * @brief Binds an INI combo string to a hold-mode input binding and returns its guard. | ||
| 279 | * @details The hold-mode mirror of press_combo. @p on_state_change fires true on the press edge and false on | ||
| 280 | * the release edge; the returned guard synthesizes a single balancing false if it cancels a still-held | ||
| 281 | * binding. The "NONE"/empty opt-out, the typo Warning, and the live-rebind on reload all match | ||
| 282 | * press_combo. | ||
| 283 | * @param section INI section name. | ||
| 284 | * @param ini_key INI key holding the combo string. | ||
| 285 | * @param log_name Human-readable name echoed by the config logger and in the typo Warning. | ||
| 286 | * @param binding_name input binding name (must be unique). | ||
| 287 | * @param on_state_change Callback fired with the hold state (true = held, false = released). | ||
| 288 | * @param default_combo Default combo string when the key is absent. | ||
| 289 | * @param consume Optional per-binding suppression facet (see press_combo). | ||
| 290 | * @return An input::BindingGuard. Destroying it may synthesize the final on_state_change(false), so treat it as | ||
| 291 | * setup/control-plane only. Fail-soft: a registration that cannot allocate yields an inert guard whose | ||
| 292 | * name() is empty and is logged (the binding is simply not installed). | ||
| 293 | * @note Setup/control-plane only: the bind registers an input binding and updates the config registry. | ||
| 294 | */ | ||
| 295 | [[nodiscard]] input::BindingGuard hold_combo( | ||
| 296 | std::string_view section, | ||
| 297 | std::string_view ini_key, | ||
| 298 | std::string_view log_name, | ||
| 299 | std::string_view binding_name, | ||
| 300 | std::function<void(bool)> on_state_change, | ||
| 301 | std::string_view default_combo, | ||
| 302 | std::optional<bool> consume = std::nullopt | ||
| 303 | ); | ||
| 304 | |||
| 305 | /** | ||
| 306 | * @brief Binds a boolean INI key that toggles input suppression for an already-registered binding. | ||
| 307 | * @details Fuses bind_bool with input suppression: the INI value decides whether @p binding_name hides its | ||
| 308 | * trigger from the game, applied at registration and on each load() / reload(). Register the binding | ||
| 309 | * first; this is a no-op for an unknown name. Suppression is honored for digital gamepad buttons and | ||
| 310 | * the mouse wheel only. | ||
| 311 | * @param section INI section name. | ||
| 312 | * @param ini_key INI key name (e.g. "SetYToggle.Consume"). | ||
| 313 | * @param display_name Human-readable name shown in log output. | ||
| 314 | * @param binding_name input binding name to toggle. | ||
| 315 | * @param default_value Suppression state when the INI key is missing. | ||
| 316 | * @note Setup/control-plane only: registration may allocate and updates the config registry. | ||
| 317 | */ | ||
| 318 | void consume_flag( | ||
| 319 | std::string_view section, | ||
| 320 | std::string_view ini_key, | ||
| 321 | std::string_view display_name, | ||
| 322 | std::string_view binding_name, | ||
| 323 | bool default_value = false | ||
| 324 | ); | ||
| 325 | |||
| 326 | /** | ||
| 327 | * @brief Registers a hotkey combo that triggers reload() on press. | ||
| 328 | * @details A press_combo whose callback requests a reload off a dedicated background servicer thread (the press | ||
| 329 | * callback only flips a flag and notifies, so per-press latency stays low). The INI-configured combo | ||
| 330 | * overrides @p default_combo on each load() / reload(). | ||
| 331 | * @param ini_key INI key that stores the combo string. | ||
| 332 | * @param default_combo Combo applied when the key is absent (e.g. "Ctrl+F5"). | ||
| 333 | * @return true if the binding was registered; false if @p default_combo is empty or the NONE sentinel (a reload | ||
| 334 | * hotkey with no keys is never useful, so it is rejected at the call site). | ||
| 335 | * @note Setup/control-plane only: the bind registers an input binding and updates the config registry. | ||
| 336 | */ | ||
| 337 | [[nodiscard]] bool reload_hotkey(std::string_view ini_key, std::string_view default_combo); | ||
| 338 | |||
| 339 | /** | ||
| 340 | * @brief Loads all bound settings from the named INI file. | ||
| 341 | * @details Resolves @p ini_filename against the mod's runtime directory, parses it, and applies each bound | ||
| 342 | * setter with the INI value (or its default if the key is missing or invalid). The path is remembered | ||
| 343 | * so reload() operates on the same file. If auto-reload is active and @p ini_filename resolves to a | ||
| 344 | * different file than the watcher is currently monitoring, the watcher is re-pointed to the new file | ||
| 345 | * (its debounce and on_reload callback are preserved), so a hot-swap of the config file keeps | ||
| 346 | * auto-reload working. Re-pointing is skipped with a logged error if load() is called from the watcher | ||
| 347 | * thread itself (a self-join hazard); re-point from another thread in that case. | ||
| 348 | * @param ini_filename The INI filename, resolved relative to the runtime directory. | ||
| 349 | * @note Setup/control-plane only: the load reads the file and runs every bound setter. | ||
| 350 | */ | ||
| 351 | void load(std::string_view ini_filename); | ||
| 352 | |||
| 353 | /** | ||
| 354 | * @brief Re-applies all bound setters against the last-loaded INI file. | ||
| 355 | * @details Re-reads the file passed to the most recent load() and re-invokes every setter with the fresh value. | ||
| 356 | * If the file's bytes are unchanged since the last successful load (content-hash short-circuit), the | ||
| 357 | * setters are skipped. If the file cannot be read (deleted or locked mid-save) or fails to parse, the | ||
| 358 | * setters are also skipped and the last-applied values are retained rather than snapped back to their | ||
| 359 | * defaults; reload() still returns true. Bindings persist across reloads. | ||
| 360 | * @return true if a previous load() path was available and the reload proceeded; false if reload() was called | ||
| 361 | * before any load(). | ||
| 362 | * @note Safe from any thread. Concurrent reload() and load() passes are serialized end to end, so two racing | ||
| 363 | * reloads apply in a well-defined order and a slower stale pass can never overwrite a fresher one. A | ||
| 364 | * bound setter must not call reload()/load()/disable_auto_reload(): those calls are refused rather than | ||
| 365 | * allowed to self-deadlock or join a worker waiting for this pass. clear() is likewise refused except | ||
| 366 | * when the setter runs on the reload-servicer thread, whose owner can retire through the off-thread | ||
| 367 | * reaper. | ||
| 368 | * A bind_* registered after the last successful load re-hydrates on the next reload even when the file | ||
| 369 | * bytes are unchanged. Only C++ exceptions from setters are caught; a structured-exception fault or a | ||
| 370 | * throwing noexcept setter is not recoverable. | ||
| 371 | * @note Setup/control-plane only: the reload reads the file and runs every bound setter. | ||
| 372 | */ | ||
| 373 | [[nodiscard]] bool reload(); | ||
| 374 | |||
| 375 | /** | ||
| 376 | * @brief Starts a background watcher that calls reload() when the INI changes. | ||
| 377 | * @details Watches the directory of the path last passed to load(), collapsing bursty editor saves into one | ||
| 378 | * reload via the @p debounce quiet window. After each reload, @p on_reload is invoked with a flag | ||
| 379 | * that is true when at least one bound setter ran and false when none did: an unchanged-content, | ||
| 380 | * read-failure, or parse-failure skip that retained the current values, a config whose bound keys | ||
| 381 | * carry no setter, or an unload latch that aborted before the first setter. The watcher and the | ||
| 382 | * callback run on the watcher's background thread. | ||
| 383 | * @param debounce Quiet window between change detection and reload (default 250 ms). | ||
| 384 | * @param on_reload Optional callback invoked after each reload attempt. | ||
| 385 | * @return Started if the watcher is now running; AlreadyRunning if one was already installed; NoPriorLoad if | ||
| 386 | * load() was never called; StartFailed if the directory open or the handshake failed. | ||
| 387 | * @note Setup/control-plane only: the start creates the watcher thread. | ||
| 388 | */ | ||
| 389 | [[nodiscard]] AutoReloadStatus enable_auto_reload( | ||
| 390 | std::chrono::milliseconds debounce = std::chrono::milliseconds{250}, | ||
| 391 | std::function<void(bool)> on_reload = {} | ||
| 392 | ); | ||
| 393 | |||
| 394 | /** | ||
| 395 | * @brief Stops the auto-reload watcher synchronously. | ||
| 396 | * @details Idempotent. This is an honest synchronous rundown: it returns once the watcher's notification drain | ||
| 397 | * (bounded), a final debounced reload callback if a change is still pending, and the worker join have | ||
| 398 | * completed, so a blocking user callback blocks this call for exactly as long. It is not time-bounded | ||
| 399 | * and never detaches a running callback. When the caller is not authorized to block (an unload phase | ||
| 400 | * is published, or the fail-closed loader-lock probe vetoes), the worker is detached instead of | ||
| 401 | * joined and the call returns without that rundown. A call from inside an on_reload callback (the | ||
| 402 | * watcher thread) is a no-op that logs and leaves the watcher running, because a self-join deadlocks. | ||
| 403 | * @note Setup/control-plane only: the stop joins the watcher thread on the authorized path. | ||
| 404 | */ | ||
| 405 | void disable_auto_reload() noexcept; | ||
| 406 | |||
| 407 | /// Logs the current value of every bound setting, grouped by section. | ||
| 408 | void log_all(); | ||
| 409 | |||
| 410 | /** | ||
| 411 | * @brief Clears every bound item and the cached load path. | ||
| 412 | * @details Does not stop the auto-reload watcher; call disable_auto_reload() first so a watcher callback cannot | ||
| 413 | * fire against a torn-down registry. | ||
| 414 | * @note Setup/control-plane only: the clear tears down the config registry. | ||
| 415 | */ | ||
| 416 | void clear() noexcept; | ||
| 417 | |||
| 418 | class Ini; | ||
| 419 | |||
| 420 | /** | ||
| 421 | * @class SectionBinder | ||
| 422 | * @brief A section-scoped view that drops the repeated section argument from the bind family. | ||
| 423 | * @details Obtained from Ini::section() or config::section(). Each method forwards to the matching free | ||
| 424 | * function with the bound section name. Lightweight and copyable; it holds only the section name. | ||
| 425 | */ | ||
| 426 | class SectionBinder | ||
| 427 | { | ||
| 428 | public: | ||
| 429 | /// Constructs a binder scoped to @p section. Prefer Ini::section() / config::section(). | ||
| 430 | explicit SectionBinder(std::string_view section) : m_section(section) {} | ||
| 431 | |||
| 432 | /// Section-scoped atomic bind. See config::bind. | ||
| 433 | template <BindableScalar T> void bind(std::string_view key, std::atomic<T> &out, T default_value) const | ||
| 434 | { | ||
| 435 | config::bind<T>(m_section, key, key, out, default_value); | ||
| 436 | } | ||
| 437 | |||
| 438 | /// Section-scoped atomic bind with the atomic's current value as the default. | ||
| 439 | template <BindableScalar T> void bind(std::string_view key, std::atomic<T> &out) const | ||
| 440 | { | ||
| 441 | config::bind<T>(m_section, key, key, out); | ||
| 442 | } | ||
| 443 | |||
| 444 | /// Section-scoped atomic bind with an explicit display name. | ||
| 445 | template <BindableScalar T> | ||
| 446 | void bind(std::string_view key, std::string_view display_name, std::atomic<T> &out, T default_value) const | ||
| 447 | { | ||
| 448 | config::bind<T>(m_section, key, display_name, out, default_value); | ||
| 449 | } | ||
| 450 | |||
| 451 | /// Section-scoped integer callback bind. See config::bind_int. | ||
| 452 | void bind_int( | ||
| 453 | std::string_view key, | ||
| 454 | std::string_view display_name, | ||
| 455 | std::function<void(int)> setter, | ||
| 456 | int default_value | ||
| 457 | ) const | ||
| 458 | { | ||
| 459 | config::bind_int(m_section, key, display_name, std::move(setter), default_value); | ||
| 460 | } | ||
| 461 | |||
| 462 | /// Section-scoped float callback bind. See config::bind_float. | ||
| 463 | void bind_float( | ||
| 464 | std::string_view key, | ||
| 465 | std::string_view display_name, | ||
| 466 | std::function<void(float)> setter, | ||
| 467 | float default_value | ||
| 468 | ) const | ||
| 469 | { | ||
| 470 | config::bind_float(m_section, key, display_name, std::move(setter), default_value); | ||
| 471 | } | ||
| 472 | |||
| 473 | /// Section-scoped bool callback bind. See config::bind_bool. | ||
| 474 | void bind_bool( | ||
| 475 | std::string_view key, | ||
| 476 | std::string_view display_name, | ||
| 477 | std::function<void(bool)> setter, | ||
| 478 | bool default_value | ||
| 479 | ) const | ||
| 480 | { | ||
| 481 | config::bind_bool(m_section, key, display_name, std::move(setter), default_value); | ||
| 482 | } | ||
| 483 | |||
| 484 | /// Section-scoped string callback bind. See config::bind_string. | ||
| 485 | void bind_string( | ||
| 486 | std::string_view key, | ||
| 487 | std::string_view display_name, | ||
| 488 | std::function<void(std::string_view)> setter, | ||
| 489 | std::string_view default_value | ||
| 490 | ) const | ||
| 491 | { | ||
| 492 | config::bind_string(m_section, key, display_name, std::move(setter), default_value); | ||
| 493 | } | ||
| 494 | |||
| 495 | /// Section-scoped combo-list bind (no input binding). See config::bind_combos. | ||
| 496 | void bind_combos( | ||
| 497 | std::string_view key, | ||
| 498 | std::string_view display_name, | ||
| 499 | std::function<void(const input::KeyComboList &)> setter, | ||
| 500 | std::string_view default_value | ||
| 501 | ) const | ||
| 502 | { | ||
| 503 | config::bind_combos(m_section, key, display_name, std::move(setter), default_value); | ||
| 504 | } | ||
| 505 | |||
| 506 | /// Section-scoped parsed atomic-uint32 bind. See config::bind_parsed. | ||
| 507 | void bind_parsed( | ||
| 508 | std::string_view key, | ||
| 509 | std::string_view display_name, | ||
| 510 | std::atomic<std::uint32_t> &out, | ||
| 511 | std::function<std::uint32_t(std::string_view)> parse, | ||
| 512 | std::string_view default_value | ||
| 513 | ) const | ||
| 514 | { | ||
| 515 | config::bind_parsed(m_section, key, display_name, out, std::move(parse), default_value); | ||
| 516 | } | ||
| 517 | |||
| 518 | /// Section-scoped log-level bind. See config::bind_log_level. | ||
| 519 | void bind_log_level(std::string_view key, std::string_view default_value = "INFO") const | ||
| 520 | { | ||
| 521 | config::bind_log_level(m_section, key, default_value); | ||
| 522 | } | ||
| 523 | |||
| 524 | /// Section-scoped press-combo fusion. See config::press_combo. | ||
| 525 | [[nodiscard]] input::BindingGuard press_combo( | ||
| 526 | std::string_view ini_key, | ||
| 527 | std::string_view log_name, | ||
| 528 | std::string_view binding_name, | ||
| 529 | std::function<void()> on_press, | ||
| 530 | std::string_view default_combo, | ||
| 531 | std::optional<bool> consume = std::nullopt | ||
| 532 | ) const | ||
| 533 | { | ||
| 534 | return config::press_combo( | ||
| 535 | m_section, | ||
| 536 | ini_key, | ||
| 537 | log_name, | ||
| 538 | binding_name, | ||
| 539 | std::move(on_press), | ||
| 540 | default_combo, | ||
| 541 | consume | ||
| 542 | ); | ||
| 543 | } | ||
| 544 | |||
| 545 | /// Section-scoped hold-combo fusion. See config::hold_combo. | ||
| 546 | [[nodiscard]] input::BindingGuard hold_combo( | ||
| 547 | std::string_view ini_key, | ||
| 548 | std::string_view log_name, | ||
| 549 | std::string_view binding_name, | ||
| 550 | std::function<void(bool)> on_state_change, | ||
| 551 | std::string_view default_combo, | ||
| 552 | std::optional<bool> consume = std::nullopt | ||
| 553 | ) const | ||
| 554 | { | ||
| 555 | return config::hold_combo( | ||
| 556 | m_section, | ||
| 557 | ini_key, | ||
| 558 | log_name, | ||
| 559 | binding_name, | ||
| 560 | std::move(on_state_change), | ||
| 561 | default_combo, | ||
| 562 | consume | ||
| 563 | ); | ||
| 564 | } | ||
| 565 | |||
| 566 | /// Section-scoped consume-flag fusion. See config::consume_flag. | ||
| 567 | void consume_flag( | ||
| 568 | std::string_view ini_key, | ||
| 569 | std::string_view display_name, | ||
| 570 | std::string_view binding_name, | ||
| 571 | bool default_value = false | ||
| 572 | ) const | ||
| 573 | { | ||
| 574 | config::consume_flag(m_section, ini_key, display_name, binding_name, default_value); | ||
| 575 | } | ||
| 576 | |||
| 577 | private: | ||
| 578 | std::string m_section; | ||
| 579 | }; | ||
| 580 | |||
| 581 | /// Returns a section-scoped binder for @p name. Equivalent to Ini{}.section(name). | ||
| 582 | [[nodiscard]] inline SectionBinder section(std::string_view name) | ||
| 583 | { | ||
| 584 | return SectionBinder{name}; | ||
| 585 | } | ||
| 586 | |||
| 587 | /** | ||
| 588 | * @class Ini | ||
| 589 | * @brief A handle to the process configuration registry. | ||
| 590 | * @details Exposes section() plus the common operations. The rest of the bind family is reached through the | ||
| 591 | * free functions or through section(). Every Ini and every free function act on one shared process | ||
| 592 | * registry, so an Ini is a thin, copyable handle rather than an independent configuration. | ||
| 593 | */ | ||
| 594 | class Ini | ||
| 595 | { | ||
| 596 | public: | ||
| 597 | Ini() = default; | ||
| 598 | |||
| 599 | /// Returns a section-scoped binder. See config::section. | ||
| 600 | [[nodiscard]] SectionBinder section(std::string_view name) const { return SectionBinder{name}; } | ||
| 601 | |||
| 602 | /// Atomic bind. See config::bind. | ||
| 603 | template <BindableScalar T> | ||
| 604 | 8 | void bind( | |
| 605 | std::string_view sec, | ||
| 606 | std::string_view key, | ||
| 607 | std::string_view display_name, | ||
| 608 | std::atomic<T> &out, | ||
| 609 | T default_value | ||
| 610 | ) const | ||
| 611 | { | ||
| 612 | 8 | config::bind<T>(sec, key, display_name, out, default_value); | |
| 613 | 8 | } | |
| 614 | |||
| 615 | /// Atomic bind with the atomic's current value as the default. See config::bind. | ||
| 616 | template <BindableScalar T> | ||
| 617 | void | ||
| 618 | bind(std::string_view sec, std::string_view key, std::string_view display_name, std::atomic<T> &out) const | ||
| 619 | { | ||
| 620 | config::bind<T>(sec, key, display_name, out); | ||
| 621 | } | ||
| 622 | |||
| 623 | /// Parsed atomic-uint32 bind. See config::bind_parsed. | ||
| 624 | void bind_parsed( | ||
| 625 | std::string_view sec, | ||
| 626 | std::string_view key, | ||
| 627 | std::string_view display_name, | ||
| 628 | std::atomic<std::uint32_t> &out, | ||
| 629 | std::function<std::uint32_t(std::string_view)> parse, | ||
| 630 | std::string_view default_value | ||
| 631 | ) const | ||
| 632 | { | ||
| 633 | config::bind_parsed(sec, key, display_name, out, std::move(parse), default_value); | ||
| 634 | } | ||
| 635 | |||
| 636 | /// Log-level bind. See config::bind_log_level. | ||
| 637 | void | ||
| 638 | bind_log_level(std::string_view sec, std::string_view key, std::string_view default_value = "INFO") const | ||
| 639 | { | ||
| 640 | config::bind_log_level(sec, key, default_value); | ||
| 641 | } | ||
| 642 | |||
| 643 | /// Loads the named INI file. See config::load. | ||
| 644 | 8 | void load(std::string_view ini_filename) const { config::load(ini_filename); } | |
| 645 | |||
| 646 | /// Re-applies bound setters. See config::reload. | ||
| 647 | [[nodiscard]] bool reload() const { return config::reload(); } | ||
| 648 | |||
| 649 | /// Starts the auto-reload watcher. See config::enable_auto_reload. | ||
| 650 | 8 | [[nodiscard]] AutoReloadStatus enable_auto_reload( | |
| 651 | std::chrono::milliseconds debounce = std::chrono::milliseconds{250}, | ||
| 652 | std::function<void(bool)> on_reload = {} | ||
| 653 | ) const | ||
| 654 | { | ||
| 655 |
1/2✓ Branch 5 → 6 taken 8 times.
✗ Branch 5 → 10 not taken.
|
8 | return config::enable_auto_reload(debounce, std::move(on_reload)); |
| 656 | } | ||
| 657 | |||
| 658 | /// Stops the auto-reload watcher. See config::disable_auto_reload. | ||
| 659 | void disable_auto_reload() const noexcept { config::disable_auto_reload(); } | ||
| 660 | |||
| 661 | /// Logs every bound setting. See config::log_all. | ||
| 662 | void log_all() const { config::log_all(); } | ||
| 663 | |||
| 664 | /// Clears every bound item. See config::clear. | ||
| 665 | void clear() const noexcept { config::clear(); } | ||
| 666 | }; | ||
| 667 | } // namespace config | ||
| 668 | } // namespace DetourModKit | ||
| 669 | |||
| 670 | #endif // DETOURMODKIT_CONFIG_HPP | ||
| 671 |