./
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
640 / 698
91.7%
Branches:
480 / 776
61.9%
Functions:
80 / 82
97.6%

src/config.cpp
Line Branch Exec Source
1 /**
2 * @file config.cpp
3 * @brief This TU implements the INI parse and registry data plane and the INI-to-input combo fusion.
4 *
5 * The config module depends on input, never the reverse. SimpleIni stays confined to this TU. The reload control
6 * plane lives in src/internal/config_reload.cpp and the watcher control plane in config_watch.cpp.
7 */
8
9 #include "DetourModKit/config.hpp"
10 #include "DetourModKit/input.hpp"
11 #include "DetourModKit/input_codes.hpp"
12 #include "DetourModKit/logger.hpp"
13 #include "DetourModKit/filesystem.hpp"
14 #include "DetourModKit/format.hpp"
15
16 #include "internal/config_pass.hpp"
17 #include "internal/config_reload_gate.hpp"
18 #include "internal/config_reload_lifecycle.hpp"
19 #include "internal/config_watch_control.hpp"
20 #include "internal/utf8_conversion.hpp"
21
22 #include <SimpleIni.h>
23
24 #include <algorithm>
25 #include <atomic>
26 #include <charconv>
27 #include <cmath>
28 #include <cstdint>
29 #include <filesystem>
30 #include <fstream>
31 #include <functional>
32 #include <limits>
33 #include <memory>
34 #include <mutex>
35 #include <optional>
36 #include <string>
37 #include <string_view>
38 #include <type_traits>
39 #include <unordered_set>
40 #include <utility>
41 #include <vector>
42
43 namespace DetourModKit::detail
44 {
45 #if defined(DMK_ENABLE_TEST_SEAMS)
46 // Fires inside load()'s watcher re-point, between the stale-watcher join and replacement start. A test can then
47 // place a disable_auto_reload() call in that lock gap deterministically.
48 void (*g_config_repoint_window_test_hook)() = nullptr;
49
50 // Forces read_ini_bytes() through its seek/tell failure classification.
51 std::atomic<bool> g_config_read_seektell_fail{false};
52
53 // Forces one parse failure so an identical-byte retry can exercise hash invalidation.
54 std::atomic<bool> g_config_parse_fail_once{false};
55 #endif
56 } // namespace DetourModKit::detail
57
58 namespace DetourModKit
59 {
60 namespace config
61 {
62 using DetourModKit::filesystem::get_runtime_directory;
63 using DetourModKit::string::trim;
64
65 namespace
66 {
67 /**
68 * @brief Parses a comma-separated string of input tokens into a vector of InputCodes.
69 * @details Named keys resolve through parse_input_name. A bare hex token falls back to a Keyboard VK
70 * code, which closes format_input_code's bare-hex keyboard round-trip. Semicolon comments and
71 * invalid tokens are skipped.
72 */
73 256 std::vector<InputCode> parse_input_code_list(const std::string &input)
74 {
75 256 std::vector<InputCode> result;
76
77 256 const size_t comment_pos = input.find(';');
78 const std::string effective =
79
3/8
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 256 times.
✗ Branch 4 → 6 not taken.
✗ Branch 4 → 77 not taken.
✓ Branch 5 → 6 taken 256 times.
✗ Branch 5 → 77 not taken.
✓ Branch 7 → 8 taken 256 times.
✗ Branch 7 → 75 not taken.
256 trim((comment_pos != std::string::npos) ? input.substr(0, comment_pos) : input);
80
1/2
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 256 times.
256 if (effective.empty())
81 {
82 ✗ return result;
83 }
84
85 256 size_t pos = 0;
86
2/2
✓ Branch 70 → 13 taken 256 times.
✓ Branch 70 → 71 taken 256 times.
512 while (pos < effective.size())
87 {
88 256 const size_t comma = effective.find(',', pos);
89
1/2
✓ Branch 14 → 15 taken 256 times.
✗ Branch 14 → 16 not taken.
256 const size_t end = (comma != std::string::npos) ? comma : effective.size();
90
2/4
✓ Branch 17 → 18 taken 256 times.
✗ Branch 17 → 80 not taken.
✓ Branch 19 → 20 taken 256 times.
✗ Branch 19 → 78 not taken.
256 const std::string token = trim(effective.substr(pos, end - pos));
91 256 pos = end + 1;
92
93
1/2
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 24 taken 256 times.
256 if (token.empty())
94 {
95 ✗ continue;
96 }
97
98
1/2
✓ Branch 25 → 26 taken 256 times.
✗ Branch 25 → 82 not taken.
256 auto named = parse_input_name(token);
99
2/2
✓ Branch 27 → 28 taken 207 times.
✓ Branch 27 → 31 taken 49 times.
256 if (named.has_value())
100 {
101
1/2
✓ Branch 29 → 30 taken 207 times.
✗ Branch 29 → 82 not taken.
207 result.push_back(*named);
102 207 continue;
103 }
104
105 49 size_t hex_start = 0;
106
8/10
✓ Branch 32 → 33 taken 49 times.
✗ Branch 32 → 40 not taken.
✓ Branch 34 → 35 taken 39 times.
✓ Branch 34 → 40 taken 10 times.
✓ Branch 36 → 37 taken 2 times.
✓ Branch 36 → 39 taken 37 times.
✓ Branch 38 → 39 taken 2 times.
✗ Branch 38 → 40 not taken.
✓ Branch 41 → 42 taken 39 times.
✓ Branch 41 → 43 taken 10 times.
49 if (token.size() >= 2 && token[0] == '0' && (token[1] == 'x' || token[1] == 'X'))
107 {
108 39 hex_start = 2;
109 }
110
2/2
✓ Branch 44 → 45 taken 4 times.
✓ Branch 44 → 46 taken 45 times.
49 if (hex_start >= token.size())
111 {
112 4 continue;
113 }
114
115 45 const std::string_view hex_part(token.data() + hex_start, token.size() - hex_start);
116
2/2
✓ Branch 50 → 51 taken 10 times.
✓ Branch 50 → 52 taken 35 times.
45 if (hex_part.find_first_not_of("0123456789abcdefABCDEF") != std::string_view::npos)
117 {
118 10 continue;
119 }
120
121 35 unsigned int value = 0;
122 35 const char *const hex_begin = hex_part.data();
123 35 const char *const hex_end = hex_begin + hex_part.size();
124
1/2
✓ Branch 54 → 55 taken 35 times.
✗ Branch 54 → 82 not taken.
35 const auto [parsed_end, parse_ec] = std::from_chars(hex_begin, hex_end, value, 16);
125
3/4
✓ Branch 55 → 56 taken 33 times.
✓ Branch 55 → 57 taken 2 times.
✗ Branch 56 → 57 not taken.
✓ Branch 56 → 58 taken 33 times.
35 if (parse_ec != std::errc{} || parsed_end != hex_end)
126 {
127 2 continue;
128 }
129
2/2
✓ Branch 59 → 60 taken 3 times.
✓ Branch 59 → 61 taken 30 times.
33 if (value > static_cast<unsigned int>(std::numeric_limits<int>::max()))
130 {
131 3 continue;
132 }
133
134
1/2
✓ Branch 61 → 62 taken 30 times.
✗ Branch 61 → 81 not taken.
30 result.push_back(InputCode{InputSource::Keyboard, static_cast<int>(value)});
135
2/2
✓ Branch 64 → 65 taken 30 times.
✓ Branch 64 → 67 taken 226 times.
256 }
136
137 256 return result;
138 256 }
139
140 /**
141 * @brief Parses a single key combo string ("mod1+mod2+trigger") into a KeyCombo struct.
142 * @details The last '+'-delimited token is the trigger. Earlier tokens are AND-logic modifiers. The
143 * function expects no commas. parse_key_combo_list splits alternatives first.
144 */
145 228 input::KeyCombo parse_key_combo(const std::string &input)
146 {
147 228 input::KeyCombo result;
148
149
1/2
✓ Branch 3 → 4 taken 228 times.
✗ Branch 3 → 65 not taken.
228 const std::string effective = trim(input);
150
1/2
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 228 times.
228 if (effective.empty())
151 {
152 ✗ return result;
153 }
154
155 228 std::vector<std::string> segments;
156 228 size_t pos = 0;
157
2/2
✓ Branch 22 → 8 taken 260 times.
✓ Branch 22 → 23 taken 228 times.
488 while (pos < effective.size())
158 {
159 260 const size_t plus = effective.find('+', pos);
160
2/2
✓ Branch 9 → 10 taken 226 times.
✓ Branch 9 → 11 taken 34 times.
260 const size_t end = (plus != std::string::npos) ? plus : effective.size();
161
2/4
✓ Branch 12 → 13 taken 260 times.
✗ Branch 12 → 51 not taken.
✓ Branch 14 → 15 taken 260 times.
✗ Branch 14 → 49 not taken.
260 const std::string segment = trim(effective.substr(pos, end - pos));
162 260 pos = end + 1;
163
2/2
✓ Branch 17 → 18 taken 256 times.
✓ Branch 17 → 19 taken 4 times.
260 if (!segment.empty())
164 {
165
1/2
✓ Branch 18 → 19 taken 256 times.
✗ Branch 18 → 52 not taken.
256 segments.push_back(segment);
166 }
167 260 }
168
169
2/2
✓ Branch 24 → 25 taken 1 time.
✓ Branch 24 → 26 taken 227 times.
228 if (segments.empty())
170 {
171 1 return result;
172 }
173
174
1/2
✓ Branch 27 → 28 taken 227 times.
✗ Branch 27 → 55 not taken.
227 result.keys = parse_input_code_list(segments.back());
175
176
2/2
✓ Branch 43 → 31 taken 29 times.
✓ Branch 43 → 44 taken 227 times.
256 for (size_t i = 0; i + 1 < segments.size(); ++i)
177 {
178
1/2
✓ Branch 32 → 33 taken 29 times.
✗ Branch 32 → 60 not taken.
29 auto mod_codes = parse_input_code_list(segments[i]);
179
1/2
✓ Branch 39 → 40 taken 29 times.
✗ Branch 39 → 56 not taken.
58 result.modifiers.insert(result.modifiers.end(), mod_codes.begin(), mod_codes.end());
180 29 }
181
182 227 return result;
183 228 }
184
185 /**
186 * @brief Returns true when @p text is the literal "NONE" sentinel (case-insensitive ASCII, pre-trimmed).
187 * @details Whole-string only: a NONE token nested inside an OR-list is indistinguishable from a typo, and
188 * an unbound slot inside an OR-list is meaningless.
189 */
190 175 [[nodiscard]] bool is_none_sentinel(std::string_view text) noexcept
191 {
192
2/2
✓ Branch 3 → 4 taken 165 times.
✓ Branch 3 → 5 taken 10 times.
175 if (text.size() != 4)
193 {
194 165 return false;
195 }
196 10 constexpr char target[] = {'N', 'O', 'N', 'E'};
197
2/2
✓ Branch 14 → 6 taken 28 times.
✓ Branch 14 → 15 taken 6 times.
34 for (size_t i = 0; i < 4; ++i)
198 {
199 28 const char ch = text[i];
200
3/4
✓ Branch 7 → 8 taken 12 times.
✓ Branch 7 → 10 taken 16 times.
✓ Branch 8 → 9 taken 12 times.
✗ Branch 8 → 10 not taken.
28 const char folded = (ch >= 'a' && ch <= 'z') ? static_cast<char>(ch - ('a' - 'A')) : ch;
201
2/2
✓ Branch 11 → 12 taken 4 times.
✓ Branch 11 → 13 taken 24 times.
28 if (folded != target[i])
202 {
203 4 return false;
204 }
205 }
206 6 return true;
207 }
208
209 } // anonymous namespace
210
211 namespace detail
212 {
213 // Contract in internal/config_pass.hpp. The grammar helpers above stay file-local.
214 205 input::KeyComboList parse_key_combo_list(
215 const std::string &input,
216 DeferredDiagnostics &diags,
217 std::string_view binding_log_name
218 )
219 {
220 205 input::KeyComboList result;
221
222 205 const size_t comment_pos = input.find(';');
223 const std::string effective =
224
5/8
✓ Branch 3 → 4 taken 4 times.
✓ Branch 3 → 5 taken 201 times.
✓ Branch 4 → 6 taken 4 times.
✗ Branch 4 → 58 not taken.
✓ Branch 5 → 6 taken 201 times.
✗ Branch 5 → 58 not taken.
✓ Branch 7 → 8 taken 205 times.
✗ Branch 7 → 56 not taken.
205 trim((comment_pos != std::string::npos) ? input.substr(0, comment_pos) : input);
225
226 // An empty string and the NONE sentinel are silent opt-outs.
227
2/2
✓ Branch 10 → 11 taken 30 times.
✓ Branch 10 → 12 taken 175 times.
205 if (effective.empty())
228 {
229 30 return result;
230 }
231
2/2
✓ Branch 14 → 15 taken 6 times.
✓ Branch 14 → 16 taken 169 times.
175 if (is_none_sentinel(effective))
232 {
233 6 return result;
234 }
235
236 169 size_t pos = 0;
237
2/2
✓ Branch 43 → 17 taken 235 times.
✓ Branch 43 → 44 taken 169 times.
404 while (pos < effective.size())
238 {
239 235 const size_t comma = effective.find(',', pos);
240
2/2
✓ Branch 18 → 19 taken 168 times.
✓ Branch 18 → 20 taken 67 times.
235 const size_t end = (comma != std::string::npos) ? comma : effective.size();
241
2/4
✓ Branch 21 → 22 taken 235 times.
✗ Branch 21 → 61 not taken.
✓ Branch 23 → 24 taken 235 times.
✗ Branch 23 → 59 not taken.
235 const std::string combo_str = trim(effective.substr(pos, end - pos));
242 235 pos = end + 1;
243
244
2/2
✓ Branch 26 → 27 taken 7 times.
✓ Branch 26 → 28 taken 228 times.
235 if (combo_str.empty())
245 {
246 7 continue;
247 }
248
249
1/2
✓ Branch 28 → 29 taken 228 times.
✗ Branch 28 → 64 not taken.
228 auto combo = parse_key_combo(combo_str);
250
2/2
✓ Branch 30 → 31 taken 209 times.
✓ Branch 30 → 34 taken 19 times.
228 if (!combo.keys.empty())
251 {
252
1/2
✓ Branch 33 → 34 taken 209 times.
✗ Branch 33 → 62 not taken.
209 result.push_back(std::move(combo));
253 }
254
2/2
✓ Branch 37 → 38 taken 228 times.
✓ Branch 37 → 40 taken 7 times.
235 }
255
256 // If non-empty, non-sentinel input has no valid token, report the user typo by name.
257
2/2
✓ Branch 45 → 46 taken 6 times.
✓ Branch 45 → 52 taken 163 times.
169 if (result.empty())
258 {
259 const std::string_view name_view =
260
1/2
✗ Branch 47 → 48 not taken.
✓ Branch 47 → 49 taken 6 times.
6 binding_log_name.empty() ? std::string_view{"<unnamed>"} : binding_log_name;
261
1/2
✓ Branch 50 → 51 taken 6 times.
✗ Branch 50 → 67 not taken.
6 defer_diagnostic(
262 diags,
263 LogLevel::Warning,
264 "Config: combo string \"{}\" for binding '{}' did not parse to any "
265 "valid keys; binding will be unbound. Use \"\" or \"NONE\" to opt "
266 "out explicitly.",
267 effective,
268 name_view
269 );
270 }
271
272 169 return result;
273 205 }
274 } // namespace detail
275
276 namespace
277 {
278 /// Formats a single KeyCombo as a human-readable string (e.g. "Ctrl+Shift+F3").
279 7 std::string format_key_combo(const input::KeyCombo &combo)
280 {
281 7 std::string result;
282
2/2
✓ Branch 21 → 5 taken 1 time.
✓ Branch 21 → 22 taken 7 times.
15 for (const auto &mod : combo.modifiers)
283 {
284
3/6
✓ Branch 7 → 8 taken 1 time.
✗ Branch 7 → 38 not taken.
✓ Branch 8 → 9 taken 1 time.
✗ Branch 8 → 36 not taken.
✓ Branch 9 → 10 taken 1 time.
✗ Branch 9 → 34 not taken.
1 result += DetourModKit::format_input_code(mod) + "+";
285 }
286
2/2
✓ Branch 31 → 23 taken 7 times.
✓ Branch 31 → 32 taken 7 times.
14 for (size_t i = 0; i < combo.keys.size(); ++i)
287 {
288
1/2
✗ Branch 23 → 24 not taken.
✓ Branch 23 → 25 taken 7 times.
7 if (i > 0)
289 {
290
0/2
✗ Branch 24 → 25 not taken.
✗ Branch 24 → 44 not taken.
✗ result += ",";
291 }
292
2/4
✓ Branch 26 → 27 taken 7 times.
✗ Branch 26 → 43 not taken.
✓ Branch 27 → 28 taken 7 times.
✗ Branch 27 → 41 not taken.
7 result += DetourModKit::format_input_code(combo.keys[i]);
293 }
294 7 return result;
295 ✗ }
296
297 /// Formats a KeyComboList as a comma-joined human-readable string (e.g. "F3,Gamepad_LT+Gamepad_B").
298 8 std::string format_key_combo_list(const input::KeyComboList &combos)
299 {
300 8 std::string result;
301
2/2
✓ Branch 12 → 4 taken 7 times.
✓ Branch 12 → 13 taken 8 times.
15 for (size_t i = 0; i < combos.size(); ++i)
302 {
303
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 7 times.
7 if (i > 0)
304 {
305
0/2
✗ Branch 5 → 6 not taken.
✗ Branch 5 → 18 not taken.
✗ result += ",";
306 }
307
2/4
✓ Branch 7 → 8 taken 7 times.
✗ Branch 7 → 17 not taken.
✓ Branch 8 → 9 taken 7 times.
✗ Branch 8 → 15 not taken.
7 result += format_key_combo(combos[i]);
308 }
309 8 return result;
310 ✗ }
311
312 /**
313 * @brief Selects the argument type the deferred setter receives.
314 * @details The string bind delivers a std::string_view valid only for the call. The combo bind delivers a
315 * const reference. Every other bound type passes the parsed value by value.
316 */
317 template <typename T>
318 using SetterArg = std::conditional_t<
319 std::same_as<T, std::string>,
320 std::string_view,
321 std::conditional_t<std::same_as<T, input::KeyComboList>, const input::KeyComboList &, T>>;
322
323 /// Copies of a shared setter share one target, so a copy under the registry mutex runs no consumer code.
324 template <typename T> using SharedSetter = std::shared_ptr<const std::function<void(SetterArg<T>)>>;
325
326 /// ConfigItemBase stores typed configuration items polymorphically in the registry.
327 struct ConfigItemBase
328 {
329 std::string section;
330 std::string ini_key;
331 std::string log_key_name;
332
333 338 ConfigItemBase(std::string sec, std::string key, std::string log_name)
334 1352 : section(std::move(sec)), ini_key(std::move(key)), log_key_name(std::move(log_name))
335 {
336 338 }
337 338 virtual ~ConfigItemBase() = default;
338 ConfigItemBase(const ConfigItemBase &) = delete;
339 ConfigItemBase &operator=(const ConfigItemBase &) = delete;
340 ConfigItemBase(ConfigItemBase &&) = delete;
341 ConfigItemBase &operator=(ConfigItemBase &&) = delete;
342
343 /// Loads the configuration value from the INI file and defers every diagnostic it produces.
344 virtual void load(CSimpleIniA &ini, detail::DeferredDiagnostics &diags) = 0;
345
346 /// Returns a deferred callback to invoke the setter outside the config mutex, or empty without one.
347 [[nodiscard]] virtual std::function<void()> take_deferred_apply() const = 0;
348
349 /// Defers one record that names the current value of the configuration item.
350 virtual void log_current_value(detail::DeferredDiagnostics &diags) const = 0;
351 };
352
353 /**
354 * @brief Trims ASCII blanks from both ends of @p text and strips one initial '+'.
355 * @details from_chars rejects an initial '+' (unlike the strtod/strtoll it replaced), so this function
356 * strips one for a positive value. It returns a view into @p text and allocates nothing.
357 */
358 198 [[nodiscard]] std::string_view trim_blanks_and_leading_plus(std::string_view text) noexcept
359 {
360 396 constexpr auto is_blank = [](char c) noexcept
361
4/8
✓ Branch 2 → 3 taken 396 times.
✗ Branch 2 → 6 not taken.
✓ Branch 3 → 4 taken 396 times.
✗ Branch 3 → 6 not taken.
✓ Branch 4 → 5 taken 396 times.
✗ Branch 4 → 6 not taken.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 396 times.
396 { return c == ' ' || c == '\t' || c == '\r' || c == '\n'; };
362
3/6
✓ Branch 5 → 6 taken 198 times.
✗ Branch 5 → 10 not taken.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 198 times.
✗ Branch 11 → 3 not taken.
✓ Branch 11 → 12 taken 198 times.
198 while (!text.empty() && is_blank(text.front()))
363 {
364 ✗ text.remove_prefix(1);
365 }
366
3/6
✓ Branch 15 → 16 taken 198 times.
✗ Branch 15 → 20 not taken.
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 20 taken 198 times.
✗ Branch 21 → 13 not taken.
✓ Branch 21 → 22 taken 198 times.
198 while (!text.empty() && is_blank(text.back()))
367 {
368 ✗ text.remove_suffix(1);
369 }
370
5/6
✓ Branch 23 → 24 taken 198 times.
✗ Branch 23 → 27 not taken.
✓ Branch 25 → 26 taken 2 times.
✓ Branch 25 → 27 taken 196 times.
✓ Branch 28 → 29 taken 2 times.
✓ Branch 28 → 30 taken 196 times.
198 if (!text.empty() && text.front() == '+')
371 {
372 2 text.remove_prefix(1);
373 }
374 198 return text;
375 }
376
377 /**
378 * @brief Parses an INI boolean and distinguishes an unrecognized value from a valid one.
379 * @details Matches SimpleIni's GetBoolValue forms.
380 * An initial t/y/1 means true, and f/n/0 means false.
381 * The exact values "on" and "off" also map to true and false. An unrecognized value returns
382 * nullopt for diagnosis. @p value must be non-null and non-empty.
383 */
384 36 [[nodiscard]] std::optional<bool> parse_ini_bool(const char *value) noexcept
385 {
386
3/4
✓ Branch 2 → 3 taken 20 times.
✓ Branch 2 → 6 taken 14 times.
✗ Branch 2 → 9 not taken.
✓ Branch 2 → 22 taken 2 times.
36 switch (value[0])
387 {
388 20 case 't':
389 case 'T':
390 case 'y':
391 case 'Y':
392 case '1':
393 20 return true;
394 14 case 'f':
395 case 'F':
396 case 'n':
397 case 'N':
398 case '0':
399 14 return false;
400 ✗ case 'o':
401 case 'O':
402
0/4
✗ Branch 9 → 10 not taken.
✗ Branch 9 → 11 not taken.
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 14 not taken.
✗ if (value[1] == 'n' || value[1] == 'N')
403 {
404 ✗ return true;
405 }
406
0/4
✗ Branch 14 → 15 not taken.
✗ Branch 14 → 16 not taken.
✗ Branch 15 → 16 not taken.
✗ Branch 15 → 19 not taken.
✗ if (value[1] == 'f' || value[1] == 'F')
407 {
408 ✗ return false;
409 }
410 ✗ return std::nullopt;
411 2 default:
412 2 return std::nullopt;
413 }
414 }
415
416 /**
417 * @brief Stores a configuration item with a shared setter.
418 * @note `[B-101]`: the registry mutex copies and drops only shared references. The last reference drops
419 * after unlock.
420 */
421 template <typename T> struct CallbackConfigItem : public ConfigItemBase
422 {
423 SharedSetter<T> setter;
424 T default_value;
425 T current_value;
426
427 338 CallbackConfigItem(
428 std::string sec,
429 std::string key,
430 std::string log_name,
431 SharedSetter<T> set_fn,
432 T def_val
433 )
434 338 : ConfigItemBase(std::move(sec), std::move(key), std::move(log_name)), setter(std::move(set_fn)),
435
2/4
DetourModKit::config::(anonymous namespace)::CallbackConfigItem<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >::CallbackConfigItem(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::shared_ptr<std::function<void (std::basic_string_view<char, std::char_traits<char> >)> const>, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >):
✓ Branch 18 → 19 taken 15 times.
✗ Branch 18 → 23 not taken.
DetourModKit::config::(anonymous namespace)::CallbackConfigItem<std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >::CallbackConfigItem(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::shared_ptr<std::function<void (std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&)> const>, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> >):
✓ Branch 18 → 19 taken 123 times.
✗ Branch 18 → 23 not taken.
1828 default_value(def_val), current_value(std::move(def_val))
436 {
437 338 }
438
439 286 void load(CSimpleIniA &ini, [[maybe_unused]] detail::DeferredDiagnostics &diags) override
440 {
441 // The generic body handles scalar and string types. KeyComboList uses the explicit specialization.
442 if constexpr (std::same_as<T, int>)
443 {
444 // SimpleIni's GetLongValue parses into a 32-bit long on LLP64 and can saturate. Parse the raw
445 // string with std::from_chars and warn-and-default on a bad value. Values with a 0x prefix
446 // are hexadecimal. All other values are decimal.
447
1/2
✓ Branch 4 → 5 taken 194 times.
✗ Branch 4 → 47 not taken.
194 const char *raw = ini.GetValue(section.c_str(), ini_key.c_str(), nullptr);
448
2/2
✓ Branch 5 → 6 taken 11 times.
✓ Branch 5 → 7 taken 183 times.
194 if (raw == nullptr)
449 {
450 11 current_value = default_value;
451 }
452 else
453 {
454 183 std::string_view text = trim_blanks_and_leading_plus(std::string_view{raw});
455
456 183 int base = 10;
457
7/10
✓ Branch 10 → 11 taken 71 times.
✓ Branch 10 → 18 taken 112 times.
✓ Branch 12 → 13 taken 1 time.
✓ Branch 12 → 18 taken 70 times.
✓ Branch 14 → 15 taken 1 time.
✗ Branch 14 → 17 not taken.
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 18 taken 1 time.
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 22 taken 183 times.
183 if (text.size() >= 2 && text[0] == '0' && (text[1] == 'x' || text[1] == 'X'))
458 {
459 ✗ text.remove_prefix(2);
460 ✗ base = 16;
461 }
462
463 183 long long parsed = 0;
464 const auto [end, ec] =
465
1/2
✓ Branch 25 → 26 taken 183 times.
✗ Branch 25 → 46 not taken.
183 std::from_chars(text.data(), text.data() + text.size(), parsed, base);
466
3/4
✓ Branch 26 → 27 taken 181 times.
✓ Branch 26 → 31 taken 2 times.
✓ Branch 29 → 30 taken 181 times.
✗ Branch 29 → 31 not taken.
183 const bool fully_consumed = (ec == std::errc{} && end == text.data() + text.size());
467
6/6
✓ Branch 32 → 33 taken 181 times.
✓ Branch 32 → 37 taken 2 times.
✓ Branch 34 → 35 taken 180 times.
✓ Branch 34 → 37 taken 1 time.
✓ Branch 39 → 40 taken 4 times.
✓ Branch 39 → 42 taken 179 times.
363 if (!fully_consumed || parsed < static_cast<long long>(std::numeric_limits<int>::min()) ||
468
2/2
✓ Branch 36 → 37 taken 1 time.
✓ Branch 36 → 38 taken 179 times.
180 parsed > static_cast<long long>(std::numeric_limits<int>::max()))
469 {
470 ✗ detail::defer_diagnostic(
471 diags,
472 LogLevel::Warning,
473 "Config: value '{}' for '{}' is not a valid int (non-numeric or out of "
474 "range); using default {}.",
475 raw,
476 4 ini_key,
477
1/2
✓ Branch 40 → 41 taken 4 times.
✗ Branch 40 → 45 not taken.
4 default_value
478 );
479 4 current_value = default_value;
480 }
481 else
482 {
483 179 current_value = static_cast<int>(parsed);
484 }
485 }
486 }
487 else if constexpr (std::same_as<T, float>)
488 {
489 // SimpleIni's GetDoubleValue routes through the locale-dependent strtod: a comma-decimal host
490 // locale silently turns "1.5" into the default. Parse the raw string with the
491 // locale-independent std::from_chars instead, with the int path's warn-and-default discipline.
492
1/2
✓ Branch 4 → 5 taken 20 times.
✗ Branch 4 → 32 not taken.
20 const char *raw = ini.GetValue(section.c_str(), ini_key.c_str(), nullptr);
493
2/2
✓ Branch 5 → 6 taken 5 times.
✓ Branch 5 → 7 taken 15 times.
20 if (raw == nullptr)
494 {
495 5 current_value = default_value;
496 }
497 else
498 {
499 15 std::string_view text = trim_blanks_and_leading_plus(std::string_view{raw});
500
501 15 float parsed = default_value;
502 15 const auto [end, ec] = std::from_chars(text.data(), text.data() + text.size(), parsed);
503
4/4
✓ Branch 13 → 14 taken 14 times.
✓ Branch 13 → 18 taken 1 time.
✓ Branch 16 → 17 taken 13 times.
✓ Branch 16 → 18 taken 1 time.
15 const bool fully_consumed = (ec == std::errc{} && end == text.data() + text.size());
504 // std::from_chars(general) accepts "inf"/"infinity"/"nan". Reject a non-finite result
505 // because it poisons bound arithmetic downstream.
506
6/6
✓ Branch 19 → 20 taken 13 times.
✓ Branch 19 → 22 taken 2 times.
✓ Branch 21 → 22 taken 2 times.
✓ Branch 21 → 23 taken 11 times.
✓ Branch 24 → 25 taken 4 times.
✓ Branch 24 → 27 taken 11 times.
15 if (!fully_consumed || !std::isfinite(parsed))
507 {
508 ✗ detail::defer_diagnostic(
509 diags,
510 LogLevel::Warning,
511 "Config: value '{}' for '{}' is not a valid finite float (non-numeric, "
512 "non-finite, or out of range); using default {}.",
513 raw,
514 4 ini_key,
515
1/2
✓ Branch 25 → 26 taken 4 times.
✗ Branch 25 → 30 not taken.
4 default_value
516 );
517 4 current_value = default_value;
518 }
519 else
520 {
521 11 current_value = parsed;
522 }
523 }
524 }
525 else if constexpr (std::same_as<T, bool>)
526 {
527
1/2
✓ Branch 4 → 5 taken 62 times.
✗ Branch 4 → 23 not taken.
62 const char *raw = ini.GetValue(section.c_str(), ini_key.c_str(), nullptr);
528
3/4
✓ Branch 5 → 6 taken 36 times.
✓ Branch 5 → 7 taken 26 times.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 36 times.
62 if (raw == nullptr || raw[0] == '\0')
529 {
530 // If absent or empty, use the default silently. This matches SimpleIni's GetBoolValue.
531 26 current_value = default_value;
532 }
533
2/2
✓ Branch 10 → 11 taken 34 times.
✓ Branch 10 → 13 taken 2 times.
36 else if (const std::optional<bool> parsed = parse_ini_bool(raw); parsed.has_value())
534 {
535 34 current_value = *parsed;
536 }
537 else
538 {
539 // If present but unrecognized, diagnose it under the int/float warn-and-default rule.
540 ✗ detail::defer_diagnostic(
541 diags,
542 LogLevel::Warning,
543 "Config: value '{}' for '{}' is not a valid bool "
544 "(true/false, yes/no, on/off, 1/0); using default {}.",
545 raw,
546 2 ini_key,
547
2/4
✓ Branch 13 → 14 taken 2 times.
✗ Branch 13 → 15 not taken.
✓ Branch 16 → 17 taken 2 times.
✗ Branch 16 → 20 not taken.
2 default_value ? "true" : "false"
548 );
549 2 current_value = default_value;
550 }
551 }
552 else if constexpr (std::same_as<T, std::string>)
553 {
554 10 current_value = ini.GetValue(section.c_str(), ini_key.c_str(), default_value.c_str());
555 }
556 286 }
557
558 13 void log_current_value(detail::DeferredDiagnostics &diags) const override
559 {
560 if constexpr (std::same_as<T, bool>)
561 {
562 ✗ detail::defer_diagnostic(
563 diags,
564 LogLevel::Debug,
565 "Config: {} = {}",
566 4 ini_key,
567
3/4
✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 4 taken 2 times.
✓ Branch 5 → 6 taken 4 times.
✗ Branch 5 → 7 not taken.
4 current_value ? "true" : "false"
568 );
569 }
570 else if constexpr (std::same_as<T, std::string>)
571 {
572 4 detail::defer_diagnostic(
573 diags,
574 LogLevel::Debug,
575 "Config: {} = \"{}\"",
576 2 ini_key,
577
1/2
✓ Branch 2 → 3 taken 2 times.
✗ Branch 2 → 4 not taken.
2 current_value
578 );
579 }
580 else // int, float
581 {
582
2/4
DetourModKit::config::(anonymous namespace)::CallbackConfigItem<float>::log_current_value(DetourModKit::config::detail::DeferredDiagnostics&) const:
✓ Branch 2 → 3 taken 2 times.
✗ Branch 2 → 4 not taken.
DetourModKit::config::(anonymous namespace)::CallbackConfigItem<int>::log_current_value(DetourModKit::config::detail::DeferredDiagnostics&) const:
✓ Branch 2 → 3 taken 5 times.
✗ Branch 2 → 4 not taken.
7 detail::defer_diagnostic(diags, LogLevel::Debug, "Config: {} = {}", ini_key, current_value);
583 }
584 13 }
585
586 /// Returns a self-contained callback that invokes setter with current_value.
587 374 [[nodiscard]] std::function<void()> take_deferred_apply() const override
588 {
589
6/10
DetourModKit::config::(anonymous namespace)::CallbackConfigItem<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >::take_deferred_apply() const:
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 10 times.
DetourModKit::config::(anonymous namespace)::CallbackConfigItem<std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >::take_deferred_apply() const:
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 88 times.
DetourModKit::config::(anonymous namespace)::CallbackConfigItem<bool>::take_deferred_apply() const:
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 62 times.
DetourModKit::config::(anonymous namespace)::CallbackConfigItem<float>::take_deferred_apply() const:
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 20 times.
DetourModKit::config::(anonymous namespace)::CallbackConfigItem<int>::take_deferred_apply() const:
✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 5 taken 192 times.
374 if (!setter)
590 2 return {};
591 if constexpr (std::same_as<T, std::string>)
592 {
593 // Capture the owned string by value and hand out a view into that copy.
594
3/8
✓ Branch 6 → 7 taken 10 times.
✗ Branch 6 → 16 not taken.
✓ Branch 7 → 8 taken 10 times.
✗ Branch 7 → 14 not taken.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 10 times.
✗ Branch 16 → 17 not taken.
✗ Branch 16 → 18 not taken.
20 return [fn = setter, val = current_value]() { (*fn)(std::string_view{val}); };
595 }
596 else
597 {
598
9/26
DetourModKit::config::(anonymous namespace)::CallbackConfigItem<std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >::take_deferred_apply() const:
✓ Branch 6 → 7 taken 88 times.
✗ Branch 6 → 16 not taken.
✓ Branch 7 → 8 taken 88 times.
✗ Branch 7 → 14 not taken.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 88 times.
✗ Branch 16 → 17 not taken.
✗ Branch 16 → 18 not taken.
DetourModKit::config::(anonymous namespace)::CallbackConfigItem<bool>::take_deferred_apply() const:
✓ Branch 6 → 7 taken 62 times.
✗ Branch 6 → 13 not taken.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 62 times.
✗ Branch 15 → 16 not taken.
✗ Branch 15 → 17 not taken.
DetourModKit::config::(anonymous namespace)::CallbackConfigItem<float>::take_deferred_apply() const:
✓ Branch 6 → 7 taken 20 times.
✗ Branch 6 → 13 not taken.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 20 times.
✗ Branch 15 → 16 not taken.
✗ Branch 15 → 17 not taken.
DetourModKit::config::(anonymous namespace)::CallbackConfigItem<int>::take_deferred_apply() const:
✓ Branch 6 → 7 taken 192 times.
✗ Branch 6 → 13 not taken.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 192 times.
✗ Branch 15 → 16 not taken.
✗ Branch 15 → 17 not taken.
1082 return [fn = setter, val = current_value]() mutable { (*fn)(std::move(val)); };
599 }
600 }
601 };
602
603 // KeyComboList needs an explicit specialization because its parse path differs.
604 template <>
605 88 void CallbackConfigItem<input::KeyComboList>::load(CSimpleIniA &ini, detail::DeferredDiagnostics &diags)
606 {
607 88 const char *ini_value_str = ini.GetValue(section.c_str(), ini_key.c_str(), nullptr);
608
2/2
✓ Branch 5 → 6 taken 56 times.
✓ Branch 5 → 16 taken 32 times.
88 if (ini_value_str != nullptr)
609 {
610
2/4
✓ Branch 9 → 10 taken 56 times.
✗ Branch 9 → 20 not taken.
✓ Branch 10 → 11 taken 56 times.
✗ Branch 10 → 18 not taken.
168 current_value = detail::parse_key_combo_list(ini_value_str, diags, log_key_name);
611 }
612 else
613 {
614 32 current_value = default_value;
615 }
616 88 }
617
618 template <>
619 8 void CallbackConfigItem<input::KeyComboList>::log_current_value(detail::DeferredDiagnostics &diags) const
620 {
621
1/2
✓ Branch 2 → 3 taken 8 times.
✗ Branch 2 → 15 not taken.
8 const std::string formatted = format_key_combo_list(current_value);
622
2/2
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 7 taken 7 times.
8 if (formatted.empty())
623 {
624
1/2
✓ Branch 5 → 6 taken 1 time.
✗ Branch 5 → 11 not taken.
1 detail::defer_diagnostic(diags, LogLevel::Debug, "Config: {} = (none)", ini_key);
625 }
626 else
627 {
628
1/2
✓ Branch 7 → 8 taken 7 times.
✗ Branch 7 → 12 not taken.
7 detail::defer_diagnostic(diags, LogLevel::Debug, "Config: {} = {}", ini_key, formatted);
629 }
630 8 }
631
632 // Stores the global registry of configuration items.
633 4712 std::mutex &get_config_mutex()
634 {
635
3/4
✓ Branch 2 → 3 taken 1821 times.
✓ Branch 2 → 8 taken 2891 times.
✓ Branch 4 → 5 taken 1821 times.
✗ Branch 4 → 8 not taken.
4712 static std::mutex s_mtx;
636 4712 return s_mtx;
637 }
638
639 4546 std::vector<std::unique_ptr<ConfigItemBase>> &get_registered_config_items()
640 {
641
3/4
✓ Branch 2 → 3 taken 1821 times.
✓ Branch 2 → 7 taken 2725 times.
✓ Branch 4 → 5 taken 1821 times.
✗ Branch 4 → 7 not taken.
4546 static std::vector<std::unique_ptr<ConfigItemBase>> s_registered_items;
642 4546 return s_registered_items;
643 }
644
645 // Holds the INI path last passed to load(). Empty until the first load() call, so reload() returns false
646 // in that window. Caller must hold get_config_mutex() for every read or write.
647 4006 std::string &get_last_loaded_ini_path()
648 {
649
3/4
✓ Branch 2 → 3 taken 1820 times.
✓ Branch 2 → 7 taken 2186 times.
✓ Branch 4 → 5 taken 1820 times.
✗ Branch 4 → 7 not taken.
4006 static std::string s_last_loaded_ini_path;
650 4006 return s_last_loaded_ini_path;
651 }
652
653 } // anonymous namespace
654
655 namespace detail
656 {
657 // The config mutex is non-recursive, so callers outside a held config-mutex section use this helper.
658 70 std::string snapshot_last_loaded_ini_path()
659 {
660
1/2
✓ Branch 3 → 4 taken 70 times.
✗ Branch 3 → 12 not taken.
70 std::lock_guard<std::mutex> lock(get_config_mutex());
661
1/2
✓ Branch 5 → 6 taken 70 times.
✗ Branch 5 → 10 not taken.
140 return get_last_loaded_ini_path();
662 70 }
663 } // namespace detail
664
665 namespace
666 {
667
668 // Stores the content hash from the last successful load. It is std::nullopt before that load and after
669 // clear(). Caller must hold get_config_mutex().
670 4237 std::optional<std::uint64_t> &get_last_loaded_ini_hash()
671 {
672 static std::optional<std::uint64_t> s_last_loaded_ini_hash;
673 4237 return s_last_loaded_ini_hash;
674 }
675
676 // This monotonic counter advances on every registration or re-bind. reload() folds it into the unchanged-
677 // content decision, so a bind_* added after a load() hydrates from disk despite unchanged bytes.
678 // Caller must hold get_config_mutex().
679 641 std::uint64_t &get_binding_generation() noexcept
680 {
681 static std::uint64_t s_generation = 0;
682 641 return s_generation;
683 }
684
685 // Stores the binding generation from the last successful apply beside the content hash. reload()
686 // hash-skips only when BOTH are unchanged. Caller must hold get_config_mutex().
687 4005 std::optional<std::uint64_t> &get_applied_binding_generation() noexcept
688 {
689 static std::optional<std::uint64_t> s_applied_generation;
690 4005 return s_applied_generation;
691 }
692
693 /**
694 * @brief Computes a 64-bit FNV-1a hash over a raw byte range.
695 * @details It hashes the
696 * pre-parse disk bytes, so SimpleIni's cosmetic churn cannot skew the result.
697 */
698 271 [[nodiscard]] std::uint64_t fnv1a_64(const std::vector<std::uint8_t> &bytes) noexcept
699 {
700 271 constexpr std::uint64_t FNV_OFFSET_BASIS{0xcbf29ce484222325ULL};
701 271 constexpr std::uint64_t FNV_PRIME{0x00000100000001b3ULL};
702 271 std::uint64_t h{FNV_OFFSET_BASIS};
703
2/2
✓ Branch 15 → 4 taken 6331 times.
✓ Branch 15 → 16 taken 271 times.
6873 for (std::uint8_t b : bytes)
704 {
705 6331 h ^= static_cast<std::uint64_t>(b);
706 6331 h *= FNV_PRIME;
707 }
708 271 return h;
709 }
710
711 /**
712 * @brief Reads all bytes of @p path into memory, or std::nullopt when the file cannot be opened.
713 * @details On nullopt, load() proceeds with the bound defaults while reload() clears the cached hash and
714 * retains the last-applied values.
715 */
716 [[nodiscard]] std::optional<std::vector<std::uint8_t>>
717 307 read_ini_bytes(const std::filesystem::path &path) noexcept
718 {
719 try
720 {
721
1/2
✓ Branch 2 → 3 taken 307 times.
✗ Branch 2 → 55 not taken.
307 std::ifstream in(path, std::ios::binary);
722
3/4
✓ Branch 3 → 4 taken 307 times.
✗ Branch 3 → 53 not taken.
✓ Branch 4 → 5 taken 35 times.
✓ Branch 4 → 6 taken 272 times.
307 if (!in)
723 {
724 35 return std::nullopt;
725 }
726
1/2
✓ Branch 6 → 7 taken 272 times.
✗ Branch 6 → 53 not taken.
272 in.seekg(0, std::ios::end);
727
1/2
✓ Branch 7 → 8 taken 272 times.
✗ Branch 7 → 47 not taken.
272 std::streamsize size = in.tellg();
728
1/2
✓ Branch 9 → 10 taken 272 times.
✗ Branch 9 → 53 not taken.
272 in.seekg(0, std::ios::beg);
729 #if defined(DMK_ENABLE_TEST_SEAMS)
730
2/2
✓ Branch 11 → 12 taken 1 time.
✓ Branch 11 → 14 taken 271 times.
272 if (DetourModKit::detail::g_config_read_seektell_fail.load(std::memory_order_acquire))
731 {
732 // Simulate a failed tellg() so the I/O-failure classification below runs deterministically.
733
1/2
✓ Branch 12 → 13 taken 1 time.
✗ Branch 12 → 53 not taken.
1 in.setstate(std::ios::failbit);
734 1 size = -1;
735 }
736 #endif
737 // A seek/tell failure is an I/O failure (nullopt), not a successful empty read. reload() then
738 // retains the last-good values instead of defaults from an empty-file hash.
739
6/8
✓ Branch 14 → 15 taken 272 times.
✗ Branch 14 → 53 not taken.
✓ Branch 15 → 16 taken 271 times.
✓ Branch 15 → 17 taken 1 time.
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 18 taken 271 times.
✓ Branch 19 → 20 taken 1 time.
✓ Branch 19 → 21 taken 271 times.
272 if (!in || size < 0)
740 {
741 1 return std::nullopt;
742 }
743
2/2
✓ Branch 21 → 22 taken 3 times.
✓ Branch 21 → 26 taken 268 times.
271 if (size == 0)
744 {
745 3 return std::vector<std::uint8_t>{};
746 }
747
1/2
✓ Branch 28 → 29 taken 268 times.
✗ Branch 28 → 48 not taken.
268 std::vector<std::uint8_t> buf(static_cast<std::size_t>(size));
748
1/2
✓ Branch 31 → 32 taken 268 times.
✗ Branch 31 → 51 not taken.
268 in.read(reinterpret_cast<char *>(buf.data()), size);
749
3/10
✓ Branch 32 → 33 taken 268 times.
✗ Branch 32 → 51 not taken.
✗ Branch 33 → 34 not taken.
✓ Branch 33 → 37 taken 268 times.
✗ Branch 34 → 35 not taken.
✗ Branch 34 → 51 not taken.
✗ Branch 35 → 36 not taken.
✗ Branch 35 → 37 not taken.
✗ Branch 38 → 39 not taken.
✓ Branch 38 → 40 taken 268 times.
268 if (!in && !in.eof())
750 {
751 ✗ return std::nullopt;
752 }
753
1/2
✓ Branch 41 → 42 taken 268 times.
✗ Branch 41 → 51 not taken.
268 buf.resize(static_cast<std::size_t>(in.gcount()));
754 268 return buf;
755 307 }
756 ✗ catch (...)
757 {
758 ✗ return std::nullopt;
759 ✗ }
760 }
761
762 /// IniLoadOutcome reports the read-hash-parse pipeline result for load() and reload().
763 struct IniLoadOutcome
764 {
765 /// Reports whether the byte read from disk succeeded.
766 bool read_succeeded{false};
767 /// Reports whether CSimpleIniA::LoadData returned SI_OK.
768 bool parse_succeeded{false};
769 /// Stores the raw SimpleIni return code when read_succeeded is true.
770 SI_Error parse_rc{SI_OK};
771 /// Stores the FNV-1a hash of the read bytes.
772 std::optional<std::uint64_t> hash;
773 };
774
775 /**
776 * @brief Reads the INI bytes once, computes their hash, and feeds those exact bytes to
777 * CSimpleIniA::LoadData.
778 * @details Closes the TOCTOU window from a LoadFile re-read after the byte snapshot. The hash and parse
779 * reflect the same file state.
780 */
781 307 [[nodiscard]] IniLoadOutcome load_ini_into(const std::filesystem::path &path, CSimpleIniA &ini) noexcept
782 {
783 307 IniLoadOutcome outcome{};
784 307 auto bytes = read_ini_bytes(path);
785
2/2
✓ Branch 4 → 5 taken 36 times.
✓ Branch 4 → 6 taken 271 times.
307 if (!bytes.has_value())
786 {
787 36 return outcome;
788 }
789 271 outcome.read_succeeded = true;
790 271 outcome.hash = fnv1a_64(*bytes);
791
792 // SimpleIni accepts empty buffers (SI_OK). Preserve the hash so an empty file can hash-skip.
793 try
794 {
795
2/2
✓ Branch 11 → 12 taken 3 times.
✓ Branch 11 → 13 taken 268 times.
271 const char *data_ptr = bytes->empty() ? "" : reinterpret_cast<const char *>(bytes->data());
796
1/2
✓ Branch 17 → 18 taken 271 times.
✗ Branch 17 → 26 not taken.
271 outcome.parse_rc = ini.LoadData(data_ptr, bytes->size());
797 271 outcome.parse_succeeded = (outcome.parse_rc >= 0);
798 }
799 ✗ catch (...)
800 {
801 ✗ outcome.parse_rc = SI_FAIL;
802 ✗ outcome.parse_succeeded = false;
803 ✗ }
804 #if defined(DMK_ENABLE_TEST_SEAMS)
805
2/2
✓ Branch 20 → 21 taken 1 time.
✓ Branch 20 → 22 taken 270 times.
271 if (DetourModKit::detail::g_config_parse_fail_once.exchange(false, std::memory_order_acq_rel))
806 {
807 // Simulate a transient SimpleIni allocation failure. The seam clears itself before the next retry.
808 1 outcome.parse_rc = SI_FAIL;
809 1 outcome.parse_succeeded = false;
810 }
811 #endif
812 271 return outcome;
813 307 }
814
815 /**
816 * @brief Replaces the item with the same section and key, or appends @p item.
817 * @details Takes get_config_mutex().
818 */
819 338 void replace_or_append(std::unique_ptr<ConfigItemBase> item)
820 {
821 // Declared before the lock, so a displaced setter drops after unlock (`[B-101]`).
822 338 std::unique_ptr<ConfigItemBase> displaced;
823
1/2
✓ Branch 3 → 4 taken 338 times.
✗ Branch 3 → 31 not taken.
338 std::lock_guard<std::mutex> lock(get_config_mutex());
824 338 auto &items = get_registered_config_items();
825
1/2
✓ Branch 5 → 6 taken 338 times.
✗ Branch 5 → 29 not taken.
338 const auto existing = std::ranges::find_if(
826 items,
827 165 [&item](const std::unique_ptr<ConfigItemBase> &candidate) noexcept
828
4/4
✓ Branch 5 → 6 taken 90 times.
✓ Branch 5 → 11 taken 75 times.
✓ Branch 9 → 10 taken 10 times.
✓ Branch 9 → 11 taken 80 times.
165 { return candidate->section == item->section && candidate->ini_key == item->ini_key; }
829 );
830
2/2
✓ Branch 13 → 14 taken 10 times.
✓ Branch 13 → 22 taken 328 times.
676 if (existing != items.end())
831 {
832 20 displaced = std::exchange(*existing, std::move(item));
833 }
834 else
835 {
836
1/2
✓ Branch 24 → 25 taken 328 times.
✗ Branch 24 → 29 not taken.
328 items.push_back(std::move(item));
837 }
838 // Advance the binding generation after the fallible append, so reload()'s unchanged-content fast path
839 // re-hydrates this item.
840 338 ++get_binding_generation();
841 338 }
842
843 } // anonymous namespace
844
845 namespace detail
846 {
847 // Contract in internal/config_pass.hpp.
848 375 std::filesystem::path get_ini_file_path(const std::string &ini_filename, DeferredDiagnostics &diags)
849 {
850
1/2
✓ Branch 3 → 4 taken 375 times.
✗ Branch 3 → 63 not taken.
375 const std::wstring wide_filename = DetourModKit::detail::widen_utf8(ini_filename);
851
5/6
✓ Branch 5 → 6 taken 375 times.
✗ Branch 5 → 9 not taken.
✓ Branch 7 → 8 taken 2 times.
✓ Branch 7 → 9 taken 373 times.
✓ Branch 10 → 11 taken 2 times.
✓ Branch 10 → 13 taken 373 times.
375 if (!ini_filename.empty() && wide_filename.empty())
852 {
853
1/2
✓ Branch 11 → 12 taken 2 times.
✗ Branch 11 → 42 not taken.
2 defer_diagnostic(
854 diags,
855 LogLevel::Error,
856 "Config: INI filename is not well-formed UTF-8 or holds a NUL."
857 );
858 2 return {};
859 }
860
861
1/2
✓ Branch 13 → 14 taken 373 times.
✗ Branch 13 → 61 not taken.
373 const std::wstring module_dir = get_runtime_directory();
862
4/8
✓ Branch 15 → 16 taken 373 times.
✗ Branch 15 → 18 not taken.
✓ Branch 16 → 17 taken 373 times.
✗ Branch 16 → 59 not taken.
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 19 taken 373 times.
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 24 taken 373 times.
373 if (module_dir.empty() || module_dir == L".")
863 {
864
0/2
✗ Branch 21 → 22 not taken.
✗ Branch 21 → 43 not taken.
✗ defer_diagnostic(
865 diags,
866 LogLevel::Warning,
867 "Config: Could not reliably determine module directory or it's current working directory. "
868 "Using relative path for INI: {}",
869 ini_filename
870 );
871
0/2
✗ Branch 22 → 23 not taken.
✗ Branch 22 → 59 not taken.
✗ return std::filesystem::path(wide_filename);
872 }
873
874
4/8
✓ Branch 24 → 25 taken 373 times.
✗ Branch 24 → 51 not taken.
✓ Branch 25 → 26 taken 373 times.
✗ Branch 25 → 48 not taken.
✓ Branch 26 → 27 taken 373 times.
✗ Branch 26 → 46 not taken.
✓ Branch 27 → 28 taken 373 times.
✗ Branch 27 → 44 not taken.
373 std::filesystem::path ini_path = (std::filesystem::path(module_dir) / wide_filename).lexically_normal();
875
1/2
✓ Branch 34 → 35 taken 373 times.
✗ Branch 34 → 53 not taken.
373 defer_diagnostic(
876 diags,
877 LogLevel::Debug,
878 "Config: Determined INI file path: {}",
879
1/2
✓ Branch 33 → 34 taken 373 times.
✗ Branch 33 → 56 not taken.
746 DetourModKit::detail::utf8_from_wide(ini_path.native())
880 );
881 373 return ini_path;
882 375 }
883 } // namespace detail
884
885 namespace
886 {
887 // All bind_* functions use the deferred callback pattern. State mutates under get_config_mutex(). The
888 // setter runs after release, so a setter can re-enter the data-plane config API with no deadlock.
889 // The load()/reload() pass lock is a separate, stricter contract documented on those functions.
890 template <typename T>
891 338 void bind_setter(
892 std::string_view section,
893 std::string_view ini_key,
894 std::string_view log_key_name,
895 std::function<void(SetterArg<T>)> setter,
896 const T &default_value
897 )
898 {
899 // Build the node and the item before replace_or_append locks, so the consumer move runs unlocked.
900 338 SharedSetter<T> shared_setter;
901
8/10
void DetourModKit::config::(anonymous namespace)::bind_setter<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::function<void (std::conditional<same_as<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::basic_string_view<char, std::char_traits<char> >, std::conditional<same_as<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >::type>::type)>, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&):
✓ Branch 3 → 4 taken 14 times.
✓ Branch 3 → 10 taken 1 time.
void DetourModKit::config::(anonymous namespace)::bind_setter<std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::function<void (std::conditional<same_as<std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::basic_string_view<char, std::char_traits<char> >, std::conditional<same_as<std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >::type>::type)>, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&):
✓ Branch 3 → 4 taken 122 times.
✓ Branch 3 → 10 taken 1 time.
void DetourModKit::config::(anonymous namespace)::bind_setter<bool>(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::function<void (std::conditional<same_as<bool, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::basic_string_view<char, std::char_traits<char> >, std::conditional<same_as<bool, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&, bool>::type>::type)>, bool const&):
✓ Branch 3 → 4 taken 63 times.
✗ Branch 3 → 10 not taken.
void DetourModKit::config::(anonymous namespace)::bind_setter<float>(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::function<void (std::conditional<same_as<float, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::basic_string_view<char, std::char_traits<char> >, std::conditional<same_as<float, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&, float>::type>::type)>, float const&):
✓ Branch 3 → 4 taken 18 times.
✗ Branch 3 → 10 not taken.
void DetourModKit::config::(anonymous namespace)::bind_setter<int>(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::function<void (std::conditional<same_as<int, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::basic_string_view<char, std::char_traits<char> >, std::conditional<same_as<int, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&, int>::type>::type)>, int const&):
✓ Branch 3 → 4 taken 115 times.
✓ Branch 3 → 10 taken 4 times.
338 if (setter)
902 {
903
5/10
void DetourModKit::config::(anonymous namespace)::bind_setter<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::function<void (std::conditional<same_as<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::basic_string_view<char, std::char_traits<char> >, std::conditional<same_as<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >::type>::type)>, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&):
✓ Branch 6 → 7 taken 14 times.
✗ Branch 6 → 37 not taken.
void DetourModKit::config::(anonymous namespace)::bind_setter<std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::function<void (std::conditional<same_as<std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::basic_string_view<char, std::char_traits<char> >, std::conditional<same_as<std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >::type>::type)>, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&):
✓ Branch 6 → 7 taken 122 times.
✗ Branch 6 → 36 not taken.
void DetourModKit::config::(anonymous namespace)::bind_setter<bool>(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::function<void (std::conditional<same_as<bool, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::basic_string_view<char, std::char_traits<char> >, std::conditional<same_as<bool, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&, bool>::type>::type)>, bool const&):
✓ Branch 6 → 7 taken 63 times.
✗ Branch 6 → 36 not taken.
void DetourModKit::config::(anonymous namespace)::bind_setter<float>(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::function<void (std::conditional<same_as<float, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::basic_string_view<char, std::char_traits<char> >, std::conditional<same_as<float, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&, float>::type>::type)>, float const&):
✓ Branch 6 → 7 taken 18 times.
✗ Branch 6 → 36 not taken.
void DetourModKit::config::(anonymous namespace)::bind_setter<int>(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::function<void (std::conditional<same_as<int, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::basic_string_view<char, std::char_traits<char> >, std::conditional<same_as<int, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&, int>::type>::type)>, int const&):
✓ Branch 6 → 7 taken 115 times.
✗ Branch 6 → 36 not taken.
332 shared_setter = std::make_shared<const std::function<void(SetterArg<T>)>>(std::move(setter));
904 }
905
5/10
void DetourModKit::config::(anonymous namespace)::bind_setter<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::function<void (std::conditional<same_as<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::basic_string_view<char, std::char_traits<char> >, std::conditional<same_as<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >::type>::type)>, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&):
✓ Branch 21 → 22 taken 15 times.
✗ Branch 21 → 38 not taken.
void DetourModKit::config::(anonymous namespace)::bind_setter<std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::function<void (std::conditional<same_as<std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::basic_string_view<char, std::char_traits<char> >, std::conditional<same_as<std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >::type>::type)>, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&):
✓ Branch 21 → 22 taken 123 times.
✗ Branch 21 → 37 not taken.
void DetourModKit::config::(anonymous namespace)::bind_setter<bool>(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::function<void (std::conditional<same_as<bool, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::basic_string_view<char, std::char_traits<char> >, std::conditional<same_as<bool, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&, bool>::type>::type)>, bool const&):
✓ Branch 21 → 22 taken 63 times.
✗ Branch 21 → 37 not taken.
void DetourModKit::config::(anonymous namespace)::bind_setter<float>(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::function<void (std::conditional<same_as<float, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::basic_string_view<char, std::char_traits<char> >, std::conditional<same_as<float, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&, float>::type>::type)>, float const&):
✓ Branch 21 → 22 taken 18 times.
✗ Branch 21 → 37 not taken.
void DetourModKit::config::(anonymous namespace)::bind_setter<int>(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::function<void (std::conditional<same_as<int, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::basic_string_view<char, std::char_traits<char> >, std::conditional<same_as<int, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&, int>::type>::type)>, int const&):
✓ Branch 21 → 22 taken 119 times.
✗ Branch 21 → 37 not taken.
338 replace_or_append(
906
5/10
void DetourModKit::config::(anonymous namespace)::bind_setter<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::function<void (std::conditional<same_as<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::basic_string_view<char, std::char_traits<char> >, std::conditional<same_as<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >::type>::type)>, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&):
✓ Branch 19 → 20 taken 15 times.
✗ Branch 19 → 42 not taken.
void DetourModKit::config::(anonymous namespace)::bind_setter<std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::function<void (std::conditional<same_as<std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::basic_string_view<char, std::char_traits<char> >, std::conditional<same_as<std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >::type>::type)>, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&):
✓ Branch 19 → 20 taken 123 times.
✗ Branch 19 → 41 not taken.
void DetourModKit::config::(anonymous namespace)::bind_setter<bool>(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::function<void (std::conditional<same_as<bool, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::basic_string_view<char, std::char_traits<char> >, std::conditional<same_as<bool, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&, bool>::type>::type)>, bool const&):
✓ Branch 19 → 20 taken 63 times.
✗ Branch 19 → 41 not taken.
void DetourModKit::config::(anonymous namespace)::bind_setter<float>(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::function<void (std::conditional<same_as<float, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::basic_string_view<char, std::char_traits<char> >, std::conditional<same_as<float, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&, float>::type>::type)>, float const&):
✓ Branch 19 → 20 taken 18 times.
✗ Branch 19 → 41 not taken.
void DetourModKit::config::(anonymous namespace)::bind_setter<int>(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::function<void (std::conditional<same_as<int, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::basic_string_view<char, std::char_traits<char> >, std::conditional<same_as<int, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&, int>::type>::type)>, int const&):
✓ Branch 19 → 20 taken 119 times.
✗ Branch 19 → 41 not taken.
676 std::make_unique<CallbackConfigItem<T>>(
907
5/10
void DetourModKit::config::(anonymous namespace)::bind_setter<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::function<void (std::conditional<same_as<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::basic_string_view<char, std::char_traits<char> >, std::conditional<same_as<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >::type>::type)>, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&):
✓ Branch 18 → 19 taken 15 times.
✗ Branch 18 → 44 not taken.
void DetourModKit::config::(anonymous namespace)::bind_setter<std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::function<void (std::conditional<same_as<std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::basic_string_view<char, std::char_traits<char> >, std::conditional<same_as<std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >::type>::type)>, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&):
✓ Branch 18 → 19 taken 123 times.
✗ Branch 18 → 43 not taken.
void DetourModKit::config::(anonymous namespace)::bind_setter<bool>(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::function<void (std::conditional<same_as<bool, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::basic_string_view<char, std::char_traits<char> >, std::conditional<same_as<bool, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&, bool>::type>::type)>, bool const&):
✓ Branch 18 → 19 taken 63 times.
✗ Branch 18 → 43 not taken.
void DetourModKit::config::(anonymous namespace)::bind_setter<float>(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::function<void (std::conditional<same_as<float, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::basic_string_view<char, std::char_traits<char> >, std::conditional<same_as<float, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&, float>::type>::type)>, float const&):
✓ Branch 18 → 19 taken 18 times.
✗ Branch 18 → 43 not taken.
void DetourModKit::config::(anonymous namespace)::bind_setter<int>(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::function<void (std::conditional<same_as<int, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::basic_string_view<char, std::char_traits<char> >, std::conditional<same_as<int, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&, int>::type>::type)>, int const&):
✓ Branch 18 → 19 taken 119 times.
✗ Branch 18 → 43 not taken.
676 std::string(section),
908
5/10
void DetourModKit::config::(anonymous namespace)::bind_setter<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::function<void (std::conditional<same_as<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::basic_string_view<char, std::char_traits<char> >, std::conditional<same_as<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >::type>::type)>, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&):
✓ Branch 15 → 16 taken 15 times.
✗ Branch 15 → 50 not taken.
void DetourModKit::config::(anonymous namespace)::bind_setter<std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::function<void (std::conditional<same_as<std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::basic_string_view<char, std::char_traits<char> >, std::conditional<same_as<std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >::type>::type)>, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&):
✓ Branch 15 → 16 taken 123 times.
✗ Branch 15 → 49 not taken.
void DetourModKit::config::(anonymous namespace)::bind_setter<bool>(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::function<void (std::conditional<same_as<bool, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::basic_string_view<char, std::char_traits<char> >, std::conditional<same_as<bool, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&, bool>::type>::type)>, bool const&):
✓ Branch 15 → 16 taken 63 times.
✗ Branch 15 → 49 not taken.
void DetourModKit::config::(anonymous namespace)::bind_setter<float>(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::function<void (std::conditional<same_as<float, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::basic_string_view<char, std::char_traits<char> >, std::conditional<same_as<float, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&, float>::type>::type)>, float const&):
✓ Branch 15 → 16 taken 18 times.
✗ Branch 15 → 49 not taken.
void DetourModKit::config::(anonymous namespace)::bind_setter<int>(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::function<void (std::conditional<same_as<int, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::basic_string_view<char, std::char_traits<char> >, std::conditional<same_as<int, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&, int>::type>::type)>, int const&):
✓ Branch 15 → 16 taken 119 times.
✗ Branch 15 → 49 not taken.
676 std::string(ini_key),
909
5/10
void DetourModKit::config::(anonymous namespace)::bind_setter<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::function<void (std::conditional<same_as<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::basic_string_view<char, std::char_traits<char> >, std::conditional<same_as<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >::type>::type)>, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&):
✓ Branch 12 → 13 taken 15 times.
✗ Branch 12 → 56 not taken.
void DetourModKit::config::(anonymous namespace)::bind_setter<std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::function<void (std::conditional<same_as<std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::basic_string_view<char, std::char_traits<char> >, std::conditional<same_as<std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >::type>::type)>, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&):
✓ Branch 12 → 13 taken 123 times.
✗ Branch 12 → 55 not taken.
void DetourModKit::config::(anonymous namespace)::bind_setter<bool>(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::function<void (std::conditional<same_as<bool, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::basic_string_view<char, std::char_traits<char> >, std::conditional<same_as<bool, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&, bool>::type>::type)>, bool const&):
✓ Branch 12 → 13 taken 63 times.
✗ Branch 12 → 55 not taken.
void DetourModKit::config::(anonymous namespace)::bind_setter<float>(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::function<void (std::conditional<same_as<float, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::basic_string_view<char, std::char_traits<char> >, std::conditional<same_as<float, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&, float>::type>::type)>, float const&):
✓ Branch 12 → 13 taken 18 times.
✗ Branch 12 → 55 not taken.
void DetourModKit::config::(anonymous namespace)::bind_setter<int>(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::function<void (std::conditional<same_as<int, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::basic_string_view<char, std::char_traits<char> >, std::conditional<same_as<int, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&, int>::type>::type)>, int const&):
✓ Branch 12 → 13 taken 119 times.
✗ Branch 12 → 55 not taken.
676 std::string(log_key_name),
910 shared_setter,
911 default_value
912 )
913 );
914
8/10
void DetourModKit::config::(anonymous namespace)::bind_setter<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::function<void (std::conditional<same_as<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::basic_string_view<char, std::char_traits<char> >, std::conditional<same_as<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >::type>::type)>, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&):
✓ Branch 31 → 32 taken 14 times.
✓ Branch 31 → 35 taken 1 time.
void DetourModKit::config::(anonymous namespace)::bind_setter<std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::function<void (std::conditional<same_as<std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::basic_string_view<char, std::char_traits<char> >, std::conditional<same_as<std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >::type>::type)>, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&):
✓ Branch 31 → 32 taken 122 times.
✓ Branch 31 → 34 taken 1 time.
void DetourModKit::config::(anonymous namespace)::bind_setter<bool>(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::function<void (std::conditional<same_as<bool, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::basic_string_view<char, std::char_traits<char> >, std::conditional<same_as<bool, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&, bool>::type>::type)>, bool const&):
✓ Branch 31 → 32 taken 63 times.
✗ Branch 31 → 34 not taken.
void DetourModKit::config::(anonymous namespace)::bind_setter<float>(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::function<void (std::conditional<same_as<float, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::basic_string_view<char, std::char_traits<char> >, std::conditional<same_as<float, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&, float>::type>::type)>, float const&):
✓ Branch 31 → 32 taken 18 times.
✗ Branch 31 → 34 not taken.
void DetourModKit::config::(anonymous namespace)::bind_setter<int>(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::function<void (std::conditional<same_as<int, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::basic_string_view<char, std::char_traits<char> >, std::conditional<same_as<int, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&, int>::type>::type)>, int const&):
✓ Branch 31 → 32 taken 115 times.
✓ Branch 31 → 34 taken 4 times.
338 if (shared_setter)
915 {
916 if constexpr (std::same_as<T, std::string>)
917 {
918
1/2
✓ Branch 34 → 35 taken 14 times.
✗ Branch 34 → 62 not taken.
14 (*shared_setter)(std::string_view{default_value});
919 }
920 else
921 {
922
4/8
void DetourModKit::config::(anonymous namespace)::bind_setter<std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::function<void (std::conditional<same_as<std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::basic_string_view<char, std::char_traits<char> >, std::conditional<same_as<std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >::type>::type)>, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&):
✓ Branch 33 → 34 taken 122 times.
✗ Branch 33 → 61 not taken.
void DetourModKit::config::(anonymous namespace)::bind_setter<bool>(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::function<void (std::conditional<same_as<bool, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::basic_string_view<char, std::char_traits<char> >, std::conditional<same_as<bool, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&, bool>::type>::type)>, bool const&):
✓ Branch 33 → 34 taken 63 times.
✗ Branch 33 → 61 not taken.
void DetourModKit::config::(anonymous namespace)::bind_setter<float>(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::function<void (std::conditional<same_as<float, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::basic_string_view<char, std::char_traits<char> >, std::conditional<same_as<float, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&, float>::type>::type)>, float const&):
✓ Branch 33 → 34 taken 18 times.
✗ Branch 33 → 61 not taken.
void DetourModKit::config::(anonymous namespace)::bind_setter<int>(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >, std::function<void (std::conditional<same_as<int, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::basic_string_view<char, std::char_traits<char> >, std::conditional<same_as<int, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > >, std::vector<DetourModKit::input::KeyCombo, std::allocator<DetourModKit::input::KeyCombo> > const&, int>::type>::type)>, int const&):
✓ Branch 33 → 34 taken 115 times.
✗ Branch 33 → 61 not taken.
318 (*shared_setter)(default_value);
923 }
924 }
925 338 }
926
927 } // anonymous namespace
928
929 119 void bind_int(
930 std::string_view section,
931 std::string_view key,
932 std::string_view display_name,
933 std::function<void(int)> setter,
934 int default_value
935 )
936 {
937
1/2
✓ Branch 5 → 6 taken 119 times.
✗ Branch 5 → 8 not taken.
119 bind_setter<int>(section, key, display_name, std::move(setter), default_value);
938 119 }
939
940 18 void bind_float(
941 std::string_view section,
942 std::string_view key,
943 std::string_view display_name,
944 std::function<void(float)> setter,
945 float default_value
946 )
947 {
948
1/2
✓ Branch 5 → 6 taken 18 times.
✗ Branch 5 → 8 not taken.
18 bind_setter<float>(section, key, display_name, std::move(setter), default_value);
949 18 }
950
951 63 void bind_bool(
952 std::string_view section,
953 std::string_view key,
954 std::string_view display_name,
955 std::function<void(bool)> setter,
956 bool default_value
957 )
958 {
959
1/2
✓ Branch 5 → 6 taken 63 times.
✗ Branch 5 → 8 not taken.
63 bind_setter<bool>(section, key, display_name, std::move(setter), default_value);
960 63 }
961
962 15 void bind_string(
963 std::string_view section,
964 std::string_view key,
965 std::string_view display_name,
966 std::function<void(std::string_view)> setter,
967 std::string_view default_value
968 )
969 {
970
2/4
✓ Branch 4 → 5 taken 15 times.
✗ Branch 4 → 18 not taken.
✓ Branch 8 → 9 taken 15 times.
✗ Branch 8 → 13 not taken.
30 bind_setter<std::string>(section, key, display_name, std::move(setter), std::string(default_value));
971 15 }
972
973 ✗ void bind_parsed(
974 std::string_view section,
975 std::string_view key,
976 std::string_view display_name,
977 std::atomic<std::uint32_t> &out,
978 std::function<std::uint32_t(std::string_view)> parse,
979 std::string_view default_value
980 )
981 {
982 // parse is captured by value so the setter stays valid across every load()/reload(). out is captured by
983 // reference and must outlive the registration.
984
0/2
✗ Branch 6 → 7 not taken.
✗ Branch 6 → 10 not taken.
✗ bind_string(
985 section,
986 key,
987 display_name,
988
0/2
✗ Branch 5 → 6 not taken.
✗ Branch 5 → 12 not taken.
✗ [&out, parse = std::move(parse)](std::string_view value)
989 ✗ { out.store(parse(value), std::memory_order_relaxed); },
990 default_value
991 );
992 ✗ }
993
994 1 void bind_log_level(std::string_view section, std::string_view key, std::string_view default_value)
995 {
996
1/2
✓ Branch 4 → 5 taken 1 time.
✗ Branch 4 → 7 not taken.
1 bind_string(
997 section,
998 key,
999 1 "Log level",
1000 4 [](std::string_view value) { log().set_log_level(string_to_log_level(value)); },
1001 default_value
1002 );
1003 1 }
1004
1005 123 void bind_combos(
1006 std::string_view section,
1007 std::string_view key,
1008 std::string_view display_name,
1009 std::function<void(const input::KeyComboList &)> setter,
1010 std::string_view default_value
1011 )
1012 {
1013 123 detail::DeferredDiagnostics diags = detail::open_deferred_diagnostics();
1014 const input::KeyComboList default_combos =
1015
2/4
✓ Branch 5 → 6 taken 123 times.
✗ Branch 5 → 20 not taken.
✓ Branch 6 → 7 taken 123 times.
✗ Branch 6 → 18 not taken.
123 detail::parse_key_combo_list(std::string(default_value), diags, display_name);
1016 123 detail::emit_deferred_diagnostics(diags);
1017
1018
1/2
✓ Branch 13 → 14 taken 123 times.
✗ Branch 13 → 24 not taken.
123 bind_setter<input::KeyComboList>(section, key, display_name, std::move(setter), default_combos);
1019 123 }
1020
1021 26 void consume_flag(
1022 std::string_view section,
1023 std::string_view ini_key,
1024 std::string_view display_name,
1025 std::string_view binding_name,
1026 bool default_value
1027 )
1028 {
1029 // An unknown name makes set_consume a no-op, so registration before the binding exists is safe.
1030
1/2
✓ Branch 4 → 5 taken 26 times.
✗ Branch 4 → 13 not taken.
26 std::string binding_name_str(binding_name);
1031
1/2
✓ Branch 8 → 9 taken 26 times.
✗ Branch 8 → 16 not taken.
26 bind_bool(
1032 section,
1033 ini_key,
1034 display_name,
1035
2/4
✓ Branch 6 → 7 taken 26 times.
✗ Branch 6 → 20 not taken.
✓ Branch 7 → 8 taken 26 times.
✗ Branch 7 → 18 not taken.
100 [binding_name_str](bool consume) { input::Input::instance().set_consume(binding_name_str, consume); },
1036 default_value
1037 );
1038 26 }
1039
1040 namespace detail
1041 {
1042 58 void bind_combo_rebind_item(
1043 std::string_view section,
1044 std::string_view ini_key,
1045 std::string_view log_name,
1046 std::string_view binding_name,
1047 std::string_view default_combo
1048 )
1049 {
1050
1/2
✓ Branch 6 → 7 taken 58 times.
✗ Branch 6 → 11 not taken.
58 bind_combos(
1051 section,
1052 ini_key,
1053 log_name,
1054
2/4
✓ Branch 4 → 5 taken 58 times.
✗ Branch 4 → 15 not taken.
✓ Branch 5 → 6 taken 58 times.
✗ Branch 5 → 13 not taken.
116 [name = std::string(binding_name)](const input::KeyComboList &combos)
1055
1/2
✓ Branch 3 → 4 taken 100 times.
✗ Branch 3 → 8 not taken.
100 { (void)input::Input::instance().rebind(name, combos); },
1056 default_combo
1057 );
1058 58 }
1059 } // namespace detail
1060
1061 namespace
1062 {
1063 /**
1064 * @brief Implements the shared path behind press_combo() and hold_combo().
1065 * @details The helper registers the input binding and wires a combo config item. An INI change rebinds it
1066 * on every load() or reload(). It optionally registers the "<ini_key>.Consume" facet. A
1067 * registration error logs and yields an inert default guard.
1068 */
1069 55 input::BindingGuard register_combo_fusion(
1070 input::Trigger trigger,
1071 std::string_view section,
1072 std::string_view ini_key,
1073 std::string_view log_name,
1074 std::string_view binding_name,
1075 std::function<void()> on_press,
1076 std::function<void(bool)> on_state_change,
1077 std::string_view default_combo,
1078 std::optional<bool> consume
1079 )
1080 {
1081
1/2
✓ Branch 4 → 5 taken 55 times.
✗ Branch 4 → 64 not taken.
55 const std::string binding_name_str(binding_name);
1082
1083 // Register the binding with an empty combo set. The combo config item below parses default_combo
1084 // exactly once and rebinds. A prior parse here duplicates the parse and any typo WARNING.
1085 55 input::ComboBinding binding{
1086 .name = binding_name_str,
1087 .trigger = trigger,
1088 .combos = {},
1089 110 .consume = consume.value_or(false),
1090 55 .on_press = std::move(on_press),
1091 55 .on_state_change = std::move(on_state_change),
1092
4/8
✓ Branch 6 → 7 taken 55 times.
✗ Branch 6 → 89 not taken.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 55 times.
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 18 taken 55 times.
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 20 taken 55 times.
165 };
1093
1094 55 input::BindingGuard guard;
1095
2/2
✓ Branch 27 → 28 taken 54 times.
✓ Branch 27 → 32 taken 1 time.
55 if (auto reg = input::register_combo(std::move(binding)); reg.has_value())
1096 {
1097 108 guard = std::move(*reg);
1098 }
1099 else
1100 {
1101 1 (void)log().try_log(
1102 LogLevel::Error,
1103 "Config: failed to register input binding '{}' for '{}'; "
1104 "binding will be inert.",
1105 binding_name_str,
1106 log_name
1107 );
1108 55 }
1109
1110 // The setter rebinds the named binding on every load()/reload() without another registration.
1111
1/2
✓ Branch 37 → 38 taken 55 times.
✗ Branch 37 → 85 not taken.
55 detail::bind_combo_rebind_item(section, ini_key, log_name, binding_name_str, default_combo);
1112
1113 // Register the consume facet only after the binding exists. Otherwise its immediate default reaches
1114 // set_consume()'s unknown-name no-op and is lost.
1115
2/2
✓ Branch 39 → 40 taken 25 times.
✓ Branch 39 → 60 taken 30 times.
55 if (consume.has_value())
1116 {
1117
1/2
✓ Branch 52 → 53 taken 25 times.
✗ Branch 52 → 67 not taken.
50 consume_flag(
1118 section,
1119
2/4
✓ Branch 49 → 50 taken 25 times.
✗ Branch 49 → 71 not taken.
✓ Branch 50 → 51 taken 25 times.
✗ Branch 50 → 69 not taken.
50 std::string(ini_key) + ".Consume",
1120
2/4
✓ Branch 44 → 45 taken 25 times.
✗ Branch 44 → 80 not taken.
✓ Branch 45 → 46 taken 25 times.
✗ Branch 45 → 78 not taken.
75 std::string(log_name) + " Consume",
1121 binding_name_str,
1122 25 *consume
1123 );
1124 }
1125
1126 55 return guard;
1127 55 }
1128 } // anonymous namespace
1129
1130 50 input::BindingGuard press_combo(
1131 std::string_view section,
1132 std::string_view ini_key,
1133 std::string_view log_name,
1134 std::string_view binding_name,
1135 std::function<void()> on_press,
1136 std::string_view default_combo,
1137 std::optional<bool> consume
1138 )
1139 {
1140 50 return register_combo_fusion(
1141 input::Trigger::Press,
1142 section,
1143 ini_key,
1144 log_name,
1145 binding_name,
1146 50 std::move(on_press),
1147 100 nullptr,
1148 default_combo,
1149 consume
1150
1/2
✓ Branch 6 → 7 taken 50 times.
✗ Branch 6 → 12 not taken.
100 );
1151 }
1152
1153 5 input::BindingGuard hold_combo(
1154 std::string_view section,
1155 std::string_view ini_key,
1156 std::string_view log_name,
1157 std::string_view binding_name,
1158 std::function<void(bool)> on_state_change,
1159 std::string_view default_combo,
1160 std::optional<bool> consume
1161 )
1162 {
1163 5 return register_combo_fusion(
1164 input::Trigger::Hold,
1165 section,
1166 ini_key,
1167 log_name,
1168 binding_name,
1169 10 nullptr,
1170 5 std::move(on_state_change),
1171 default_combo,
1172 consume
1173
1/2
✓ Branch 6 → 7 taken 5 times.
✗ Branch 6 → 12 not taken.
10 );
1174 }
1175
1176 namespace
1177 {
1178 /**
1179 * @brief Loads every registered item from @p ini and moves each deferred setter into @p deferred_callbacks.
1180 * @details The caller holds get_config_mutex().
1181 */
1182 284 void load_items_and_collect_setters(
1183 CSimpleIniA &ini,
1184 detail::DeferredDiagnostics &diags,
1185 std::vector<std::function<void()>> &deferred_callbacks
1186 )
1187 {
1188
2/2
✓ Branch 26 → 5 taken 374 times.
✓ Branch 26 → 27 taken 284 times.
942 for (const auto &item : get_registered_config_items())
1189 {
1190
1/2
✓ Branch 8 → 9 taken 374 times.
✗ Branch 8 → 30 not taken.
374 item->load(ini, diags);
1191
1/2
✓ Branch 10 → 11 taken 374 times.
✗ Branch 10 → 30 not taken.
374 auto cb = item->take_deferred_apply();
1192
2/2
✓ Branch 12 → 13 taken 372 times.
✓ Branch 12 → 16 taken 2 times.
374 if (cb)
1193 {
1194
1/2
✓ Branch 15 → 16 taken 372 times.
✗ Branch 15 → 28 not taken.
372 deferred_callbacks.push_back(std::move(cb));
1195 }
1196 374 }
1197 284 }
1198
1199 /**
1200 * @brief Defers the load() outcome diagnostic, clears the cached hash, and stages the hash of a parsed INI
1201 * in @p hash_to_commit.
1202 * @details The caller holds get_config_mutex().
1203 */
1204 227 void stage_load_outcome(
1205 const IniLoadOutcome &outcome,
1206 const std::string &ini_path_text,
1207 detail::DeferredDiagnostics &diags,
1208 std::optional<std::uint64_t> &hash_to_commit
1209 )
1210 {
1211
2/2
✓ Branch 2 → 3 taken 32 times.
✓ Branch 2 → 6 taken 195 times.
227 if (!outcome.read_succeeded)
1212 {
1213
1/2
✓ Branch 3 → 4 taken 32 times.
✗ Branch 3 → 15 not taken.
32 detail::defer_diagnostic(
1214 diags,
1215 LogLevel::Error,
1216 "Config: Failed to open '{}'. Using defaults.",
1217 ini_path_text
1218 );
1219 // Wipe the cached hash so the next reload() does not short-circuit against a stale value.
1220 32 get_last_loaded_ini_hash().reset();
1221 }
1222
1/2
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 10 taken 195 times.
195 else if (!outcome.parse_succeeded)
1223 {
1224 ✗ detail::defer_diagnostic(
1225 diags,
1226 LogLevel::Error,
1227 "Config: Failed to parse '{}' (error {}). Using defaults.",
1228 ini_path_text,
1229
0/2
✗ Branch 7 → 8 not taken.
✗ Branch 7 → 16 not taken.
✗ static_cast<int>(outcome.parse_rc)
1230 );
1231 // Clear the hash: it was computed for bytes that did not parse and must not enable a hash-skip.
1232 ✗ get_last_loaded_ini_hash().reset();
1233 }
1234 else
1235 {
1236
1/2
✓ Branch 10 → 11 taken 195 times.
✗ Branch 10 → 18 not taken.
195 detail::defer_diagnostic(diags, LogLevel::Debug, "Config: Opened {}", ini_path_text);
1237 // Do not publish this hash until every deferred setter succeeds.
1238 // Reset the prior snapshot so a setter failure cannot suppress an identical-byte retry.
1239 195 get_last_loaded_ini_hash().reset();
1240 195 hash_to_commit = outcome.hash;
1241 }
1242 227 }
1243
1244 /**
1245 * @brief Invokes each deferred load() setter and returns true when no setter throws.
1246 * @details The caller holds the pass lock and does not hold get_config_mutex(). A setter that throws does
1247 * not stop the later setters.
1248 */
1249 227 [[nodiscard]] bool run_load_setters(const std::vector<std::function<void()>> &deferred_callbacks)
1250 {
1251 227 Logger &setter_logger = log();
1252 227 bool all_setters_applied = true;
1253
2/2
✓ Branch 17 → 5 taken 294 times.
✓ Branch 17 → 18 taken 227 times.
748 for (const auto &cb : deferred_callbacks)
1254 {
1255 try
1256 {
1257
2/2
✓ Branch 7 → 8 taken 293 times.
✓ Branch 7 → 20 taken 1 time.
294 cb();
1258 }
1259
1/2
✓ Branch 20 → 21 taken 1 time.
✗ Branch 20 → 25 not taken.
1 catch (const std::exception &e)
1260 {
1261 1 all_setters_applied = false;
1262
1/2
✓ Branch 23 → 24 taken 1 time.
✗ Branch 23 → 28 not taken.
1 setter_logger.error("Config: load setter threw: {}", e.what());
1263 1 }
1264 ✗ catch (...)
1265 {
1266 ✗ all_setters_applied = false;
1267
0/2
✗ Branch 26 → 27 not taken.
✗ Branch 26 → 32 not taken.
✗ setter_logger.error("Config: load setter threw unknown exception.");
1268
0/2
✗ Branch 27 → 8 not taken.
✗ Branch 27 → 35 not taken.
✗ }
1269 }
1270 227 return all_setters_applied;
1271 }
1272 } // anonymous namespace
1273
1274 228 void load(std::string_view ini_filename)
1275 {
1276 // A Logic DLL that (re)loads and calls load() must be able to hot-reload again after an unload latched
1277 // background reloads off.
1278 228 detail::rearm_reloads();
1279
1280 // Declare the vector before the pass lock so an unwind drops setter references after that lock releases.
1281 228 std::vector<std::function<void()>> deferred_callbacks;
1282
1283 // Serialize the whole pass (see internal/config_reload_lifecycle.hpp). Fail fast on same-thread re-entry.
1284
1/2
✓ Branch 3 → 4 taken 228 times.
✗ Branch 3 → 108 not taken.
228 detail::ReloadApplyLock apply_lock;
1285
2/2
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 9 taken 227 times.
228 if (!apply_lock.engaged())
1286 {
1287 1 (void)log().try_log(
1288 LogLevel::Error,
1289 "Config: load() re-entered from a bound setter on the same thread; ignoring to "
1290 "avoid a self-deadlock. Do not call load()/reload() from a config setter."
1291 );
1292 1 return;
1293 }
1294
1295 227 std::optional<std::uint64_t> hash_to_commit;
1296 227 std::uint64_t generation_to_commit = 0;
1297
1298 // The filename is a caller argument, so the whole path resolution runs outside get_config_mutex().
1299 227 detail::DeferredDiagnostics diags = detail::open_deferred_diagnostics();
1300
2/4
✓ Branch 12 → 13 taken 227 times.
✗ Branch 12 → 81 not taken.
✓ Branch 13 → 14 taken 227 times.
✗ Branch 13 → 79 not taken.
227 const std::filesystem::path ini_path = detail::get_ini_file_path(std::string(ini_filename), diags);
1301
1/2
✓ Branch 18 → 19 taken 227 times.
✗ Branch 18 → 102 not taken.
227 const std::string ini_path_text = DetourModKit::detail::utf8_from_wide(ini_path.native());
1302
1303 {
1304
1/2
✓ Branch 20 → 21 taken 227 times.
✗ Branch 20 → 95 not taken.
227 std::lock_guard<std::mutex> lock(get_config_mutex());
1305
1306 227 CSimpleIniA ini;
1307 227 ini.SetUnicode(false); // Assume ASCII/MBCS INI
1308 227 ini.SetMultiKey(false); // A duplicate key in a section keeps its last value
1309
1310 227 IniLoadOutcome outcome = load_ini_into(ini_path, ini);
1311
1312
1/2
✓ Branch 25 → 26 taken 227 times.
✗ Branch 25 → 91 not taken.
227 stage_load_outcome(outcome, ini_path_text, diags, hash_to_commit);
1313
1314
1/2
✓ Branch 26 → 27 taken 227 times.
✗ Branch 26 → 91 not taken.
227 load_items_and_collect_setters(ini, diags, deferred_callbacks);
1315 // Snapshot the binding generation for the item set just read. Commit it with the hash.
1316 227 generation_to_commit = get_binding_generation();
1317
1318 // Remember the INI path on every outcome, not just success. A ship-with-defaults first run has no INI
1319 // on disk yet. enable_auto_reload() needs the recorded path to detect the file after it appears. A
1320 // failed load resets the hash, and reload() retains the last values, so this path remains safe.
1321
1/2
✓ Branch 30 → 31 taken 227 times.
✗ Branch 30 → 85 not taken.
227 get_last_loaded_ini_path() = std::string(ini_filename);
1322
1323 ✗ detail::defer_diagnostic(
1324 diags,
1325 LogLevel::Info,
1326 "Config: Loaded {} items from {}",
1327
1/2
✓ Branch 37 → 38 taken 227 times.
✗ Branch 37 → 89 not taken.
227 get_registered_config_items().size(),
1328 ini_path_text
1329 );
1330 227 }
1331
1332 227 detail::emit_deferred_diagnostics(diags);
1333
1334 // Setters can re-enter the data-plane API. The held pass lock forbids
1335 // load()/reload()/disable_auto_reload()/clear() because those calls self-deadlock (see config.hpp).
1336
1/2
✓ Branch 41 → 42 taken 227 times.
✗ Branch 41 → 100 not taken.
227 const bool all_setters_applied = run_load_setters(deferred_callbacks);
1337
6/6
✓ Branch 42 → 43 taken 226 times.
✓ Branch 42 → 46 taken 1 time.
✓ Branch 44 → 45 taken 194 times.
✓ Branch 44 → 46 taken 32 times.
✓ Branch 47 → 48 taken 194 times.
✓ Branch 47 → 55 taken 33 times.
227 if (all_setters_applied && hash_to_commit.has_value())
1338 {
1339
1/2
✓ Branch 49 → 50 taken 194 times.
✗ Branch 49 → 96 not taken.
194 std::lock_guard<std::mutex> lock(get_config_mutex());
1340 194 get_last_loaded_ini_hash() = hash_to_commit;
1341 194 get_applied_binding_generation() = generation_to_commit;
1342 194 }
1343
1344 // Re-point the auto-reload watcher if load() switched the config file out from under it. Otherwise edits
1345 // to the active file never trigger reload. The stale watcher joins only after pass-lock release, so a
1346 // queued background reload can finish and let the old watcher exit.
1347 {
1348
1/2
✓ Branch 57 → 58 taken 227 times.
✗ Branch 57 → 99 not taken.
227 detail::WatchRepoint repoint = detail::detach_watcher_if_repointed(ini_path.native());
1349
1350 // Drop the pass lock before the stale-watcher join. Perform the join OUTSIDE both mutexes. The stale
1351 // worker's final callback can enter disable/enable. A held watcher mutex then causes deadlock.
1352 227 apply_lock.unlock();
1353 227 repoint.stale.reset();
1354
2/2
✓ Branch 60 → 61 taken 8 times.
✓ Branch 60 → 64 taken 219 times.
227 if (repoint.repoint)
1355 {
1356 // Deterministically drive disable_auto_reload() into the lost-disable window.
1357 #if defined(DMK_ENABLE_TEST_SEAMS)
1358
2/2
✓ Branch 61 → 62 taken 2 times.
✓ Branch 61 → 63 taken 6 times.
8 if (const auto hook = DetourModKit::detail::g_config_repoint_window_test_hook)
1359 {
1360
1/2
✓ Branch 62 → 63 taken 2 times.
✗ Branch 62 → 97 not taken.
2 hook();
1361 }
1362 #endif
1363
1/2
✓ Branch 63 → 64 taken 8 times.
✗ Branch 63 → 97 not taken.
8 detail::restart_watcher_after_repoint(repoint.debounce, repoint.generation_at_move);
1364 }
1365 227 }
1366
4/4
✓ Branch 70 → 71 taken 227 times.
✓ Branch 70 → 72 taken 1 time.
✓ Branch 74 → 75 taken 227 times.
✓ Branch 74 → 77 taken 1 time.
229 }
1367
1368 namespace
1369 {
1370 /**
1371 * @brief Stages the hash of parsed bytes in @p hash_to_commit unless the bytes and the binding generation
1372 * match the last successful apply.
1373 * @details The caller holds get_config_mutex(). Writes the current binding generation to
1374 * @p generation_to_commit once the read succeeds. Clears the cached hash on a read failure. The
1375 * match check runs before the parse check, and an unmatched parse failure also clears the cached
1376 * hash. Clears the cached hash and the applied generation before it stages the hash.
1377 * @return True when the function stages the hash and the setter pass proceeds. False on a read failure, a
1378 * parse failure, or a match with the last successful apply.
1379 */
1380 80 [[nodiscard]] bool stage_reload_outcome(
1381 const IniLoadOutcome &outcome,
1382 const std::string &ini_path_text,
1383 detail::DeferredDiagnostics &diags,
1384 std::optional<std::uint64_t> &hash_to_commit,
1385 std::uint64_t &generation_to_commit
1386 )
1387 {
1388
2/2
✓ Branch 2 → 3 taken 4 times.
✓ Branch 2 → 7 taken 76 times.
80 if (!outcome.read_succeeded)
1389 {
1390 // A read can fail when another process locks the file mid-save. Clear the cached hash so an
1391 // identical-byte retry cannot hash-skip. Return false so the caller skips item->load and the
1392 // setter pass. item->load against the unpopulated CSimpleIniA replaces live state with defaults.
1393 4 get_last_loaded_ini_hash() = std::nullopt;
1394
1/2
✓ Branch 5 → 6 taken 4 times.
✗ Branch 5 → 38 not taken.
4 detail::defer_diagnostic(
1395 diags,
1396 LogLevel::Warning,
1397 "Config: reload() could not open '{}'; retaining last values (setters not "
1398 "re-run).",
1399 ini_path_text
1400 );
1401 4 return false;
1402 }
1403
1404 // load_ini_into sets hash whenever read_succeeded, and this is the read_succeeded branch.
1405 // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
1406 76 const std::uint64_t current_hash = *outcome.hash;
1407 76 generation_to_commit = get_binding_generation();
1408 76 const auto &cached_hash = get_last_loaded_ini_hash();
1409 76 const auto &applied_generation = get_applied_binding_generation();
1410
7/8
✓ Branch 12 → 13 taken 70 times.
✓ Branch 12 → 20 taken 6 times.
✓ Branch 14 → 15 taken 19 times.
✓ Branch 14 → 20 taken 51 times.
✓ Branch 16 → 17 taken 19 times.
✗ Branch 16 → 20 not taken.
✓ Branch 21 → 22 taken 18 times.
✓ Branch 21 → 24 taken 58 times.
95 if (cached_hash.has_value() && current_hash == *cached_hash && applied_generation.has_value() &&
1411
2/2
✓ Branch 18 → 19 taken 18 times.
✓ Branch 18 → 20 taken 1 time.
19 *applied_generation == generation_to_commit)
1412 {
1413
1/2
✓ Branch 22 → 23 taken 18 times.
✗ Branch 22 → 39 not taken.
18 detail::defer_diagnostic(
1414 diags,
1415 LogLevel::Debug,
1416 "Config: reload content unchanged (hash {:016x}, binding gen {}); skipping "
1417 "setters.",
1418 current_hash,
1419 generation_to_commit
1420 );
1421 18 return false;
1422 }
1423
1424
2/2
✓ Branch 24 → 25 taken 1 time.
✓ Branch 24 → 29 taken 57 times.
58 if (!outcome.parse_succeeded)
1425 {
1426 // LoadData accepts any byte content, so a negative code is a transient SimpleIni allocation
1427 // failure, not a property of the bytes. Handle it as a read failure. Both branches retain the
1428 // last values and clear the cached hash, so the same bytes stay retryable.
1429 1 get_last_loaded_ini_hash() = std::nullopt;
1430 ✗ detail::defer_diagnostic(
1431 diags,
1432 LogLevel::Warning,
1433 "Config: reload() parse error on '{}' (error {}); retaining last values "
1434 "(setters not re-run).",
1435 ini_path_text,
1436
1/2
✓ Branch 27 → 28 taken 1 time.
✗ Branch 27 → 40 not taken.
1 static_cast<int>(outcome.parse_rc)
1437 );
1438 1 return false;
1439 }
1440
1441 // Drop the previous snapshot now. The caller commits this one only after every setter succeeds, as
1442 // load() does. A setter failure or unload-latch interruption leaves partial state. A retained prior
1443 // snapshot then lets old bytes hash-skip and pin that state.
1444 57 get_last_loaded_ini_hash().reset();
1445 57 get_applied_binding_generation().reset();
1446 57 hash_to_commit = current_hash;
1447
1/2
✓ Branch 34 → 35 taken 57 times.
✗ Branch 34 → 42 not taken.
57 detail::defer_diagnostic(diags, LogLevel::Debug, "Config: Reloading from {}", ini_path_text);
1448 57 return true;
1449 }
1450
1451 /**
1452 * @brief Invokes each deferred reload() setter until an unload latch or a stale @p background_guard
1453 * stops the pass.
1454 * @details The caller holds the pass lock and does not hold get_config_mutex(). Sets @p out_setters_ran
1455 * per the reload_impl() contract. A setter that throws counts as invoked and does not stop the
1456 * later setters.
1457 * @return True when every setter ran and none threw.
1458 */
1459 57 [[nodiscard]] bool run_reload_setters(
1460 const std::vector<std::function<void()>> &deferred_callbacks,
1461 const detail::BackgroundReloadGuard *background_guard,
1462 bool &out_setters_ran
1463 )
1464 {
1465 57 DetourModKit::Logger &logger = DetourModKit::log();
1466 57 bool all_setters_applied = true;
1467 57 bool any_setter_invoked = false;
1468
2/2
✓ Branch 26 → 5 taken 78 times.
✓ Branch 26 → 27 taken 55 times.
190 for (const auto &cb : deferred_callbacks)
1469 {
1470 // If a Logic DLL unload latched reloads off mid-pass, abort early. Every later setter resides in
1471 // the Logic DLL under unload. Partial application is acceptable in teardown.
1472
6/6
✓ Branch 8 → 9 taken 77 times.
✓ Branch 8 → 12 taken 1 time.
✓ Branch 9 → 10 taken 41 times.
✓ Branch 9 → 13 taken 36 times.
✓ Branch 14 → 15 taken 2 times.
✓ Branch 14 → 16 taken 76 times.
119 if (detail::background_reloads_disabled() ||
1473
2/2
✓ Branch 11 → 12 taken 1 time.
✓ Branch 11 → 13 taken 40 times.
41 (background_guard != nullptr && !background_guard->current()))
1474 {
1475 2 all_setters_applied = false;
1476 2 break;
1477 }
1478 76 any_setter_invoked = true;
1479 try
1480 {
1481
2/2
✓ Branch 16 → 17 taken 73 times.
✓ Branch 16 → 29 taken 3 times.
76 cb();
1482 }
1483
1/2
✓ Branch 29 → 30 taken 3 times.
✗ Branch 29 → 34 not taken.
3 catch (const std::exception &e)
1484 {
1485 3 all_setters_applied = false;
1486
1/2
✓ Branch 32 → 33 taken 3 times.
✗ Branch 32 → 37 not taken.
3 logger.error("Config: reload setter threw: {}", e.what());
1487 3 }
1488 ✗ catch (...)
1489 {
1490 ✗ all_setters_applied = false;
1491
0/2
✗ Branch 35 → 36 not taken.
✗ Branch 35 → 41 not taken.
✗ logger.error("Config: reload setter threw unknown exception.");
1492
0/2
✗ Branch 36 → 17 not taken.
✗ Branch 36 → 44 not taken.
✗ }
1493 }
1494 // out_setters_ran reflects actual invocation, which closes the race where a concurrent load() re-arm
1495 // clears the latch between an abort and the watcher's downstream re-check.
1496 57 out_setters_ran = any_setter_invoked;
1497 57 return all_setters_applied;
1498 }
1499 } // anonymous namespace
1500
1501 namespace detail
1502 {
1503 // Contract in internal/config_pass.hpp.
1504 84 bool reload_impl(bool &out_setters_ran, const BackgroundReloadGuard *background_guard)
1505 {
1506 84 out_setters_ran = false;
1507
1508 // Declared before the pass lock, so a reference that outlived its registry entry drops after unlock.
1509 84 std::vector<std::function<void()>> deferred_callbacks;
1510
1511 // Serialize the whole pass (see internal/config_reload_lifecycle.hpp). Fail fast on same-thread
1512 // re-entry.
1513
1/2
✓ Branch 2 → 3 taken 84 times.
✗ Branch 2 → 40 not taken.
84 ReloadApplyLock apply_lock;
1514
2/2
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 8 taken 83 times.
84 if (!apply_lock.engaged())
1515 {
1516 1 (void)DetourModKit::log().try_log(
1517 LogLevel::Error,
1518 "Config: reload() re-entered from a bound setter on the same thread; ignoring to avoid a "
1519 "self-deadlock. Do not call load()/reload() from a config setter."
1520 );
1521 1 return false;
1522 }
1523
1524 83 std::string ini_filename;
1525 83 std::optional<std::uint64_t> hash_to_commit;
1526 83 std::uint64_t generation_to_commit = 0;
1527
1528 // reload() takes its path from registry state, so resolution stays under the lock and reports through
1529 // deferred records. The pass emits them immediately after the unlock, before any setter runs.
1530 83 DeferredDiagnostics diags = open_deferred_diagnostics();
1531
1532 // The locked pass returns a value only when it stops early. Every exit path then reaches one emit.
1533 ✗ const std::optional<bool> early_result = [&]() -> std::optional<bool>
1534 {
1535
1/2
✓ Branch 3 → 4 taken 83 times.
✗ Branch 3 → 47 not taken.
83 std::lock_guard<std::mutex> lock(get_config_mutex());
1536
1537
1/2
✓ Branch 5 → 6 taken 83 times.
✗ Branch 5 → 45 not taken.
83 ini_filename = get_last_loaded_ini_path();
1538
2/2
✓ Branch 7 → 8 taken 3 times.
✓ Branch 7 → 11 taken 80 times.
83 if (ini_filename.empty())
1539 {
1540 // Without a prior load(), reload has no path to use.
1541 3 return false;
1542 }
1543
1544
1/2
✓ Branch 11 → 12 taken 80 times.
✗ Branch 11 → 45 not taken.
80 const std::filesystem::path ini_path = get_ini_file_path(ini_filename, diags);
1545
1/2
✓ Branch 14 → 15 taken 80 times.
✗ Branch 14 → 43 not taken.
80 const std::string ini_path_text = DetourModKit::detail::utf8_from_wide(ini_path.native());
1546
1547 80 CSimpleIniA ini;
1548 80 ini.SetUnicode(false);
1549 80 ini.SetMultiKey(false);
1550
1551 80 IniLoadOutcome outcome = load_ini_into(ini_path, ini);
1552
1553
3/4
✓ Branch 19 → 20 taken 80 times.
✗ Branch 19 → 39 not taken.
✓ Branch 20 → 21 taken 23 times.
✓ Branch 20 → 24 taken 57 times.
80 if (!stage_reload_outcome(outcome, ini_path_text, diags, hash_to_commit, generation_to_commit))
1554 {
1555 23 return true;
1556 }
1557
1558
1/2
✓ Branch 24 → 25 taken 57 times.
✗ Branch 24 → 39 not taken.
57 load_items_and_collect_setters(ini, diags, deferred_callbacks);
1559
1560 ✗ defer_diagnostic(
1561 diags,
1562 LogLevel::Info,
1563 "Config: Reloaded {} items from {}",
1564
1/2
✓ Branch 27 → 28 taken 57 times.
✗ Branch 27 → 37 not taken.
57 get_registered_config_items().size(),
1565 ini_path_text
1566 );
1567 57 return std::nullopt;
1568
1/2
✓ Branch 10 → 11 taken 83 times.
✗ Branch 10 → 32 not taken.
166 }();
1569
1570 83 emit_deferred_diagnostics(diags);
1571
2/2
✓ Branch 13 → 14 taken 26 times.
✓ Branch 13 → 16 taken 57 times.
83 if (early_result.has_value())
1572 {
1573 26 return *early_result;
1574 }
1575
1576 const bool all_setters_applied =
1577
1/2
✓ Branch 16 → 17 taken 57 times.
✗ Branch 16 → 34 not taken.
57 run_reload_setters(deferred_callbacks, background_guard, out_setters_ran);
1578
2/2
✓ Branch 17 → 18 taken 52 times.
✓ Branch 17 → 25 taken 5 times.
57 if (all_setters_applied)
1579 {
1580
1/2
✓ Branch 19 → 20 taken 52 times.
✗ Branch 19 → 33 not taken.
52 std::lock_guard<std::mutex> lock(get_config_mutex());
1581 52 get_last_loaded_ini_hash() = hash_to_commit;
1582 52 get_applied_binding_generation() = generation_to_commit;
1583 52 }
1584 57 return true;
1585 84 }
1586 } // namespace detail
1587
1588 49 bool reload()
1589 {
1590 49 bool ignored = false;
1591
1/2
✓ Branch 2 → 3 taken 49 times.
✗ Branch 2 → 6 not taken.
98 return detail::reload_impl(ignored);
1592 }
1593
1594 14 void log_all()
1595 {
1596 14 detail::DeferredDiagnostics diags = detail::open_deferred_diagnostics();
1597
1598 {
1599
1/2
✓ Branch 4 → 5 taken 14 times.
✗ Branch 4 → 78 not taken.
14 std::lock_guard<std::mutex> lock(get_config_mutex());
1600
1601 14 const auto &items = get_registered_config_items();
1602
2/2
✓ Branch 7 → 8 taken 3 times.
✓ Branch 7 → 10 taken 11 times.
14 if (items.empty())
1603 {
1604
1/2
✓ Branch 8 → 9 taken 3 times.
✗ Branch 8 → 62 not taken.
3 detail::defer_diagnostic(diags, LogLevel::Info, "Config: No configuration items registered.");
1605 }
1606 else
1607 {
1608 11 std::unordered_set<std::string_view> sections;
1609
2/2
✓ Branch 27 → 13 taken 21 times.
✓ Branch 27 → 28 taken 11 times.
43 for (const auto &item : items)
1610 {
1611
1/2
✓ Branch 17 → 18 taken 21 times.
✗ Branch 17 → 63 not taken.
21 sections.insert(item->section);
1612 }
1613 ✗ detail::defer_diagnostic(
1614 diags,
1615 LogLevel::Info,
1616 "Config: {} registered values across {} section(s)",
1617
1/2
✓ Branch 30 → 31 taken 11 times.
✗ Branch 30 → 66 not taken.
11 items.size(),
1618 11 sections.size()
1619 );
1620
1621 11 std::string current_section;
1622
2/2
✓ Branch 54 → 34 taken 21 times.
✓ Branch 54 → 55 taken 11 times.
43 for (const auto &item : items)
1623 {
1624
2/2
✓ Branch 38 → 39 taken 12 times.
✓ Branch 38 → 43 taken 9 times.
21 if (item->section != current_section)
1625 {
1626
1/2
✓ Branch 40 → 41 taken 12 times.
✗ Branch 40 → 70 not taken.
12 current_section = item->section;
1627
1/2
✓ Branch 41 → 42 taken 12 times.
✗ Branch 41 → 69 not taken.
12 detail::defer_diagnostic(diags, LogLevel::Debug, "Config: [{}]", current_section);
1628 }
1629
1/2
✓ Branch 44 → 45 taken 21 times.
✗ Branch 44 → 70 not taken.
21 item->log_current_value(diags);
1630 }
1631 11 }
1632 14 }
1633
1634 14 detail::emit_deferred_diagnostics(diags);
1635 14 }
1636
1637 3626 void clear() noexcept
1638 {
1639 3626 Logger &logger = log();
1640
1641
4/6
✓ Branch 4 → 5 taken 2 times.
✓ Branch 4 → 8 taken 3624 times.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 2 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 12 taken 3626 times.
3626 if (detail::reload_apply_lock_held_by_current_thread() && !detail::on_reload_servicer_thread())
1642 {
1643 ✗ (void)logger.try_log(
1644 LogLevel::Error,
1645 "Config: clear() called from a bound setter; ignoring to avoid joining a "
1646 "reload worker that may be waiting for the active pass."
1647 );
1648 ✗ return;
1649 }
1650
1651 3626 size_t count = 0;
1652 // Declared before the lock, so the retired setters are destroyed after unlock (`[B-101]`).
1653 3626 std::vector<std::unique_ptr<ConfigItemBase>> retired_items;
1654
1655 {
1656 3626 std::lock_guard<std::mutex> lock(get_config_mutex());
1657 3626 retired_items.swap(get_registered_config_items());
1658 3626 count = retired_items.size();
1659
1660 // Drop the remembered path, hash, and generation so the next load() starts clean. The watcher's
1661 // lifecycle stays with disable_auto_reload().
1662 3626 get_last_loaded_ini_path().clear();
1663 3626 get_last_loaded_ini_hash().reset();
1664 3626 get_applied_binding_generation().reset();
1665 3626 }
1666 3626 retired_items.clear();
1667
1668 // Move the hotkey guards and servicer out under the watcher mutex. Dispose after unlock. A guard release
1669 // can wait on a drain whose disposal joins the servicer, whose worker needs this mutex.
1670 3626 detail::WatchHotkeyControl hotkey_control = detail::detach_hotkey_control();
1671 3626 detail::dispose_reload_hotkey_guards(hotkey_control.guards);
1672 // This reset can be the final drop. It runs outside get_config_mutex so a worker inside reload() cannot
1673 // deadlock.
1674 3626 hotkey_control.servicer.reset();
1675
1676 // Use try_log rather than debug(). A sink exception breaks this noexcept contract.
1677
2/2
✓ Branch 28 → 29 taken 226 times.
✓ Branch 28 → 31 taken 3400 times.
3626 if (count > 0)
1678 {
1679 226 (void)logger.try_log(LogLevel::Debug, "Config: Cleared {} registered configuration items.", count);
1680 }
1681 else
1682 {
1683 3400 (void)logger.try_log(LogLevel::Debug, "Config: clear called, but no items were registered.");
1684 }
1685 3626 }
1686 } // namespace config
1687
1688 #if defined(DMK_ENABLE_TEST_SEAMS)
1689 namespace detail
1690 {
1691 // Reports whether the registry mutex is free right now. A record producer cannot pass this probe under the
1692 // same non-recursive mutex.
1693 108 bool config_registry_mutex_free_for_test() noexcept
1694 {
1695 108 std::unique_lock<std::mutex> probe(config::get_config_mutex(), std::try_to_lock);
1696 108 return probe.owns_lock();
1697 108 }
1698 } // namespace detail
1699 #endif
1700 } // namespace DetourModKit
1701