GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 93.2% 316 / 0 / 339
Functions: 100.0% 50 / 0 / 50
Branches: 55.5% 266 / 0 / 479

src/config.cpp
Line Branch Exec Source
1 /**
2 * @file config.cpp
3 * @brief Implementation of configuration loading and management.
4 *
5 * Provides a system for registering configuration variables, loading their
6 * values from an INI file, and logging them. This allows mods to define their
7 * configuration needs and have DetourModKit handle the INI parsing and value
8 * assignment.
9 */
10
11 #include "DetourModKit/config.hpp"
12 #include "DetourModKit/input_codes.hpp"
13 #include "DetourModKit/logger.hpp"
14 #include "DetourModKit/filesystem.hpp"
15 #include "DetourModKit/format.hpp"
16 #include "SimpleIni.h"
17
18 #include <windows.h>
19 #include <cerrno>
20 #include <cstdlib>
21 #include <filesystem>
22 #include <limits>
23 #include <memory>
24 #include <mutex>
25 #include <string>
26 #include <string_view>
27 #include <unordered_set>
28 #include <vector>
29
30 using namespace DetourModKit;
31 using namespace DetourModKit::Filesystem;
32 using namespace DetourModKit::String;
33
34 // Anonymous namespace for internal helpers and storage
35 namespace
36 {
37 /**
38 * @brief Parses a comma-separated string of input tokens into a vector of InputCodes.
39 * @details Each token is first matched against the named key table (case-insensitive).
40 * If no name matches, the token is parsed as a hexadecimal VK code (with or
41 * without 0x prefix), defaulting to InputSource::Keyboard. Handles inline
42 * semicolon comments, whitespace, and gracefully skips invalid tokens.
43 * @param input The raw string to parse.
44 * @return std::vector<InputCode> Parsed valid input codes.
45 */
46 90 std::vector<InputCode> parse_input_code_list(const std::string &input)
47 {
48 90 std::vector<InputCode> result;
49
50 // Strip trailing comment from the full line
51 90 const size_t comment_pos = input.find(';');
52 const std::string effective = trim(
53
3/8
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 90 times.
✗ Branch 4 → 6 not taken.
✗ Branch 4 → 82 not taken.
✓ Branch 5 → 6 taken 90 times.
✗ Branch 5 → 82 not taken.
✓ Branch 7 → 8 taken 90 times.
✗ Branch 7 → 80 not taken.
90 (comment_pos != std::string::npos) ? input.substr(0, comment_pos) : input);
54
1/2
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 90 times.
90 if (effective.empty())
55 {
56 return result;
57 }
58
59 // Walk comma-delimited tokens without istringstream overhead
60 90 size_t pos = 0;
61
2/2
✓ Branch 75 → 13 taken 90 times.
✓ Branch 75 → 76 taken 90 times.
180 while (pos < effective.size())
62 {
63 90 const size_t comma = effective.find(',', pos);
64
1/2
✓ Branch 14 → 15 taken 90 times.
✗ Branch 14 → 16 not taken.
90 const size_t end = (comma != std::string::npos) ? comma : effective.size();
65
2/4
✓ Branch 17 → 18 taken 90 times.
✗ Branch 17 → 85 not taken.
✓ Branch 19 → 20 taken 90 times.
✗ Branch 19 → 83 not taken.
90 const std::string token = trim(effective.substr(pos, end - pos));
66 90 pos = end + 1;
67
68
1/2
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 24 taken 90 times.
90 if (token.empty())
69 {
70 continue;
71 }
72
73 // Try named key lookup first (case-insensitive)
74
1/2
✓ Branch 25 → 26 taken 90 times.
✗ Branch 25 → 87 not taken.
90 auto named = parse_input_name(token);
75
2/2
✓ Branch 27 → 28 taken 54 times.
✓ Branch 27 → 31 taken 36 times.
90 if (named.has_value())
76 {
77
1/2
✓ Branch 29 → 30 taken 54 times.
✗ Branch 29 → 87 not taken.
54 result.push_back(*named);
78 54 continue;
79 }
80
81 // Fall back to hex parsing (defaults to Keyboard source)
82 36 size_t hex_start = 0;
83
8/10
✓ Branch 32 → 33 taken 36 times.
✗ Branch 32 → 40 not taken.
✓ Branch 34 → 35 taken 33 times.
✓ Branch 34 → 40 taken 3 times.
✓ Branch 36 → 37 taken 1 time.
✓ Branch 36 → 39 taken 32 times.
✓ Branch 38 → 39 taken 1 time.
✗ Branch 38 → 40 not taken.
✓ Branch 41 → 42 taken 33 times.
✓ Branch 41 → 43 taken 3 times.
36 if (token.size() >= 2 && token[0] == '0' && (token[1] == 'x' || token[1] == 'X'))
84 {
85 33 hex_start = 2;
86 }
87
2/2
✓ Branch 44 → 45 taken 2 times.
✓ Branch 44 → 46 taken 34 times.
36 if (hex_start >= token.size())
88 {
89 2 continue;
90 }
91
92 // Validate all remaining characters are hex digits
93 34 const std::string_view hex_part(token.data() + hex_start, token.size() - hex_start);
94
2/2
✓ Branch 50 → 51 taken 3 times.
✓ Branch 50 → 52 taken 31 times.
34 if (hex_part.find_first_not_of("0123456789abcdefABCDEF") != std::string_view::npos)
95 {
96 3 continue;
97 }
98
99 // Convert via strtoul — no exception overhead on invalid input
100
1/2
✓ Branch 52 → 53 taken 31 times.
✗ Branch 52 → 87 not taken.
31 errno = 0;
101 31 char *end_ptr = nullptr;
102 31 const unsigned long value = std::strtoul(token.c_str() + hex_start, &end_ptr, 16);
103
6/8
✓ Branch 56 → 57 taken 31 times.
✗ Branch 56 → 59 not taken.
✓ Branch 57 → 58 taken 31 times.
✗ Branch 57 → 87 not taken.
✓ Branch 58 → 59 taken 2 times.
✓ Branch 58 → 60 taken 29 times.
✓ Branch 61 → 62 taken 2 times.
✓ Branch 61 → 63 taken 29 times.
31 if (end_ptr == token.c_str() + hex_start || errno == ERANGE)
104 {
105 2 continue;
106 }
107
2/2
✓ Branch 64 → 65 taken 2 times.
✓ Branch 64 → 66 taken 27 times.
29 if (value > static_cast<unsigned long>(std::numeric_limits<int>::max()))
108 {
109 2 continue;
110 }
111
112
1/2
✓ Branch 66 → 67 taken 27 times.
✗ Branch 66 → 86 not taken.
27 result.push_back(InputCode{InputSource::Keyboard, static_cast<int>(value)});
113
2/2
✓ Branch 69 → 70 taken 27 times.
✓ Branch 69 → 72 taken 63 times.
90 }
114
115 90 return result;
116 90 }
117
118 /**
119 * @brief Parses a single key combo string into a KeyCombo struct.
120 * @details Format: "modifier1+modifier2+trigger_key" where each token is a
121 * named key or hex VK code. The last '+'-delimited token is the trigger
122 * key, all preceding tokens are modifier keys (AND logic). This function
123 * expects a single combo with no commas; use parse_key_combo_list to
124 * split comma-separated alternatives first.
125 * @param input The raw string to parse (no commas expected).
126 * @return Config::KeyCombo Parsed key combination.
127 */
128 68 Config::KeyCombo parse_key_combo(const std::string &input)
129 {
130 68 Config::KeyCombo result;
131
132
1/2
✓ Branch 3 → 4 taken 68 times.
✗ Branch 3 → 65 not taken.
68 const std::string effective = trim(input);
133
1/2
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 68 times.
68 if (effective.empty())
134 {
135 return result;
136 }
137
138 // Split by '+' to get segments
139 68 std::vector<std::string> segments;
140 68 size_t pos = 0;
141
2/2
✓ Branch 22 → 8 taken 90 times.
✓ Branch 22 → 23 taken 68 times.
158 while (pos < effective.size())
142 {
143 90 const size_t plus = effective.find('+', pos);
144
2/2
✓ Branch 9 → 10 taken 68 times.
✓ Branch 9 → 11 taken 22 times.
90 const size_t end = (plus != std::string::npos) ? plus : effective.size();
145
2/4
✓ Branch 12 → 13 taken 90 times.
✗ Branch 12 → 51 not taken.
✓ Branch 14 → 15 taken 90 times.
✗ Branch 14 → 49 not taken.
90 const std::string segment = trim(effective.substr(pos, end - pos));
146 90 pos = end + 1;
147
1/2
✓ Branch 17 → 18 taken 90 times.
✗ Branch 17 → 19 not taken.
90 if (!segment.empty())
148 {
149
1/2
✓ Branch 18 → 19 taken 90 times.
✗ Branch 18 → 52 not taken.
90 segments.push_back(segment);
150 }
151 90 }
152
153
1/2
✗ Branch 24 → 25 not taken.
✓ Branch 24 → 26 taken 68 times.
68 if (segments.empty())
154 {
155 return result;
156 }
157
158 // Last segment is the trigger key
159
1/2
✓ Branch 27 → 28 taken 68 times.
✗ Branch 27 → 55 not taken.
68 result.keys = parse_input_code_list(segments.back());
160
161 // All preceding segments are individual modifier keys
162
2/2
✓ Branch 43 → 31 taken 22 times.
✓ Branch 43 → 44 taken 68 times.
90 for (size_t i = 0; i + 1 < segments.size(); ++i)
163 {
164
1/2
✓ Branch 32 → 33 taken 22 times.
✗ Branch 32 → 60 not taken.
22 auto mod_codes = parse_input_code_list(segments[i]);
165
1/2
✓ Branch 39 → 40 taken 22 times.
✗ Branch 39 → 56 not taken.
44 result.modifiers.insert(result.modifiers.end(), mod_codes.begin(), mod_codes.end());
166 22 }
167
168 68 return result;
169 68 }
170
171 /**
172 * @brief Parses a comma-separated string of key combos into a KeyComboList.
173 * @details Commas at the top level separate independent combos (OR logic between
174 * combos). Each combo is parsed by parse_key_combo. Handles inline
175 * semicolon comments, whitespace, and gracefully skips empty/invalid combos.
176 * @param input The raw string to parse.
177 * @return Config::KeyComboList Parsed list of key combinations.
178 */
179 59 Config::KeyComboList parse_key_combo_list(const std::string &input)
180 {
181 59 Config::KeyComboList result;
182
183 // Strip trailing comment from the full line
184 59 const size_t comment_pos = input.find(';');
185 const std::string effective = trim(
186
5/8
✓ Branch 3 → 4 taken 3 times.
✓ Branch 3 → 5 taken 56 times.
✓ Branch 4 → 6 taken 3 times.
✗ Branch 4 → 46 not taken.
✓ Branch 5 → 6 taken 56 times.
✗ Branch 5 → 46 not taken.
✓ Branch 7 → 8 taken 59 times.
✗ Branch 7 → 44 not taken.
59 (comment_pos != std::string::npos) ? input.substr(0, comment_pos) : input);
187
2/2
✓ Branch 10 → 11 taken 19 times.
✓ Branch 10 → 12 taken 40 times.
59 if (effective.empty())
188 {
189 19 return result;
190 }
191
192 // Split by comma into independent combo strings
193 40 size_t pos = 0;
194
2/2
✓ Branch 39 → 13 taken 70 times.
✓ Branch 39 → 40 taken 40 times.
110 while (pos < effective.size())
195 {
196 70 const size_t comma = effective.find(',', pos);
197
2/2
✓ Branch 14 → 15 taken 40 times.
✓ Branch 14 → 16 taken 30 times.
70 const size_t end = (comma != std::string::npos) ? comma : effective.size();
198
2/4
✓ Branch 17 → 18 taken 70 times.
✗ Branch 17 → 49 not taken.
✓ Branch 19 → 20 taken 70 times.
✗ Branch 19 → 47 not taken.
70 const std::string combo_str = trim(effective.substr(pos, end - pos));
199 70 pos = end + 1;
200
201
2/2
✓ Branch 22 → 23 taken 2 times.
✓ Branch 22 → 24 taken 68 times.
70 if (combo_str.empty())
202 {
203 2 continue;
204 }
205
206
1/2
✓ Branch 24 → 25 taken 68 times.
✗ Branch 24 → 52 not taken.
68 auto combo = parse_key_combo(combo_str);
207
2/2
✓ Branch 26 → 27 taken 60 times.
✓ Branch 26 → 30 taken 8 times.
68 if (!combo.keys.empty())
208 {
209
1/2
✓ Branch 29 → 30 taken 60 times.
✗ Branch 29 → 50 not taken.
60 result.push_back(std::move(combo));
210 }
211
2/2
✓ Branch 33 → 34 taken 68 times.
✓ Branch 33 → 36 taken 2 times.
70 }
212
213 40 return result;
214 59 }
215
216 /**
217 * @brief Formats a single KeyCombo as a human-readable string.
218 * @details Uses named keys where available, falls back to hex for unknown codes.
219 * @param combo The key combination to format.
220 * @return std::string Formatted string (e.g., "Ctrl+Shift+F3").
221 */
222 2 std::string format_key_combo(const Config::KeyCombo &combo)
223 {
224 2 std::string result;
225
2/2
✓ Branch 21 → 5 taken 1 time.
✓ Branch 21 → 22 taken 2 times.
5 for (const auto &mod : combo.modifiers)
226 {
227
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) + "+";
228 }
229
2/2
✓ Branch 31 → 23 taken 2 times.
✓ Branch 31 → 32 taken 2 times.
4 for (size_t i = 0; i < combo.keys.size(); ++i)
230 {
231
1/2
✗ Branch 23 → 24 not taken.
✓ Branch 23 → 25 taken 2 times.
2 if (i > 0)
232 {
233 result += ",";
234 }
235
2/4
✓ Branch 26 → 27 taken 2 times.
✗ Branch 26 → 43 not taken.
✓ Branch 27 → 28 taken 2 times.
✗ Branch 27 → 41 not taken.
2 result += DetourModKit::format_input_code(combo.keys[i]);
236 }
237 2 return result;
238 }
239
240 /**
241 * @brief Formats a KeyComboList as a human-readable string.
242 * @details Joins individual combos with commas.
243 * @param combos The list of key combinations to format.
244 * @return std::string Formatted string (e.g., "F3,Gamepad_LT+Gamepad_B").
245 */
246 3 std::string format_key_combo_list(const Config::KeyComboList &combos)
247 {
248 3 std::string result;
249
2/2
✓ Branch 12 → 4 taken 2 times.
✓ Branch 12 → 13 taken 3 times.
5 for (size_t i = 0; i < combos.size(); ++i)
250 {
251
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 2 times.
2 if (i > 0)
252 {
253 result += ",";
254 }
255
2/4
✓ Branch 7 → 8 taken 2 times.
✗ Branch 7 → 17 not taken.
✓ Branch 8 → 9 taken 2 times.
✗ Branch 8 → 15 not taken.
2 result += format_key_combo(combos[i]);
256 }
257 3 return result;
258 }
259
260 /**
261 * @brief Base class for typed configuration items.
262 * @details This allows storing different types of configuration items
263 * polymorphically in a collection.
264 */
265 struct ConfigItemBase
266 {
267 std::string section;
268 std::string ini_key;
269 std::string log_key_name;
270
271 94 ConfigItemBase(std::string sec, std::string key, std::string log_name)
272 376 : section(std::move(sec)), ini_key(std::move(key)), log_key_name(std::move(log_name)) {}
273 94 virtual ~ConfigItemBase() = default;
274 ConfigItemBase(const ConfigItemBase &) = delete;
275 ConfigItemBase &operator=(const ConfigItemBase &) = delete;
276 ConfigItemBase(ConfigItemBase &&) = delete;
277 ConfigItemBase &operator=(ConfigItemBase &&) = delete;
278
279 /**
280 * @brief Loads the configuration value from the INI file.
281 * @param ini Reference to the CSimpleIniA object.
282 * @param logger Reference to the Logger object.
283 */
284 virtual void load(CSimpleIniA &ini, Logger &logger) = 0;
285
286 /**
287 * @brief Returns a deferred callback to invoke the setter outside the config mutex.
288 * @return A self-contained callable, or empty if no setter is configured.
289 */
290 [[nodiscard]] virtual std::function<void()> take_deferred_apply() const = 0;
291
292 /**
293 * @brief Logs the current value of the configuration item.
294 * @param logger Reference to the Logger object.
295 */
296 virtual void log_current_value(Logger &logger) const = 0;
297 };
298
299 /**
300 * @brief Configuration item using std::function callback for value setting.
301 * @tparam T The data type of the configuration item (e.g., int, bool, std::string).
302 * @note Setter callbacks are invoked outside the config mutex to prevent deadlocks.
303 * See register_* and load() for the deferred invocation pattern.
304 */
305 template <typename T>
306 struct CallbackConfigItem : public ConfigItemBase
307 {
308 std::function<void(T)> setter; // Callback function to set the value
309 T default_value;
310 T current_value;
311
312 94 CallbackConfigItem(std::string sec, std::string key, std::string log_name,
313 std::function<void(T)> set_fn, T def_val)
314 282 : ConfigItemBase(std::move(sec), std::move(key), std::move(log_name)),
315 94 setter(std::move(set_fn)),
316
2/4
(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::function<void (std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >)>, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >):
✓ Branch 18 → 19 taken 9 times.
✗ Branch 18 → 23 not taken.
(anonymous namespace)::CallbackConfigItem<std::vector<DetourModKit::Config::KeyCombo, std::allocator<DetourModKit::Config::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::function<void (std::vector<DetourModKit::Config::KeyCombo, std::allocator<DetourModKit::Config::KeyCombo> >)>, std::vector<DetourModKit::Config::KeyCombo, std::allocator<DetourModKit::Config::KeyCombo> >):
✓ Branch 18 → 19 taken 41 times.
✗ Branch 18 → 23 not taken.
94 default_value(def_val),
317 476 current_value(std::move(def_val))
318 {
319 94 }
320
321 void load(CSimpleIniA &ini, [[maybe_unused]] Logger &logger) override;
322 void log_current_value(Logger &logger) const override;
323
324 /// Returns a self-contained callback that invokes setter with current_value.
325 86 [[nodiscard]] std::function<void()> take_deferred_apply() const override
326 {
327
5/10
(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 7 times.
(anonymous namespace)::CallbackConfigItem<std::vector<DetourModKit::Config::KeyCombo, std::allocator<DetourModKit::Config::KeyCombo> > >::take_deferred_apply() const:
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 34 times.
(anonymous namespace)::CallbackConfigItem<bool>::take_deferred_apply() const:
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 9 times.
(anonymous namespace)::CallbackConfigItem<float>::take_deferred_apply() const:
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 8 times.
(anonymous namespace)::CallbackConfigItem<int>::take_deferred_apply() const:
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 28 times.
86 if (!setter)
328 return {};
329
12/34
(anonymous namespace)::CallbackConfigItem<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >::take_deferred_apply() const:
✓ Branch 5 → 6 taken 7 times.
✗ Branch 5 → 19 not taken.
✓ Branch 6 → 7 taken 7 times.
✗ Branch 6 → 16 not taken.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 7 times.
✗ Branch 16 → 17 not taken.
✗ Branch 16 → 18 not taken.
(anonymous namespace)::CallbackConfigItem<std::vector<DetourModKit::Config::KeyCombo, std::allocator<DetourModKit::Config::KeyCombo> > >::take_deferred_apply() const:
✓ Branch 5 → 6 taken 34 times.
✗ Branch 5 → 19 not taken.
✓ Branch 6 → 7 taken 34 times.
✗ Branch 6 → 16 not taken.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 34 times.
✗ Branch 16 → 17 not taken.
✗ Branch 16 → 18 not taken.
(anonymous namespace)::CallbackConfigItem<bool>::take_deferred_apply() const:
✓ Branch 5 → 6 taken 9 times.
✗ Branch 5 → 18 not taken.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 9 times.
✗ Branch 15 → 16 not taken.
✗ Branch 15 → 17 not taken.
(anonymous namespace)::CallbackConfigItem<float>::take_deferred_apply() const:
✓ Branch 5 → 6 taken 8 times.
✗ Branch 5 → 18 not taken.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 8 times.
✗ Branch 15 → 16 not taken.
✗ Branch 15 → 17 not taken.
(anonymous namespace)::CallbackConfigItem<int>::take_deferred_apply() const:
✓ Branch 5 → 6 taken 28 times.
✗ Branch 5 → 18 not taken.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 28 times.
✗ Branch 15 → 16 not taken.
✗ Branch 15 → 17 not taken.
344 return [fn = setter, val = current_value]() mutable
330
6/12
None:
✓ Branch 5 → 6 taken 41 times.
✗ Branch 5 → 8 not taken.
(anonymous namespace)::CallbackConfigItem<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >::take_deferred_apply() const:
✓ Branch 7 → 8 taken 7 times.
✗ Branch 7 → 14 not taken.
(anonymous namespace)::CallbackConfigItem<std::vector<DetourModKit::Config::KeyCombo, std::allocator<DetourModKit::Config::KeyCombo> > >::take_deferred_apply() const:
✓ Branch 7 → 8 taken 34 times.
✗ Branch 7 → 14 not taken.
(anonymous namespace)::CallbackConfigItem<bool>::take_deferred_apply() const:
✓ Branch 6 → 7 taken 9 times.
✗ Branch 6 → 13 not taken.
(anonymous namespace)::CallbackConfigItem<float>::take_deferred_apply() const:
✓ Branch 6 → 7 taken 8 times.
✗ Branch 6 → 13 not taken.
(anonymous namespace)::CallbackConfigItem<int>::take_deferred_apply() const:
✓ Branch 6 → 7 taken 28 times.
✗ Branch 6 → 13 not taken.
258 { fn(std::move(val)); };
331 }
332 };
333
334 // --- Specializations for CallbackConfigItem<T>::load and ::log_current_value ---
335
336 // For int
337 template <>
338 28 void CallbackConfigItem<int>::load(CSimpleIniA &ini, [[maybe_unused]] Logger &logger)
339 {
340 28 current_value = static_cast<int>(ini.GetLongValue(section.c_str(), ini_key.c_str(), default_value));
341 28 }
342
343 template <>
344 4 void CallbackConfigItem<int>::log_current_value(Logger &logger) const
345 {
346
1/2
✓ Branch 2 → 3 taken 4 times.
✗ Branch 2 → 4 not taken.
4 logger.debug("Config: {} = {}", ini_key, current_value);
347 4 }
348
349 // For float
350 template <>
351 8 void CallbackConfigItem<float>::load(CSimpleIniA &ini, [[maybe_unused]] Logger &logger)
352 {
353 8 current_value = static_cast<float>(ini.GetDoubleValue(section.c_str(), ini_key.c_str(), static_cast<double>(default_value)));
354 8 }
355
356 template <>
357 1 void CallbackConfigItem<float>::log_current_value(Logger &logger) const
358 {
359
1/2
✓ Branch 2 → 3 taken 1 time.
✗ Branch 2 → 4 not taken.
1 logger.debug("Config: {} = {}", ini_key, current_value);
360 1 }
361
362 // For bool
363 template <>
364 9 void CallbackConfigItem<bool>::load(CSimpleIniA &ini, [[maybe_unused]] Logger &logger)
365 {
366 9 current_value = ini.GetBoolValue(section.c_str(), ini_key.c_str(), default_value);
367 9 }
368
369 template <>
370 1 void CallbackConfigItem<bool>::log_current_value(Logger &logger) const
371 {
372
2/4
✓ Branch 2 → 3 taken 1 time.
✗ Branch 2 → 4 not taken.
✓ Branch 5 → 6 taken 1 time.
✗ Branch 5 → 7 not taken.
1 logger.debug("Config: {} = {}", ini_key, current_value ? "true" : "false");
373 1 }
374
375 // For std::string
376 template <>
377 7 void CallbackConfigItem<std::string>::load(CSimpleIniA &ini, [[maybe_unused]] Logger &logger)
378 {
379 7 current_value = ini.GetValue(section.c_str(), ini_key.c_str(), default_value.c_str());
380 7 }
381
382 template <>
383 2 void CallbackConfigItem<std::string>::log_current_value(Logger &logger) const
384 {
385
1/2
✓ Branch 2 → 3 taken 2 times.
✗ Branch 2 → 4 not taken.
2 logger.debug("Config: {} = \"{}\"", ini_key, current_value);
386 2 }
387
388 // For Config::KeyComboList (list of key combinations)
389 template <>
390 34 void CallbackConfigItem<Config::KeyComboList>::load(CSimpleIniA &ini, [[maybe_unused]] Logger &logger)
391 {
392 34 const char *ini_value_str = ini.GetValue(section.c_str(), ini_key.c_str(), nullptr);
393
2/2
✓ Branch 5 → 6 taken 18 times.
✓ Branch 5 → 15 taken 16 times.
34 if (ini_value_str != nullptr)
394 {
395
2/4
✓ Branch 8 → 9 taken 18 times.
✗ Branch 8 → 19 not taken.
✓ Branch 9 → 10 taken 18 times.
✗ Branch 9 → 17 not taken.
36 current_value = parse_key_combo_list(ini_value_str);
396 }
397 else
398 {
399 16 current_value = default_value;
400 }
401 34 }
402
403 template <>
404 3 void CallbackConfigItem<Config::KeyComboList>::log_current_value(Logger &logger) const
405 {
406
1/2
✓ Branch 2 → 3 taken 3 times.
✗ Branch 2 → 15 not taken.
3 const std::string formatted = format_key_combo_list(current_value);
407
2/2
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 7 taken 2 times.
3 if (formatted.empty())
408 {
409
1/2
✓ Branch 5 → 6 taken 1 time.
✗ Branch 5 → 11 not taken.
1 logger.debug("Config: {} = (none)", ini_key);
410 }
411 else
412 {
413
1/2
✓ Branch 7 → 8 taken 2 times.
✗ Branch 7 → 12 not taken.
2 logger.debug("Config: {} = {}", ini_key, formatted);
414 }
415 3 }
416
417 // --- Global storage for registered configuration items ---
418 316 std::mutex &getConfigMutex()
419 {
420
3/4
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 8 taken 315 times.
✓ Branch 4 → 5 taken 1 time.
✗ Branch 4 → 8 not taken.
316 static std::mutex mtx;
421 316 return mtx;
422 }
423
424 454 std::vector<std::unique_ptr<ConfigItemBase>> &getRegisteredConfigItems()
425 {
426 // Function-local static to ensure controlled initialization order.
427
3/4
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 7 taken 453 times.
✓ Branch 4 → 5 taken 1 time.
✗ Branch 4 → 7 not taken.
454 static std::vector<std::unique_ptr<ConfigItemBase>> s_registered_items;
428 454 return s_registered_items;
429 }
430
431 /// Replaces an existing item with the same section+key, or appends if none found.
432 /// Caller must hold getConfigMutex().
433 94 void replace_or_append(std::unique_ptr<ConfigItemBase> item)
434 {
435 94 auto &items = getRegisteredConfigItems();
436
2/2
✓ Branch 31 → 5 taken 41 times.
✓ Branch 31 → 32 taken 91 times.
226 for (auto &existing : items)
437 {
438
6/6
✓ Branch 10 → 11 taken 32 times.
✓ Branch 10 → 16 taken 9 times.
✓ Branch 14 → 15 taken 3 times.
✓ Branch 14 → 16 taken 29 times.
✓ Branch 17 → 18 taken 3 times.
✓ Branch 17 → 22 taken 38 times.
41 if (existing->section == item->section && existing->ini_key == item->ini_key)
439 {
440 3 existing = std::move(item);
441 3 return;
442 }
443 }
444 91 items.push_back(std::move(item));
445 }
446
447 /**
448 * @brief Determines the full absolute path for the INI configuration file.
449 */
450 68 std::string getIniFilePath(const std::string &ini_filename, Logger &logger)
451 {
452
1/2
✓ Branch 2 → 3 taken 68 times.
✗ Branch 2 → 67 not taken.
68 std::string module_dir = get_runtime_directory();
453
454
4/8
✓ Branch 4 → 5 taken 68 times.
✗ Branch 4 → 7 not taken.
✓ Branch 5 → 6 taken 68 times.
✗ Branch 5 → 65 not taken.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 68 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 13 taken 68 times.
68 if (module_dir.empty() || module_dir == ".")
455 {
456 logger.warning("Config: Could not reliably determine module directory or it's current working directory. Using relative path for INI: {}", ini_filename);
457 return ini_filename; // Fallback to relative path
458 }
459
460 try
461 {
462
3/6
✓ Branch 13 → 14 taken 68 times.
✗ Branch 13 → 37 not taken.
✓ Branch 14 → 15 taken 68 times.
✗ Branch 14 → 34 not taken.
✓ Branch 15 → 16 taken 68 times.
✗ Branch 15 → 32 not taken.
68 std::filesystem::path ini_path_obj = std::filesystem::path(module_dir) / ini_filename;
463
2/4
✓ Branch 18 → 19 taken 68 times.
✗ Branch 18 → 40 not taken.
✓ Branch 19 → 20 taken 68 times.
✗ Branch 19 → 38 not taken.
68 std::string full_path = ini_path_obj.lexically_normal().string(); // Normalize (e.g., C:/path/./file -> C:/path/file)
464
1/2
✓ Branch 21 → 22 taken 68 times.
✗ Branch 21 → 41 not taken.
68 logger.debug("Config: Determined INI file path: {}", full_path);
465 68 return full_path;
466 68 }
467 catch (const std::filesystem::filesystem_error &fs_err)
468 {
469 logger.warning("Config: Filesystem error constructing INI path: {}. Using relative path for INI: {}", fs_err.what(), ini_filename);
470 }
471 catch (const std::exception &e) // Catch other potential exceptions
472 {
473 logger.warning("Config: General error constructing INI path: {}. Using relative path for INI: {}", e.what(), ini_filename);
474 }
475 return ini_filename; // Fallback
476 68 }
477
478 } // anonymous namespace
479
480 27 void DetourModKit::Config::register_int(std::string_view section, std::string_view ini_key,
481 std::string_view log_key_name, std::function<void(int)> setter,
482 int default_value)
483 {
484 27 std::function<void()> deferred;
485 {
486
1/2
✓ Branch 4 → 5 taken 27 times.
✗ Branch 4 → 71 not taken.
27 std::lock_guard<std::mutex> lock(getConfigMutex());
487
1/2
✓ Branch 16 → 17 taken 27 times.
✗ Branch 16 → 39 not taken.
27 replace_or_append(
488
4/8
✓ Branch 7 → 8 taken 27 times.
✗ Branch 7 → 57 not taken.
✓ Branch 10 → 11 taken 27 times.
✗ Branch 10 → 51 not taken.
✓ Branch 13 → 14 taken 27 times.
✗ Branch 13 → 45 not taken.
✓ Branch 14 → 15 taken 27 times.
✗ Branch 14 → 43 not taken.
162 std::make_unique<CallbackConfigItem<int>>(std::string(section), std::string(ini_key), std::string(log_key_name), setter, default_value));
489
2/2
✓ Branch 26 → 27 taken 24 times.
✓ Branch 26 → 33 taken 3 times.
27 if (setter)
490 {
491
3/8
✓ Branch 27 → 28 taken 24 times.
✗ Branch 27 → 68 not taken.
✓ Branch 28 → 29 taken 24 times.
✗ Branch 28 → 63 not taken.
✗ Branch 30 → 31 not taken.
✓ Branch 30 → 32 taken 24 times.
✗ Branch 65 → 66 not taken.
✗ Branch 65 → 67 not taken.
48 deferred = [setter, default_value]() { setter(default_value); };
492 }
493 27 }
494
2/2
✓ Branch 35 → 36 taken 24 times.
✓ Branch 35 → 37 taken 3 times.
27 if (deferred)
495 {
496
1/2
✓ Branch 36 → 37 taken 24 times.
✗ Branch 36 → 72 not taken.
24 deferred();
497 }
498 27 }
499
500 8 void DetourModKit::Config::register_float(std::string_view section, std::string_view ini_key,
501 std::string_view log_key_name, std::function<void(float)> setter,
502 float default_value)
503 {
504 8 std::function<void()> deferred;
505 {
506
1/2
✓ Branch 4 → 5 taken 8 times.
✗ Branch 4 → 71 not taken.
8 std::lock_guard<std::mutex> lock(getConfigMutex());
507
1/2
✓ Branch 16 → 17 taken 8 times.
✗ Branch 16 → 39 not taken.
8 replace_or_append(
508
4/8
✓ Branch 7 → 8 taken 8 times.
✗ Branch 7 → 57 not taken.
✓ Branch 10 → 11 taken 8 times.
✗ Branch 10 → 51 not taken.
✓ Branch 13 → 14 taken 8 times.
✗ Branch 13 → 45 not taken.
✓ Branch 14 → 15 taken 8 times.
✗ Branch 14 → 43 not taken.
48 std::make_unique<CallbackConfigItem<float>>(std::string(section), std::string(ini_key), std::string(log_key_name), setter, default_value));
509
1/2
✓ Branch 26 → 27 taken 8 times.
✗ Branch 26 → 33 not taken.
8 if (setter)
510 {
511
3/8
✓ Branch 27 → 28 taken 8 times.
✗ Branch 27 → 68 not taken.
✓ Branch 28 → 29 taken 8 times.
✗ Branch 28 → 63 not taken.
✗ Branch 30 → 31 not taken.
✓ Branch 30 → 32 taken 8 times.
✗ Branch 65 → 66 not taken.
✗ Branch 65 → 67 not taken.
16 deferred = [setter, default_value]() { setter(default_value); };
512 }
513 8 }
514
1/2
✓ Branch 35 → 36 taken 8 times.
✗ Branch 35 → 37 not taken.
8 if (deferred)
515 {
516
1/2
✓ Branch 36 → 37 taken 8 times.
✗ Branch 36 → 72 not taken.
8 deferred();
517 }
518 8 }
519
520 9 void DetourModKit::Config::register_bool(std::string_view section, std::string_view ini_key,
521 std::string_view log_key_name, std::function<void(bool)> setter,
522 bool default_value)
523 {
524 9 std::function<void()> deferred;
525 {
526
1/2
✓ Branch 4 → 5 taken 9 times.
✗ Branch 4 → 71 not taken.
9 std::lock_guard<std::mutex> lock(getConfigMutex());
527
1/2
✓ Branch 16 → 17 taken 9 times.
✗ Branch 16 → 39 not taken.
9 replace_or_append(
528
4/8
✓ Branch 7 → 8 taken 9 times.
✗ Branch 7 → 57 not taken.
✓ Branch 10 → 11 taken 9 times.
✗ Branch 10 → 51 not taken.
✓ Branch 13 → 14 taken 9 times.
✗ Branch 13 → 45 not taken.
✓ Branch 14 → 15 taken 9 times.
✗ Branch 14 → 43 not taken.
54 std::make_unique<CallbackConfigItem<bool>>(std::string(section), std::string(ini_key), std::string(log_key_name), setter, default_value));
529
1/2
✓ Branch 26 → 27 taken 9 times.
✗ Branch 26 → 33 not taken.
9 if (setter)
530 {
531
3/8
✓ Branch 27 → 28 taken 9 times.
✗ Branch 27 → 68 not taken.
✓ Branch 28 → 29 taken 9 times.
✗ Branch 28 → 63 not taken.
✗ Branch 30 → 31 not taken.
✓ Branch 30 → 32 taken 9 times.
✗ Branch 65 → 66 not taken.
✗ Branch 65 → 67 not taken.
18 deferred = [setter, default_value]() { setter(default_value); };
532 }
533 9 }
534
1/2
✓ Branch 35 → 36 taken 9 times.
✗ Branch 35 → 37 not taken.
9 if (deferred)
535 {
536
1/2
✓ Branch 36 → 37 taken 9 times.
✗ Branch 36 → 72 not taken.
9 deferred();
537 }
538 9 }
539
540 9 void DetourModKit::Config::register_string(std::string_view section, std::string_view ini_key,
541 std::string_view log_key_name, std::function<void(const std::string &)> setter,
542 std::string default_value)
543 {
544 9 std::function<void()> deferred;
545 {
546
1/2
✓ Branch 4 → 5 taken 9 times.
✗ Branch 4 → 74 not taken.
9 std::lock_guard<std::mutex> lock(getConfigMutex());
547
1/2
✓ Branch 16 → 17 taken 9 times.
✗ Branch 16 → 42 not taken.
9 replace_or_append(
548
4/8
✓ Branch 7 → 8 taken 9 times.
✗ Branch 7 → 60 not taken.
✓ Branch 10 → 11 taken 9 times.
✗ Branch 10 → 54 not taken.
✓ Branch 13 → 14 taken 9 times.
✗ Branch 13 → 48 not taken.
✓ Branch 14 → 15 taken 9 times.
✗ Branch 14 → 46 not taken.
54 std::make_unique<CallbackConfigItem<std::string>>(std::string(section), std::string(ini_key), std::string(log_key_name), setter, default_value));
549
2/2
✓ Branch 26 → 27 taken 8 times.
✓ Branch 26 → 36 taken 1 time.
9 if (setter)
550 {
551
3/8
✓ Branch 27 → 28 taken 8 times.
✗ Branch 27 → 71 not taken.
✓ Branch 31 → 32 taken 8 times.
✗ Branch 31 → 66 not taken.
✗ Branch 33 → 34 not taken.
✓ Branch 33 → 35 taken 8 times.
✗ Branch 68 → 69 not taken.
✗ Branch 68 → 70 not taken.
24 deferred = [setter, val = std::move(default_value)]() { setter(val); };
552 }
553 9 }
554
2/2
✓ Branch 38 → 39 taken 8 times.
✓ Branch 38 → 40 taken 1 time.
9 if (deferred)
555 {
556
1/2
✓ Branch 39 → 40 taken 8 times.
✗ Branch 39 → 75 not taken.
8 deferred();
557 }
558 9 }
559
560 41 void DetourModKit::Config::register_key_combo(std::string_view section, std::string_view ini_key,
561 std::string_view log_key_name, std::function<void(const KeyComboList &)> setter,
562 std::string_view default_value_str)
563 {
564
2/4
✓ Branch 4 → 5 taken 41 times.
✗ Branch 4 → 51 not taken.
✓ Branch 5 → 6 taken 41 times.
✗ Branch 5 → 49 not taken.
41 Config::KeyComboList default_combos = parse_key_combo_list(std::string(default_value_str));
565
566 41 std::function<void()> deferred;
567 {
568
1/2
✓ Branch 10 → 11 taken 41 times.
✗ Branch 10 → 87 not taken.
41 std::lock_guard<std::mutex> lock(getConfigMutex());
569
1/2
✓ Branch 22 → 23 taken 41 times.
✗ Branch 22 → 55 not taken.
41 replace_or_append(
570
4/8
✓ Branch 13 → 14 taken 41 times.
✗ Branch 13 → 73 not taken.
✓ Branch 16 → 17 taken 41 times.
✗ Branch 16 → 67 not taken.
✓ Branch 19 → 20 taken 41 times.
✗ Branch 19 → 61 not taken.
✓ Branch 20 → 21 taken 41 times.
✗ Branch 20 → 59 not taken.
246 std::make_unique<CallbackConfigItem<Config::KeyComboList>>(std::string(section), std::string(ini_key), std::string(log_key_name), setter, default_combos));
571
2/2
✓ Branch 32 → 33 taken 40 times.
✓ Branch 32 → 42 taken 1 time.
41 if (setter)
572 {
573
3/8
✓ Branch 33 → 34 taken 40 times.
✗ Branch 33 → 84 not taken.
✓ Branch 37 → 38 taken 40 times.
✗ Branch 37 → 79 not taken.
✗ Branch 39 → 40 not taken.
✓ Branch 39 → 41 taken 40 times.
✗ Branch 81 → 82 not taken.
✗ Branch 81 → 83 not taken.
120 deferred = [setter, combos = std::move(default_combos)]() { setter(combos); };
574 }
575 41 }
576
2/2
✓ Branch 44 → 45 taken 40 times.
✓ Branch 44 → 46 taken 1 time.
41 if (deferred)
577 {
578
1/2
✓ Branch 45 → 46 taken 40 times.
✗ Branch 45 → 88 not taken.
40 deferred();
579 }
580 41 }
581
582 68 void DetourModKit::Config::load(std::string_view ini_filename)
583 {
584 68 std::vector<std::function<void()>> deferred_callbacks;
585
586 {
587
1/2
✓ Branch 3 → 4 taken 68 times.
✗ Branch 3 → 89 not taken.
68 std::lock_guard<std::mutex> lock(getConfigMutex());
588
589
1/2
✓ Branch 4 → 5 taken 68 times.
✗ Branch 4 → 87 not taken.
68 Logger &logger = Logger::get_instance();
590
2/4
✓ Branch 7 → 8 taken 68 times.
✗ Branch 7 → 71 not taken.
✓ Branch 8 → 9 taken 68 times.
✗ Branch 8 → 69 not taken.
68 std::string ini_path = getIniFilePath(std::string(ini_filename), logger);
591 68 CSimpleIniA ini;
592 68 ini.SetUnicode(false); // Assume ASCII/MBCS INI
593 68 ini.SetMultiKey(false); // Disallow duplicate keys in a section
594
595
1/2
✓ Branch 15 → 16 taken 68 times.
✗ Branch 15 → 83 not taken.
68 SI_Error rc = ini.LoadFile(ini_path.c_str());
596
2/2
✓ Branch 16 → 17 taken 26 times.
✓ Branch 16 → 19 taken 42 times.
68 if (rc < 0)
597 {
598
1/2
✓ Branch 17 → 18 taken 26 times.
✗ Branch 17 → 75 not taken.
26 logger.error("Config: Failed to open '{}' (error {}). Using defaults.", ini_path, rc);
599 }
600 else
601 {
602
1/2
✓ Branch 19 → 20 taken 42 times.
✗ Branch 19 → 76 not taken.
42 logger.debug("Config: Opened {}", ini_path);
603 }
604
605 // Read all values under lock, but defer setter callbacks
606
2/2
✓ Branch 45 → 24 taken 86 times.
✓ Branch 45 → 46 taken 68 times.
222 for (const auto &item : getRegisteredConfigItems())
607 {
608
1/2
✓ Branch 27 → 28 taken 86 times.
✗ Branch 27 → 79 not taken.
86 item->load(ini, logger);
609
1/2
✓ Branch 29 → 30 taken 86 times.
✗ Branch 29 → 79 not taken.
86 auto cb = item->take_deferred_apply();
610
1/2
✓ Branch 31 → 32 taken 86 times.
✗ Branch 31 → 35 not taken.
86 if (cb)
611 {
612
1/2
✓ Branch 34 → 35 taken 86 times.
✗ Branch 34 → 77 not taken.
86 deferred_callbacks.push_back(std::move(cb));
613 }
614 86 }
615
616
1/2
✓ Branch 48 → 49 taken 68 times.
✗ Branch 48 → 81 not taken.
68 logger.info("Config: Loaded {} items from {}", getRegisteredConfigItems().size(), ini_path);
617 68 }
618
619 // Invoke setter callbacks outside the config mutex to prevent deadlocks
620
2/2
✓ Branch 66 → 54 taken 86 times.
✓ Branch 66 → 67 taken 68 times.
222 for (auto &cb : deferred_callbacks)
621 {
622
1/2
✓ Branch 56 → 57 taken 86 times.
✗ Branch 56 → 90 not taken.
86 cb();
623 }
624 68 }
625
626 8 void DetourModKit::Config::log_all()
627 {
628
1/2
✓ Branch 3 → 4 taken 8 times.
✗ Branch 3 → 56 not taken.
8 std::lock_guard<std::mutex> lock(getConfigMutex());
629
630
1/2
✓ Branch 4 → 5 taken 8 times.
✗ Branch 4 → 54 not taken.
8 Logger &logger = Logger::get_instance();
631 8 const auto &items = getRegisteredConfigItems();
632
2/2
✓ Branch 7 → 8 taken 2 times.
✓ Branch 7 → 10 taken 6 times.
8 if (items.empty())
633 {
634
1/2
✓ Branch 8 → 9 taken 2 times.
✗ Branch 8 → 45 not taken.
2 logger.info("Config: No configuration items registered.");
635 2 return;
636 }
637
638 logger.info("Config: {} registered values across {} section(s)",
639
1/2
✓ Branch 12 → 13 taken 6 times.
✗ Branch 12 → 46 not taken.
6 items.size(), [&items]()
640 {
641 6 std::unordered_set<std::string_view> seen;
642
2/2
✓ Branch 19 → 5 taken 11 times.
✓ Branch 19 → 20 taken 6 times.
23 for (const auto &item : items)
643 {
644
1/2
✓ Branch 9 → 10 taken 11 times.
✗ Branch 9 → 24 not taken.
11 seen.insert(item->section);
645 }
646 12 return seen.size();
647
1/2
✓ Branch 10 → 11 taken 6 times.
✗ Branch 10 → 48 not taken.
12 }());
648
649 6 std::string current_section;
650
2/2
✓ Branch 36 → 16 taken 11 times.
✓ Branch 36 → 37 taken 6 times.
23 for (const auto &item : items)
651 {
652
2/2
✓ Branch 20 → 21 taken 7 times.
✓ Branch 20 → 25 taken 4 times.
11 if (item->section != current_section)
653 {
654
1/2
✓ Branch 22 → 23 taken 7 times.
✗ Branch 22 → 51 not taken.
7 current_section = item->section;
655
1/2
✓ Branch 23 → 24 taken 7 times.
✗ Branch 23 → 50 not taken.
7 logger.debug("Config: [{}]", current_section);
656 }
657
1/2
✓ Branch 26 → 27 taken 11 times.
✗ Branch 26 → 51 not taken.
11 item->log_current_value(logger);
658 }
659
2/2
✓ Branch 40 → 41 taken 6 times.
✓ Branch 40 → 43 taken 2 times.
8 }
660
661 146 void DetourModKit::Config::clear_registered_items()
662 {
663
1/2
✓ Branch 3 → 4 taken 146 times.
✗ Branch 3 → 20 not taken.
146 std::lock_guard<std::mutex> lock(getConfigMutex());
664
665
1/2
✓ Branch 4 → 5 taken 146 times.
✗ Branch 4 → 18 not taken.
146 Logger &logger = Logger::get_instance();
666 146 size_t count = getRegisteredConfigItems().size();
667
2/2
✓ Branch 7 → 8 taken 70 times.
✓ Branch 7 → 12 taken 76 times.
146 if (count > 0)
668 {
669 70 getRegisteredConfigItems().clear();
670
1/2
✓ Branch 10 → 11 taken 70 times.
✗ Branch 10 → 16 not taken.
70 logger.debug("Config: Cleared {} registered configuration items.", count);
671 }
672 else
673 {
674
1/2
✓ Branch 12 → 13 taken 76 times.
✗ Branch 12 → 17 not taken.
76 logger.debug("Config: clear_registered_items called, but no items were registered.");
675 }
676 146 }
677