GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 100.0% 28 / 0 / 28
Functions: 100.0% 12 / 0 / 12
Branches: 83.3% 10 / 0 / 12

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