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 <concepts> | ||
| 10 | #include <functional> | ||
| 11 | #include <memory> | ||
| 12 | #include <optional> | ||
| 13 | #include <string> | ||
| 14 | #include <string_view> | ||
| 15 | #include <utility> | ||
| 16 | #include <vector> | ||
| 17 | |||
| 18 | namespace DetourModKit | ||
| 19 | { | ||
| 20 | // Forward-declared to keep the filesystem watcher out of this header. Full definition lives in config_watcher.hpp. | ||
| 21 | class ConfigWatcher; | ||
| 22 | |||
| 23 | /** | ||
| 24 | * @namespace Config | ||
| 25 | * @brief Provides functions for registering, loading, and logging configuration settings. | ||
| 26 | * @details This system allows mods to register their configuration variables with DetourModKit. The kit handles | ||
| 27 | * loading values from an INI file and provides logging functionality. Uses std::function callbacks for | ||
| 28 | * type-safe value setting. | ||
| 29 | * | ||
| 30 | * All `register_*` functions share these common parameters: | ||
| 31 | * - @p section INI section name. | ||
| 32 | * - @p ini_key INI key name. | ||
| 33 | * - @p log_key_name Human-readable name shown in log output. | ||
| 34 | * - @p setter Callback invoked with the loaded (or default) value. | ||
| 35 | * | ||
| 36 | * @note Setter callbacks are invoked at two points: immediately during registration (with the default value) and | ||
| 37 | * again during load() (with the INI or default value). Consumers that accumulate state (e.g. building a | ||
| 38 | * lookup map) must be idempotent -- clear accumulated state before applying the new value to avoid stale | ||
| 39 | * entries. | ||
| 40 | * | ||
| 41 | * **Thread safety:** All `register_*` and `load()` functions use a deferred callback | ||
| 42 | * pattern: state is read/written under the config mutex, but setter callbacks are | ||
| 43 | * invoked *after* the mutex is released. This means setter callbacks may safely call | ||
| 44 | * back into the Config API (e.g. `register_*`, `load`, `log_all`) without deadlocking. | ||
| 45 | * A reentrancy guard is therefore unnecessary. `log_all()` and `clear_registered_items()` | ||
| 46 | * hold the mutex for the entire call but only invoke Logger methods, which use an independent lock hierarchy. | ||
| 47 | */ | ||
| 48 | namespace Config | ||
| 49 | { | ||
| 50 | |||
| 51 | /** | ||
| 52 | * @struct KeyCombo | ||
| 53 | * @brief Represents a single key combination with trigger keys and modifiers. | ||
| 54 | * @details Contains trigger keys (OR logic) and modifier keys (AND logic). Designed for direct use with | ||
| 55 | * InputManager::register_press/register_hold. Each key is an InputCode identifying both the device | ||
| 56 | * source and button. | ||
| 57 | * | ||
| 58 | * Within a single combo, modifiers are separated by '+' and the last '+'-delimited token is the | ||
| 59 | * trigger key. Tokens can be human-readable 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 a KeyComboList. Each combo is | ||
| 68 | * 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 register_press_combo() / register_hold_combo(). | ||
| 84 | * @details The guard owns a shared atomic flag that gates the user callback. On destruction (or explicit | ||
| 85 | * release()) the flag is cleared and subsequent input events become no-ops. The underlying | ||
| 86 | * InputManager binding remains registered; it is only torn down by InputManager::shutdown() or | ||
| 87 | * DMK_Shutdown(). | ||
| 88 | * | ||
| 89 | * A hold-combo guard additionally carries an optional one-shot release action. A hold callback has | ||
| 90 | * lingering state -- the consumer is told "held" (true) until told "released" (false) -- so simply | ||
| 91 | * gating the callback off mid-hold would strand the consumer in the held state. The release action | ||
| 92 | * synthesizes a single balancing on_state_change(false) when a true edge was the last one forwarded. | ||
| 93 | * A press guard has no such state and carries no action. | ||
| 94 | * | ||
| 95 | * Non-copyable, movable. Moving transfers ownership of the cancellation flag and the release action; | ||
| 96 | * the moved-from guard becomes inert. | ||
| 97 | * @note Setup/control-plane only for hold guards: release() (and therefore the destructor) may invoke the hold | ||
| 98 | * release callback, so destroy a hold guard from init/shutdown or a worker thread, never from inside an | ||
| 99 | * input callback running on a game thread. | ||
| 100 | */ | ||
| 101 | class InputBindingGuard | ||
| 102 | { | ||
| 103 | public: | ||
| 104 | 1 | InputBindingGuard() = default; | |
| 105 | 7 | InputBindingGuard(std::string name, std::shared_ptr<std::atomic<bool>> enabled) noexcept | |
| 106 | 21 | : m_name(std::move(name)), m_enabled(std::move(enabled)) | |
| 107 | { | ||
| 108 | 7 | } | |
| 109 | |||
| 110 | /** | ||
| 111 | * @brief Constructs a guard that also runs @p on_release once when the binding is cancelled. | ||
| 112 | * @details Used by the hold-combo fusion to synthesize the balancing on_state_change(false). The action is | ||
| 113 | * invoked under this guard's noexcept teardown; any exception it raises is caught and logged | ||
| 114 | * best-effort, never propagated. | ||
| 115 | * @param name InputManager binding name this guard reports via name(). | ||
| 116 | * @param enabled Shared cancellation flag the binding's callback wrapper gates on; release() clears it so | ||
| 117 | * subsequent events become no-ops. | ||
| 118 | * @param on_release One-shot action run once by release() (the hold's balancing on_state_change(false)); | ||
| 119 | * pass an empty function for a press binding, which then behaves like the two-argument | ||
| 120 | * constructor. | ||
| 121 | */ | ||
| 122 | 21 | InputBindingGuard(std::string name, std::shared_ptr<std::atomic<bool>> enabled, | |
| 123 | std::function<void()> on_release) noexcept | ||
| 124 | 84 | : m_name(std::move(name)), m_enabled(std::move(enabled)), m_on_release(std::move(on_release)) | |
| 125 | { | ||
| 126 | 21 | } | |
| 127 | |||
| 128 | 34 | ~InputBindingGuard() noexcept { release(); } | |
| 129 | |||
| 130 | InputBindingGuard(const InputBindingGuard &) = delete; | ||
| 131 | InputBindingGuard &operator=(const InputBindingGuard &) = delete; | ||
| 132 | |||
| 133 | 5 | InputBindingGuard(InputBindingGuard &&other) noexcept | |
| 134 | 15 | : m_name(std::move(other.m_name)), m_enabled(std::move(other.m_enabled)), | |
| 135 | 10 | m_on_release(std::move(other.m_on_release)) | |
| 136 | { | ||
| 137 | // A moved-from std::function is left in a valid-but-unspecified state; null it explicitly so the | ||
| 138 | // moved-from guard's release() cannot re-run the action this guard now owns. | ||
| 139 | 5 | other.m_on_release = nullptr; | |
| 140 | 5 | } | |
| 141 | |||
| 142 | 3 | InputBindingGuard &operator=(InputBindingGuard &&other) noexcept | |
| 143 | { | ||
| 144 |
2/2✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 14 taken 1 time.
|
3 | if (this != &other) |
| 145 | { | ||
| 146 | 2 | release(); | |
| 147 | 4 | m_name = std::move(other.m_name); | |
| 148 | 4 | m_enabled = std::move(other.m_enabled); | |
| 149 | 4 | m_on_release = std::move(other.m_on_release); | |
| 150 | 2 | other.m_on_release = nullptr; | |
| 151 | } | ||
| 152 | 3 | return *this; | |
| 153 | } | ||
| 154 | |||
| 155 | /** | ||
| 156 | * @brief Disables the binding's callback, then runs the release action once if present. Idempotent. | ||
| 157 | */ | ||
| 158 | 57 | void release() noexcept | |
| 159 | { | ||
| 160 |
2/2✓ Branch 3 → 4 taken 28 times.
✓ Branch 3 → 7 taken 29 times.
|
57 | if (m_enabled) |
| 161 | { | ||
| 162 | 28 | m_enabled->store(false, std::memory_order_release); | |
| 163 | 28 | m_enabled.reset(); | |
| 164 | } | ||
| 165 | // Run the optional release action exactly once. std::exchange clears the member first so a repeated or | ||
| 166 | // re-entrant release() cannot double-fire it, and the catch keeps this noexcept teardown honest even | ||
| 167 | // though the action may invoke a user-supplied hold callback. | ||
| 168 |
2/2✓ Branch 8 → 9 taken 11 times.
✓ Branch 8 → 13 taken 46 times.
|
57 | if (m_on_release) |
| 169 | { | ||
| 170 | 11 | const std::function<void()> action = std::exchange(m_on_release, nullptr); | |
| 171 | try | ||
| 172 | { | ||
| 173 |
2/2✓ Branch 10 → 11 taken 10 times.
✓ Branch 10 → 14 taken 1 time.
|
11 | action(); |
| 174 | } | ||
| 175 | 1 | catch (...) | |
| 176 | { | ||
| 177 | 2 | (void)Logger::get_instance().log_noexcept( | |
| 178 | LogLevel::Error, | ||
| 179 | 1 | "InputBindingGuard: hold release action threw; suppressed in noexcept teardown"); | |
| 180 | 1 | } | |
| 181 | 11 | } | |
| 182 | 57 | } | |
| 183 | |||
| 184 | /** | ||
| 185 | * @brief Returns the binding's InputManager name. | ||
| 186 | */ | ||
| 187 | 2 | [[nodiscard]] const std::string &name() const noexcept { return m_name; } | |
| 188 | |||
| 189 | /** | ||
| 190 | * @brief Returns true while the binding's callback is still live. | ||
| 191 | */ | ||
| 192 | 12 | [[nodiscard]] bool is_active() const noexcept | |
| 193 | { | ||
| 194 |
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 m_enabled && m_enabled->load(std::memory_order_acquire); |
| 195 | } | ||
| 196 | |||
| 197 | private: | ||
| 198 | std::string m_name; | ||
| 199 | std::shared_ptr<std::atomic<bool>> m_enabled; | ||
| 200 | // Optional one-shot action run on release(); empty for press bindings, set by the hold-combo fusion to | ||
| 201 | // synthesize the balancing on_state_change(false) so a cancelled hold cannot strand the consumer as held. | ||
| 202 | std::function<void()> m_on_release; | ||
| 203 | }; | ||
| 204 | |||
| 205 | /** | ||
| 206 | * @brief Registers an integer configuration item. | ||
| 207 | * @details The setter is invoked immediately with @p default_value and again on every load() / reload() with | ||
| 208 | * the parsed INI value. | ||
| 209 | * @param section INI section name. | ||
| 210 | * @param ini_key Key within the section. | ||
| 211 | * @param log_key_name Human-readable name used in log output. | ||
| 212 | * @param setter Callback applied with the resolved value. Must be reentrant and thread-safe. | ||
| 213 | * @param default_value Value used when the key is absent or unparsable. | ||
| 214 | */ | ||
| 215 | void register_int(std::string_view section, std::string_view ini_key, std::string_view log_key_name, | ||
| 216 | std::function<void(int)> setter, int default_value); | ||
| 217 | |||
| 218 | /** | ||
| 219 | * @brief Registers a floating-point configuration item. | ||
| 220 | * @details The setter is invoked immediately with @p default_value and again on every load() / reload() with | ||
| 221 | * the parsed INI value. | ||
| 222 | * @param section INI section name. | ||
| 223 | * @param ini_key Key within the section. | ||
| 224 | * @param log_key_name Human-readable name used in log output. | ||
| 225 | * @param setter Callback applied with the resolved value. Must be reentrant and thread-safe. | ||
| 226 | * @param default_value Value used when the key is absent or unparsable. | ||
| 227 | */ | ||
| 228 | void register_float(std::string_view section, std::string_view ini_key, std::string_view log_key_name, | ||
| 229 | std::function<void(float)> setter, float default_value); | ||
| 230 | |||
| 231 | /** | ||
| 232 | * @brief Registers a boolean configuration item. | ||
| 233 | * @details The setter is invoked immediately with @p default_value and again on every load() / reload() with | ||
| 234 | * the parsed INI value. | ||
| 235 | * @param section INI section name. | ||
| 236 | * @param ini_key Key within the section. | ||
| 237 | * @param log_key_name Human-readable name used in log output. | ||
| 238 | * @param setter Callback applied with the resolved value. Must be reentrant and thread-safe. | ||
| 239 | * @param default_value Value used when the key is absent or unparsable. | ||
| 240 | */ | ||
| 241 | void register_bool(std::string_view section, std::string_view ini_key, std::string_view log_key_name, | ||
| 242 | std::function<void(bool)> setter, bool default_value); | ||
| 243 | |||
| 244 | /** | ||
| 245 | * @brief Registers a string configuration item. | ||
| 246 | * @details The setter is invoked immediately with @p default_value and again on every load() / reload() with | ||
| 247 | * the parsed INI value. | ||
| 248 | * @param section INI section name. | ||
| 249 | * @param ini_key Key within the section. | ||
| 250 | * @param log_key_name Human-readable name used in log output. | ||
| 251 | * @param setter Callback applied with the resolved value. Must be reentrant and thread-safe. | ||
| 252 | * @param default_value Value used when the key is absent or unparsable. | ||
| 253 | * @note The INI is parsed as narrow bytes (the underlying SimpleIni uses SetUnicode(false)), so a value is | ||
| 254 | * delivered to @p setter verbatim as the bytes on disk -- not transcoded. ASCII values (the common case) | ||
| 255 | * pass through unchanged; a value with non-ASCII characters arrives as raw bytes (e.g. UTF-8 from a | ||
| 256 | * UTF-8-saved INI), and any encoding interpretation is the consumer's responsibility. | ||
| 257 | */ | ||
| 258 | void register_string(std::string_view section, std::string_view ini_key, std::string_view log_key_name, | ||
| 259 | std::function<void(const std::string &)> setter, std::string default_value); | ||
| 260 | |||
| 261 | /** | ||
| 262 | * @brief Registers a log-level INI item that applies directly to Logger. | ||
| 263 | * @details Parses @p default_value via Logger::string_to_log_level and calls Logger::set_log_level both at | ||
| 264 | * registration and on each load() / reload(). Unrecognized values fall back to | ||
| 265 | * LogLevel::Info per Logger::string_to_log_level. | ||
| 266 | * @param section INI section name. | ||
| 267 | * @param ini_key INI key name. | ||
| 268 | * @param default_value Default level string (e.g. "INFO", "DEBUG"). | ||
| 269 | */ | ||
| 270 | void register_log_level(std::string_view section, std::string_view ini_key, | ||
| 271 | std::string_view default_value = "INFO"); | ||
| 272 | |||
| 273 | /** | ||
| 274 | * @brief Registers an INI item whose value is stored into a caller-supplied atomic. | ||
| 275 | * @details Convenience wrapper over the matching register_<T> overload that stores the parsed value with | ||
| 276 | * std::memory_order_relaxed. Supported | ||
| 277 | * T: int, bool, float. The reference must outlive every load() and | ||
| 278 | * reload() call: the setter captures @p out by reference. | ||
| 279 | * @tparam T One of int, bool, float. | ||
| 280 | * @param section INI section name. | ||
| 281 | * @param ini_key INI key name. | ||
| 282 | * @param log_key_name Human-readable name shown in log output. | ||
| 283 | * @param out Atomic destination updated on every successful parse. | ||
| 284 | * @param default_value Value applied when the INI key is missing. | ||
| 285 | * @note Setup/control-plane only: registration may allocate and updates the Config registry. | ||
| 286 | */ | ||
| 287 | // A single constrained template rather than explicit specializations: | ||
| 288 | // specializing a function template is discouraged (Core Guidelines | ||
| 289 | // T.144) and an in-class explicit specialization is non-standard. The requires-clause caps the supported set to | ||
| 290 | // int, bool, and float, so an unsupported T (e.g. double, uint64_t) is a crisp constraint error at the call | ||
| 291 | // site rather than a mangled unresolved-symbol link error. | ||
| 292 | template <typename T> | ||
| 293 | requires(std::same_as<T, int> || std::same_as<T, bool> || std::same_as<T, float>) | ||
| 294 | 8 | void register_atomic(std::string_view section, std::string_view ini_key, std::string_view log_key_name, | |
| 295 | std::atomic<T> &out, T default_value) | ||
| 296 | { | ||
| 297 | if constexpr (std::same_as<T, int>) | ||
| 298 | { | ||
| 299 |
1/2✓ Branch 3 → 4 taken 3 times.
✗ Branch 3 → 6 not taken.
|
3 | register_int( |
| 300 | 12 | section, ini_key, log_key_name, [&out](int v) { out.store(v, std::memory_order_relaxed); }, | |
| 301 | default_value); | ||
| 302 | } | ||
| 303 | else if constexpr (std::same_as<T, bool>) | ||
| 304 | { | ||
| 305 |
1/2✓ Branch 3 → 4 taken 3 times.
✗ Branch 3 → 6 not taken.
|
3 | register_bool( |
| 306 | 12 | section, ini_key, log_key_name, [&out](bool v) { out.store(v, std::memory_order_relaxed); }, | |
| 307 | default_value); | ||
| 308 | } | ||
| 309 | else | ||
| 310 | { | ||
| 311 |
1/2✓ Branch 3 → 4 taken 2 times.
✗ Branch 3 → 6 not taken.
|
2 | register_float( |
| 312 | 8 | section, ini_key, log_key_name, [&out](float v) { out.store(v, std::memory_order_relaxed); }, | |
| 313 | default_value); | ||
| 314 | } | ||
| 315 | 8 | } | |
| 316 | |||
| 317 | /** | ||
| 318 | * @brief Registers an atomic-backed INI item using the atomic's current value as the default. | ||
| 319 | * @details Convenience overload for the supported atomic scalar set: int, bool, and float. The registration | ||
| 320 | * default is sampled once from @p out with std::memory_order_relaxed at registration time, then the | ||
| 321 | * matching typed registration stores parsed values back to @p out with relaxed ordering on every | ||
| 322 | * load() / reload(). Initialize the atomic deliberately before calling this overload; an accidental | ||
| 323 | * default-initialized value becomes the INI fallback. | ||
| 324 | * @tparam T One of int, bool, float. | ||
| 325 | * @param section INI section name. | ||
| 326 | * @param ini_key INI key name. | ||
| 327 | * @param log_key_name Human-readable name shown in log output. | ||
| 328 | * @param out Atomic destination updated on every successful parse and used as the registration default. | ||
| 329 | * @note Setup/control-plane only: registration may allocate and updates the Config registry. | ||
| 330 | */ | ||
| 331 | template <typename T> | ||
| 332 | requires(std::same_as<T, int> || std::same_as<T, bool> || std::same_as<T, float>) | ||
| 333 | 6 | void register_atomic(std::string_view section, std::string_view ini_key, std::string_view log_key_name, | |
| 334 | std::atomic<T> &out) | ||
| 335 | { | ||
| 336 | 8 | register_atomic<T>(section, ini_key, log_key_name, out, out.load(std::memory_order_relaxed)); | |
| 337 | 6 | } | |
| 338 | |||
| 339 | /** | ||
| 340 | * @brief Registers a key combo configuration item. | ||
| 341 | * @details Parses an INI value as one or more key combinations. Commas at the top level separate independent | ||
| 342 | * combos (OR logic). Within each combo, '+' separates modifier keys from the trigger key (last token). | ||
| 343 | * Tokens can be human-readable names (e.g., "Ctrl", "F3", "Gamepad_A") or hex | ||
| 344 | * VK codes (e.g., "0x72"). See KeyCombo for full parsing semantics. | ||
| 345 | * | ||
| 346 | * Two opt-out sentinels yield an empty KeyComboList silently: | ||
| 347 | * an empty string and the literal "NONE" (case-insensitive, surrounding whitespace OK, whole-string | ||
| 348 | * only). A non-empty, non-sentinel value whose every token fails to parse is logged at WARNING level | ||
| 349 | * naming @p log_key_name and the offending raw string. | ||
| 350 | * @param section INI section name. | ||
| 351 | * @param ini_key INI key name. | ||
| 352 | * @param log_key_name Human-readable name shown in log output and in the typo WARNING described above. | ||
| 353 | * @param setter Callback invoked with the parsed KeyComboList. | ||
| 354 | * @param default_value_str Default value string in the same format. | ||
| 355 | * @note The setter is called immediately with the parsed default and again on load(). | ||
| 356 | */ | ||
| 357 | void register_key_combo(std::string_view section, std::string_view ini_key, std::string_view log_key_name, | ||
| 358 | std::function<void(const KeyComboList &)> setter, std::string_view default_value_str); | ||
| 359 | |||
| 360 | /** | ||
| 361 | * @brief Registers a key combo INI item and wires it to InputManager. | ||
| 362 | * @details Fuses register_key_combo() with InputManager::register_press(). On registration the InputManager | ||
| 363 | * binding is created with the parsed default combo. On each subsequent load() the setter invokes | ||
| 364 | * InputManager::update_binding_combos() so the bound keys and modifiers pick up the INI-sourced value | ||
| 365 | * without re-registering the binding. Live updates accept any | ||
| 366 | * cardinality: the binding's combo set is rebuilt on the fly | ||
| 367 | * and any held-state release callbacks fire before the swap completes. | ||
| 368 | * | ||
| 369 | * To opt a binding out at runtime (no keys bound), set the | ||
| 370 | * INI value to either an empty string or the literal "NONE" (case-insensitive, surrounding whitespace | ||
| 371 | * OK). Both forms produce an unbound binding silently and the binding name remains addressable for a | ||
| 372 | * future non-empty update. The "NONE" sentinel is only recognized as the entire trimmed value; "NONE" | ||
| 373 | * appearing as one token in a comma-separated list is treated as an unparseable token and contributes | ||
| 374 | * nothing. | ||
| 375 | * | ||
| 376 | * A non-empty INI value whose every comma-separated token fails to parse is treated as a user typo and | ||
| 377 | * logged at | ||
| 378 | * WARNING level naming the binding and the offending raw string; the binding becomes unbound. | ||
| 379 | * | ||
| 380 | * The returned guard holds a cancellation flag that short-circuits the user callback when released, | ||
| 381 | * because | ||
| 382 | * InputManager does not support per-binding removal post-start(). | ||
| 383 | * | ||
| 384 | * Safe to call before or after InputManager::start(). A binding registered while the poller is running | ||
| 385 | * is appended to the live binding set and starts firing on the next poll cycle. | ||
| 386 | * | ||
| 387 | * @param section INI section name. | ||
| 388 | * @param ini_key INI key name. | ||
| 389 | * @param log_name Human-readable name echoed by the config logger and in the typo WARNING described above. | ||
| 390 | * @param input_binding_name InputManager binding name (must be unique). | ||
| 391 | * @param on_press User callback fired on key-down edge. | ||
| 392 | * @param default_value Default combo string (same format as register_key_combo). | ||
| 393 | * @param consume Optional per-binding input-suppression facet. std::nullopt (default) registers no extra INI | ||
| 394 | * key and preserves the historic behavior exactly. A value registers a bool item named | ||
| 395 | * "<ini_key>.Consume" defaulting to that value and wired to InputManager::set_consume on this | ||
| 396 | * binding, so the user can toggle suppression from the INI file. | ||
| 397 | * @return InputBindingGuard RAII cancellation token for the callback. | ||
| 398 | * @note Suppression via @p consume is honored only for digital gamepad buttons and the mouse wheel, never for | ||
| 399 | * keyboard keys, mouse buttons, or analog axes (see InputBinding::consume). A "<ini_key>.Consume" key on | ||
| 400 | * a keyboard-only binding is therefore inert. | ||
| 401 | */ | ||
| 402 | [[nodiscard]] InputBindingGuard | ||
| 403 | register_press_combo(std::string_view section, std::string_view ini_key, std::string_view log_name, | ||
| 404 | std::string_view input_binding_name, std::function<void()> on_press, | ||
| 405 | std::string_view default_value, std::optional<bool> consume = std::nullopt); | ||
| 406 | |||
| 407 | /** | ||
| 408 | * @brief Registers a key combo INI item and wires it to InputManager as a hold binding. | ||
| 409 | * @details The hold-mode mirror of register_press_combo(). Fuses register_key_combo() with | ||
| 410 | * InputManager::register_hold(): the binding is created with the parsed default combo, and on each | ||
| 411 | * subsequent load() the setter invokes InputManager::update_binding_combos() so the bound keys and | ||
| 412 | * modifiers pick up the INI-sourced value without re-registering the binding. @p on_state_change fires | ||
| 413 | * with true on the press edge (any listed input pressed, all modifiers held) and false on the release | ||
| 414 | * edge. | ||
| 415 | * | ||
| 416 | * The returned guard cancels the callback when released, and -- because a hold carries lingering | ||
| 417 | * state -- synthesizes a single balancing on_state_change(false) if the binding was held at the moment | ||
| 418 | * of cancellation, so a cancelled hold cannot strand the consumer in the held state. That synthesis is | ||
| 419 | * serialized against any in-flight callback, fires at most once, and never re-enters the callback | ||
| 420 | * while it is on the stack (see InputBindingGuard). | ||
| 421 | * | ||
| 422 | * The "NONE"/empty opt-out sentinels, the typo WARNING, and the before/after-start() semantics all | ||
| 423 | * match register_press_combo(). | ||
| 424 | * @param section INI section name. | ||
| 425 | * @param ini_key INI key name. | ||
| 426 | * @param log_name Human-readable name echoed by the config logger and in the typo WARNING. | ||
| 427 | * @param input_binding_name InputManager binding name (must be unique). | ||
| 428 | * @param on_state_change User callback fired with the hold state (true = held, false = released). | ||
| 429 | * @param default_value Default combo string (same format as register_key_combo). | ||
| 430 | * @param consume Optional per-binding input-suppression facet. std::nullopt (default) registers no extra INI | ||
| 431 | * key. A value registers a bool item named "<ini_key>.Consume" defaulting to that value and | ||
| 432 | * wired to InputManager::set_consume on this binding. | ||
| 433 | * @return InputBindingGuard RAII cancellation token for the callback; destroying it may synthesize the final | ||
| 434 | * on_state_change(false), so treat it as setup/control-plane only (see the class note). | ||
| 435 | * @note Suppression via @p consume is honored only for digital gamepad buttons and the mouse wheel, never for | ||
| 436 | * keyboard keys, mouse buttons, or analog axes (see InputBinding::consume). A "<ini_key>.Consume" key on | ||
| 437 | * a keyboard-only binding is therefore inert. | ||
| 438 | */ | ||
| 439 | [[nodiscard]] InputBindingGuard | ||
| 440 | register_hold_combo(std::string_view section, std::string_view ini_key, std::string_view log_name, | ||
| 441 | std::string_view input_binding_name, std::function<void(bool)> on_state_change, | ||
| 442 | std::string_view default_value, std::optional<bool> consume = std::nullopt); | ||
| 443 | |||
| 444 | /** | ||
| 445 | * @brief Registers a boolean INI item that toggles input suppression for a binding. | ||
| 446 | * @details Fuses register_bool() with InputManager::set_consume(): the INI value (parsed as a bool) decides | ||
| 447 | * whether @p input_binding_name hides its trigger from the game, applied both at registration (with | ||
| 448 | * the default) and on every load() / reload(). This is the INI-driven counterpart to calling | ||
| 449 | * InputManager::set_consume() directly, letting users opt individual bindings into passthrough | ||
| 450 | * blocking from the config file (for example a `SetYToggle.Consume = true` key beside the combo). | ||
| 451 | * | ||
| 452 | * Register the binding first (via register_press_combo or | ||
| 453 | * InputManager::register_press); set_consume() is a no-op for an unknown name. Suppression is honored | ||
| 454 | * for digital gamepad buttons and the mouse wheel only (analog triggers and stick directions cannot be | ||
| 455 | * masked; see | ||
| 456 | * InputBinding::consume). | ||
| 457 | * @param section INI section name. | ||
| 458 | * @param ini_key INI key name (e.g. "SetYToggle.Consume"). | ||
| 459 | * @param log_key_name Human-readable name shown in log output. | ||
| 460 | * @param input_binding_name InputManager binding name to toggle. | ||
| 461 | * @param default_value Suppression state applied when the INI key is missing. | ||
| 462 | */ | ||
| 463 | void register_consume_flag(std::string_view section, std::string_view ini_key, std::string_view log_key_name, | ||
| 464 | std::string_view input_binding_name, bool default_value = false); | ||
| 465 | |||
| 466 | /** | ||
| 467 | * @brief Loads all registered configuration settings from the specified INI file. | ||
| 468 | * @details Parses the INI file and attempts to read values for each registered item. If a key is missing or | ||
| 469 | * invalid, the default value provided during registration is used. The INI path is remembered | ||
| 470 | * internally so that subsequent reload() calls operate on the same file without needing the caller to | ||
| 471 | * pass it again. | ||
| 472 | * @param ini_filename The base filename of the INI file. Path will be resolved relative to the mod's runtime | ||
| 473 | * directory. | ||
| 474 | */ | ||
| 475 | void load(std::string_view ini_filename); | ||
| 476 | |||
| 477 | /** | ||
| 478 | * @brief Re-runs all registered setters against the last-loaded INI file. | ||
| 479 | * @details Reads the INI file previously passed to load() and re-invokes every registered setter with the fresh | ||
| 480 | * value (or its default if the key is missing). Registrations themselves are not touched: user lambdas | ||
| 481 | * persist across reloads. The deferred-setter invocation pattern used by load() applies here as well, | ||
| 482 | * so setters may freely call back into the Config API without deadlocking. | ||
| 483 | * @return true if a previous load() path was available and the reload proceeded, false if reload() was called | ||
| 484 | * before any load(). | ||
| 485 | * @note Safe to call from any thread. Commonly wired to a filesystem watcher (see enable_auto_reload) or a | ||
| 486 | * hotkey (see register_reload_hotkey). | ||
| 487 | * @note Only C++ exceptions are caught. Structured-exception (SEH) faults such as access violations bypass the | ||
| 488 | * handler. A `noexcept`-marked user setter that throws still invokes std::terminate. | ||
| 489 | */ | ||
| 490 | [[nodiscard]] bool reload(); | ||
| 491 | |||
| 492 | /** | ||
| 493 | * @enum AutoReloadStatus | ||
| 494 | * @brief Outcome of a call to enable_auto_reload(). | ||
| 495 | */ | ||
| 496 | enum class AutoReloadStatus | ||
| 497 | { | ||
| 498 | /// Watcher is now running. | ||
| 499 | Started, | ||
| 500 | /// Called twice; the existing watcher was kept. | ||
| 501 | AlreadyRunning, | ||
| 502 | /// Config::load() was never called; no path to watch. | ||
| 503 | NoPriorLoad, | ||
| 504 | /// Directory could not be opened or start handshake failed. | ||
| 505 | StartFailed | ||
| 506 | }; | ||
| 507 | |||
| 508 | /** | ||
| 509 | * @brief Starts a background watcher that calls reload() when the INI changes. | ||
| 510 | * @details Creates a ConfigWatcher on the INI path last passed to load() and starts its worker thread. The | ||
| 511 | * watcher collapses bursty editor save events (e.g. Notepad++ atomic save) into a single reload via | ||
| 512 | * the @p debounce quiet window. After the reload completes, @p on_reload is invoked if provided, | ||
| 513 | * allowing the caller to refresh derived state (e.g. rebuild caches, reformat log output). | ||
| 514 | * | ||
| 515 | * If load() has not been called yet, or if auto-reload is already enabled, this is a no-op and a | ||
| 516 | * Warning-level log message is emitted. | ||
| 517 | * | ||
| 518 | * The watcher and any @p on_reload callback run on the watcher's background thread. User setters | ||
| 519 | * invoked by reload() also run on that thread; they must handle their own synchronization. | ||
| 520 | * | ||
| 521 | * The @p on_reload callback receives a `bool content_changed` argument. When the file's byte contents | ||
| 522 | * are identical to the last successfully loaded version (e.g. after a `touch` or a no-op save), | ||
| 523 | * setters are skipped and the flag is false; the callback still fires so derived state can observe the | ||
| 524 | * event. | ||
| 525 | * | ||
| 526 | * @param debounce Quiet-window length between change detection and reload (default 250 ms). | ||
| 527 | * @param on_reload Optional callback invoked after each successful reload. The bool argument is true when | ||
| 528 | * setters ran, false when the content-hash skip short-circuited the reload. | ||
| 529 | * @return AutoReloadStatus::Started if the watcher is now running; | ||
| 530 | * AutoReloadStatus::AlreadyRunning if a watcher was already installed | ||
| 531 | * (no-op, existing watcher kept); | ||
| 532 | * AutoReloadStatus::NoPriorLoad if load() has not been called yet | ||
| 533 | * (no-op, no watcher installed); | ||
| 534 | * AutoReloadStatus::StartFailed if the parent directory could not be opened or the start handshake | ||
| 535 | * failed (watcher reset, error logged). | ||
| 536 | */ | ||
| 537 | [[nodiscard]] AutoReloadStatus | ||
| 538 | enable_auto_reload(std::chrono::milliseconds debounce = std::chrono::milliseconds{250}, | ||
| 539 | std::function<void(bool)> on_reload = {}); | ||
| 540 | |||
| 541 | /** | ||
| 542 | * @brief Stops the filesystem watcher started by enable_auto_reload(). | ||
| 543 | * @details Idempotent. Returns only once the watcher thread has exited (or been detached under the Windows | ||
| 544 | * loader lock). | ||
| 545 | * @note When invoked from inside an on_reload callback (i.e. on the watcher thread itself) this is a no-op: | ||
| 546 | * joining the worker from its own thread would raise std::system_error(resource_deadlock_would_occur). | ||
| 547 | * The error is logged and the watcher remains running. Tear the watcher down from a different thread, | ||
| 548 | * e.g. by posting the disable request to a deferred shutdown hook. | ||
| 549 | * @note A config change still inside the debounce window when this is called fires one final reload (running | ||
| 550 | * the registered setters) during the stop, so disabling auto-reload does not guarantee no further setter | ||
| 551 | * invocation. Callers that require a hard stop should latch their own guard around setter side effects. | ||
| 552 | */ | ||
| 553 | void disable_auto_reload() noexcept; | ||
| 554 | |||
| 555 | /** | ||
| 556 | * @brief Registers a hotkey binding that triggers reload() on press. | ||
| 557 | * @details Thin wrapper around register_press_combo() whose on-press callback calls Config::reload(). Like the | ||
| 558 | * underlying helper, this must be called before InputManager::start() so the binding is picked up by | ||
| 559 | * the poller. | ||
| 560 | * | ||
| 561 | * The INI-configured combo overrides @p default_combo on each load() / reload() cycle via the standard | ||
| 562 | * register_press_combo machinery. | ||
| 563 | * | ||
| 564 | * @param ini_key INI key that stores the combo string (e.g. "ReloadConfig"). | ||
| 565 | * @param default_combo Combo string applied when the INI key is absent (e.g. "Ctrl+F5"). | ||
| 566 | * @return true if the binding was registered, false if @p default_combo is empty or the NONE sentinel. @ref | ||
| 567 | * register_press_combo accepts both as silent opt-out and registers the binding name with no keys | ||
| 568 | * (addressable later by @ref update_binding_combos), but a reload hotkey with no default keys is never | ||
| 569 | * useful, so this helper rejects that case at the call site rather than ship an inert reload binding. | ||
| 570 | * @note The on-press callback runs on the InputManager poll thread, but the actual reload() work is deferred to | ||
| 571 | * a dedicated background servicer thread. The press callback only flips an atomic flag and notifies a | ||
| 572 | * condition variable, so per-press latency on the poll thread stays in the microsecond range regardless | ||
| 573 | * of INI size. Multiple presses during a running reload coalesce into at most one follow-up. Any | ||
| 574 | * exception thrown by reload() on the servicer thread is caught and logged so the servicer stays alive. | ||
| 575 | * @note Only C++ exceptions are caught. Structured-exception (SEH) faults such as access violations bypass the | ||
| 576 | * handler. A `noexcept`-marked user setter that throws still invokes std::terminate. | ||
| 577 | */ | ||
| 578 | [[nodiscard]] bool register_reload_hotkey(std::string_view ini_key, std::string_view default_combo); | ||
| 579 | |||
| 580 | /** | ||
| 581 | * @brief Logs the current values of all registered configuration settings. | ||
| 582 | * @details Iterates through all items registered with the config system and outputs their current values to the | ||
| 583 | * Logger. | ||
| 584 | */ | ||
| 585 | void log_all(); | ||
| 586 | |||
| 587 | /** | ||
| 588 | * @brief Clears all currently registered configuration items. | ||
| 589 | * @details Useful if the configuration system needs to be reset without restarting the application. This does | ||
| 590 | * NOT stop the auto-reload watcher; call disable_auto_reload() first (DMK_Shutdown() already does so | ||
| 591 | * in the correct order) so a watcher callback cannot fire against state torn down afterwards. | ||
| 592 | * @note noexcept: clearing the registry and dropping the cached path/hash and reload-servicer refs are all | ||
| 593 | * no-throw, and any diagnostic logging routes through the best-effort no-throw log path. | ||
| 594 | */ | ||
| 595 | void clear_registered_items() noexcept; | ||
| 596 | |||
| 597 | } // namespace Config | ||
| 598 | } // namespace DetourModKit | ||
| 599 | |||
| 600 | #endif // DETOURMODKIT_CONFIG_HPP | ||
| 601 |