GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 100.0% 61 / 0 / 61
Functions: 100.0% 18 / 0 / 18
Branches: 83.3% 15 / 0 / 18

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