GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 97.6% 82 / 0 / 84
Functions: 100.0% 10 / 0 / 10
Branches: 72.8% 67 / 0 / 92

src/input_codes.cpp
Line Branch Exec Source
1 /**
2 * @file input_codes.cpp
3 * @brief Implementation of named input code resolution and formatting.
4 *
5 * Provides the name lookup table and resolution functions for converting between human-readable input names (e.g.,
6 * "Ctrl", "Mouse4", "Gamepad_A") and their corresponding InputCode values.
7 */
8
9 #include "DetourModKit/input_codes.hpp"
10 #include "DetourModKit/format.hpp"
11
12 #include <algorithm>
13 #include <cctype>
14 #include <charconv>
15 #include <limits>
16 #include <unordered_map>
17
18 namespace DetourModKit
19 {
20 namespace
21 {
22 struct NameEntry
23 {
24 const char *name;
25 InputCode code;
26 };
27
28 // clang-format off
29 constexpr NameEntry name_table[] = {
30 // --- Keyboard: Modifiers ---
31 {"Ctrl", {InputSource::Keyboard, 0x11}},
32 {"LCtrl", {InputSource::Keyboard, 0xA2}},
33 {"RCtrl", {InputSource::Keyboard, 0xA3}},
34 {"Shift", {InputSource::Keyboard, 0x10}},
35 {"LShift", {InputSource::Keyboard, 0xA0}},
36 {"RShift", {InputSource::Keyboard, 0xA1}},
37 {"Alt", {InputSource::Keyboard, 0x12}},
38 {"LAlt", {InputSource::Keyboard, 0xA4}},
39 {"RAlt", {InputSource::Keyboard, 0xA5}},
40
41 // --- Keyboard: Function keys ---
42 {"F1", {InputSource::Keyboard, 0x70}},
43 {"F2", {InputSource::Keyboard, 0x71}},
44 {"F3", {InputSource::Keyboard, 0x72}},
45 {"F4", {InputSource::Keyboard, 0x73}},
46 {"F5", {InputSource::Keyboard, 0x74}},
47 {"F6", {InputSource::Keyboard, 0x75}},
48 {"F7", {InputSource::Keyboard, 0x76}},
49 {"F8", {InputSource::Keyboard, 0x77}},
50 {"F9", {InputSource::Keyboard, 0x78}},
51 {"F10", {InputSource::Keyboard, 0x79}},
52 {"F11", {InputSource::Keyboard, 0x7A}},
53 {"F12", {InputSource::Keyboard, 0x7B}},
54 {"F13", {InputSource::Keyboard, 0x7C}},
55 {"F14", {InputSource::Keyboard, 0x7D}},
56 {"F15", {InputSource::Keyboard, 0x7E}},
57 {"F16", {InputSource::Keyboard, 0x7F}},
58 {"F17", {InputSource::Keyboard, 0x80}},
59 {"F18", {InputSource::Keyboard, 0x81}},
60 {"F19", {InputSource::Keyboard, 0x82}},
61 {"F20", {InputSource::Keyboard, 0x83}},
62 {"F21", {InputSource::Keyboard, 0x84}},
63 {"F22", {InputSource::Keyboard, 0x85}},
64 {"F23", {InputSource::Keyboard, 0x86}},
65 {"F24", {InputSource::Keyboard, 0x87}},
66
67 // --- Keyboard: Letters ---
68 {"A", {InputSource::Keyboard, 0x41}},
69 {"B", {InputSource::Keyboard, 0x42}},
70 {"C", {InputSource::Keyboard, 0x43}},
71 {"D", {InputSource::Keyboard, 0x44}},
72 {"E", {InputSource::Keyboard, 0x45}},
73 {"F", {InputSource::Keyboard, 0x46}},
74 {"G", {InputSource::Keyboard, 0x47}},
75 {"H", {InputSource::Keyboard, 0x48}},
76 {"I", {InputSource::Keyboard, 0x49}},
77 {"J", {InputSource::Keyboard, 0x4A}},
78 {"K", {InputSource::Keyboard, 0x4B}},
79 {"L", {InputSource::Keyboard, 0x4C}},
80 {"M", {InputSource::Keyboard, 0x4D}},
81 {"N", {InputSource::Keyboard, 0x4E}},
82 {"O", {InputSource::Keyboard, 0x4F}},
83 {"P", {InputSource::Keyboard, 0x50}},
84 {"Q", {InputSource::Keyboard, 0x51}},
85 {"R", {InputSource::Keyboard, 0x52}},
86 {"S", {InputSource::Keyboard, 0x53}},
87 {"T", {InputSource::Keyboard, 0x54}},
88 {"U", {InputSource::Keyboard, 0x55}},
89 {"V", {InputSource::Keyboard, 0x56}},
90 {"W", {InputSource::Keyboard, 0x57}},
91 {"X", {InputSource::Keyboard, 0x58}},
92 {"Y", {InputSource::Keyboard, 0x59}},
93 {"Z", {InputSource::Keyboard, 0x5A}},
94
95 // --- Keyboard: Digits ---
96 {"0", {InputSource::Keyboard, 0x30}},
97 {"1", {InputSource::Keyboard, 0x31}},
98 {"2", {InputSource::Keyboard, 0x32}},
99 {"3", {InputSource::Keyboard, 0x33}},
100 {"4", {InputSource::Keyboard, 0x34}},
101 {"5", {InputSource::Keyboard, 0x35}},
102 {"6", {InputSource::Keyboard, 0x36}},
103 {"7", {InputSource::Keyboard, 0x37}},
104 {"8", {InputSource::Keyboard, 0x38}},
105 {"9", {InputSource::Keyboard, 0x39}},
106
107 // --- Keyboard: Navigation ---
108 {"Up", {InputSource::Keyboard, 0x26}},
109 {"Down", {InputSource::Keyboard, 0x28}},
110 {"Left", {InputSource::Keyboard, 0x25}},
111 {"Right", {InputSource::Keyboard, 0x27}},
112 {"Home", {InputSource::Keyboard, 0x24}},
113 {"End", {InputSource::Keyboard, 0x23}},
114 {"PageUp", {InputSource::Keyboard, 0x21}},
115 {"PageDown", {InputSource::Keyboard, 0x22}},
116 {"Insert", {InputSource::Keyboard, 0x2D}},
117 {"Delete", {InputSource::Keyboard, 0x2E}},
118
119 // --- Keyboard: Common keys ---
120 {"Space", {InputSource::Keyboard, 0x20}},
121 {"Enter", {InputSource::Keyboard, 0x0D}},
122 {"Escape", {InputSource::Keyboard, 0x1B}},
123 {"Tab", {InputSource::Keyboard, 0x09}},
124 {"Backspace", {InputSource::Keyboard, 0x08}},
125 {"CapsLock", {InputSource::Keyboard, 0x14}},
126 {"NumLock", {InputSource::Keyboard, 0x90}},
127 {"ScrollLock", {InputSource::Keyboard, 0x91}},
128 {"PrintScreen", {InputSource::Keyboard, 0x2C}},
129 {"Pause", {InputSource::Keyboard, 0x13}},
130
131 // --- Keyboard: Windows / application keys ---
132 {"LWin", {InputSource::Keyboard, 0x5B}},
133 {"RWin", {InputSource::Keyboard, 0x5C}},
134 {"Apps", {InputSource::Keyboard, 0x5D}},
135 {"Menu", {InputSource::Keyboard, 0x5D}}, // alias for the application/context-menu key
136
137 // --- Keyboard: OEM punctuation (US layout) ---
138 // The canonical name is listed first so the reverse lookup picks it; trailing aliases still parse. Grave
139 // (VK_OEM_3) is the backtick/tilde key many games use to open the console.
140 {"Semicolon", {InputSource::Keyboard, 0xBA}},
141 {"Equals", {InputSource::Keyboard, 0xBB}},
142 {"Comma", {InputSource::Keyboard, 0xBC}},
143 {"Minus", {InputSource::Keyboard, 0xBD}},
144 {"Period", {InputSource::Keyboard, 0xBE}},
145 {"Slash", {InputSource::Keyboard, 0xBF}},
146 {"Grave", {InputSource::Keyboard, 0xC0}},
147 {"Backtick", {InputSource::Keyboard, 0xC0}}, // alias for Grave
148 {"Tilde", {InputSource::Keyboard, 0xC0}}, // alias for Grave
149 {"LBracket", {InputSource::Keyboard, 0xDB}},
150 {"Backslash", {InputSource::Keyboard, 0xDC}},
151 {"RBracket", {InputSource::Keyboard, 0xDD}},
152 {"Apostrophe", {InputSource::Keyboard, 0xDE}},
153 {"Quote", {InputSource::Keyboard, 0xDE}}, // alias for Apostrophe
154
155 // --- Keyboard: Numpad ---
156 {"Numpad0", {InputSource::Keyboard, 0x60}},
157 {"Numpad1", {InputSource::Keyboard, 0x61}},
158 {"Numpad2", {InputSource::Keyboard, 0x62}},
159 {"Numpad3", {InputSource::Keyboard, 0x63}},
160 {"Numpad4", {InputSource::Keyboard, 0x64}},
161 {"Numpad5", {InputSource::Keyboard, 0x65}},
162 {"Numpad6", {InputSource::Keyboard, 0x66}},
163 {"Numpad7", {InputSource::Keyboard, 0x67}},
164 {"Numpad8", {InputSource::Keyboard, 0x68}},
165 {"Numpad9", {InputSource::Keyboard, 0x69}},
166 {"NumpadAdd", {InputSource::Keyboard, 0x6B}},
167 {"NumpadSubtract", {InputSource::Keyboard, 0x6D}},
168 {"NumpadMultiply", {InputSource::Keyboard, 0x6A}},
169 {"NumpadDivide", {InputSource::Keyboard, 0x6F}},
170 {"NumpadDecimal", {InputSource::Keyboard, 0x6E}},
171
172 // --- Mouse buttons ---
173 {"Mouse1", {InputSource::Mouse, 0x01}},
174 {"Mouse2", {InputSource::Mouse, 0x02}},
175 {"Mouse3", {InputSource::Mouse, 0x04}},
176 {"Mouse4", {InputSource::Mouse, 0x05}},
177 {"Mouse5", {InputSource::Mouse, 0x06}},
178
179 // --- Mouse wheel (event-only; captured via the input layer's WndProc hook) ---
180 {"WheelUp", {InputSource::MouseWheel, WheelCode::Up}},
181 {"WheelDown", {InputSource::MouseWheel, WheelCode::Down}},
182 {"WheelLeft", {InputSource::MouseWheel, WheelCode::Left}},
183 {"WheelRight", {InputSource::MouseWheel, WheelCode::Right}},
184
185 // --- Gamepad buttons ---
186 {"Gamepad_A", {InputSource::Gamepad, GamepadCode::A}},
187 {"Gamepad_B", {InputSource::Gamepad, GamepadCode::B}},
188 {"Gamepad_X", {InputSource::Gamepad, GamepadCode::X}},
189 {"Gamepad_Y", {InputSource::Gamepad, GamepadCode::Y}},
190 {"Gamepad_LB", {InputSource::Gamepad, GamepadCode::LeftBumper}},
191 {"Gamepad_RB", {InputSource::Gamepad, GamepadCode::RightBumper}},
192 {"Gamepad_LT", {InputSource::Gamepad, GamepadCode::LeftTrigger}},
193 {"Gamepad_RT", {InputSource::Gamepad, GamepadCode::RightTrigger}},
194 {"Gamepad_Start", {InputSource::Gamepad, GamepadCode::Start}},
195 {"Gamepad_Back", {InputSource::Gamepad, GamepadCode::Back}},
196 {"Gamepad_LS", {InputSource::Gamepad, GamepadCode::LeftStick}},
197 {"Gamepad_RS", {InputSource::Gamepad, GamepadCode::RightStick}},
198 {"Gamepad_DpadUp", {InputSource::Gamepad, GamepadCode::DpadUp}},
199 {"Gamepad_DpadDown", {InputSource::Gamepad, GamepadCode::DpadDown}},
200 {"Gamepad_DpadLeft", {InputSource::Gamepad, GamepadCode::DpadLeft}},
201 {"Gamepad_DpadRight", {InputSource::Gamepad, GamepadCode::DpadRight}},
202
203 // --- Gamepad thumbstick axes (digital) ---
204 {"Gamepad_LSUp", {InputSource::Gamepad, GamepadCode::LeftStickUp}},
205 {"Gamepad_LSDown", {InputSource::Gamepad, GamepadCode::LeftStickDown}},
206 {"Gamepad_LSLeft", {InputSource::Gamepad, GamepadCode::LeftStickLeft}},
207 {"Gamepad_LSRight", {InputSource::Gamepad, GamepadCode::LeftStickRight}},
208 {"Gamepad_RSUp", {InputSource::Gamepad, GamepadCode::RightStickUp}},
209 {"Gamepad_RSDown", {InputSource::Gamepad, GamepadCode::RightStickDown}},
210 {"Gamepad_RSLeft", {InputSource::Gamepad, GamepadCode::RightStickLeft}},
211 {"Gamepad_RSRight", {InputSource::Gamepad, GamepadCode::RightStickRight}},
212 };
213 // clang-format on
214
215 constexpr size_t name_table_size = sizeof(name_table) / sizeof(name_table[0]);
216
217 97742 int icompare(std::string_view a, std::string_view b) noexcept
218 {
219 97742 const size_t len = std::min(a.size(), b.size());
220
2/2
✓ Branch 11 → 6 taken 223970 times.
✓ Branch 11 → 12 taken 6446 times.
230416 for (size_t i = 0; i < len; ++i)
221 {
222 223970 const int ca = std::tolower(static_cast<unsigned char>(a[i]));
223 223970 const int cb = std::tolower(static_cast<unsigned char>(b[i]));
224
2/2
✓ Branch 8 → 9 taken 91296 times.
✓ Branch 8 → 10 taken 132674 times.
223970 if (ca != cb)
225 {
226 91296 return ca - cb;
227 }
228 }
229
2/2
✓ Branch 14 → 15 taken 4535 times.
✓ Branch 14 → 16 taken 1911 times.
6446 if (a.size() < b.size())
230 {
231 4535 return -1;
232 }
233
2/2
✓ Branch 18 → 19 taken 1765 times.
✓ Branch 18 → 20 taken 146 times.
1911 if (a.size() > b.size())
234 {
235 1765 return 1;
236 }
237 146 return 0;
238 }
239
240 /**
241 * @brief Parses a source-tagged hex token such as "Mouse:0xFE" into an InputCode.
242 * @details The inverse of format_input_code's off-table form for non-keyboard sources. The source tag matches
243 * input_source_to_string ("Keyboard", "Mouse", "Gamepad", "MouseWheel"), case-insensitively, and the
244 * value parses as hex with or without a 0x prefix.
245 * @param source_token The device-source tag (text before the ':').
246 * @param hex_token The hex value (text after the ':'), with or without a 0x/0X prefix.
247 * @return The resolved InputCode, or std::nullopt if the tag is unknown or the value is not a valid
248 * non-negative hex int.
249 */
250 10 std::optional<InputCode> parse_source_tagged_hex(std::string_view source_token,
251 std::string_view hex_token) noexcept
252 {
253 10 InputSource source = InputSource::Keyboard;
254
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 10 times.
10 if (icompare(source_token, "Keyboard") == 0)
255 {
256 source = InputSource::Keyboard;
257 }
258
2/2
✓ Branch 8 → 9 taken 5 times.
✓ Branch 8 → 10 taken 5 times.
10 else if (icompare(source_token, "Mouse") == 0)
259 {
260 5 source = InputSource::Mouse;
261 }
262
2/2
✓ Branch 12 → 13 taken 2 times.
✓ Branch 12 → 14 taken 3 times.
5 else if (icompare(source_token, "Gamepad") == 0)
263 {
264 2 source = InputSource::Gamepad;
265 }
266
2/2
✓ Branch 16 → 17 taken 2 times.
✓ Branch 16 → 18 taken 1 time.
3 else if (icompare(source_token, "MouseWheel") == 0)
267 {
268 2 source = InputSource::MouseWheel;
269 }
270 else
271 {
272 1 return std::nullopt;
273 }
274
275 // Strip an optional 0x / 0X prefix; std::from_chars(base 16) does not accept one.
276
7/10
✓ Branch 20 → 21 taken 8 times.
✓ Branch 20 → 28 taken 1 time.
✓ Branch 22 → 23 taken 7 times.
✓ Branch 22 → 28 taken 1 time.
✗ Branch 24 → 25 not taken.
✓ Branch 24 → 27 taken 7 times.
✗ Branch 26 → 27 not taken.
✗ Branch 26 → 28 not taken.
✓ Branch 29 → 30 taken 7 times.
✓ Branch 29 → 31 taken 2 times.
9 if (hex_token.size() >= 2 && hex_token[0] == '0' && (hex_token[1] == 'x' || hex_token[1] == 'X'))
277 {
278 7 hex_token.remove_prefix(2);
279 }
280
2/2
✓ Branch 32 → 33 taken 1 time.
✓ Branch 32 → 34 taken 8 times.
9 if (hex_token.empty())
281 {
282 1 return std::nullopt;
283 }
284
285 8 unsigned long value = 0;
286 8 const char *const first = hex_token.data();
287 8 const char *const last = hex_token.data() + hex_token.size();
288 8 const auto [ptr, ec] = std::from_chars(first, last, value, 16);
289
3/4
✓ Branch 38 → 39 taken 7 times.
✓ Branch 38 → 40 taken 1 time.
✗ Branch 39 → 40 not taken.
✓ Branch 39 → 41 taken 7 times.
8 if (ec != std::errc{} || ptr != last)
290 {
291 1 return std::nullopt;
292 }
293
1/2
✗ Branch 42 → 43 not taken.
✓ Branch 42 → 44 taken 7 times.
7 if (value > static_cast<unsigned long>(std::numeric_limits<int>::max()))
294 {
295 return std::nullopt;
296 }
297 7 return InputCode{source, static_cast<int>(value)};
298 }
299
300 struct SortedNameIndex
301 {
302 size_t indices[name_table_size]{};
303 size_t count = name_table_size;
304
305 76 SortedNameIndex()
306 76 {
307
2/2
✓ Branch 4 → 3 taken 11780 times.
✓ Branch 4 → 5 taken 76 times.
11856 for (size_t i = 0; i < name_table_size; ++i)
308 {
309 11780 indices[i] = i;
310 }
311 76 std::sort(indices, indices + count,
312 96444 [](size_t a, size_t b) { return icompare(name_table[a].name, name_table[b].name) < 0; });
313 76 }
314 };
315
316 190 const SortedNameIndex &get_sorted_index()
317 {
318
4/8
✓ Branch 2 → 3 taken 76 times.
✓ Branch 2 → 7 taken 114 times.
✓ Branch 4 → 5 taken 76 times.
✗ Branch 4 → 7 not taken.
✓ Branch 5 → 6 taken 76 times.
✗ Branch 5 → 9 not taken.
✗ Branch 9 → 10 not taken.
✗ Branch 9 → 11 not taken.
190 static const SortedNameIndex instance;
319 190 return instance;
320 }
321
322 struct CodeNameMap
323 {
324 std::unordered_map<InputCode, std::string_view, InputCodeHash> map;
325
326 15 CodeNameMap()
327 15 {
328
1/2
✓ Branch 3 → 4 taken 15 times.
✗ Branch 3 → 10 not taken.
15 map.reserve(name_table_size);
329
2/2
✓ Branch 7 → 5 taken 2325 times.
✓ Branch 7 → 8 taken 15 times.
2340 for (size_t i = 0; i < name_table_size; ++i)
330 {
331
1/2
✓ Branch 5 → 6 taken 2325 times.
✗ Branch 5 → 9 not taken.
2325 map.emplace(name_table[i].code, name_table[i].name);
332 }
333 15 }
334 };
335
336 33 const CodeNameMap &get_code_name_map()
337 {
338
4/8
✓ Branch 2 → 3 taken 15 times.
✓ Branch 2 → 8 taken 18 times.
✓ Branch 4 → 5 taken 15 times.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 15 times.
✗ Branch 5 → 10 not taken.
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 12 not taken.
33 static const CodeNameMap instance;
339 33 return instance;
340 }
341 } // anonymous namespace
342
343 197 std::optional<InputCode> parse_input_name(std::string_view name)
344 {
345 // A source-tagged hex token ("Mouse:0xFE", "Gamepad:0x800") is the inverse of format_input_code's off-table
346 // form: it carries the device source so a non-keyboard code survives a config round-trip. No table name
347 // contains ':', so a token with one is only ever a tag; a malformed tag falls through to the table search
348 // (which cannot match it) and yields nullopt.
349
2/2
✓ Branch 3 → 4 taken 10 times.
✓ Branch 3 → 10 taken 187 times.
197 if (const size_t colon = name.find(':'); colon != std::string_view::npos)
350 {
351
2/2
✓ Branch 8 → 9 taken 7 times.
✓ Branch 8 → 10 taken 3 times.
10 if (const auto tagged = parse_source_tagged_hex(name.substr(0, colon), name.substr(colon + 1)))
352 {
353 7 return tagged;
354 }
355 }
356
357 190 const auto &idx = get_sorted_index();
358 190 size_t lo = 0;
359 190 size_t hi = idx.count;
360
2/2
✓ Branch 20 → 12 taken 1270 times.
✓ Branch 20 → 21 taken 53 times.
1323 while (lo < hi)
361 {
362 1270 const size_t mid = lo + (hi - lo) / 2;
363 1270 const int cmp = icompare(name_table[idx.indices[mid]].name, name);
364
2/2
✓ Branch 14 → 15 taken 388 times.
✓ Branch 14 → 16 taken 882 times.
1270 if (cmp < 0)
365 {
366 388 lo = mid + 1;
367 }
368
2/2
✓ Branch 16 → 17 taken 745 times.
✓ Branch 16 → 18 taken 137 times.
882 else if (cmp > 0)
369 {
370 745 hi = mid;
371 }
372 else
373 {
374 137 return name_table[idx.indices[mid]].code;
375 }
376 }
377 53 return std::nullopt;
378 }
379
380 33 std::string_view input_code_to_name(const InputCode &code)
381 {
382
1/2
✓ Branch 2 → 3 taken 33 times.
✗ Branch 2 → 12 not taken.
33 const auto &map = get_code_name_map().map;
383
1/2
✓ Branch 3 → 4 taken 33 times.
✗ Branch 3 → 12 not taken.
33 const auto it = map.find(code);
384
2/2
✓ Branch 6 → 7 taken 26 times.
✓ Branch 6 → 9 taken 7 times.
33 if (it != map.end())
385 {
386 26 return it->second;
387 }
388 7 return {};
389 }
390
391 23 std::string format_input_code(const InputCode &code)
392 {
393
1/2
✓ Branch 2 → 3 taken 23 times.
✗ Branch 2 → 44 not taken.
23 const auto name = input_code_to_name(code);
394
2/2
✓ Branch 4 → 5 taken 17 times.
✓ Branch 4 → 10 taken 6 times.
23 if (!name.empty())
395 {
396
1/2
✓ Branch 7 → 8 taken 17 times.
✗ Branch 7 → 28 not taken.
34 return std::string(name);
397 }
398 // Off-table fallback. A Keyboard code emits bare hex ("0xNN"), which round-trips through the config
399 // keyboard hex fallback and preserves the existing serialized form. Any other source is tagged with its
400 // device name ("Mouse:0xNN") so the source is not lost: an untagged hex always parses back as Keyboard,
401 // which would silently turn an off-table Mouse/Gamepad code into a keyboard key on a config round-trip.
402
2/2
✓ Branch 10 → 11 taken 2 times.
✓ Branch 10 → 13 taken 4 times.
6 if (code.source == InputSource::Keyboard)
403 {
404
1/2
✓ Branch 11 → 12 taken 2 times.
✗ Branch 11 → 44 not taken.
2 return Format::format_hex(code.code, 2);
405 }
406
4/8
✓ Branch 13 → 14 taken 4 times.
✗ Branch 13 → 43 not taken.
✓ Branch 17 → 18 taken 4 times.
✗ Branch 17 → 35 not taken.
✓ Branch 18 → 19 taken 4 times.
✗ Branch 18 → 33 not taken.
✓ Branch 19 → 20 taken 4 times.
✗ Branch 19 → 31 not taken.
8 return std::string(input_source_to_string(code.source)) + ':' + Format::format_hex(code.code, 2);
407 }
408
409 } // namespace DetourModKit
410