include/DetourModKit/config.hpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #ifndef DETOURMODKIT_CONFIG_HPP | ||
| 2 | #define DETOURMODKIT_CONFIG_HPP | ||
| 3 | |||
| 4 | #include "DetourModKit/input_codes.hpp" | ||
| 5 | #include "DetourModKit/logger.hpp" | ||
| 6 | |||
| 7 | #include <atomic> | ||
| 8 | #include <chrono> | ||
| 9 | #include <functional> | ||
| 10 | #include <memory> | ||
| 11 | #include <string> | ||
| 12 | #include <string_view> | ||
| 13 | #include <vector> | ||
| 14 | |||
| 15 | namespace DetourModKit | ||
| 16 | { | ||
| 17 | // Forward-declared to keep the filesystem watcher out of this header. | ||
| 18 | // Full definition lives in config_watcher.hpp. | ||
| 19 | class ConfigWatcher; | ||
| 20 | |||
| 21 | /** | ||
| 22 | * @namespace Config | ||
| 23 | * @brief Provides functions for registering, loading, and logging configuration settings. | ||
| 24 | * @details This system allows mods to register their configuration variables with DetourModKit. | ||
| 25 | * The kit handles loading values from an INI file and provides logging functionality. | ||
| 26 | * Uses std::function callbacks for type-safe value setting. | ||
| 27 | * | ||
| 28 | * All `register_*` functions share these common parameters: | ||
| 29 | * - @p section INI section name. | ||
| 30 | * - @p ini_key INI key name. | ||
| 31 | * - @p log_key_name Human-readable name shown in log output. | ||
| 32 | * - @p setter Callback invoked with the loaded (or default) value. | ||
| 33 | * | ||
| 34 | * @note Setter callbacks are invoked at two points: immediately during registration | ||
| 35 | * (with the default value) and again during load() (with the INI or default value). | ||
| 36 | * Consumers that accumulate state (e.g. building a lookup map) must be idempotent -- | ||
| 37 | * clear accumulated state before applying the new value to avoid stale entries. | ||
| 38 | * | ||
| 39 | * **Thread safety:** All `register_*` and `load()` functions use a deferred callback | ||
| 40 | * pattern: state is read/written under the config mutex, but setter callbacks are | ||
| 41 | * invoked *after* the mutex is released. This means setter callbacks may safely call | ||
| 42 | * back into the Config API (e.g. `register_*`, `load`, `log_all`) without deadlocking. | ||
| 43 | * A reentrancy guard is therefore unnecessary. `log_all()` and `clear_registered_items()` | ||
| 44 | * hold the mutex for the entire call but only invoke Logger methods, which use an | ||
| 45 | * independent lock hierarchy. | ||
| 46 | */ | ||
| 47 | namespace Config | ||
| 48 | { | ||
| 49 | |||
| 50 | /** | ||
| 51 | * @struct KeyCombo | ||
| 52 | * @brief Represents a single key combination with trigger keys and modifiers. | ||
| 53 | * @details Contains trigger keys (OR logic) and modifier keys (AND logic). | ||
| 54 | * Designed for direct use with InputManager::register_press/register_hold. | ||
| 55 | * Each key is an InputCode identifying both the device source and button. | ||
| 56 | * | ||
| 57 | * Within a single combo, modifiers are separated by '+' and the last | ||
| 58 | * '+'-delimited token is the trigger key. Tokens can be human-readable | ||
| 59 | * names or hex VK codes: | ||
| 60 | * - "F3" → keys=[F3], modifiers=[] | ||
| 61 | * - "Ctrl+F3" → keys=[F3], modifiers=[Ctrl] | ||
| 62 | * - "Ctrl+Shift+F3" → keys=[F3], modifiers=[Ctrl, Shift] | ||
| 63 | * - "Mouse4" → keys=[Mouse4], modifiers=[] | ||
| 64 | * - "Gamepad_LB+Gamepad_A" → keys=[Gamepad_A], modifiers=[Gamepad_LB] | ||
| 65 | * - "0x11+0x72" → keys=[0x72], modifiers=[0x11] (hex fallback) | ||
| 66 | * | ||
| 67 | * Multiple combos are separated by commas in INI values, parsed into | ||
| 68 | * a KeyComboList. Each combo is independent (OR logic between combos): | ||
| 69 | * - "F3,Gamepad_LT+Gamepad_B" → [{keys=[F3]}, {keys=[Gamepad_B], mods=[Gamepad_LT]}] | ||
| 70 | * - "Ctrl+F3,Ctrl+F4" → [{keys=[F3], mods=[Ctrl]}, {keys=[F4], mods=[Ctrl]}] | ||
| 71 | */ | ||
| 72 | struct KeyCombo | ||
| 73 | { | ||
| 74 | std::vector<InputCode> keys; | ||
| 75 | std::vector<InputCode> modifiers; | ||
| 76 | }; | ||
| 77 | |||
| 78 | /// A list of alternative key combinations (OR logic between combos). | ||
| 79 | using KeyComboList = std::vector<KeyCombo>; | ||
| 80 | |||
| 81 | /** | ||
| 82 | * @class InputBindingGuard | ||
| 83 | * @brief RAII cancellation token for bindings registered via | ||
| 84 | * register_press_combo(). | ||
| 85 | * @details The guard owns a shared atomic flag that gates the user | ||
| 86 | * callback. On destruction (or explicit release()) the flag | ||
| 87 | * is cleared and subsequent key events become no-ops. The | ||
| 88 | * underlying InputManager binding remains registered; it is | ||
| 89 | * only torn down by InputManager::shutdown() or | ||
| 90 | * DMK_Shutdown(). | ||
| 91 | * | ||
| 92 | * Non-copyable, movable. Moving transfers ownership of the | ||
| 93 | * cancellation flag; the moved-from guard becomes inert. | ||
| 94 | */ | ||
| 95 | class InputBindingGuard | ||
| 96 | { | ||
| 97 | public: | ||
| 98 | 1 | InputBindingGuard() = default; | |
| 99 | 15 | InputBindingGuard(std::string name, std::shared_ptr<std::atomic<bool>> enabled) noexcept | |
| 100 | 45 | : name_(std::move(name)), enabled_(std::move(enabled)) {} | |
| 101 | |||
| 102 | 20 | ~InputBindingGuard() noexcept { release(); } | |
| 103 | |||
| 104 | InputBindingGuard(const InputBindingGuard &) = delete; | ||
| 105 | InputBindingGuard &operator=(const InputBindingGuard &) = delete; | ||
| 106 | |||
| 107 | 4 | InputBindingGuard(InputBindingGuard &&other) noexcept | |
| 108 | 12 | : name_(std::move(other.name_)), enabled_(std::move(other.enabled_)) {} | |
| 109 | |||
| 110 | 2 | InputBindingGuard &operator=(InputBindingGuard &&other) noexcept | |
| 111 | { | ||
| 112 |
2/2✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 10 taken 1 time.
|
2 | if (this != &other) |
| 113 | { | ||
| 114 | 1 | release(); | |
| 115 | 2 | name_ = std::move(other.name_); | |
| 116 | 2 | enabled_ = std::move(other.enabled_); | |
| 117 | } | ||
| 118 | 2 | return *this; | |
| 119 | } | ||
| 120 | |||
| 121 | /** | ||
| 122 | * @brief Disables the binding's callback. Idempotent. | ||
| 123 | */ | ||
| 124 | 29 | void release() noexcept | |
| 125 | { | ||
| 126 |
2/2✓ Branch 3 → 4 taken 15 times.
✓ Branch 3 → 7 taken 14 times.
|
29 | if (enabled_) |
| 127 | { | ||
| 128 | 15 | enabled_->store(false, std::memory_order_release); | |
| 129 | 15 | enabled_.reset(); | |
| 130 | } | ||
| 131 | 29 | } | |
| 132 | |||
| 133 | /** | ||
| 134 | * @brief Returns the binding's InputManager name. | ||
| 135 | */ | ||
| 136 | 2 | [[nodiscard]] const std::string &name() const noexcept { return name_; } | |
| 137 | |||
| 138 | /** | ||
| 139 | * @brief Returns true while the binding's callback is still live. | ||
| 140 | */ | ||
| 141 | 12 | [[nodiscard]] bool is_active() const noexcept | |
| 142 | { | ||
| 143 |
4/4✓ Branch 3 → 4 taken 7 times.
✓ Branch 3 → 8 taken 5 times.
✓ Branch 6 → 7 taken 6 times.
✓ Branch 6 → 8 taken 1 time.
|
12 | return enabled_ && enabled_->load(std::memory_order_acquire); |
| 144 | } | ||
| 145 | |||
| 146 | private: | ||
| 147 | std::string name_; | ||
| 148 | std::shared_ptr<std::atomic<bool>> enabled_; | ||
| 149 | }; | ||
| 150 | |||
| 151 | /// Registers an integer configuration item. | ||
| 152 | /// @note The setter is called immediately with default_value and again on load(). | ||
| 153 | void register_int(std::string_view section, std::string_view ini_key, std::string_view log_key_name, | ||
| 154 | std::function<void(int)> setter, int default_value); | ||
| 155 | |||
| 156 | /// Registers a floating-point configuration item. | ||
| 157 | /// @note The setter is called immediately with default_value and again on load(). | ||
| 158 | void register_float(std::string_view section, std::string_view ini_key, std::string_view log_key_name, | ||
| 159 | std::function<void(float)> setter, float default_value); | ||
| 160 | |||
| 161 | /// Registers a boolean configuration item. | ||
| 162 | /// @note The setter is called immediately with default_value and again on load(). | ||
| 163 | void register_bool(std::string_view section, std::string_view ini_key, std::string_view log_key_name, | ||
| 164 | std::function<void(bool)> setter, bool default_value); | ||
| 165 | |||
| 166 | /// Registers a string configuration item. | ||
| 167 | /// @note The setter is called immediately with default_value and again on load(). | ||
| 168 | void register_string(std::string_view section, std::string_view ini_key, std::string_view log_key_name, | ||
| 169 | std::function<void(const std::string &)> setter, std::string default_value); | ||
| 170 | |||
| 171 | /** | ||
| 172 | * @brief Registers a log-level INI item that applies directly to Logger. | ||
| 173 | * @details Parses @p default_value via Logger::string_to_log_level and | ||
| 174 | * calls Logger::set_log_level both at registration and on each | ||
| 175 | * load() / reload(). Unrecognized values fall back to | ||
| 176 | * LogLevel::Info per Logger::string_to_log_level. | ||
| 177 | * @param section INI section name. | ||
| 178 | * @param ini_key INI key name. | ||
| 179 | * @param default_value Default level string (e.g. "INFO", "DEBUG"). | ||
| 180 | */ | ||
| 181 | void register_log_level(std::string_view section, std::string_view ini_key, | ||
| 182 | std::string_view default_value = "INFO"); | ||
| 183 | |||
| 184 | /** | ||
| 185 | * @brief Registers an INI item whose value is stored into a caller-supplied atomic. | ||
| 186 | * @details Convenience wrapper over the matching register_<T> overload that | ||
| 187 | * stores the parsed value with std::memory_order_relaxed. Supported | ||
| 188 | * T: int, bool, float. The reference must outlive every load() and | ||
| 189 | * reload() call: the setter captures @p out by reference. | ||
| 190 | * @tparam T One of int, bool, float. | ||
| 191 | * @param section INI section name. | ||
| 192 | * @param ini_key INI key name. | ||
| 193 | * @param log_key_name Human-readable name shown in log output. | ||
| 194 | * @param out Atomic destination updated on every successful parse. | ||
| 195 | * @param default_value Value applied when the INI key is missing. | ||
| 196 | */ | ||
| 197 | // Marked = delete so unsupported T (e.g. double, uint64_t) becomes a | ||
| 198 | // crisp compile error pointing at the call site rather than a mangled | ||
| 199 | // unresolved-symbol link error. The supported instantiations are the | ||
| 200 | // explicit specialisations below (int, bool, float). | ||
| 201 | template <typename T> | ||
| 202 | void register_atomic(std::string_view section, std::string_view ini_key, | ||
| 203 | std::string_view log_key_name, std::atomic<T> &out, T default_value) = delete; | ||
| 204 | |||
| 205 | template <> | ||
| 206 | 1 | inline void register_atomic<int>(std::string_view section, std::string_view ini_key, | |
| 207 | std::string_view log_key_name, std::atomic<int> &out, int default_value) | ||
| 208 | { | ||
| 209 |
1/2✓ Branch 3 → 4 taken 1 time.
✗ Branch 3 → 6 not taken.
|
1 | register_int(section, ini_key, log_key_name, |
| 210 | 4 | [&out](int v) { out.store(v, std::memory_order_relaxed); }, | |
| 211 | default_value); | ||
| 212 | 1 | } | |
| 213 | |||
| 214 | template <> | ||
| 215 | 1 | inline void register_atomic<bool>(std::string_view section, std::string_view ini_key, | |
| 216 | std::string_view log_key_name, std::atomic<bool> &out, bool default_value) | ||
| 217 | { | ||
| 218 |
1/2✓ Branch 3 → 4 taken 1 time.
✗ Branch 3 → 6 not taken.
|
1 | register_bool(section, ini_key, log_key_name, |
| 219 | 4 | [&out](bool v) { out.store(v, std::memory_order_relaxed); }, | |
| 220 | default_value); | ||
| 221 | 1 | } | |
| 222 | |||
| 223 | template <> | ||
| 224 | inline void register_atomic<float>(std::string_view section, std::string_view ini_key, | ||
| 225 | std::string_view log_key_name, std::atomic<float> &out, float default_value) | ||
| 226 | { | ||
| 227 | register_float(section, ini_key, log_key_name, | ||
| 228 | [&out](float v) { out.store(v, std::memory_order_relaxed); }, | ||
| 229 | default_value); | ||
| 230 | } | ||
| 231 | |||
| 232 | /** | ||
| 233 | * @brief Registers a key combo configuration item. | ||
| 234 | * @details Parses an INI value as one or more key combinations. Commas at the | ||
| 235 | * top level separate independent combos (OR logic). Within each combo, | ||
| 236 | * '+' separates modifier keys from the trigger key (last token). Tokens | ||
| 237 | * can be human-readable names (e.g., "Ctrl", "F3", "Gamepad_A") or hex | ||
| 238 | * VK codes (e.g., "0x72"). See KeyCombo for full parsing semantics. | ||
| 239 | * | ||
| 240 | * Two opt-out sentinels yield an empty KeyComboList silently: | ||
| 241 | * an empty string and the literal "NONE" (case-insensitive, | ||
| 242 | * surrounding whitespace OK, whole-string only). A non-empty, | ||
| 243 | * non-sentinel value whose every token fails to parse is | ||
| 244 | * logged at WARNING level naming @p log_key_name and the | ||
| 245 | * offending raw string. | ||
| 246 | * @param section INI section name. | ||
| 247 | * @param ini_key INI key name. | ||
| 248 | * @param log_key_name Human-readable name shown in log output and in the | ||
| 249 | * typo WARNING described above. | ||
| 250 | * @param setter Callback invoked with the parsed KeyComboList. | ||
| 251 | * @param default_value_str Default value string in the same format. | ||
| 252 | * @note The setter is called immediately with the parsed default and again on load(). | ||
| 253 | */ | ||
| 254 | void register_key_combo(std::string_view section, std::string_view ini_key, std::string_view log_key_name, | ||
| 255 | std::function<void(const KeyComboList &)> setter, std::string_view default_value_str); | ||
| 256 | |||
| 257 | /** | ||
| 258 | * @brief Registers a key combo INI item and wires it to InputManager. | ||
| 259 | * @details Fuses register_key_combo() with InputManager::register_press(). | ||
| 260 | * On registration the InputManager binding is created with the | ||
| 261 | * parsed default combo. On each subsequent load() the setter | ||
| 262 | * invokes InputManager::update_binding_combos() so the bound | ||
| 263 | * keys and modifiers pick up the INI-sourced value without | ||
| 264 | * re-registering the binding. Live updates accept any | ||
| 265 | * cardinality: the binding's combo set is rebuilt on the fly | ||
| 266 | * and any held-state release callbacks fire before the swap | ||
| 267 | * completes. | ||
| 268 | * | ||
| 269 | * To opt a binding out at runtime (no keys bound), set the | ||
| 270 | * INI value to either an empty string or the literal | ||
| 271 | * "NONE" (case-insensitive, surrounding whitespace OK). | ||
| 272 | * Both forms produce an unbound binding silently and the | ||
| 273 | * binding name remains addressable for a future non-empty | ||
| 274 | * update. The "NONE" sentinel is only recognized as the | ||
| 275 | * entire trimmed value; "NONE" appearing as one token in a | ||
| 276 | * comma-separated list is treated as an unparseable token | ||
| 277 | * and contributes nothing. | ||
| 278 | * | ||
| 279 | * A non-empty INI value whose every comma-separated token | ||
| 280 | * fails to parse is treated as a user typo and logged at | ||
| 281 | * WARNING level naming the binding and the offending raw | ||
| 282 | * string; the binding becomes unbound. | ||
| 283 | * | ||
| 284 | * The returned guard holds a cancellation flag that | ||
| 285 | * short-circuits the user callback when released, because | ||
| 286 | * InputManager does not support per-binding removal | ||
| 287 | * post-start(). | ||
| 288 | * | ||
| 289 | * Safe to call before or after InputManager::start(). A | ||
| 290 | * binding registered while the poller is running is | ||
| 291 | * appended to the live binding set and starts firing on | ||
| 292 | * the next poll cycle. | ||
| 293 | * | ||
| 294 | * @param section INI section name. | ||
| 295 | * @param ini_key INI key name. | ||
| 296 | * @param log_name Human-readable name echoed by the config logger and | ||
| 297 | * in the typo WARNING described above. | ||
| 298 | * @param input_binding_name InputManager binding name (must be unique). | ||
| 299 | * @param on_press User callback fired on key-down edge. | ||
| 300 | * @param default_value Default combo string (same format as register_key_combo). | ||
| 301 | * @return InputBindingGuard RAII cancellation token for the callback. | ||
| 302 | */ | ||
| 303 | [[nodiscard]] InputBindingGuard register_press_combo(std::string_view section, | ||
| 304 | std::string_view ini_key, | ||
| 305 | std::string_view log_name, | ||
| 306 | std::string_view input_binding_name, | ||
| 307 | std::function<void()> on_press, | ||
| 308 | std::string_view default_value); | ||
| 309 | |||
| 310 | /** | ||
| 311 | * @brief Loads all registered configuration settings from the specified INI file. | ||
| 312 | * @details Parses the INI file and attempts to read values for each registered item. | ||
| 313 | * If a key is missing or invalid, the default value provided during | ||
| 314 | * registration is used. The INI path is remembered internally so that | ||
| 315 | * subsequent reload() calls operate on the same file without needing | ||
| 316 | * the caller to pass it again. | ||
| 317 | * @param ini_filename The base filename of the INI file. Path will be resolved | ||
| 318 | * relative to the mod's runtime directory. | ||
| 319 | */ | ||
| 320 | void load(std::string_view ini_filename); | ||
| 321 | |||
| 322 | /** | ||
| 323 | * @brief Re-runs all registered setters against the last-loaded INI file. | ||
| 324 | * @details Reads the INI file previously passed to load() and re-invokes every | ||
| 325 | * registered setter with the fresh value (or its default if the key is | ||
| 326 | * missing). Registrations themselves are not touched: user lambdas | ||
| 327 | * persist across reloads. The deferred-setter invocation pattern used | ||
| 328 | * by load() applies here as well, so setters may freely call back into | ||
| 329 | * the Config API without deadlocking. | ||
| 330 | * @return true if a previous load() path was available and the reload proceeded, | ||
| 331 | * false if reload() was called before any load(). | ||
| 332 | * @note Safe to call from any thread. Commonly wired to a filesystem watcher | ||
| 333 | * (see enable_auto_reload) or a hotkey (see register_reload_hotkey). | ||
| 334 | * @note Only C++ exceptions are caught. Structured-exception (SEH) faults | ||
| 335 | * such as access violations bypass the handler. A `noexcept`-marked | ||
| 336 | * user setter that throws still invokes std::terminate. | ||
| 337 | */ | ||
| 338 | [[nodiscard]] bool reload(); | ||
| 339 | |||
| 340 | /** | ||
| 341 | * @enum AutoReloadStatus | ||
| 342 | * @brief Outcome of a call to enable_auto_reload(). | ||
| 343 | */ | ||
| 344 | enum class AutoReloadStatus | ||
| 345 | { | ||
| 346 | Started, ///< Watcher is now running. | ||
| 347 | AlreadyRunning, ///< Called twice; the existing watcher was kept. | ||
| 348 | NoPriorLoad, ///< Config::load() was never called; no path to watch. | ||
| 349 | StartFailed ///< Directory could not be opened or start handshake failed. | ||
| 350 | }; | ||
| 351 | |||
| 352 | /** | ||
| 353 | * @brief Starts a background watcher that calls reload() when the INI changes. | ||
| 354 | * @details Creates a ConfigWatcher on the INI path last passed to load() and | ||
| 355 | * starts its worker thread. The watcher collapses bursty editor save | ||
| 356 | * events (e.g. Notepad++ atomic save) into a single reload via the | ||
| 357 | * @p debounce quiet window. After the reload completes, @p on_reload | ||
| 358 | * is invoked if provided, allowing the caller to refresh derived | ||
| 359 | * state (e.g. rebuild caches, reformat log output). | ||
| 360 | * | ||
| 361 | * If load() has not been called yet, or if auto-reload is already | ||
| 362 | * enabled, this is a no-op and a Warning-level log message is emitted. | ||
| 363 | * | ||
| 364 | * The watcher and any @p on_reload callback run on the watcher's | ||
| 365 | * background thread. User setters invoked by reload() also run on | ||
| 366 | * that thread; they must handle their own synchronization. | ||
| 367 | * | ||
| 368 | * The @p on_reload callback receives a `bool content_changed` | ||
| 369 | * argument. When the file's byte contents are identical to the | ||
| 370 | * last successfully loaded version (e.g. after a `touch` or a | ||
| 371 | * no-op save), setters are skipped and the flag is false; the | ||
| 372 | * callback still fires so derived state can observe the event. | ||
| 373 | * | ||
| 374 | * @param debounce Quiet-window length between change detection and reload | ||
| 375 | * (default 250 ms). | ||
| 376 | * @param on_reload Optional callback invoked after each successful reload. | ||
| 377 | * The bool argument is true when setters ran, false when | ||
| 378 | * the content-hash skip short-circuited the reload. | ||
| 379 | * @return AutoReloadStatus::Started if the watcher is now running; | ||
| 380 | * AutoReloadStatus::AlreadyRunning if a watcher was already installed | ||
| 381 | * (no-op, existing watcher kept); | ||
| 382 | * AutoReloadStatus::NoPriorLoad if load() has not been called yet | ||
| 383 | * (no-op, no watcher installed); | ||
| 384 | * AutoReloadStatus::StartFailed if the parent directory could not | ||
| 385 | * be opened or the start handshake failed (watcher reset, error | ||
| 386 | * logged). | ||
| 387 | */ | ||
| 388 | [[nodiscard]] AutoReloadStatus enable_auto_reload( | ||
| 389 | std::chrono::milliseconds debounce = std::chrono::milliseconds{250}, | ||
| 390 | std::function<void(bool)> on_reload = {}); | ||
| 391 | |||
| 392 | /** | ||
| 393 | * @brief Stops the filesystem watcher started by enable_auto_reload(). | ||
| 394 | * @details Idempotent. Returns only once the watcher thread has exited | ||
| 395 | * (or been detached under the Windows loader lock). | ||
| 396 | * @note When invoked from inside an on_reload callback (i.e. on the | ||
| 397 | * watcher thread itself) this is a no-op: joining the worker | ||
| 398 | * from its own thread would raise | ||
| 399 | * std::system_error(resource_deadlock_would_occur). The error | ||
| 400 | * is logged and the watcher remains running. Tear the watcher | ||
| 401 | * down from a different thread, e.g. by posting the disable | ||
| 402 | * request to a deferred shutdown hook. | ||
| 403 | */ | ||
| 404 | void disable_auto_reload() noexcept; | ||
| 405 | |||
| 406 | /** | ||
| 407 | * @brief Registers a hotkey binding that triggers reload() on press. | ||
| 408 | * @details Thin wrapper around register_press_combo() whose on-press | ||
| 409 | * callback calls Config::reload(). Like the underlying helper, | ||
| 410 | * this must be called before InputManager::start() so the | ||
| 411 | * binding is picked up by the poller. | ||
| 412 | * | ||
| 413 | * The INI-configured combo overrides @p default_combo on each | ||
| 414 | * load() / reload() cycle via the standard register_press_combo | ||
| 415 | * machinery. | ||
| 416 | * | ||
| 417 | * @param ini_key INI key that stores the combo string (e.g. "ReloadConfig"). | ||
| 418 | * @param default_combo Combo string applied when the INI key is absent | ||
| 419 | * (e.g. "Ctrl+F5"). | ||
| 420 | * @return true if the binding was registered, false if @p default_combo | ||
| 421 | * is empty or the NONE sentinel. @ref register_press_combo accepts | ||
| 422 | * both as silent opt-out and registers the binding name with no | ||
| 423 | * keys (addressable later by @ref update_binding_combos), but a | ||
| 424 | * reload hotkey with no default keys is never useful, so this | ||
| 425 | * helper rejects that case at the call site rather than ship an | ||
| 426 | * inert reload binding. | ||
| 427 | * @note The on-press callback runs on the InputManager poll thread, | ||
| 428 | * but the actual reload() work is deferred to a dedicated | ||
| 429 | * background servicer thread. The press callback only flips | ||
| 430 | * an atomic flag and notifies a condition variable, so | ||
| 431 | * per-press latency on the poll thread stays in the | ||
| 432 | * microsecond range regardless of INI size. Multiple presses | ||
| 433 | * during a running reload coalesce into at most one follow-up. | ||
| 434 | * Any exception thrown by reload() on the servicer thread is | ||
| 435 | * caught and logged so the servicer stays alive. | ||
| 436 | * @note Only C++ exceptions are caught. Structured-exception (SEH) faults | ||
| 437 | * such as access violations bypass the handler. A `noexcept`-marked | ||
| 438 | * user setter that throws still invokes std::terminate. | ||
| 439 | */ | ||
| 440 | [[nodiscard]] bool register_reload_hotkey(std::string_view ini_key, | ||
| 441 | std::string_view default_combo); | ||
| 442 | |||
| 443 | /** | ||
| 444 | * @brief Logs the current values of all registered configuration settings. | ||
| 445 | * @details Iterates through all items registered with the config system and | ||
| 446 | * outputs their current values to the Logger. | ||
| 447 | */ | ||
| 448 | void log_all(); | ||
| 449 | |||
| 450 | /** | ||
| 451 | * @brief Clears all currently registered configuration items. | ||
| 452 | * @details Useful if the configuration system needs to be reset without | ||
| 453 | * restarting the application. | ||
| 454 | */ | ||
| 455 | void clear_registered_items(); | ||
| 456 | |||
| 457 | } // namespace Config | ||
| 458 | } // namespace DetourModKit | ||
| 459 | |||
| 460 | #endif // DETOURMODKIT_CONFIG_HPP | ||
| 461 |