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/config_watcher.hpp" | ||
| 13 | #include "DetourModKit/input.hpp" | ||
| 14 | #include "DetourModKit/input_codes.hpp" | ||
| 15 | #include "DetourModKit/logger.hpp" | ||
| 16 | #include "DetourModKit/filesystem.hpp" | ||
| 17 | #include "DetourModKit/format.hpp" | ||
| 18 | #include "DetourModKit/worker.hpp" | ||
| 19 | #include "SimpleIni.h" | ||
| 20 | |||
| 21 | #include <atomic> | ||
| 22 | #include <memory> | ||
| 23 | |||
| 24 | #include <windows.h> | ||
| 25 | #include <cctype> | ||
| 26 | #include <cerrno> | ||
| 27 | #include <condition_variable> | ||
| 28 | #include <cstdint> | ||
| 29 | #include <cstdlib> | ||
| 30 | #include <filesystem> | ||
| 31 | #include <fstream> | ||
| 32 | #include <limits> | ||
| 33 | #include <mutex> | ||
| 34 | #include <optional> | ||
| 35 | #include <string> | ||
| 36 | #include <string_view> | ||
| 37 | #include <thread> | ||
| 38 | #include <unordered_set> | ||
| 39 | #include <vector> | ||
| 40 | |||
| 41 | using namespace DetourModKit; | ||
| 42 | using namespace DetourModKit::Filesystem; | ||
| 43 | using namespace DetourModKit::String; | ||
| 44 | |||
| 45 | // Anonymous namespace for internal helpers and storage | ||
| 46 | namespace | ||
| 47 | { | ||
| 48 | /** | ||
| 49 | * @brief Parses a comma-separated string of input tokens into a vector of InputCodes. | ||
| 50 | * @details Each token is first matched against the named key table (case-insensitive). | ||
| 51 | * If no name matches, the token is parsed as a hexadecimal VK code (with or | ||
| 52 | * without 0x prefix), defaulting to InputSource::Keyboard. Handles inline | ||
| 53 | * semicolon comments, whitespace, and gracefully skips invalid tokens. | ||
| 54 | * @param input The raw string to parse. | ||
| 55 | * @return std::vector<InputCode> Parsed valid input codes. | ||
| 56 | */ | ||
| 57 | 136 | std::vector<InputCode> parse_input_code_list(const std::string &input) | |
| 58 | { | ||
| 59 | 136 | std::vector<InputCode> result; | |
| 60 | |||
| 61 | // Strip trailing comment from the full line | ||
| 62 | 136 | const size_t comment_pos = input.find(';'); | |
| 63 | const std::string effective = trim( | ||
| 64 |
3/8✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 136 times.
✗ Branch 4 → 6 not taken.
✗ Branch 4 → 82 not taken.
✓ Branch 5 → 6 taken 136 times.
✗ Branch 5 → 82 not taken.
✓ Branch 7 → 8 taken 136 times.
✗ Branch 7 → 80 not taken.
|
136 | (comment_pos != std::string::npos) ? input.substr(0, comment_pos) : input); |
| 65 |
1/2✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 136 times.
|
136 | if (effective.empty()) |
| 66 | { | ||
| 67 | ✗ | return result; | |
| 68 | } | ||
| 69 | |||
| 70 | // Walk comma-delimited tokens without istringstream overhead | ||
| 71 | 136 | size_t pos = 0; | |
| 72 |
2/2✓ Branch 75 → 13 taken 136 times.
✓ Branch 75 → 76 taken 136 times.
|
272 | while (pos < effective.size()) |
| 73 | { | ||
| 74 | 136 | const size_t comma = effective.find(',', pos); | |
| 75 |
1/2✓ Branch 14 → 15 taken 136 times.
✗ Branch 14 → 16 not taken.
|
136 | const size_t end = (comma != std::string::npos) ? comma : effective.size(); |
| 76 |
2/4✓ Branch 17 → 18 taken 136 times.
✗ Branch 17 → 85 not taken.
✓ Branch 19 → 20 taken 136 times.
✗ Branch 19 → 83 not taken.
|
136 | const std::string token = trim(effective.substr(pos, end - pos)); |
| 77 | 136 | pos = end + 1; | |
| 78 | |||
| 79 |
1/2✗ Branch 22 → 23 not taken.
✓ Branch 22 → 24 taken 136 times.
|
136 | if (token.empty()) |
| 80 | { | ||
| 81 | ✗ | continue; | |
| 82 | } | ||
| 83 | |||
| 84 | // Try named key lookup first (case-insensitive) | ||
| 85 |
1/2✓ Branch 25 → 26 taken 136 times.
✗ Branch 25 → 87 not taken.
|
136 | auto named = parse_input_name(token); |
| 86 |
2/2✓ Branch 27 → 28 taken 89 times.
✓ Branch 27 → 31 taken 47 times.
|
136 | if (named.has_value()) |
| 87 | { | ||
| 88 |
1/2✓ Branch 29 → 30 taken 89 times.
✗ Branch 29 → 87 not taken.
|
89 | result.push_back(*named); |
| 89 | 89 | continue; | |
| 90 | } | ||
| 91 | |||
| 92 | // Fall back to hex parsing (defaults to Keyboard source) | ||
| 93 | 47 | size_t hex_start = 0; | |
| 94 |
8/10✓ Branch 32 → 33 taken 47 times.
✗ Branch 32 → 40 not taken.
✓ Branch 34 → 35 taken 38 times.
✓ Branch 34 → 40 taken 9 times.
✓ Branch 36 → 37 taken 2 times.
✓ Branch 36 → 39 taken 36 times.
✓ Branch 38 → 39 taken 2 times.
✗ Branch 38 → 40 not taken.
✓ Branch 41 → 42 taken 38 times.
✓ Branch 41 → 43 taken 9 times.
|
47 | if (token.size() >= 2 && token[0] == '0' && (token[1] == 'x' || token[1] == 'X')) |
| 95 | { | ||
| 96 | 38 | hex_start = 2; | |
| 97 | } | ||
| 98 |
2/2✓ Branch 44 → 45 taken 4 times.
✓ Branch 44 → 46 taken 43 times.
|
47 | if (hex_start >= token.size()) |
| 99 | { | ||
| 100 | 4 | continue; | |
| 101 | } | ||
| 102 | |||
| 103 | // Validate all remaining characters are hex digits | ||
| 104 | 43 | const std::string_view hex_part(token.data() + hex_start, token.size() - hex_start); | |
| 105 |
2/2✓ Branch 50 → 51 taken 9 times.
✓ Branch 50 → 52 taken 34 times.
|
43 | if (hex_part.find_first_not_of("0123456789abcdefABCDEF") != std::string_view::npos) |
| 106 | { | ||
| 107 | 9 | continue; | |
| 108 | } | ||
| 109 | |||
| 110 | // Convert via strtoul — no exception overhead on invalid input | ||
| 111 |
1/2✓ Branch 52 → 53 taken 34 times.
✗ Branch 52 → 87 not taken.
|
34 | errno = 0; |
| 112 | 34 | char *end_ptr = nullptr; | |
| 113 | 34 | const unsigned long value = std::strtoul(token.c_str() + hex_start, &end_ptr, 16); | |
| 114 |
6/8✓ Branch 56 → 57 taken 34 times.
✗ Branch 56 → 59 not taken.
✓ Branch 57 → 58 taken 34 times.
✗ Branch 57 → 87 not taken.
✓ Branch 58 → 59 taken 2 times.
✓ Branch 58 → 60 taken 32 times.
✓ Branch 61 → 62 taken 2 times.
✓ Branch 61 → 63 taken 32 times.
|
34 | if (end_ptr == token.c_str() + hex_start || errno == ERANGE) |
| 115 | { | ||
| 116 | 2 | continue; | |
| 117 | } | ||
| 118 |
2/2✓ Branch 64 → 65 taken 3 times.
✓ Branch 64 → 66 taken 29 times.
|
32 | if (value > static_cast<unsigned long>(std::numeric_limits<int>::max())) |
| 119 | { | ||
| 120 | 3 | continue; | |
| 121 | } | ||
| 122 | |||
| 123 |
1/2✓ Branch 66 → 67 taken 29 times.
✗ Branch 66 → 86 not taken.
|
29 | result.push_back(InputCode{InputSource::Keyboard, static_cast<int>(value)}); |
| 124 |
2/2✓ Branch 69 → 70 taken 29 times.
✓ Branch 69 → 72 taken 107 times.
|
136 | } |
| 125 | |||
| 126 | 136 | return result; | |
| 127 | 136 | } | |
| 128 | |||
| 129 | /** | ||
| 130 | * @brief Parses a single key combo string into a KeyCombo struct. | ||
| 131 | * @details Format: "modifier1+modifier2+trigger_key" where each token is a | ||
| 132 | * named key or hex VK code. The last '+'-delimited token is the trigger | ||
| 133 | * key, all preceding tokens are modifier keys (AND logic). This function | ||
| 134 | * expects a single combo with no commas; use parse_key_combo_list to | ||
| 135 | * split comma-separated alternatives first. | ||
| 136 | * @param input The raw string to parse (no commas expected). | ||
| 137 | * @return Config::KeyCombo Parsed key combination. | ||
| 138 | */ | ||
| 139 | 109 | Config::KeyCombo parse_key_combo(const std::string &input) | |
| 140 | { | ||
| 141 | 109 | Config::KeyCombo result; | |
| 142 | |||
| 143 |
1/2✓ Branch 3 → 4 taken 109 times.
✗ Branch 3 → 65 not taken.
|
109 | const std::string effective = trim(input); |
| 144 |
1/2✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 109 times.
|
109 | if (effective.empty()) |
| 145 | { | ||
| 146 | ✗ | return result; | |
| 147 | } | ||
| 148 | |||
| 149 | // Split by '+' to get segments | ||
| 150 | 109 | std::vector<std::string> segments; | |
| 151 | 109 | size_t pos = 0; | |
| 152 |
2/2✓ Branch 22 → 8 taken 140 times.
✓ Branch 22 → 23 taken 109 times.
|
249 | while (pos < effective.size()) |
| 153 | { | ||
| 154 | 140 | const size_t plus = effective.find('+', pos); | |
| 155 |
2/2✓ Branch 9 → 10 taken 107 times.
✓ Branch 9 → 11 taken 33 times.
|
140 | const size_t end = (plus != std::string::npos) ? plus : effective.size(); |
| 156 |
2/4✓ Branch 12 → 13 taken 140 times.
✗ Branch 12 → 51 not taken.
✓ Branch 14 → 15 taken 140 times.
✗ Branch 14 → 49 not taken.
|
140 | const std::string segment = trim(effective.substr(pos, end - pos)); |
| 157 | 140 | pos = end + 1; | |
| 158 |
2/2✓ Branch 17 → 18 taken 136 times.
✓ Branch 17 → 19 taken 4 times.
|
140 | if (!segment.empty()) |
| 159 | { | ||
| 160 |
1/2✓ Branch 18 → 19 taken 136 times.
✗ Branch 18 → 52 not taken.
|
136 | segments.push_back(segment); |
| 161 | } | ||
| 162 | 140 | } | |
| 163 | |||
| 164 |
2/2✓ Branch 24 → 25 taken 1 time.
✓ Branch 24 → 26 taken 108 times.
|
109 | if (segments.empty()) |
| 165 | { | ||
| 166 | 1 | return result; | |
| 167 | } | ||
| 168 | |||
| 169 | // Last segment is the trigger key | ||
| 170 |
1/2✓ Branch 27 → 28 taken 108 times.
✗ Branch 27 → 55 not taken.
|
108 | result.keys = parse_input_code_list(segments.back()); |
| 171 | |||
| 172 | // All preceding segments are individual modifier keys | ||
| 173 |
2/2✓ Branch 43 → 31 taken 28 times.
✓ Branch 43 → 44 taken 108 times.
|
136 | for (size_t i = 0; i + 1 < segments.size(); ++i) |
| 174 | { | ||
| 175 |
1/2✓ Branch 32 → 33 taken 28 times.
✗ Branch 32 → 60 not taken.
|
28 | auto mod_codes = parse_input_code_list(segments[i]); |
| 176 |
1/2✓ Branch 39 → 40 taken 28 times.
✗ Branch 39 → 56 not taken.
|
56 | result.modifiers.insert(result.modifiers.end(), mod_codes.begin(), mod_codes.end()); |
| 177 | 28 | } | |
| 178 | |||
| 179 | 108 | return result; | |
| 180 | 109 | } | |
| 181 | |||
| 182 | /** | ||
| 183 | * @brief Returns true when @p text is the literal "NONE" sentinel | ||
| 184 | * (case-insensitive ASCII, exact length match). | ||
| 185 | * @details The whole-string-only rule keeps the sentinel unambiguous: | ||
| 186 | * a NONE token nested inside a comma-separated list cannot be | ||
| 187 | * told apart from a key-name typo without a per-token lookup, | ||
| 188 | * and the OR-of-combos semantic makes "an unbound slot inside | ||
| 189 | * an OR-list" meaningless. Caller must pass a pre-trimmed view. | ||
| 190 | */ | ||
| 191 | 79 | [[nodiscard]] bool is_none_sentinel(std::string_view text) noexcept | |
| 192 | { | ||
| 193 |
2/2✓ Branch 3 → 4 taken 69 times.
✓ Branch 3 → 5 taken 10 times.
|
79 | if (text.size() != 4) |
| 194 | { | ||
| 195 | 69 | return false; | |
| 196 | } | ||
| 197 | 10 | constexpr char target[] = {'N', 'O', 'N', 'E'}; | |
| 198 |
2/2✓ Branch 10 → 6 taken 31 times.
✓ Branch 10 → 11 taken 7 times.
|
38 | for (size_t i = 0; i < 4; ++i) |
| 199 | { | ||
| 200 | 31 | const auto ch = static_cast<unsigned char>(text[i]); | |
| 201 |
2/2✓ Branch 7 → 8 taken 3 times.
✓ Branch 7 → 9 taken 28 times.
|
31 | if (static_cast<char>(std::toupper(ch)) != target[i]) |
| 202 | { | ||
| 203 | 3 | return false; | |
| 204 | } | ||
| 205 | } | ||
| 206 | 7 | return true; | |
| 207 | } | ||
| 208 | |||
| 209 | /** | ||
| 210 | * @brief Parses a comma-separated string of key combos into a KeyComboList. | ||
| 211 | * @details Commas at the top level separate independent combos (OR logic between | ||
| 212 | * combos). Each combo is parsed by parse_key_combo. Handles inline | ||
| 213 | * semicolon comments and whitespace. Two opt-out sentinels yield an | ||
| 214 | * empty result silently: an empty (post-trim) input, and the literal | ||
| 215 | * "NONE" (case-insensitive, whole-string only). A non-empty input | ||
| 216 | * that is not the NONE sentinel and whose every comma-separated token | ||
| 217 | * fails to parse is treated as a user typo and emits a single WARNING | ||
| 218 | * naming the binding and the offending raw string. Empty inner tokens | ||
| 219 | * (e.g. "F4,,F5") are silently skipped; the WARNING fires only when | ||
| 220 | * the entire result list is empty. | ||
| 221 | * @param input The raw string to parse. | ||
| 222 | * @param binding_log_name Optional human-readable binding name used in the | ||
| 223 | * typo WARNING. Defaults to an empty view, in which | ||
| 224 | * case the WARNING uses "<unnamed>". | ||
| 225 | * @return Config::KeyComboList Parsed list of key combinations. | ||
| 226 | */ | ||
| 227 | 111 | Config::KeyComboList parse_key_combo_list(const std::string &input, | |
| 228 | std::string_view binding_log_name = {}) | ||
| 229 | { | ||
| 230 | 111 | Config::KeyComboList result; | |
| 231 | |||
| 232 | // Strip trailing comment from the full line | ||
| 233 | 111 | const size_t comment_pos = input.find(';'); | |
| 234 | const std::string effective = trim( | ||
| 235 |
5/8✓ Branch 3 → 4 taken 4 times.
✓ Branch 3 → 5 taken 107 times.
✓ Branch 4 → 6 taken 4 times.
✗ Branch 4 → 59 not taken.
✓ Branch 5 → 6 taken 107 times.
✗ Branch 5 → 59 not taken.
✓ Branch 7 → 8 taken 111 times.
✗ Branch 7 → 57 not taken.
|
111 | (comment_pos != std::string::npos) ? input.substr(0, comment_pos) : input); |
| 236 | |||
| 237 | // Disposition 1: explicit opt-out via empty string. Silent. | ||
| 238 |
2/2✓ Branch 10 → 11 taken 32 times.
✓ Branch 10 → 12 taken 79 times.
|
111 | if (effective.empty()) |
| 239 | { | ||
| 240 | 32 | return result; | |
| 241 | } | ||
| 242 | |||
| 243 | // Disposition 2: explicit opt-out via NONE sentinel (whole-string, | ||
| 244 | // case-insensitive, post-trim). Silent. | ||
| 245 |
2/2✓ Branch 14 → 15 taken 7 times.
✓ Branch 14 → 16 taken 72 times.
|
79 | if (is_none_sentinel(effective)) |
| 246 | { | ||
| 247 | 7 | return result; | |
| 248 | } | ||
| 249 | |||
| 250 | // Split by comma into independent combo strings | ||
| 251 | 72 | size_t pos = 0; | |
| 252 |
2/2✓ Branch 43 → 17 taken 116 times.
✓ Branch 43 → 44 taken 72 times.
|
188 | while (pos < effective.size()) |
| 253 | { | ||
| 254 | 116 | const size_t comma = effective.find(',', pos); | |
| 255 |
2/2✓ Branch 18 → 19 taken 71 times.
✓ Branch 18 → 20 taken 45 times.
|
116 | const size_t end = (comma != std::string::npos) ? comma : effective.size(); |
| 256 |
2/4✓ Branch 21 → 22 taken 116 times.
✗ Branch 21 → 62 not taken.
✓ Branch 23 → 24 taken 116 times.
✗ Branch 23 → 60 not taken.
|
116 | const std::string combo_str = trim(effective.substr(pos, end - pos)); |
| 257 | 116 | pos = end + 1; | |
| 258 | |||
| 259 |
2/2✓ Branch 26 → 27 taken 7 times.
✓ Branch 26 → 28 taken 109 times.
|
116 | if (combo_str.empty()) |
| 260 | { | ||
| 261 | 7 | continue; | |
| 262 | } | ||
| 263 | |||
| 264 |
1/2✓ Branch 28 → 29 taken 109 times.
✗ Branch 28 → 65 not taken.
|
109 | auto combo = parse_key_combo(combo_str); |
| 265 |
2/2✓ Branch 30 → 31 taken 91 times.
✓ Branch 30 → 34 taken 18 times.
|
109 | if (!combo.keys.empty()) |
| 266 | { | ||
| 267 |
1/2✓ Branch 33 → 34 taken 91 times.
✗ Branch 33 → 63 not taken.
|
91 | result.push_back(std::move(combo)); |
| 268 | } | ||
| 269 |
2/2✓ Branch 37 → 38 taken 109 times.
✓ Branch 37 → 40 taken 7 times.
|
116 | } |
| 270 | |||
| 271 | // Disposition 3: input was non-empty and not the NONE sentinel, | ||
| 272 | // yet every token failed to parse. Real user typo, name it. | ||
| 273 |
2/2✓ Branch 45 → 46 taken 5 times.
✓ Branch 45 → 53 taken 67 times.
|
72 | if (result.empty()) |
| 274 | { | ||
| 275 | const std::string_view name_view = | ||
| 276 |
1/2✗ Branch 47 → 48 not taken.
✓ Branch 47 → 49 taken 5 times.
|
5 | binding_log_name.empty() ? std::string_view{"<unnamed>"} : binding_log_name; |
| 277 |
2/4✓ Branch 50 → 51 taken 5 times.
✗ Branch 50 → 69 not taken.
✓ Branch 51 → 52 taken 5 times.
✗ Branch 51 → 68 not taken.
|
5 | Logger::get_instance().warning( |
| 278 | "Config: combo string \"{}\" for binding '{}' did not parse to any " | ||
| 279 | "valid keys; binding will be unbound. Use \"\" or \"NONE\" to opt " | ||
| 280 | "out explicitly.", | ||
| 281 | effective, name_view); | ||
| 282 | } | ||
| 283 | |||
| 284 | 72 | return result; | |
| 285 | 111 | } | |
| 286 | |||
| 287 | /** | ||
| 288 | * @brief Formats a single KeyCombo as a human-readable string. | ||
| 289 | * @details Uses named keys where available, falls back to hex for unknown codes. | ||
| 290 | * @param combo The key combination to format. | ||
| 291 | * @return std::string Formatted string (e.g., "Ctrl+Shift+F3"). | ||
| 292 | */ | ||
| 293 | 2 | std::string format_key_combo(const Config::KeyCombo &combo) | |
| 294 | { | ||
| 295 | 2 | std::string result; | |
| 296 |
2/2✓ Branch 21 → 5 taken 1 time.
✓ Branch 21 → 22 taken 2 times.
|
5 | for (const auto &mod : combo.modifiers) |
| 297 | { | ||
| 298 |
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) + "+"; |
| 299 | } | ||
| 300 |
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) |
| 301 | { | ||
| 302 |
1/2✗ Branch 23 → 24 not taken.
✓ Branch 23 → 25 taken 2 times.
|
2 | if (i > 0) |
| 303 | { | ||
| 304 | ✗ | result += ","; | |
| 305 | } | ||
| 306 |
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]); |
| 307 | } | ||
| 308 | 2 | return result; | |
| 309 | ✗ | } | |
| 310 | |||
| 311 | /** | ||
| 312 | * @brief Formats a KeyComboList as a human-readable string. | ||
| 313 | * @details Joins individual combos with commas. | ||
| 314 | * @param combos The list of key combinations to format. | ||
| 315 | * @return std::string Formatted string (e.g., "F3,Gamepad_LT+Gamepad_B"). | ||
| 316 | */ | ||
| 317 | 3 | std::string format_key_combo_list(const Config::KeyComboList &combos) | |
| 318 | { | ||
| 319 | 3 | std::string result; | |
| 320 |
2/2✓ Branch 12 → 4 taken 2 times.
✓ Branch 12 → 13 taken 3 times.
|
5 | for (size_t i = 0; i < combos.size(); ++i) |
| 321 | { | ||
| 322 |
1/2✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 2 times.
|
2 | if (i > 0) |
| 323 | { | ||
| 324 | ✗ | result += ","; | |
| 325 | } | ||
| 326 |
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]); |
| 327 | } | ||
| 328 | 3 | return result; | |
| 329 | ✗ | } | |
| 330 | |||
| 331 | /** | ||
| 332 | * @brief Base class for typed configuration items. | ||
| 333 | * @details This allows storing different types of configuration items | ||
| 334 | * polymorphically in a collection. | ||
| 335 | */ | ||
| 336 | struct ConfigItemBase | ||
| 337 | { | ||
| 338 | std::string section; | ||
| 339 | std::string ini_key; | ||
| 340 | std::string log_key_name; | ||
| 341 | |||
| 342 | 151 | ConfigItemBase(std::string sec, std::string key, std::string log_name) | |
| 343 | 604 | : section(std::move(sec)), ini_key(std::move(key)), log_key_name(std::move(log_name)) {} | |
| 344 | 151 | virtual ~ConfigItemBase() = default; | |
| 345 | ConfigItemBase(const ConfigItemBase &) = delete; | ||
| 346 | ConfigItemBase &operator=(const ConfigItemBase &) = delete; | ||
| 347 | ConfigItemBase(ConfigItemBase &&) = delete; | ||
| 348 | ConfigItemBase &operator=(ConfigItemBase &&) = delete; | ||
| 349 | |||
| 350 | /** | ||
| 351 | * @brief Loads the configuration value from the INI file. | ||
| 352 | * @param ini Reference to the CSimpleIniA object. | ||
| 353 | * @param logger Reference to the Logger object. | ||
| 354 | */ | ||
| 355 | virtual void load(CSimpleIniA &ini, Logger &logger) = 0; | ||
| 356 | |||
| 357 | /** | ||
| 358 | * @brief Returns a deferred callback to invoke the setter outside the config mutex. | ||
| 359 | * @return A self-contained callable, or empty if no setter is configured. | ||
| 360 | */ | ||
| 361 | [[nodiscard]] virtual std::function<void()> take_deferred_apply() const = 0; | ||
| 362 | |||
| 363 | /** | ||
| 364 | * @brief Logs the current value of the configuration item. | ||
| 365 | * @param logger Reference to the Logger object. | ||
| 366 | */ | ||
| 367 | virtual void log_current_value(Logger &logger) const = 0; | ||
| 368 | }; | ||
| 369 | |||
| 370 | /** | ||
| 371 | * @brief Configuration item using std::function callback for value setting. | ||
| 372 | * @tparam T The data type of the configuration item (e.g., int, bool, std::string). | ||
| 373 | * @note Setter callbacks are invoked outside the config mutex to prevent deadlocks. | ||
| 374 | * See register_* and load() for the deferred invocation pattern. | ||
| 375 | */ | ||
| 376 | template <typename T> | ||
| 377 | struct CallbackConfigItem : public ConfigItemBase | ||
| 378 | { | ||
| 379 | std::function<void(T)> setter; // Callback function to set the value | ||
| 380 | T default_value; | ||
| 381 | T current_value; | ||
| 382 | |||
| 383 | 151 | CallbackConfigItem(std::string sec, std::string key, std::string log_name, | |
| 384 | std::function<void(T)> set_fn, T def_val) | ||
| 385 | 453 | : ConfigItemBase(std::move(sec), std::move(key), std::move(log_name)), | |
| 386 | 151 | setter(std::move(set_fn)), | |
| 387 |
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 13 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 69 times.
✗ Branch 18 → 23 not taken.
|
151 | default_value(def_val), |
| 388 | 768 | current_value(std::move(def_val)) | |
| 389 | { | ||
| 390 | 151 | } | |
| 391 | |||
| 392 | void load(CSimpleIniA &ini, [[maybe_unused]] Logger &logger) override; | ||
| 393 | void log_current_value(Logger &logger) const override; | ||
| 394 | |||
| 395 | /// Returns a self-contained callback that invokes setter with current_value. | ||
| 396 | 145 | [[nodiscard]] std::function<void()> take_deferred_apply() const override | |
| 397 | { | ||
| 398 |
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 10 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 50 times.
(anonymous namespace)::CallbackConfigItem<bool>::take_deferred_apply() const:
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 12 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 65 times.
|
145 | if (!setter) |
| 399 | ✗ | return {}; | |
| 400 |
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 10 times.
✗ Branch 5 → 19 not taken.
✓ Branch 6 → 7 taken 10 times.
✗ Branch 6 → 16 not taken.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 10 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 50 times.
✗ Branch 5 → 19 not taken.
✓ Branch 6 → 7 taken 50 times.
✗ Branch 6 → 16 not taken.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 50 times.
✗ Branch 16 → 17 not taken.
✗ Branch 16 → 18 not taken.
(anonymous namespace)::CallbackConfigItem<bool>::take_deferred_apply() const:
✓ Branch 5 → 6 taken 12 times.
✗ Branch 5 → 18 not taken.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 12 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 65 times.
✗ Branch 5 → 18 not taken.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 65 times.
✗ Branch 15 → 16 not taken.
✗ Branch 15 → 17 not taken.
|
579 | return [fn = setter, val = current_value]() mutable |
| 401 |
6/12None:
✓ Branch 5 → 6 taken 60 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 10 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 50 times.
✗ Branch 7 → 14 not taken.
(anonymous namespace)::CallbackConfigItem<bool>::take_deferred_apply() const:
✓ Branch 6 → 7 taken 12 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 65 times.
✗ Branch 6 → 13 not taken.
|
435 | { fn(std::move(val)); }; |
| 402 | } | ||
| 403 | }; | ||
| 404 | |||
| 405 | // --- Specializations for CallbackConfigItem<T>::load and ::log_current_value --- | ||
| 406 | |||
| 407 | // For int | ||
| 408 | template <> | ||
| 409 | 65 | void CallbackConfigItem<int>::load(CSimpleIniA &ini, [[maybe_unused]] Logger &logger) | |
| 410 | { | ||
| 411 | 65 | current_value = static_cast<int>(ini.GetLongValue(section.c_str(), ini_key.c_str(), default_value)); | |
| 412 | 65 | } | |
| 413 | |||
| 414 | template <> | ||
| 415 | 4 | void CallbackConfigItem<int>::log_current_value(Logger &logger) const | |
| 416 | { | ||
| 417 |
1/2✓ Branch 2 → 3 taken 4 times.
✗ Branch 2 → 4 not taken.
|
4 | logger.debug("Config: {} = {}", ini_key, current_value); |
| 418 | 4 | } | |
| 419 | |||
| 420 | // For float | ||
| 421 | template <> | ||
| 422 | 8 | void CallbackConfigItem<float>::load(CSimpleIniA &ini, [[maybe_unused]] Logger &logger) | |
| 423 | { | ||
| 424 | 8 | current_value = static_cast<float>(ini.GetDoubleValue(section.c_str(), ini_key.c_str(), static_cast<double>(default_value))); | |
| 425 | 8 | } | |
| 426 | |||
| 427 | template <> | ||
| 428 | 1 | void CallbackConfigItem<float>::log_current_value(Logger &logger) const | |
| 429 | { | ||
| 430 |
1/2✓ Branch 2 → 3 taken 1 time.
✗ Branch 2 → 4 not taken.
|
1 | logger.debug("Config: {} = {}", ini_key, current_value); |
| 431 | 1 | } | |
| 432 | |||
| 433 | // For bool | ||
| 434 | template <> | ||
| 435 | 12 | void CallbackConfigItem<bool>::load(CSimpleIniA &ini, [[maybe_unused]] Logger &logger) | |
| 436 | { | ||
| 437 | 12 | current_value = ini.GetBoolValue(section.c_str(), ini_key.c_str(), default_value); | |
| 438 | 12 | } | |
| 439 | |||
| 440 | template <> | ||
| 441 | 1 | void CallbackConfigItem<bool>::log_current_value(Logger &logger) const | |
| 442 | { | ||
| 443 |
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"); |
| 444 | 1 | } | |
| 445 | |||
| 446 | // For std::string | ||
| 447 | template <> | ||
| 448 | 10 | void CallbackConfigItem<std::string>::load(CSimpleIniA &ini, [[maybe_unused]] Logger &logger) | |
| 449 | { | ||
| 450 | 10 | current_value = ini.GetValue(section.c_str(), ini_key.c_str(), default_value.c_str()); | |
| 451 | 10 | } | |
| 452 | |||
| 453 | template <> | ||
| 454 | 2 | void CallbackConfigItem<std::string>::log_current_value(Logger &logger) const | |
| 455 | { | ||
| 456 |
1/2✓ Branch 2 → 3 taken 2 times.
✗ Branch 2 → 4 not taken.
|
2 | logger.debug("Config: {} = \"{}\"", ini_key, current_value); |
| 457 | 2 | } | |
| 458 | |||
| 459 | // For Config::KeyComboList (list of key combinations) | ||
| 460 | template <> | ||
| 461 | 50 | void CallbackConfigItem<Config::KeyComboList>::load(CSimpleIniA &ini, [[maybe_unused]] Logger &logger) | |
| 462 | { | ||
| 463 | 50 | const char *ini_value_str = ini.GetValue(section.c_str(), ini_key.c_str(), nullptr); | |
| 464 |
2/2✓ Branch 5 → 6 taken 30 times.
✓ Branch 5 → 16 taken 20 times.
|
50 | if (ini_value_str != nullptr) |
| 465 | { | ||
| 466 |
2/4✓ Branch 9 → 10 taken 30 times.
✗ Branch 9 → 20 not taken.
✓ Branch 10 → 11 taken 30 times.
✗ Branch 10 → 18 not taken.
|
90 | current_value = parse_key_combo_list(ini_value_str, log_key_name); |
| 467 | } | ||
| 468 | else | ||
| 469 | { | ||
| 470 | 20 | current_value = default_value; | |
| 471 | } | ||
| 472 | 50 | } | |
| 473 | |||
| 474 | template <> | ||
| 475 | 3 | void CallbackConfigItem<Config::KeyComboList>::log_current_value(Logger &logger) const | |
| 476 | { | ||
| 477 |
1/2✓ Branch 2 → 3 taken 3 times.
✗ Branch 2 → 15 not taken.
|
3 | const std::string formatted = format_key_combo_list(current_value); |
| 478 |
2/2✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 7 taken 2 times.
|
3 | if (formatted.empty()) |
| 479 | { | ||
| 480 |
1/2✓ Branch 5 → 6 taken 1 time.
✗ Branch 5 → 11 not taken.
|
1 | logger.debug("Config: {} = (none)", ini_key); |
| 481 | } | ||
| 482 | else | ||
| 483 | { | ||
| 484 |
1/2✓ Branch 7 → 8 taken 2 times.
✗ Branch 7 → 12 not taken.
|
2 | logger.debug("Config: {} = {}", ini_key, formatted); |
| 485 | } | ||
| 486 | 3 | } | |
| 487 | |||
| 488 | // --- Global storage for registered configuration items --- | ||
| 489 | 603 | std::mutex &getConfigMutex() | |
| 490 | { | ||
| 491 |
3/4✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 8 taken 602 times.
✓ Branch 4 → 5 taken 1 time.
✗ Branch 4 → 8 not taken.
|
603 | static std::mutex mtx; |
| 492 | 603 | return mtx; | |
| 493 | } | ||
| 494 | |||
| 495 | 812 | std::vector<std::unique_ptr<ConfigItemBase>> &getRegisteredConfigItems() | |
| 496 | { | ||
| 497 | // Function-local static to ensure controlled initialization order. | ||
| 498 |
3/4✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 7 taken 811 times.
✓ Branch 4 → 5 taken 1 time.
✗ Branch 4 → 7 not taken.
|
812 | static std::vector<std::unique_ptr<ConfigItemBase>> s_registered_items; |
| 499 | 812 | return s_registered_items; | |
| 500 | } | ||
| 501 | |||
| 502 | // Holds the INI path last passed to Config::load(). Empty until the | ||
| 503 | // first load() call -- reload() returns false in that window. | ||
| 504 | // Caller must hold getConfigMutex() when reading or writing. | ||
| 505 | 415 | std::string &getLastLoadedIniPath() | |
| 506 | { | ||
| 507 |
3/4✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 7 taken 414 times.
✓ Branch 4 → 5 taken 1 time.
✗ Branch 4 → 7 not taken.
|
415 | static std::string s_last_loaded_ini_path; |
| 508 | 415 | return s_last_loaded_ini_path; | |
| 509 | } | ||
| 510 | |||
| 511 | // Content hash of the bytes last successfully loaded from the INI file. | ||
| 512 | // std::nullopt until the first successful load() (or after | ||
| 513 | // clear_registered_items(), which wipes it alongside the path). | ||
| 514 | // Caller must hold getConfigMutex() when reading or writing. | ||
| 515 | 433 | std::optional<std::uint64_t> &getLastLoadedIniHash() | |
| 516 | { | ||
| 517 | static std::optional<std::uint64_t> s_last_loaded_ini_hash; | ||
| 518 | 433 | return s_last_loaded_ini_hash; | |
| 519 | } | ||
| 520 | |||
| 521 | /** | ||
| 522 | * @brief 64-bit FNV-1a hash over a raw byte range. | ||
| 523 | * @details Computed on the disk bytes (pre-parse) so cosmetic churn by | ||
| 524 | * SimpleIni's own parser (comment stripping, whitespace | ||
| 525 | * normalisation) cannot skew the result. Produces a stable | ||
| 526 | * value on any platform without pulling in a dependency. | ||
| 527 | */ | ||
| 528 | 102 | [[nodiscard]] std::uint64_t fnv1a_64(const std::vector<std::uint8_t> &bytes) noexcept | |
| 529 | { | ||
| 530 | 102 | constexpr std::uint64_t offset{0xcbf29ce484222325ULL}; | |
| 531 | 102 | constexpr std::uint64_t prime{0x00000100000001b3ULL}; | |
| 532 | 102 | std::uint64_t h{offset}; | |
| 533 |
2/2✓ Branch 15 → 4 taken 2566 times.
✓ Branch 15 → 16 taken 102 times.
|
2770 | for (std::uint8_t b : bytes) |
| 534 | { | ||
| 535 | 2566 | h ^= static_cast<std::uint64_t>(b); | |
| 536 | 2566 | h *= prime; | |
| 537 | } | ||
| 538 | 102 | return h; | |
| 539 | } | ||
| 540 | |||
| 541 | /** | ||
| 542 | * @brief Reads all bytes of @p path into memory. | ||
| 543 | * @details Returns std::nullopt when the file cannot be opened (e.g. | ||
| 544 | * mid-save by an editor that locks exclusively). Callers | ||
| 545 | * should treat a nullopt return as "unable to verify | ||
| 546 | * content; proceed with a full reload" -- erring on the | ||
| 547 | * side of reloading is safer than skipping a real change. | ||
| 548 | */ | ||
| 549 | [[nodiscard]] std::optional<std::vector<std::uint8_t>> | ||
| 550 | 133 | read_ini_bytes(const std::filesystem::path &path) noexcept | |
| 551 | { | ||
| 552 | try | ||
| 553 | { | ||
| 554 |
1/2✓ Branch 2 → 3 taken 133 times.
✗ Branch 2 → 44 not taken.
|
133 | std::ifstream in(path, std::ios::binary); |
| 555 |
3/4✓ Branch 3 → 4 taken 133 times.
✗ Branch 3 → 42 not taken.
✓ Branch 4 → 5 taken 31 times.
✓ Branch 4 → 6 taken 102 times.
|
133 | if (!in) |
| 556 | { | ||
| 557 | 31 | return std::nullopt; | |
| 558 | } | ||
| 559 |
1/2✓ Branch 6 → 7 taken 102 times.
✗ Branch 6 → 42 not taken.
|
102 | in.seekg(0, std::ios::end); |
| 560 |
1/2✓ Branch 7 → 8 taken 102 times.
✗ Branch 7 → 36 not taken.
|
102 | const std::streamsize size = in.tellg(); |
| 561 |
1/2✓ Branch 9 → 10 taken 102 times.
✗ Branch 9 → 42 not taken.
|
102 | in.seekg(0, std::ios::beg); |
| 562 |
2/2✓ Branch 10 → 11 taken 3 times.
✓ Branch 10 → 15 taken 99 times.
|
102 | if (size <= 0) |
| 563 | { | ||
| 564 | 3 | return std::vector<std::uint8_t>{}; | |
| 565 | } | ||
| 566 |
1/2✓ Branch 17 → 18 taken 99 times.
✗ Branch 17 → 37 not taken.
|
99 | std::vector<std::uint8_t> buf(static_cast<std::size_t>(size)); |
| 567 |
1/2✓ Branch 20 → 21 taken 99 times.
✗ Branch 20 → 40 not taken.
|
99 | in.read(reinterpret_cast<char *>(buf.data()), size); |
| 568 |
3/10✓ Branch 21 → 22 taken 99 times.
✗ Branch 21 → 40 not taken.
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 26 taken 99 times.
✗ Branch 23 → 24 not taken.
✗ Branch 23 → 40 not taken.
✗ Branch 24 → 25 not taken.
✗ Branch 24 → 26 not taken.
✗ Branch 27 → 28 not taken.
✓ Branch 27 → 29 taken 99 times.
|
99 | if (!in && !in.eof()) |
| 569 | { | ||
| 570 | ✗ | return std::nullopt; | |
| 571 | } | ||
| 572 |
1/2✓ Branch 30 → 31 taken 99 times.
✗ Branch 30 → 40 not taken.
|
99 | buf.resize(static_cast<std::size_t>(in.gcount())); |
| 573 | 99 | return buf; | |
| 574 | 133 | } | |
| 575 | ✗ | catch (...) | |
| 576 | { | ||
| 577 | ✗ | return std::nullopt; | |
| 578 | ✗ | } | |
| 579 | } | ||
| 580 | |||
| 581 | /** | ||
| 582 | * @brief Result of read-hash-parse pipeline used by load() and reload(). | ||
| 583 | */ | ||
| 584 | struct IniLoadOutcome | ||
| 585 | { | ||
| 586 | bool read_succeeded{false}; ///< Bytes successfully read from disk | ||
| 587 | bool parse_succeeded{false}; ///< CSimpleIniA::LoadData returned SI_OK | ||
| 588 | SI_Error parse_rc{SI_OK}; ///< Raw SimpleIni return code (when read_succeeded) | ||
| 589 | std::optional<std::uint64_t> hash; ///< FNV-1a hash of the read bytes | ||
| 590 | }; | ||
| 591 | |||
| 592 | /** | ||
| 593 | * @brief Reads the INI bytes once, computes their hash, and feeds | ||
| 594 | * those exact bytes to CSimpleIniA::LoadData. | ||
| 595 | * @details Closes the TOCTOU window where LoadFile would re-read the | ||
| 596 | * file after our byte snapshot: if the file was rewritten | ||
| 597 | * between the two reads, the cached hash would reflect one | ||
| 598 | * version and the parsed INI another. By using LoadData on | ||
| 599 | * the already-buffered bytes, the hash and the parse are | ||
| 600 | * guaranteed to reflect the same file state. | ||
| 601 | * @param path Absolute path to the INI file. | ||
| 602 | * @param ini SimpleIni instance to populate. | ||
| 603 | * @return IniLoadOutcome describing each pipeline stage. | ||
| 604 | */ | ||
| 605 | [[nodiscard]] IniLoadOutcome | ||
| 606 | 133 | load_ini_into(const std::filesystem::path &path, CSimpleIniA &ini) noexcept | |
| 607 | { | ||
| 608 | 133 | IniLoadOutcome outcome{}; | |
| 609 | 133 | auto bytes = read_ini_bytes(path); | |
| 610 |
2/2✓ Branch 4 → 5 taken 31 times.
✓ Branch 4 → 6 taken 102 times.
|
133 | if (!bytes.has_value()) |
| 611 | { | ||
| 612 | 31 | return outcome; | |
| 613 | } | ||
| 614 | 102 | outcome.read_succeeded = true; | |
| 615 | 102 | outcome.hash = fnv1a_64(*bytes); | |
| 616 | |||
| 617 | // CSimpleIniA::LoadData(const char*, size_t). Empty buffers are | ||
| 618 | // accepted by SimpleIni (SI_OK, zero sections) -- we still | ||
| 619 | // preserve the hash so an empty file can be content-hash-skipped. | ||
| 620 | try | ||
| 621 | { | ||
| 622 | 102 | const char *data_ptr = bytes->empty() | |
| 623 |
2/2✓ Branch 11 → 12 taken 3 times.
✓ Branch 11 → 13 taken 99 times.
|
102 | ? "" |
| 624 | 102 | : reinterpret_cast<const char *>(bytes->data()); | |
| 625 |
1/2✓ Branch 17 → 18 taken 102 times.
✗ Branch 17 → 23 not taken.
|
102 | outcome.parse_rc = ini.LoadData(data_ptr, bytes->size()); |
| 626 | 102 | outcome.parse_succeeded = (outcome.parse_rc >= 0); | |
| 627 | } | ||
| 628 | ✗ | catch (...) | |
| 629 | { | ||
| 630 | ✗ | outcome.parse_rc = SI_FAIL; | |
| 631 | ✗ | outcome.parse_succeeded = false; | |
| 632 | ✗ | } | |
| 633 | 102 | return outcome; | |
| 634 | 133 | } | |
| 635 | |||
| 636 | // Filesystem watcher owned by enable_auto_reload(). Separate mutex so | ||
| 637 | // start / stop transitions do not contend with registration traffic. | ||
| 638 | 330 | std::mutex &getWatcherMutex() | |
| 639 | { | ||
| 640 |
3/4✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 8 taken 329 times.
✓ Branch 4 → 5 taken 1 time.
✗ Branch 4 → 8 not taken.
|
330 | static std::mutex mtx; |
| 641 | 330 | return mtx; | |
| 642 | } | ||
| 643 | |||
| 644 | 25 | std::unique_ptr<ConfigWatcher> &getConfigWatcher() | |
| 645 | { | ||
| 646 |
3/4✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 7 taken 24 times.
✓ Branch 4 → 5 taken 1 time.
✗ Branch 4 → 7 not taken.
|
25 | static std::unique_ptr<ConfigWatcher> s_watcher; |
| 647 | 25 | return s_watcher; | |
| 648 | } | ||
| 649 | |||
| 650 | // Keeps reload-hotkey InputBindingGuards alive for the process lifetime. | ||
| 651 | // Returning the guard by value from register_reload_hotkey would | ||
| 652 | // immediately destroy it (the call site has nowhere to store it), and | ||
| 653 | // ~InputBindingGuard flips the binding's enabled flag to false, so the | ||
| 654 | // press callback would silently no-op forever. Protected by | ||
| 655 | // getWatcherMutex() because it already serialises lifetime state that | ||
| 656 | // lives alongside the watcher (both are Config-wide, not per-item). | ||
| 657 | 302 | std::vector<DetourModKit::Config::InputBindingGuard> &getReloadHotkeyGuards() noexcept | |
| 658 | { | ||
| 659 |
3/4✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 7 taken 301 times.
✓ Branch 4 → 5 taken 1 time.
✗ Branch 4 → 7 not taken.
|
302 | static std::vector<DetourModKit::Config::InputBindingGuard> s_guards; |
| 660 | 302 | return s_guards; | |
| 661 | } | ||
| 662 | |||
| 663 | /** | ||
| 664 | * @class ReloadServicer | ||
| 665 | * @brief Background thread that coalesces reload-hotkey presses and | ||
| 666 | * invokes Config::reload() off the InputManager poll thread. | ||
| 667 | * @details The hotkey press callback must return in microseconds so | ||
| 668 | * other hotkeys do not jitter while a 30-item INI parse runs. | ||
| 669 | * The servicer latches a pending-reload flag; its worker | ||
| 670 | * thread blocks on a condition variable, drains the flag on | ||
| 671 | * wake, and invokes reload() at most once per batch of | ||
| 672 | * presses. Exceptions from reload() are caught so the | ||
| 673 | * servicer never dies. | ||
| 674 | * | ||
| 675 | * Lazy lifetime: created on the first register_reload_hotkey | ||
| 676 | * call, kept alive until clear_registered_items() tears it | ||
| 677 | * down. Shared via std::shared_ptr so a press callback that | ||
| 678 | * races with shutdown cannot dereference a freed channel. | ||
| 679 | */ | ||
| 680 | class ReloadServicer | ||
| 681 | { | ||
| 682 | public: | ||
| 683 | 2 | ReloadServicer() | |
| 684 | 2 | { | |
| 685 | // Launch the servicer worker. StoppableWorker passes its | ||
| 686 | // own stop_token into the body; we observe it via | ||
| 687 | // stop_requested() inside the wait predicate. To make | ||
| 688 | // request_stop() wake a currently blocked cv.wait, we | ||
| 689 | // install a stop_callback on the body's token (captured | ||
| 690 | // inside service_loop) that flips m_shutdown and notifies | ||
| 691 | // the CV. | ||
| 692 | 2 | m_worker = std::make_unique<DetourModKit::StoppableWorker>( | |
| 693 | "ConfigReloadServicer", | ||
| 694 |
1/2✓ Branch 7 → 8 taken 2 times.
✗ Branch 7 → 11 not taken.
|
2 | [this](std::stop_token st) |
| 695 | { | ||
| 696 | 4 | service_loop(std::move(st)); | |
| 697 | 2 | }); | |
| 698 | 2 | } | |
| 699 | |||
| 700 | 2 | ~ReloadServicer() noexcept | |
| 701 | { | ||
| 702 | // Flip the shutdown flag and wake the worker before the | ||
| 703 | // StoppableWorker destructor asks it to stop + join. | ||
| 704 | // notify_all() is harmless if the worker already exited. | ||
| 705 | { | ||
| 706 | 2 | std::lock_guard<std::mutex> lock(m_mutex); | |
| 707 | 2 | m_shutdown.store(true, std::memory_order_release); | |
| 708 | 2 | } | |
| 709 | 2 | m_cv.notify_all(); | |
| 710 | |||
| 711 | // ~StoppableWorker requests stop + joins (or detaches under | ||
| 712 | // loader lock). Safe to let it run as-is. | ||
| 713 | 2 | m_worker.reset(); | |
| 714 | 2 | } | |
| 715 | |||
| 716 | ReloadServicer(const ReloadServicer &) = delete; | ||
| 717 | ReloadServicer &operator=(const ReloadServicer &) = delete; | ||
| 718 | ReloadServicer(ReloadServicer &&) = delete; | ||
| 719 | ReloadServicer &operator=(ReloadServicer &&) = delete; | ||
| 720 | |||
| 721 | /** | ||
| 722 | * @brief Requests a reload. noexcept and allocation-free on the | ||
| 723 | * fast path; the press callback uses this and must not | ||
| 724 | * throw back onto the InputManager poll thread. | ||
| 725 | */ | ||
| 726 | ✗ | void request_reload() noexcept | |
| 727 | { | ||
| 728 | // The predicate variable m_reload_requested must be mutated | ||
| 729 | // under m_mutex (or at minimum the notifier must take the | ||
| 730 | // mutex before notify_one) to close the lost-wakeup window | ||
| 731 | // on the waiter side: waiter evaluates the predicate false | ||
| 732 | // (pre-lock), then parks; if we stored + notified in that | ||
| 733 | // gap without touching the mutex, the press could be | ||
| 734 | // dropped until the next one. Taking the mutex here serialises | ||
| 735 | // against the waiter's predicate re-check under m_mutex, | ||
| 736 | // making the wakeup observation guaranteed. | ||
| 737 | { | ||
| 738 | ✗ | std::lock_guard<std::mutex> lock(m_mutex); | |
| 739 | ✗ | m_reload_requested.store(true, std::memory_order_release); | |
| 740 | ✗ | } | |
| 741 | ✗ | m_cv.notify_one(); | |
| 742 | ✗ | } | |
| 743 | |||
| 744 | private: | ||
| 745 | 2 | void service_loop(std::stop_token st) noexcept | |
| 746 | { | ||
| 747 | 2 | DetourModKit::Logger &logger = DetourModKit::Logger::get_instance(); | |
| 748 | |||
| 749 | // Wake the CV when the worker is asked to stop so the blocked | ||
| 750 | // wait exits promptly instead of waiting for the next press. | ||
| 751 | std::stop_callback stop_cb( | ||
| 752 | st, | ||
| 753 | 6 | [this]() -> void | |
| 754 | { | ||
| 755 | { | ||
| 756 |
1/2✓ Branch 2 → 3 taken 2 times.
✗ Branch 2 → 7 not taken.
|
2 | std::lock_guard<std::mutex> lock(m_mutex); |
| 757 | 2 | m_shutdown.store(true, std::memory_order_release); | |
| 758 | 2 | } | |
| 759 | 2 | m_cv.notify_all(); | |
| 760 | 4 | }); | |
| 761 | |||
| 762 |
2/4✓ Branch 23 → 24 taken 2 times.
✗ Branch 23 → 27 not taken.
✓ Branch 28 → 5 taken 2 times.
✗ Branch 28 → 29 not taken.
|
4 | while (!st.stop_requested() && |
| 763 |
1/2✓ Branch 25 → 26 taken 2 times.
✗ Branch 25 → 27 not taken.
|
2 | !m_shutdown.load(std::memory_order_acquire)) |
| 764 | { | ||
| 765 | { | ||
| 766 | 2 | std::unique_lock<std::mutex> lock(m_mutex); | |
| 767 | 2 | m_cv.wait(lock, [&]() | |
| 768 | { | ||
| 769 |
1/2✓ Branch 5 → 6 taken 2 times.
✗ Branch 5 → 8 not taken.
|
6 | return st.stop_requested() || |
| 770 |
3/4✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 8 taken 2 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 2 times.
|
6 | m_shutdown.load(std::memory_order_acquire) || |
| 771 | 6 | m_reload_requested.load(std::memory_order_acquire); | |
| 772 | }); | ||
| 773 | 2 | } | |
| 774 | |||
| 775 |
2/6✗ Branch 9 → 10 not taken.
✓ Branch 9 → 12 taken 2 times.
✗ Branch 11 → 12 not taken.
✗ Branch 11 → 13 not taken.
✓ Branch 14 → 15 taken 2 times.
✗ Branch 14 → 16 not taken.
|
2 | if (st.stop_requested() || |
| 776 | ✗ | m_shutdown.load(std::memory_order_acquire)) | |
| 777 | { | ||
| 778 | 2 | break; | |
| 779 | } | ||
| 780 | |||
| 781 | // Coalesce: a burst of presses during the reload below | ||
| 782 | // collapses into at most one follow-up pass because the | ||
| 783 | // next iteration will exchange the flag once. | ||
| 784 | ✗ | while (m_reload_requested.exchange(false, std::memory_order_acq_rel)) | |
| 785 | { | ||
| 786 | try | ||
| 787 | { | ||
| 788 | ✗ | (void)DetourModKit::Config::reload(); | |
| 789 | } | ||
| 790 | ✗ | catch (const std::exception &e) | |
| 791 | { | ||
| 792 | ✗ | logger.error( | |
| 793 | ✗ | "Config: reload servicer caught exception: {}", e.what()); | |
| 794 | ✗ | } | |
| 795 | ✗ | catch (...) | |
| 796 | { | ||
| 797 | ✗ | logger.error( | |
| 798 | "Config: reload servicer caught unknown exception."); | ||
| 799 | ✗ | } | |
| 800 | } | ||
| 801 | } | ||
| 802 | 2 | } | |
| 803 | |||
| 804 | std::mutex m_mutex; | ||
| 805 | std::condition_variable m_cv; | ||
| 806 | std::atomic<bool> m_reload_requested{false}; | ||
| 807 | std::atomic<bool> m_shutdown{false}; | ||
| 808 | std::unique_ptr<DetourModKit::StoppableWorker> m_worker; | ||
| 809 | }; | ||
| 810 | |||
| 811 | // Shared_ptr so a press callback holding its own strong reference | ||
| 812 | // cannot crash when clear_registered_items() resets the slot. | ||
| 813 | 302 | std::shared_ptr<ReloadServicer> &getReloadServicer() noexcept | |
| 814 | { | ||
| 815 |
3/4✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 7 taken 301 times.
✓ Branch 4 → 5 taken 1 time.
✗ Branch 4 → 7 not taken.
|
302 | static std::shared_ptr<ReloadServicer> s_servicer; |
| 816 | 302 | return s_servicer; | |
| 817 | } | ||
| 818 | |||
| 819 | /// Replaces an existing item with the same section+key, or appends if none found. | ||
| 820 | /// Caller must hold getConfigMutex(). | ||
| 821 | 151 | void replace_or_append(std::unique_ptr<ConfigItemBase> item) | |
| 822 | { | ||
| 823 | 151 | auto &items = getRegisteredConfigItems(); | |
| 824 |
2/2✓ Branch 31 → 5 taken 51 times.
✓ Branch 31 → 32 taken 147 times.
|
349 | for (auto &existing : items) |
| 825 | { | ||
| 826 |
6/6✓ Branch 10 → 11 taken 39 times.
✓ Branch 10 → 16 taken 12 times.
✓ Branch 14 → 15 taken 4 times.
✓ Branch 14 → 16 taken 35 times.
✓ Branch 17 → 18 taken 4 times.
✓ Branch 17 → 22 taken 47 times.
|
51 | if (existing->section == item->section && existing->ini_key == item->ini_key) |
| 827 | { | ||
| 828 | 4 | existing = std::move(item); | |
| 829 | 4 | return; | |
| 830 | } | ||
| 831 | } | ||
| 832 | 147 | items.push_back(std::move(item)); | |
| 833 | } | ||
| 834 | |||
| 835 | /** | ||
| 836 | * @brief Determines the full absolute path for the INI configuration file. | ||
| 837 | */ | ||
| 838 | 141 | std::filesystem::path getIniFilePath(const std::string &ini_filename, Logger &logger) | |
| 839 | { | ||
| 840 |
1/2✓ Branch 2 → 3 taken 141 times.
✗ Branch 2 → 68 not taken.
|
141 | std::wstring module_dir = get_runtime_directory(); |
| 841 | |||
| 842 |
4/8✓ Branch 4 → 5 taken 141 times.
✗ Branch 4 → 7 not taken.
✓ Branch 5 → 6 taken 141 times.
✗ Branch 5 → 66 not taken.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 141 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 13 taken 141 times.
|
141 | if (module_dir.empty() || module_dir == L".") |
| 843 | { | ||
| 844 | ✗ | logger.warning("Config: Could not reliably determine module directory or it's current working directory. Using relative path for INI: {}", ini_filename); | |
| 845 | ✗ | return std::filesystem::path(ini_filename); // Fallback to relative path | |
| 846 | } | ||
| 847 | |||
| 848 | try | ||
| 849 | { | ||
| 850 |
4/8✓ Branch 13 → 14 taken 141 times.
✗ Branch 13 → 39 not taken.
✓ Branch 14 → 15 taken 141 times.
✗ Branch 14 → 36 not taken.
✓ Branch 15 → 16 taken 141 times.
✗ Branch 15 → 34 not taken.
✓ Branch 16 → 17 taken 141 times.
✗ Branch 16 → 32 not taken.
|
141 | std::filesystem::path ini_path_obj = (std::filesystem::path(module_dir) / ini_filename).lexically_normal(); |
| 851 |
2/4✓ Branch 20 → 21 taken 141 times.
✗ Branch 20 → 44 not taken.
✓ Branch 21 → 22 taken 141 times.
✗ Branch 21 → 41 not taken.
|
141 | logger.debug("Config: Determined INI file path: {}", ini_path_obj.string()); |
| 852 | 141 | return ini_path_obj; | |
| 853 | 141 | } | |
| 854 | ✗ | catch (const std::filesystem::filesystem_error &fs_err) | |
| 855 | { | ||
| 856 | ✗ | logger.warning("Config: Filesystem error constructing INI path: {}. Using relative path for INI: {}", fs_err.what(), ini_filename); | |
| 857 | ✗ | } | |
| 858 | ✗ | catch (const std::exception &e) | |
| 859 | { | ||
| 860 | ✗ | logger.warning("Config: General error constructing INI path: {}. Using relative path for INI: {}", e.what(), ini_filename); | |
| 861 | ✗ | } | |
| 862 | ✗ | return std::filesystem::path(ini_filename); // Fallback | |
| 863 | 141 | } | |
| 864 | |||
| 865 | } // anonymous namespace | ||
| 866 | |||
| 867 | // All register_* functions use the deferred callback pattern: state is | ||
| 868 | // mutated under getConfigMutex(), but the setter callback is invoked after | ||
| 869 | // the lock is released. This allows setters to call back into the Config | ||
| 870 | // API without deadlocking (no reentrancy guard needed). | ||
| 871 | 50 | void DetourModKit::Config::register_int(std::string_view section, std::string_view ini_key, | |
| 872 | std::string_view log_key_name, std::function<void(int)> setter, | ||
| 873 | int default_value) | ||
| 874 | { | ||
| 875 | 50 | std::function<void()> deferred; | |
| 876 | { | ||
| 877 |
1/2✓ Branch 4 → 5 taken 50 times.
✗ Branch 4 → 71 not taken.
|
50 | std::lock_guard<std::mutex> lock(getConfigMutex()); |
| 878 |
1/2✓ Branch 16 → 17 taken 50 times.
✗ Branch 16 → 39 not taken.
|
50 | replace_or_append( |
| 879 |
4/8✓ Branch 7 → 8 taken 50 times.
✗ Branch 7 → 57 not taken.
✓ Branch 10 → 11 taken 50 times.
✗ Branch 10 → 51 not taken.
✓ Branch 13 → 14 taken 50 times.
✗ Branch 13 → 45 not taken.
✓ Branch 14 → 15 taken 50 times.
✗ Branch 14 → 43 not taken.
|
300 | std::make_unique<CallbackConfigItem<int>>(std::string(section), std::string(ini_key), std::string(log_key_name), setter, default_value)); |
| 880 |
2/2✓ Branch 26 → 27 taken 47 times.
✓ Branch 26 → 33 taken 3 times.
|
50 | if (setter) |
| 881 | { | ||
| 882 |
2/6✓ Branch 27 → 28 taken 47 times.
✗ Branch 27 → 68 not taken.
✗ Branch 30 → 31 not taken.
✓ Branch 30 → 32 taken 47 times.
✗ Branch 65 → 66 not taken.
✗ Branch 65 → 67 not taken.
|
94 | deferred = [setter, default_value]() |
| 883 |
1/2✓ Branch 28 → 29 taken 47 times.
✗ Branch 28 → 63 not taken.
|
94 | { setter(default_value); }; |
| 884 | } | ||
| 885 | 50 | } | |
| 886 |
2/2✓ Branch 35 → 36 taken 47 times.
✓ Branch 35 → 37 taken 3 times.
|
50 | if (deferred) |
| 887 | { | ||
| 888 |
1/2✓ Branch 36 → 37 taken 47 times.
✗ Branch 36 → 72 not taken.
|
47 | deferred(); |
| 889 | } | ||
| 890 | 50 | } | |
| 891 | |||
| 892 | 8 | void DetourModKit::Config::register_float(std::string_view section, std::string_view ini_key, | |
| 893 | std::string_view log_key_name, std::function<void(float)> setter, | ||
| 894 | float default_value) | ||
| 895 | { | ||
| 896 | 8 | std::function<void()> deferred; | |
| 897 | { | ||
| 898 |
1/2✓ Branch 4 → 5 taken 8 times.
✗ Branch 4 → 71 not taken.
|
8 | std::lock_guard<std::mutex> lock(getConfigMutex()); |
| 899 |
1/2✓ Branch 16 → 17 taken 8 times.
✗ Branch 16 → 39 not taken.
|
8 | replace_or_append( |
| 900 |
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)); |
| 901 |
1/2✓ Branch 26 → 27 taken 8 times.
✗ Branch 26 → 33 not taken.
|
8 | if (setter) |
| 902 | { | ||
| 903 |
2/6✓ Branch 27 → 28 taken 8 times.
✗ Branch 27 → 68 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]() |
| 904 |
1/2✓ Branch 28 → 29 taken 8 times.
✗ Branch 28 → 63 not taken.
|
16 | { setter(default_value); }; |
| 905 | } | ||
| 906 | 8 | } | |
| 907 |
1/2✓ Branch 35 → 36 taken 8 times.
✗ Branch 35 → 37 not taken.
|
8 | if (deferred) |
| 908 | { | ||
| 909 |
1/2✓ Branch 36 → 37 taken 8 times.
✗ Branch 36 → 72 not taken.
|
8 | deferred(); |
| 910 | } | ||
| 911 | 8 | } | |
| 912 | |||
| 913 | 11 | void DetourModKit::Config::register_bool(std::string_view section, std::string_view ini_key, | |
| 914 | std::string_view log_key_name, std::function<void(bool)> setter, | ||
| 915 | bool default_value) | ||
| 916 | { | ||
| 917 | 11 | std::function<void()> deferred; | |
| 918 | { | ||
| 919 |
1/2✓ Branch 4 → 5 taken 11 times.
✗ Branch 4 → 71 not taken.
|
11 | std::lock_guard<std::mutex> lock(getConfigMutex()); |
| 920 |
1/2✓ Branch 16 → 17 taken 11 times.
✗ Branch 16 → 39 not taken.
|
11 | replace_or_append( |
| 921 |
4/8✓ Branch 7 → 8 taken 11 times.
✗ Branch 7 → 57 not taken.
✓ Branch 10 → 11 taken 11 times.
✗ Branch 10 → 51 not taken.
✓ Branch 13 → 14 taken 11 times.
✗ Branch 13 → 45 not taken.
✓ Branch 14 → 15 taken 11 times.
✗ Branch 14 → 43 not taken.
|
66 | std::make_unique<CallbackConfigItem<bool>>(std::string(section), std::string(ini_key), std::string(log_key_name), setter, default_value)); |
| 922 |
1/2✓ Branch 26 → 27 taken 11 times.
✗ Branch 26 → 33 not taken.
|
11 | if (setter) |
| 923 | { | ||
| 924 |
2/6✓ Branch 27 → 28 taken 11 times.
✗ Branch 27 → 68 not taken.
✗ Branch 30 → 31 not taken.
✓ Branch 30 → 32 taken 11 times.
✗ Branch 65 → 66 not taken.
✗ Branch 65 → 67 not taken.
|
22 | deferred = [setter, default_value]() |
| 925 |
1/2✓ Branch 28 → 29 taken 11 times.
✗ Branch 28 → 63 not taken.
|
22 | { setter(default_value); }; |
| 926 | } | ||
| 927 | 11 | } | |
| 928 |
1/2✓ Branch 35 → 36 taken 11 times.
✗ Branch 35 → 37 not taken.
|
11 | if (deferred) |
| 929 | { | ||
| 930 |
1/2✓ Branch 36 → 37 taken 11 times.
✗ Branch 36 → 72 not taken.
|
11 | deferred(); |
| 931 | } | ||
| 932 | 11 | } | |
| 933 | |||
| 934 | 13 | void DetourModKit::Config::register_string(std::string_view section, std::string_view ini_key, | |
| 935 | std::string_view log_key_name, std::function<void(const std::string &)> setter, | ||
| 936 | std::string default_value) | ||
| 937 | { | ||
| 938 | 13 | std::function<void()> deferred; | |
| 939 | { | ||
| 940 |
1/2✓ Branch 4 → 5 taken 13 times.
✗ Branch 4 → 74 not taken.
|
13 | std::lock_guard<std::mutex> lock(getConfigMutex()); |
| 941 |
1/2✓ Branch 16 → 17 taken 13 times.
✗ Branch 16 → 42 not taken.
|
13 | replace_or_append( |
| 942 |
4/8✓ Branch 7 → 8 taken 13 times.
✗ Branch 7 → 60 not taken.
✓ Branch 10 → 11 taken 13 times.
✗ Branch 10 → 54 not taken.
✓ Branch 13 → 14 taken 13 times.
✗ Branch 13 → 48 not taken.
✓ Branch 14 → 15 taken 13 times.
✗ Branch 14 → 46 not taken.
|
78 | std::make_unique<CallbackConfigItem<std::string>>(std::string(section), std::string(ini_key), std::string(log_key_name), setter, default_value)); |
| 943 |
2/2✓ Branch 26 → 27 taken 12 times.
✓ Branch 26 → 36 taken 1 time.
|
13 | if (setter) |
| 944 | { | ||
| 945 |
2/6✓ Branch 27 → 28 taken 12 times.
✗ Branch 27 → 71 not taken.
✗ Branch 33 → 34 not taken.
✓ Branch 33 → 35 taken 12 times.
✗ Branch 68 → 69 not taken.
✗ Branch 68 → 70 not taken.
|
36 | deferred = [setter, val = std::move(default_value)]() |
| 946 |
1/2✓ Branch 31 → 32 taken 12 times.
✗ Branch 31 → 66 not taken.
|
24 | { setter(val); }; |
| 947 | } | ||
| 948 | 13 | } | |
| 949 |
2/2✓ Branch 38 → 39 taken 12 times.
✓ Branch 38 → 40 taken 1 time.
|
13 | if (deferred) |
| 950 | { | ||
| 951 |
1/2✓ Branch 39 → 40 taken 12 times.
✗ Branch 39 → 75 not taken.
|
12 | deferred(); |
| 952 | } | ||
| 953 | 13 | } | |
| 954 | |||
| 955 | 1 | void DetourModKit::Config::register_log_level(std::string_view section, std::string_view ini_key, | |
| 956 | std::string_view default_value) | ||
| 957 | { | ||
| 958 |
1/2✓ Branch 7 → 8 taken 1 time.
✗ Branch 7 → 12 not taken.
|
1 | register_string(section, ini_key, "Log level", |
| 959 | 2 | [](const std::string &value) | |
| 960 | { | ||
| 961 | 2 | Logger::get_instance().set_log_level(Logger::string_to_log_level(value)); | |
| 962 | 2 | }, | |
| 963 |
1/2✓ Branch 4 → 5 taken 1 time.
✗ Branch 4 → 19 not taken.
|
2 | std::string(default_value)); |
| 964 | 1 | } | |
| 965 | |||
| 966 | 69 | void DetourModKit::Config::register_key_combo(std::string_view section, std::string_view ini_key, | |
| 967 | std::string_view log_key_name, std::function<void(const KeyComboList &)> setter, | ||
| 968 | std::string_view default_value_str) | ||
| 969 | { | ||
| 970 |
2/4✓ Branch 4 → 5 taken 69 times.
✗ Branch 4 → 51 not taken.
✓ Branch 5 → 6 taken 69 times.
✗ Branch 5 → 49 not taken.
|
69 | Config::KeyComboList default_combos = parse_key_combo_list(std::string(default_value_str), log_key_name); |
| 971 | |||
| 972 | 69 | std::function<void()> deferred; | |
| 973 | { | ||
| 974 |
1/2✓ Branch 10 → 11 taken 69 times.
✗ Branch 10 → 87 not taken.
|
69 | std::lock_guard<std::mutex> lock(getConfigMutex()); |
| 975 |
1/2✓ Branch 22 → 23 taken 69 times.
✗ Branch 22 → 55 not taken.
|
69 | replace_or_append( |
| 976 |
4/8✓ Branch 13 → 14 taken 69 times.
✗ Branch 13 → 73 not taken.
✓ Branch 16 → 17 taken 69 times.
✗ Branch 16 → 67 not taken.
✓ Branch 19 → 20 taken 69 times.
✗ Branch 19 → 61 not taken.
✓ Branch 20 → 21 taken 69 times.
✗ Branch 20 → 59 not taken.
|
414 | std::make_unique<CallbackConfigItem<Config::KeyComboList>>(std::string(section), std::string(ini_key), std::string(log_key_name), setter, default_combos)); |
| 977 |
2/2✓ Branch 32 → 33 taken 68 times.
✓ Branch 32 → 42 taken 1 time.
|
69 | if (setter) |
| 978 | { | ||
| 979 |
2/6✓ Branch 33 → 34 taken 68 times.
✗ Branch 33 → 84 not taken.
✗ Branch 39 → 40 not taken.
✓ Branch 39 → 41 taken 68 times.
✗ Branch 81 → 82 not taken.
✗ Branch 81 → 83 not taken.
|
204 | deferred = [setter, combos = std::move(default_combos)]() |
| 980 |
1/2✓ Branch 37 → 38 taken 68 times.
✗ Branch 37 → 79 not taken.
|
136 | { setter(combos); }; |
| 981 | } | ||
| 982 | 69 | } | |
| 983 |
2/2✓ Branch 44 → 45 taken 68 times.
✓ Branch 44 → 46 taken 1 time.
|
69 | if (deferred) |
| 984 | { | ||
| 985 |
1/2✓ Branch 45 → 46 taken 68 times.
✗ Branch 45 → 88 not taken.
|
68 | deferred(); |
| 986 | } | ||
| 987 | 69 | } | |
| 988 | |||
| 989 | 8 | DetourModKit::Config::InputBindingGuard DetourModKit::Config::register_press_combo( | |
| 990 | std::string_view section, | ||
| 991 | std::string_view ini_key, | ||
| 992 | std::string_view log_name, | ||
| 993 | std::string_view input_binding_name, | ||
| 994 | std::function<void()> on_press, | ||
| 995 | std::string_view default_value) | ||
| 996 | { | ||
| 997 |
1/2✓ Branch 2 → 3 taken 8 times.
✗ Branch 2 → 50 not taken.
|
8 | auto enabled_flag = std::make_shared<std::atomic<bool>>(true); |
| 998 |
3/6✓ Branch 5 → 6 taken 8 times.
✗ Branch 5 → 55 not taken.
✓ Branch 6 → 7 taken 8 times.
✗ Branch 6 → 53 not taken.
✓ Branch 7 → 8 taken 8 times.
✗ Branch 7 → 51 not taken.
|
16 | auto current_combos = std::make_shared<KeyComboList>(parse_key_combo_list(std::string(default_value), log_name)); |
| 999 |
1/2✓ Branch 13 → 14 taken 8 times.
✗ Branch 13 → 60 not taken.
|
8 | std::string binding_name_str(input_binding_name); |
| 1000 | |||
| 1001 |
4/10✓ Branch 16 → 17 taken 8 times.
✗ Branch 16 → 67 not taken.
✓ Branch 17 → 18 taken 8 times.
✗ Branch 17 → 65 not taken.
✓ Branch 18 → 19 taken 8 times.
✗ Branch 18 → 63 not taken.
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 23 taken 8 times.
✗ Branch 67 → 68 not taken.
✗ Branch 67 → 69 not taken.
|
8 | register_key_combo(section, ini_key, log_name, [current_combos, binding_name_str](const KeyComboList &combos) |
| 1002 | { | ||
| 1003 | 16 | *current_combos = combos; | |
| 1004 | 16 | InputManager::get_instance().update_binding_combos(binding_name_str, combos); }, default_value); | |
| 1005 | |||
| 1006 |
1/2✓ Branch 31 → 32 taken 8 times.
✗ Branch 31 → 72 not taken.
|
16 | InputManager::get_instance().register_press( |
| 1007 | binding_name_str, | ||
| 1008 | 8 | *current_combos, | |
| 1009 |
2/6✓ Branch 28 → 29 taken 8 times.
✗ Branch 28 → 74 not taken.
✗ Branch 34 → 35 not taken.
✓ Branch 34 → 36 taken 8 times.
✗ Branch 76 → 77 not taken.
✗ Branch 76 → 78 not taken.
|
24 | [enabled_flag, cb = std::move(on_press)]() |
| 1010 | { | ||
| 1011 | ✗ | if (cb && enabled_flag->load(std::memory_order_acquire)) | |
| 1012 | { | ||
| 1013 | ✗ | cb(); | |
| 1014 | } | ||
| 1015 | ✗ | }); | |
| 1016 | |||
| 1017 | 24 | return InputBindingGuard{std::move(binding_name_str), std::move(enabled_flag)}; | |
| 1018 | 8 | } | |
| 1019 | |||
| 1020 | 103 | void DetourModKit::Config::load(std::string_view ini_filename) | |
| 1021 | { | ||
| 1022 | 103 | std::vector<std::function<void()>> deferred_callbacks; | |
| 1023 | |||
| 1024 | { | ||
| 1025 |
1/2✓ Branch 3 → 4 taken 103 times.
✗ Branch 3 → 117 not taken.
|
103 | std::lock_guard<std::mutex> lock(getConfigMutex()); |
| 1026 | |||
| 1027 |
1/2✓ Branch 4 → 5 taken 103 times.
✗ Branch 4 → 115 not taken.
|
103 | Logger &logger = Logger::get_instance(); |
| 1028 |
2/4✓ Branch 7 → 8 taken 103 times.
✗ Branch 7 → 91 not taken.
✓ Branch 8 → 9 taken 103 times.
✗ Branch 8 → 89 not taken.
|
103 | std::filesystem::path ini_path = getIniFilePath(std::string(ini_filename), logger); |
| 1029 |
1/2✓ Branch 11 → 12 taken 103 times.
✗ Branch 11 → 113 not taken.
|
103 | std::string ini_path_str = ini_path.string(); // convert to narrow string for logger formatting |
| 1030 | 103 | CSimpleIniA ini; | |
| 1031 | 103 | ini.SetUnicode(false); // Assume ASCII/MBCS INI | |
| 1032 | 103 | ini.SetMultiKey(false); // Disallow duplicate keys in a section | |
| 1033 | |||
| 1034 | // Read-hash-parse pipeline: read bytes once, hash them, feed the | ||
| 1035 | // same buffer into CSimpleIniA::LoadData so the cached hash and | ||
| 1036 | // the parsed INI state are guaranteed to reflect identical file | ||
| 1037 | // contents (TOCTOU-free vs. a separate LoadFile call). | ||
| 1038 | 103 | IniLoadOutcome outcome = load_ini_into(ini_path, ini); | |
| 1039 | |||
| 1040 |
3/4✓ Branch 16 → 17 taken 74 times.
✓ Branch 16 → 19 taken 29 times.
✓ Branch 17 → 18 taken 74 times.
✗ Branch 17 → 19 not taken.
|
103 | const bool load_succeeded = outcome.read_succeeded && outcome.parse_succeeded; |
| 1041 |
2/2✓ Branch 20 → 21 taken 29 times.
✓ Branch 20 → 24 taken 74 times.
|
103 | if (!outcome.read_succeeded) |
| 1042 | { | ||
| 1043 |
1/2✓ Branch 21 → 22 taken 29 times.
✗ Branch 21 → 95 not taken.
|
29 | logger.error("Config: Failed to open '{}'. Using defaults.", ini_path_str); |
| 1044 | // File unreadable: wipe the cached hash so the next reload() | ||
| 1045 | // does not short-circuit against a stale value. | ||
| 1046 | 29 | getLastLoadedIniHash().reset(); | |
| 1047 | } | ||
| 1048 |
1/2✗ Branch 24 → 25 not taken.
✓ Branch 24 → 28 taken 74 times.
|
74 | else if (!outcome.parse_succeeded) |
| 1049 | { | ||
| 1050 | ✗ | logger.error("Config: Failed to parse '{}' (error {}). Using defaults.", | |
| 1051 | ✗ | ini_path_str, static_cast<int>(outcome.parse_rc)); | |
| 1052 | // Parse failed: clear the hash so a subsequent successful | ||
| 1053 | // load() does not spuriously hash-skip a reload against a | ||
| 1054 | // hash computed for bytes we could not actually parse. | ||
| 1055 | ✗ | getLastLoadedIniHash().reset(); | |
| 1056 | } | ||
| 1057 | else | ||
| 1058 | { | ||
| 1059 |
1/2✓ Branch 28 → 29 taken 74 times.
✗ Branch 28 → 98 not taken.
|
74 | logger.debug("Config: Opened {}", ini_path_str); |
| 1060 | 74 | getLastLoadedIniHash() = outcome.hash; | |
| 1061 | } | ||
| 1062 | |||
| 1063 | // Read all values under lock, but defer setter callbacks | ||
| 1064 |
2/2✓ Branch 55 → 34 taken 125 times.
✓ Branch 55 → 56 taken 103 times.
|
331 | for (const auto &item : getRegisteredConfigItems()) |
| 1065 | { | ||
| 1066 |
1/2✓ Branch 37 → 38 taken 125 times.
✗ Branch 37 → 101 not taken.
|
125 | item->load(ini, logger); |
| 1067 |
1/2✓ Branch 39 → 40 taken 125 times.
✗ Branch 39 → 101 not taken.
|
125 | auto cb = item->take_deferred_apply(); |
| 1068 |
1/2✓ Branch 41 → 42 taken 125 times.
✗ Branch 41 → 45 not taken.
|
125 | if (cb) |
| 1069 | { | ||
| 1070 |
1/2✓ Branch 44 → 45 taken 125 times.
✗ Branch 44 → 99 not taken.
|
125 | deferred_callbacks.push_back(std::move(cb)); |
| 1071 | } | ||
| 1072 | 125 | } | |
| 1073 | |||
| 1074 | // Remember the INI path so reload() can re-run setters against the | ||
| 1075 | // same file without the caller passing it again. Only update the | ||
| 1076 | // stored path on success; a failed load must leave the previously | ||
| 1077 | // remembered path (if any) untouched so subsequent reload() calls | ||
| 1078 | // keep targeting the last good file rather than a missing or | ||
| 1079 | // malformed one. | ||
| 1080 |
2/2✓ Branch 56 → 57 taken 74 times.
✓ Branch 56 → 65 taken 29 times.
|
103 | if (load_succeeded) |
| 1081 | { | ||
| 1082 |
1/2✓ Branch 59 → 60 taken 74 times.
✗ Branch 59 → 103 not taken.
|
148 | getLastLoadedIniPath() = std::string(ini_filename); |
| 1083 | } | ||
| 1084 | |||
| 1085 |
1/2✓ Branch 67 → 68 taken 103 times.
✗ Branch 67 → 107 not taken.
|
103 | logger.info("Config: Loaded {} items from {}", getRegisteredConfigItems().size(), ini_path_str); |
| 1086 | 103 | } | |
| 1087 | |||
| 1088 | // Invoke setter callbacks outside the config mutex -- same deferred | ||
| 1089 | // pattern as register_*(). Setters may safely call back into Config. | ||
| 1090 |
2/2✓ Branch 86 → 74 taken 125 times.
✓ Branch 86 → 87 taken 103 times.
|
331 | for (auto &cb : deferred_callbacks) |
| 1091 | { | ||
| 1092 |
1/2✓ Branch 76 → 77 taken 125 times.
✗ Branch 76 → 118 not taken.
|
125 | cb(); |
| 1093 | } | ||
| 1094 | 103 | } | |
| 1095 | |||
| 1096 | namespace | ||
| 1097 | { | ||
| 1098 | /** | ||
| 1099 | * @brief Internal reload implementation that also reports whether | ||
| 1100 | * setters actually ran. | ||
| 1101 | * @param[out] out_setters_ran Set to true when setters were invoked. | ||
| 1102 | * False when the content-hash short-circuit | ||
| 1103 | * skipped the reload. | ||
| 1104 | * @return true if a previous load() path was available and the reload | ||
| 1105 | * proceeded; false if reload() was called before any load(). | ||
| 1106 | */ | ||
| 1107 | 32 | bool reload_impl(bool &out_setters_ran) | |
| 1108 | { | ||
| 1109 | 32 | out_setters_ran = false; | |
| 1110 | |||
| 1111 | 32 | std::vector<std::function<void()>> deferred_callbacks; | |
| 1112 | 32 | std::string ini_filename; | |
| 1113 | |||
| 1114 | { | ||
| 1115 |
1/2✓ Branch 4 → 5 taken 32 times.
✗ Branch 4 → 125 not taken.
|
32 | std::lock_guard<std::mutex> lock(getConfigMutex()); |
| 1116 | |||
| 1117 |
1/2✓ Branch 6 → 7 taken 32 times.
✗ Branch 6 → 123 not taken.
|
32 | ini_filename = getLastLoadedIniPath(); |
| 1118 |
2/2✓ Branch 8 → 9 taken 2 times.
✓ Branch 8 → 10 taken 30 times.
|
32 | if (ini_filename.empty()) |
| 1119 | { | ||
| 1120 | // No prior load() -- nothing to reload. Caller is expected | ||
| 1121 | // to check the return value and either call load() first | ||
| 1122 | // or surface a user-facing error. | ||
| 1123 | 2 | return false; | |
| 1124 | } | ||
| 1125 | |||
| 1126 |
1/2✓ Branch 10 → 11 taken 30 times.
✗ Branch 10 → 123 not taken.
|
30 | DetourModKit::Logger &logger = DetourModKit::Logger::get_instance(); |
| 1127 |
1/2✓ Branch 11 → 12 taken 30 times.
✗ Branch 11 → 123 not taken.
|
30 | std::filesystem::path ini_path = getIniFilePath(ini_filename, logger); |
| 1128 |
1/2✓ Branch 12 → 13 taken 30 times.
✗ Branch 12 → 121 not taken.
|
30 | std::string ini_path_str = ini_path.string(); |
| 1129 | |||
| 1130 | 30 | CSimpleIniA ini; | |
| 1131 | 30 | ini.SetUnicode(false); | |
| 1132 | 30 | ini.SetMultiKey(false); | |
| 1133 | |||
| 1134 | // Read-hash-parse pipeline: the hash we compare against the | ||
| 1135 | // cache and the bytes SimpleIni parses come from a single | ||
| 1136 | // read. Splitting the read (one for hashing, another via | ||
| 1137 | // LoadFile for parsing) would let an editor save slip | ||
| 1138 | // between them and desync the cached hash from the parsed | ||
| 1139 | // state. | ||
| 1140 | 30 | IniLoadOutcome outcome = load_ini_into(ini_path, ini); | |
| 1141 | |||
| 1142 |
2/2✓ Branch 17 → 18 taken 2 times.
✓ Branch 17 → 22 taken 28 times.
|
30 | if (!outcome.read_succeeded) |
| 1143 | { | ||
| 1144 | // Read failure: clear the cached hash before falling | ||
| 1145 | // through to run setters with defaults. Leaving it in | ||
| 1146 | // place would let a later reload find identical bytes | ||
| 1147 | // (same as the last successful load), match the stale | ||
| 1148 | // hash, and hash-skip -- silently leaving in-memory | ||
| 1149 | // state at the defaults from this failed reload. | ||
| 1150 | 2 | getLastLoadedIniHash() = std::nullopt; | |
| 1151 |
1/2✓ Branch 20 → 21 taken 2 times.
✗ Branch 20 → 105 not taken.
|
2 | logger.warning("Config: reload() could not open '{}'; retaining last values where setters keep state.", |
| 1152 | ini_path_str); | ||
| 1153 | } | ||
| 1154 | else | ||
| 1155 | { | ||
| 1156 | // Content-hash skip: compare against the hash stored on | ||
| 1157 | // the last successful load()/reload(). Identical bytes | ||
| 1158 | // -> no setters. Uses the hash we just computed in the | ||
| 1159 | // pipeline; no second read. | ||
| 1160 |
2/2✓ Branch 24 → 25 taken 27 times.
✓ Branch 24 → 32 taken 1 time.
|
28 | if (auto &cached_hash = getLastLoadedIniHash(); cached_hash.has_value()) |
| 1161 | { | ||
| 1162 | 27 | const std::uint64_t current_hash = *outcome.hash; | |
| 1163 |
2/2✓ Branch 27 → 28 taken 16 times.
✓ Branch 27 → 30 taken 11 times.
|
27 | if (current_hash == *cached_hash) |
| 1164 | { | ||
| 1165 |
1/2✓ Branch 28 → 29 taken 16 times.
✗ Branch 28 → 106 not taken.
|
16 | logger.debug( |
| 1166 | "Config::reload: content unchanged (hash {:016x}); skipping setters.", | ||
| 1167 | current_hash); | ||
| 1168 | 16 | return true; | |
| 1169 | } | ||
| 1170 | // Content changed: remember the new hash so a | ||
| 1171 | // subsequent no-op reload can short-circuit. | ||
| 1172 | 11 | cached_hash = current_hash; | |
| 1173 | } | ||
| 1174 | else | ||
| 1175 | { | ||
| 1176 | // No cached hash (prior failure or never-loaded): | ||
| 1177 | // adopt the current one so a subsequent no-op | ||
| 1178 | // reload short-circuits. | ||
| 1179 | 1 | getLastLoadedIniHash() = outcome.hash; | |
| 1180 | } | ||
| 1181 | |||
| 1182 |
1/2✗ Branch 34 → 35 not taken.
✓ Branch 34 → 37 taken 12 times.
|
12 | if (!outcome.parse_succeeded) |
| 1183 | { | ||
| 1184 | // Asymmetry with the read-failure branch above is | ||
| 1185 | // intentional: we have already advanced the cached | ||
| 1186 | // hash to these new bytes, so a later reload with | ||
| 1187 | // identical bytes correctly short-circuits -- the | ||
| 1188 | // partial state produced by re-parsing would be the | ||
| 1189 | // same. The read-failure branch cannot make that | ||
| 1190 | // guarantee because it never observed the bytes. | ||
| 1191 | ✗ | logger.warning("Config: reload() parse error on '{}' (error {}); retaining last values where setters keep state.", | |
| 1192 | ✗ | ini_path_str, static_cast<int>(outcome.parse_rc)); | |
| 1193 | } | ||
| 1194 | else | ||
| 1195 | { | ||
| 1196 |
1/2✓ Branch 37 → 38 taken 12 times.
✗ Branch 37 → 110 not taken.
|
12 | logger.debug("Config: Reloading from {}", ini_path_str); |
| 1197 | } | ||
| 1198 | } | ||
| 1199 | |||
| 1200 |
2/2✓ Branch 63 → 42 taken 20 times.
✓ Branch 63 → 64 taken 14 times.
|
48 | for (const auto &item : getRegisteredConfigItems()) |
| 1201 | { | ||
| 1202 |
1/2✓ Branch 45 → 46 taken 20 times.
✗ Branch 45 → 113 not taken.
|
20 | item->load(ini, logger); |
| 1203 |
1/2✓ Branch 47 → 48 taken 20 times.
✗ Branch 47 → 113 not taken.
|
20 | auto cb = item->take_deferred_apply(); |
| 1204 |
1/2✓ Branch 49 → 50 taken 20 times.
✗ Branch 49 → 53 not taken.
|
20 | if (cb) |
| 1205 | { | ||
| 1206 |
1/2✓ Branch 52 → 53 taken 20 times.
✗ Branch 52 → 111 not taken.
|
20 | deferred_callbacks.push_back(std::move(cb)); |
| 1207 | } | ||
| 1208 | 20 | } | |
| 1209 | |||
| 1210 | ✗ | logger.info("Config: Reloaded {} items from {}", | |
| 1211 |
1/2✓ Branch 66 → 67 taken 14 times.
✗ Branch 66 → 115 not taken.
|
14 | getRegisteredConfigItems().size(), ini_path_str); |
| 1212 |
8/8✓ Branch 69 → 70 taken 14 times.
✓ Branch 69 → 71 taken 16 times.
✓ Branch 73 → 74 taken 14 times.
✓ Branch 73 → 75 taken 16 times.
✓ Branch 77 → 78 taken 14 times.
✓ Branch 77 → 79 taken 16 times.
✓ Branch 81 → 82 taken 14 times.
✓ Branch 81 → 84 taken 18 times.
|
80 | } |
| 1213 | |||
| 1214 | // The registry mutex is released by the scope above; setters run | ||
| 1215 | // unlocked (the standard deferred-setter pattern). Wrap each call | ||
| 1216 | // so a single throwing setter cannot prevent the remaining setters | ||
| 1217 | // from seeing the refreshed values. Logger::error() below is also | ||
| 1218 | // outside the config mutex -- a custom Logger sink that re-enters | ||
| 1219 | // Config cannot AB/BA deadlock here. | ||
| 1220 |
1/2✓ Branch 83 → 85 taken 14 times.
✗ Branch 83 → 142 not taken.
|
14 | DetourModKit::Logger &logger = DetourModKit::Logger::get_instance(); |
| 1221 |
2/2✓ Branch 99 → 87 taken 20 times.
✓ Branch 99 → 100 taken 14 times.
|
48 | for (auto &cb : deferred_callbacks) |
| 1222 | { | ||
| 1223 | try | ||
| 1224 | { | ||
| 1225 |
2/2✓ Branch 89 → 90 taken 19 times.
✓ Branch 89 → 126 taken 1 time.
|
20 | cb(); |
| 1226 | } | ||
| 1227 |
1/2✓ Branch 126 → 127 taken 1 time.
✗ Branch 126 → 131 not taken.
|
1 | catch (const std::exception &e) |
| 1228 | { | ||
| 1229 |
1/2✓ Branch 129 → 130 taken 1 time.
✗ Branch 129 → 134 not taken.
|
1 | logger.error("Config: reload setter threw: {}", e.what()); |
| 1230 | 1 | } | |
| 1231 | ✗ | catch (...) | |
| 1232 | { | ||
| 1233 | ✗ | logger.error("Config: reload setter threw unknown exception."); | |
| 1234 | ✗ | } | |
| 1235 | } | ||
| 1236 | 14 | out_setters_ran = true; | |
| 1237 | 14 | return true; | |
| 1238 | 32 | } | |
| 1239 | } // anonymous namespace | ||
| 1240 | |||
| 1241 | 28 | bool DetourModKit::Config::reload() | |
| 1242 | { | ||
| 1243 | 28 | bool ignored = false; | |
| 1244 |
1/2✓ Branch 2 → 3 taken 28 times.
✗ Branch 2 → 6 not taken.
|
56 | return reload_impl(ignored); |
| 1245 | } | ||
| 1246 | |||
| 1247 | 10 | DetourModKit::Config::AutoReloadStatus DetourModKit::Config::enable_auto_reload( | |
| 1248 | std::chrono::milliseconds debounce_window, | ||
| 1249 | std::function<void(bool)> on_reload) | ||
| 1250 | { | ||
| 1251 | 10 | std::string ini_filename; | |
| 1252 | { | ||
| 1253 |
1/2✓ Branch 4 → 5 taken 10 times.
✗ Branch 4 → 50 not taken.
|
10 | std::lock_guard<std::mutex> lock(getConfigMutex()); |
| 1254 |
1/2✓ Branch 6 → 7 taken 10 times.
✗ Branch 6 → 48 not taken.
|
10 | ini_filename = getLastLoadedIniPath(); |
| 1255 | 10 | } | |
| 1256 | |||
| 1257 |
1/2✓ Branch 8 → 9 taken 10 times.
✗ Branch 8 → 67 not taken.
|
10 | Logger &logger = Logger::get_instance(); |
| 1258 | |||
| 1259 |
2/2✓ Branch 10 → 11 taken 2 times.
✓ Branch 10 → 13 taken 8 times.
|
10 | if (ini_filename.empty()) |
| 1260 | { | ||
| 1261 |
1/2✓ Branch 11 → 12 taken 2 times.
✗ Branch 11 → 51 not taken.
|
2 | logger.warning("Config: enable_auto_reload() called before load(); watcher not started."); |
| 1262 | 2 | return AutoReloadStatus::NoPriorLoad; | |
| 1263 | } | ||
| 1264 | |||
| 1265 | // Resolve to the same absolute path load() uses so the watcher observes | ||
| 1266 | // the actual file on disk rather than a caller-supplied relative stub. | ||
| 1267 |
1/2✓ Branch 13 → 14 taken 8 times.
✗ Branch 13 → 67 not taken.
|
8 | std::filesystem::path ini_path = getIniFilePath(ini_filename, logger); |
| 1268 |
1/2✓ Branch 14 → 15 taken 8 times.
✗ Branch 14 → 65 not taken.
|
8 | std::string resolved_path = ini_path.string(); |
| 1269 | |||
| 1270 | // Hold getWatcherMutex() across start() to serialize against a | ||
| 1271 | // concurrent disable_auto_reload(). start() normally returns in | ||
| 1272 | // milliseconds; under a pathological handshake stall it returns | ||
| 1273 | // within the 5 s timeout, which is preferable to a use-after-free | ||
| 1274 | // on the watcher if we released the lock and disable_auto_reload() | ||
| 1275 | // moved the unique_ptr out and destroyed it mid-start(). | ||
| 1276 | { | ||
| 1277 |
1/2✓ Branch 16 → 17 taken 8 times.
✗ Branch 16 → 60 not taken.
|
8 | std::lock_guard<std::mutex> wlock(getWatcherMutex()); |
| 1278 | |||
| 1279 | 8 | auto &watcher = getConfigWatcher(); | |
| 1280 | // Guard on existence, not is_running(): there is a window between | ||
| 1281 | // make_unique<ConfigWatcher> + start() and the worker flipping its | ||
| 1282 | // running flag true, during which a second concurrent caller would | ||
| 1283 | // otherwise overwrite the still-starting unique_ptr. | ||
| 1284 |
2/2✓ Branch 19 → 20 taken 1 time.
✓ Branch 19 → 22 taken 7 times.
|
8 | if (watcher) |
| 1285 | { | ||
| 1286 |
1/2✓ Branch 20 → 21 taken 1 time.
✗ Branch 20 → 52 not taken.
|
1 | logger.warning("Config: enable_auto_reload() called while a watcher is already present; call disable_auto_reload() first."); |
| 1287 | 1 | return AutoReloadStatus::AlreadyRunning; | |
| 1288 | } | ||
| 1289 | |||
| 1290 |
1/2✓ Branch 25 → 26 taken 7 times.
✗ Branch 25 → 53 not taken.
|
14 | watcher = std::make_unique<ConfigWatcher>( |
| 1291 | resolved_path, | ||
| 1292 | debounce_window, | ||
| 1293 | 14 | [user_cb = std::move(on_reload)]() | |
| 1294 | { | ||
| 1295 | // Reload first so any user callback observes the refreshed | ||
| 1296 | // values. The internal impl reports whether setters | ||
| 1297 | // actually ran (false when the content-hash short-circuit | ||
| 1298 | // skipped the work) so the user callback can distinguish | ||
| 1299 | // a real reload from a no-op touch. | ||
| 1300 | 4 | bool setters_ran = false; | |
| 1301 |
1/2✓ Branch 2 → 3 taken 4 times.
✗ Branch 2 → 7 not taken.
|
4 | (void)reload_impl(setters_ran); |
| 1302 |
1/2✓ Branch 4 → 5 taken 4 times.
✗ Branch 4 → 6 not taken.
|
4 | if (user_cb) |
| 1303 | { | ||
| 1304 |
1/2✓ Branch 5 → 6 taken 4 times.
✗ Branch 5 → 7 not taken.
|
4 | user_cb(setters_ran); |
| 1305 | } | ||
| 1306 | 11 | }); | |
| 1307 | |||
| 1308 |
3/4✓ Branch 30 → 31 taken 7 times.
✗ Branch 30 → 58 not taken.
✓ Branch 31 → 32 taken 2 times.
✓ Branch 31 → 35 taken 5 times.
|
7 | if (!watcher->start()) |
| 1309 | { | ||
| 1310 |
1/2✓ Branch 32 → 33 taken 2 times.
✗ Branch 32 → 57 not taken.
|
2 | logger.error("Config: Auto-reload watcher failed to start for {}", resolved_path); |
| 1311 | 2 | watcher.reset(); | |
| 1312 | 2 | return AutoReloadStatus::StartFailed; | |
| 1313 | } | ||
| 1314 |
2/2✓ Branch 37 → 38 taken 5 times.
✓ Branch 37 → 41 taken 3 times.
|
8 | } |
| 1315 | |||
| 1316 | ✗ | logger.info("Config: Auto-reload enabled for {} (debounce {} ms)", | |
| 1317 | resolved_path, | ||
| 1318 |
1/2✓ Branch 40 → 42 taken 5 times.
✗ Branch 40 → 61 not taken.
|
5 | static_cast<long long>(debounce_window.count())); |
| 1319 | 5 | return AutoReloadStatus::Started; | |
| 1320 | 10 | } | |
| 1321 | |||
| 1322 | 17 | void DetourModKit::Config::disable_auto_reload() noexcept | |
| 1323 | { | ||
| 1324 | 17 | std::unique_ptr<ConfigWatcher> to_drop; | |
| 1325 | { | ||
| 1326 | 17 | std::lock_guard<std::mutex> wlock(getWatcherMutex()); | |
| 1327 | 17 | auto &watcher = getConfigWatcher(); | |
| 1328 | // Detect self-invocation from a setter that fires on the watcher | ||
| 1329 | // thread. Moving out and destroying the unique_ptr here would | ||
| 1330 | // force the worker to join itself inside ~StoppableWorker, | ||
| 1331 | // raising std::system_error(resource_deadlock_would_occur) from | ||
| 1332 | // std::thread::join(). Log and return instead -- callers that | ||
| 1333 | // want to cancel from inside a reload should release the | ||
| 1334 | // InputBindingGuard or flip their own disable flag. | ||
| 1335 |
6/6✓ Branch 6 → 7 taken 6 times.
✓ Branch 6 → 12 taken 11 times.
✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 12 taken 5 times.
✓ Branch 13 → 14 taken 1 time.
✓ Branch 13 → 17 taken 16 times.
|
17 | if (watcher && watcher->is_worker_thread(std::this_thread::get_id())) |
| 1336 | { | ||
| 1337 | 1 | Logger::get_instance().error( | |
| 1338 | "Config::disable_auto_reload() called from the watcher thread; ignoring to avoid self-join deadlock. " | ||
| 1339 | "Call from a different thread or disable the hotkey binding instead."); | ||
| 1340 | 1 | return; | |
| 1341 | } | ||
| 1342 | 16 | to_drop = std::move(watcher); | |
| 1343 |
2/2✓ Branch 22 → 23 taken 16 times.
✓ Branch 22 → 25 taken 1 time.
|
17 | } |
| 1344 | // Destructor of ConfigWatcher joins its worker outside our mutex | ||
| 1345 | // to avoid holding the watcher mutex across a thread-join. | ||
| 1346 |
2/2✓ Branch 27 → 28 taken 16 times.
✓ Branch 27 → 30 taken 1 time.
|
17 | } |
| 1347 | |||
| 1348 | 5 | bool DetourModKit::Config::register_reload_hotkey(std::string_view ini_key, | |
| 1349 | std::string_view default_combo) | ||
| 1350 | { | ||
| 1351 | // An empty or explicitly-opt-out default would leave the hotkey | ||
| 1352 | // silently inert (a binding registered without trigger keys never | ||
| 1353 | // fires). Surface that to the caller as a false return so they can | ||
| 1354 | // decide whether to fall back to a different combo or skip the | ||
| 1355 | // hotkey entirely. | ||
| 1356 |
2/2✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 12 taken 4 times.
|
5 | if (default_combo.empty()) |
| 1357 | { | ||
| 1358 |
2/4✓ Branch 4 → 5 taken 1 time.
✗ Branch 4 → 127 not taken.
✓ Branch 8 → 9 taken 1 time.
✗ Branch 8 → 82 not taken.
|
2 | Logger::get_instance().warning( |
| 1359 | "Config: register_reload_hotkey('{}', '<empty>') rejected; provide a non-empty default combo.", | ||
| 1360 |
1/2✓ Branch 7 → 8 taken 1 time.
✗ Branch 7 → 85 not taken.
|
2 | std::string(ini_key)); |
| 1361 | 1 | return false; | |
| 1362 | } | ||
| 1363 | |||
| 1364 | // Pre-parse the default. The parser emits its own WARNING when a | ||
| 1365 | // non-empty, non-sentinel string fails to parse, so no extra log is | ||
| 1366 | // needed for the typo path. Explicit opt-out via the NONE sentinel | ||
| 1367 | // still returns false because a hotkey with no keys is useless. | ||
| 1368 | const Config::KeyComboList parsed = parse_key_combo_list( | ||
| 1369 |
2/4✓ Branch 15 → 16 taken 4 times.
✗ Branch 15 → 91 not taken.
✓ Branch 16 → 17 taken 4 times.
✗ Branch 16 → 89 not taken.
|
8 | std::string(default_combo), "Config reload hotkey"); |
| 1370 |
2/2✓ Branch 20 → 21 taken 1 time.
✓ Branch 20 → 22 taken 3 times.
|
4 | if (parsed.empty()) |
| 1371 | { | ||
| 1372 | 1 | return false; | |
| 1373 | } | ||
| 1374 | |||
| 1375 | // Stable binding name keyed off the INI key so repeat registrations | ||
| 1376 | // (e.g. across reload cycles) update in place rather than stacking. | ||
| 1377 |
2/4✓ Branch 24 → 25 taken 3 times.
✗ Branch 24 → 98 not taken.
✓ Branch 25 → 26 taken 3 times.
✗ Branch 25 → 96 not taken.
|
3 | std::string binding_name = "config_reload:" + std::string(ini_key); |
| 1378 | |||
| 1379 | // Lazily spin up the reload servicer thread on the first hotkey | ||
| 1380 | // registration. Holding getWatcherMutex() here keeps the lifetime | ||
| 1381 | // invariants aligned with disable_auto_reload / clear_registered_items. | ||
| 1382 | 3 | std::shared_ptr<ReloadServicer> servicer; | |
| 1383 | { | ||
| 1384 |
1/2✓ Branch 29 → 30 taken 3 times.
✗ Branch 29 → 105 not taken.
|
3 | std::lock_guard<std::mutex> lock(getWatcherMutex()); |
| 1385 | 3 | auto &slot = getReloadServicer(); | |
| 1386 |
2/2✓ Branch 32 → 33 taken 2 times.
✓ Branch 32 → 37 taken 1 time.
|
3 | if (!slot) |
| 1387 | { | ||
| 1388 |
1/2✓ Branch 33 → 34 taken 2 times.
✗ Branch 33 → 102 not taken.
|
2 | slot = std::make_shared<ReloadServicer>(); |
| 1389 | } | ||
| 1390 | 3 | servicer = slot; | |
| 1391 | 3 | } | |
| 1392 | |||
| 1393 | InputBindingGuard guard = Config::register_press_combo( | ||
| 1394 | 3 | "Input", | |
| 1395 | ini_key, | ||
| 1396 | 3 | "Config reload hotkey", | |
| 1397 | binding_name, | ||
| 1398 |
1/2✓ Branch 40 → 41 taken 3 times.
✗ Branch 40 → 110 not taken.
|
6 | [servicer]() noexcept |
| 1399 | { | ||
| 1400 | // InputManager press callbacks run on the input-poll thread | ||
| 1401 | // and must return promptly. Defer the actual reload() work to | ||
| 1402 | // the servicer thread so a 30-item INI parse cannot jitter | ||
| 1403 | // other hotkeys. The servicer holds the shared_ptr slot and | ||
| 1404 | // cannot be destroyed while this capture is alive. | ||
| 1405 | ✗ | if (servicer) | |
| 1406 | { | ||
| 1407 | ✗ | servicer->request_reload(); | |
| 1408 | } | ||
| 1409 | ✗ | }, | |
| 1410 |
1/2✓ Branch 44 → 45 taken 3 times.
✗ Branch 44 → 106 not taken.
|
6 | default_combo); |
| 1411 | |||
| 1412 | // Stash the guard under the watcher mutex so its destructor does not | ||
| 1413 | // fire at the end of this function (which would disable the binding). | ||
| 1414 | // Replace any prior guard registered for the same INI key so repeat | ||
| 1415 | // calls update in place rather than stacking. | ||
| 1416 | { | ||
| 1417 |
1/2✓ Branch 48 → 49 taken 3 times.
✗ Branch 48 → 118 not taken.
|
3 | std::lock_guard<std::mutex> lock(getWatcherMutex()); |
| 1418 | 3 | auto &guards = getReloadHotkeyGuards(); | |
| 1419 |
2/2✓ Branch 71 → 51 taken 1 time.
✓ Branch 71 → 72 taken 2 times.
|
6 | for (auto it = guards.begin(); it != guards.end(); ++it) |
| 1420 | { | ||
| 1421 |
1/2✓ Branch 55 → 56 taken 1 time.
✗ Branch 55 → 61 not taken.
|
1 | if (it->name() == binding_name) |
| 1422 | { | ||
| 1423 |
1/2✓ Branch 59 → 60 taken 1 time.
✗ Branch 59 → 114 not taken.
|
1 | guards.erase(it); |
| 1424 | 1 | break; | |
| 1425 | } | ||
| 1426 | } | ||
| 1427 |
1/2✓ Branch 74 → 75 taken 3 times.
✗ Branch 74 → 116 not taken.
|
3 | guards.emplace_back(std::move(guard)); |
| 1428 | 3 | } | |
| 1429 | |||
| 1430 | 3 | return true; | |
| 1431 | 4 | } | |
| 1432 | |||
| 1433 | 8 | void DetourModKit::Config::log_all() | |
| 1434 | { | ||
| 1435 |
1/2✓ Branch 3 → 4 taken 8 times.
✗ Branch 3 → 56 not taken.
|
8 | std::lock_guard<std::mutex> lock(getConfigMutex()); |
| 1436 | |||
| 1437 |
1/2✓ Branch 4 → 5 taken 8 times.
✗ Branch 4 → 54 not taken.
|
8 | Logger &logger = Logger::get_instance(); |
| 1438 | 8 | const auto &items = getRegisteredConfigItems(); | |
| 1439 |
2/2✓ Branch 7 → 8 taken 2 times.
✓ Branch 7 → 10 taken 6 times.
|
8 | if (items.empty()) |
| 1440 | { | ||
| 1441 |
1/2✓ Branch 8 → 9 taken 2 times.
✗ Branch 8 → 45 not taken.
|
2 | logger.info("Config: No configuration items registered."); |
| 1442 | 2 | return; | |
| 1443 | } | ||
| 1444 | |||
| 1445 | ✗ | logger.info("Config: {} registered values across {} section(s)", | |
| 1446 |
1/2✓ Branch 12 → 13 taken 6 times.
✗ Branch 12 → 46 not taken.
|
6 | items.size(), [&items]() |
| 1447 | { | ||
| 1448 | 6 | std::unordered_set<std::string_view> seen; | |
| 1449 |
2/2✓ Branch 19 → 5 taken 11 times.
✓ Branch 19 → 20 taken 6 times.
|
23 | for (const auto &item : items) |
| 1450 | { | ||
| 1451 |
1/2✓ Branch 9 → 10 taken 11 times.
✗ Branch 9 → 24 not taken.
|
11 | seen.insert(item->section); |
| 1452 | } | ||
| 1453 |
1/2✓ Branch 10 → 11 taken 6 times.
✗ Branch 10 → 48 not taken.
|
18 | return seen.size(); }()); |
| 1454 | |||
| 1455 | 6 | std::string current_section; | |
| 1456 |
2/2✓ Branch 36 → 16 taken 11 times.
✓ Branch 36 → 37 taken 6 times.
|
23 | for (const auto &item : items) |
| 1457 | { | ||
| 1458 |
2/2✓ Branch 20 → 21 taken 7 times.
✓ Branch 20 → 25 taken 4 times.
|
11 | if (item->section != current_section) |
| 1459 | { | ||
| 1460 |
1/2✓ Branch 22 → 23 taken 7 times.
✗ Branch 22 → 51 not taken.
|
7 | current_section = item->section; |
| 1461 |
1/2✓ Branch 23 → 24 taken 7 times.
✗ Branch 23 → 50 not taken.
|
7 | logger.debug("Config: [{}]", current_section); |
| 1462 | } | ||
| 1463 |
1/2✓ Branch 26 → 27 taken 11 times.
✗ Branch 26 → 51 not taken.
|
11 | item->log_current_value(logger); |
| 1464 | } | ||
| 1465 |
2/2✓ Branch 40 → 41 taken 6 times.
✓ Branch 40 → 43 taken 2 times.
|
8 | } |
| 1466 | |||
| 1467 | 299 | void DetourModKit::Config::clear_registered_items() | |
| 1468 | { | ||
| 1469 |
1/2✓ Branch 3 → 4 taken 299 times.
✗ Branch 3 → 38 not taken.
|
299 | std::lock_guard<std::mutex> lock(getConfigMutex()); |
| 1470 | |||
| 1471 |
1/2✓ Branch 4 → 5 taken 299 times.
✗ Branch 4 → 36 not taken.
|
299 | Logger &logger = Logger::get_instance(); |
| 1472 | 299 | size_t count = getRegisteredConfigItems().size(); | |
| 1473 |
2/2✓ Branch 7 → 8 taken 120 times.
✓ Branch 7 → 12 taken 179 times.
|
299 | if (count > 0) |
| 1474 | { | ||
| 1475 | 120 | getRegisteredConfigItems().clear(); | |
| 1476 |
1/2✓ Branch 10 → 11 taken 120 times.
✗ Branch 10 → 31 not taken.
|
120 | logger.debug("Config: Cleared {} registered configuration items.", count); |
| 1477 | } | ||
| 1478 | else | ||
| 1479 | { | ||
| 1480 |
1/2✓ Branch 12 → 13 taken 179 times.
✗ Branch 12 → 32 not taken.
|
179 | logger.debug("Config: clear_registered_items called, but no items were registered."); |
| 1481 | } | ||
| 1482 | |||
| 1483 | // Drop the remembered INI path too so reload() does not act on a | ||
| 1484 | // previous file after a full reset. Leaves the watcher alone; the | ||
| 1485 | // caller owns its lifecycle via disable_auto_reload(). | ||
| 1486 | 299 | getLastLoadedIniPath().clear(); | |
| 1487 | // Wipe the cached content hash alongside the path so the next load() | ||
| 1488 | // starts from a clean slate. | ||
| 1489 | 299 | getLastLoadedIniHash().reset(); | |
| 1490 | |||
| 1491 | // Release any reload-hotkey guards so the cancellation flags flip | ||
| 1492 | // deterministically. Held under the watcher mutex because that is | ||
| 1493 | // where the vector itself is serialised. Also drop our strong | ||
| 1494 | // reference to the reload servicer. | ||
| 1495 | 299 | std::shared_ptr<ReloadServicer> servicer_to_drop; | |
| 1496 | { | ||
| 1497 |
1/2✓ Branch 19 → 20 taken 299 times.
✗ Branch 19 → 33 not taken.
|
299 | std::lock_guard<std::mutex> wlock(getWatcherMutex()); |
| 1498 | 299 | getReloadHotkeyGuards().clear(); | |
| 1499 | 598 | servicer_to_drop = std::move(getReloadServicer()); | |
| 1500 | 299 | } | |
| 1501 | // Release our strong reference to the servicer. The InputManager | ||
| 1502 | // binding registered by register_reload_hotkey() still holds another | ||
| 1503 | // strong ref via its captured lambda (InputBindingGuard::release() | ||
| 1504 | // only flips the cancellation flag; it does not unregister the | ||
| 1505 | // binding or drop the captured shared_ptr). The servicer worker | ||
| 1506 | // therefore joins when InputManager::shutdown() ultimately tears | ||
| 1507 | // down that binding, not at this reset() call. | ||
| 1508 | 299 | servicer_to_drop.reset(); | |
| 1509 | 299 | } | |
| 1510 |