GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 100.0% 30 / 0 / 30
Functions: 100.0% 12 / 0 / 12
Branches: 50.0% 4 / 0 / 8

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