GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 67.5% 224 / 0 / 332
Functions: 97.1% 33 / 0 / 34
Branches: 44.8% 142 / 0 / 317

src/input.cpp
Line Branch Exec Source
1 /**
2 * @file input.cpp
3 * @brief Implementation of the input polling and hotkey management system.
4 *
5 * Provides InputPoller (RAII polling engine) and InputManager (singleton wrapper)
6 * for monitoring keyboard, mouse, and gamepad input states on a background thread.
7 * Supports press (edge-triggered) and hold (level-triggered) input modes with
8 * modifier combinations, focus-aware polling, and XInput gamepad support.
9 */
10
11 #include "DetourModKit/input.hpp"
12 #include "DetourModKit/config.hpp"
13 #include "DetourModKit/logger.hpp"
14
15 #include <windows.h>
16 #include <Xinput.h>
17 #include <algorithm>
18 #include <exception>
19 #include <unordered_set>
20
21 namespace DetourModKit
22 {
23 namespace
24 {
25 /**
26 * @brief Checks whether a single InputCode is currently pressed.
27 * @param code The input code to check.
28 * @param gamepad_state Cached XInput state for the current poll cycle.
29 * @param gamepad_connected Whether the gamepad is connected.
30 * @param trigger_threshold Analog trigger deadzone threshold.
31 * @param stick_threshold Thumbstick deadzone threshold.
32 * @return true if the input is currently pressed.
33 */
34 3 bool is_code_pressed(const InputCode &code,
35 const XINPUT_STATE &gamepad_state,
36 bool gamepad_connected,
37 int trigger_threshold,
38 int stick_threshold) noexcept
39 {
40
1/3
✓ Branch 2 → 3 taken 3 times.
✗ Branch 2 → 9 not taken.
✗ Branch 2 → 25 not taken.
3 switch (code.source)
41 {
42 3 case InputSource::Keyboard:
43 case InputSource::Mouse:
44
2/4
✓ Branch 3 → 4 taken 3 times.
✗ Branch 3 → 7 not taken.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 3 times.
3 return code.code != 0 && (GetAsyncKeyState(code.code) & 0x8000) != 0;
45 case InputSource::Gamepad:
46 {
47 if (!gamepad_connected)
48 {
49 return false;
50 }
51 // Fast path: digital button bitmask (all codes below synthetic range)
52 if (code.code < GamepadCode::LeftTrigger)
53 {
54 return (gamepad_state.Gamepad.wButtons & static_cast<WORD>(code.code)) != 0;
55 }
56 // Synthetic analog codes
57 switch (code.code)
58 {
59 case GamepadCode::LeftTrigger:
60 return gamepad_state.Gamepad.bLeftTrigger > trigger_threshold;
61 case GamepadCode::RightTrigger:
62 return gamepad_state.Gamepad.bRightTrigger > trigger_threshold;
63 case GamepadCode::LeftStickUp:
64 return gamepad_state.Gamepad.sThumbLY > stick_threshold;
65 case GamepadCode::LeftStickDown:
66 return gamepad_state.Gamepad.sThumbLY < -stick_threshold;
67 case GamepadCode::LeftStickLeft:
68 return gamepad_state.Gamepad.sThumbLX < -stick_threshold;
69 case GamepadCode::LeftStickRight:
70 return gamepad_state.Gamepad.sThumbLX > stick_threshold;
71 case GamepadCode::RightStickUp:
72 return gamepad_state.Gamepad.sThumbRY > stick_threshold;
73 case GamepadCode::RightStickDown:
74 return gamepad_state.Gamepad.sThumbRY < -stick_threshold;
75 case GamepadCode::RightStickLeft:
76 return gamepad_state.Gamepad.sThumbRX < -stick_threshold;
77 case GamepadCode::RightStickRight:
78 return gamepad_state.Gamepad.sThumbRX > stick_threshold;
79 default:
80 return false;
81 }
82 }
83 }
84 return false;
85 }
86
87 /**
88 * @brief Checks if a held input satisfies a required modifier.
89 * @details Returns true when the codes match exactly, or when both are
90 * keyboard modifiers in the same family (e.g., LShift satisfies
91 * generic Shift, and generic Shift satisfies LShift).
92 */
93 bool modifier_satisfies(const InputCode &required, const InputCode &held) noexcept
94 {
95 if (required == held)
96 {
97 return true;
98 }
99 if (required.source != InputSource::Keyboard || held.source != InputSource::Keyboard)
100 {
101 return false;
102 }
103 // Modifier family groups: {generic, left, right}
104 constexpr int families[][3] = {
105 {0x11, 0xA2, 0xA3}, // Ctrl, LCtrl, RCtrl
106 {0x10, 0xA0, 0xA1}, // Shift, LShift, RShift
107 {0x12, 0xA4, 0xA5}, // Alt, LAlt, RAlt
108 };
109 for (const auto &family : families)
110 {
111 bool req_in = false;
112 bool held_in = false;
113 for (int vk : family)
114 {
115 if (required.code == vk)
116 {
117 req_in = true;
118 }
119 if (held.code == vk)
120 {
121 held_in = true;
122 }
123 }
124 if (req_in && held_in)
125 {
126 return true;
127 }
128 }
129 return false;
130 }
131
132 /**
133 * @brief Scans bindings to determine if any use gamepad input codes.
134 * @param bindings The vector of bindings to scan.
135 * @return true if at least one binding contains a gamepad InputCode.
136 */
137 62 bool scan_for_gamepad_bindings(const std::vector<InputBinding> &bindings) noexcept
138 {
139
2/2
✓ Branch 47 → 4 taken 65 times.
✓ Branch 47 → 48 taken 50 times.
177 for (const auto &binding : bindings)
140 {
141
2/2
✓ Branch 21 → 8 taken 68 times.
✓ Branch 21 → 22 taken 53 times.
186 for (const auto &key : binding.keys)
142 {
143
2/2
✓ Branch 10 → 11 taken 12 times.
✓ Branch 10 → 12 taken 56 times.
68 if (key.source == InputSource::Gamepad)
144 {
145 12 return true;
146 }
147 }
148
2/2
✓ Branch 37 → 24 taken 10 times.
✓ Branch 37 → 38 taken 53 times.
116 for (const auto &mod : binding.modifiers)
149 {
150
1/2
✗ Branch 26 → 27 not taken.
✓ Branch 26 → 28 taken 10 times.
10 if (mod.source == InputSource::Gamepad)
151 {
152 return true;
153 }
154 }
155 }
156 50 return false;
157 }
158 } // anonymous namespace
159
160 // --- InputPoller ---
161
162 62 InputPoller::InputPoller(std::vector<InputBinding> bindings,
163 std::chrono::milliseconds poll_interval,
164 bool require_focus,
165 int gamepad_index,
166 int trigger_threshold,
167 62 int stick_threshold)
168 124 : bindings_(std::move(bindings)),
169
1/2
✓ Branch 7 → 8 taken 62 times.
✗ Branch 7 → 73 not taken.
62 poll_interval_(std::clamp(poll_interval, MIN_POLL_INTERVAL, MAX_POLL_INTERVAL)),
170 62 require_focus_(require_focus),
171
1/2
✓ Branch 14 → 15 taken 62 times.
✗ Branch 14 → 67 not taken.
62 active_states_(std::make_unique<std::atomic<uint8_t>[]>(bindings_.size())),
172
1/2
✓ Branch 15 → 16 taken 62 times.
✗ Branch 15 → 53 not taken.
62 gamepad_index_(std::clamp(gamepad_index, 0, 3)),
173
1/2
✓ Branch 16 → 17 taken 62 times.
✗ Branch 16 → 55 not taken.
62 trigger_threshold_(std::clamp(trigger_threshold, 0, 255)),
174
1/2
✓ Branch 17 → 18 taken 62 times.
✗ Branch 17 → 57 not taken.
62 stick_threshold_(std::clamp(stick_threshold, 0, 32767)),
175
1/2
✓ Branch 12 → 13 taken 62 times.
✗ Branch 12 → 69 not taken.
186 has_gamepad_bindings_(scan_for_gamepad_bindings(bindings_))
176 {
177
1/2
✓ Branch 20 → 21 taken 62 times.
✗ Branch 20 → 64 not taken.
62 name_index_.reserve(bindings_.size());
178 62 std::unordered_set<InputCode, InputCodeHash> modifier_set;
179
2/2
✓ Branch 47 → 23 taken 69 times.
✓ Branch 47 → 48 taken 62 times.
131 for (size_t i = 0; i < bindings_.size(); ++i)
180 {
181
1/2
✓ Branch 25 → 26 taken 69 times.
✗ Branch 25 → 29 not taken.
69 if (!bindings_[i].name.empty())
182 {
183
2/4
✓ Branch 27 → 28 taken 69 times.
✗ Branch 27 → 61 not taken.
✓ Branch 28 → 29 taken 69 times.
✗ Branch 28 → 61 not taken.
69 name_index_[bindings_[i].name].push_back(i);
184 }
185
2/2
✓ Branch 44 → 32 taken 14 times.
✓ Branch 44 → 45 taken 69 times.
152 for (const auto &mod : bindings_[i].modifiers)
186 {
187
1/2
✓ Branch 34 → 35 taken 14 times.
✗ Branch 34 → 59 not taken.
14 modifier_set.insert(mod);
188 }
189 }
190
1/2
✓ Branch 50 → 51 taken 62 times.
✗ Branch 50 → 62 not taken.
62 known_modifiers_.assign(modifier_set.begin(), modifier_set.end());
191 62 }
192
193 62 InputPoller::~InputPoller() noexcept
194 {
195 62 shutdown();
196 62 }
197
198 53 void InputPoller::start()
199 {
200
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 7 taken 52 times.
53 if (poll_thread_.joinable())
201 {
202
1/2
✓ Branch 5 → 6 taken 1 time.
✗ Branch 5 → 13 not taken.
1 Logger::get_instance().warning("InputPoller: start() called while already running");
203 1 return;
204 }
205
206 52 running_.store(true, std::memory_order_release);
207 try
208 {
209 52 poll_thread_ = std::jthread([this](std::stop_token token)
210
2/4
✓ Branch 5 → 6 taken 52 times.
✗ Branch 5 → 8 not taken.
✓ Branch 8 → 9 taken 52 times.
✗ Branch 8 → 14 not taken.
156 { poll_loop(std::move(token)); });
211 }
212 catch (...)
213 {
214 running_.store(false, std::memory_order_release);
215 throw;
216 }
217 }
218
219 44 bool InputPoller::is_running() const noexcept
220 {
221 44 return running_.load(std::memory_order_acquire);
222 }
223
224 27 size_t InputPoller::binding_count() const noexcept
225 {
226 27 return bindings_.size();
227 }
228
229 4 std::chrono::milliseconds InputPoller::poll_interval() const noexcept
230 {
231 4 return poll_interval_;
232 }
233
234 2 int InputPoller::gamepad_index() const noexcept
235 {
236 2 return gamepad_index_;
237 }
238
239 5 bool InputPoller::is_binding_active(size_t index) const noexcept
240 {
241
2/2
✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 5 taken 3 times.
5 if (index >= bindings_.size())
242 {
243 2 return false;
244 }
245 6 return active_states_[index].load(std::memory_order_relaxed) != 0;
246 }
247
248 10 bool InputPoller::is_binding_active(std::string_view name) const noexcept
249 {
250 10 const auto it = name_index_.find(name);
251
2/2
✓ Branch 5 → 6 taken 7 times.
✓ Branch 5 → 32 taken 3 times.
10 if (it != name_index_.end())
252 {
253
2/2
✓ Branch 30 → 9 taken 8 times.
✓ Branch 30 → 31 taken 7 times.
22 for (const size_t idx : it->second)
254 {
255
1/2
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 21 taken 8 times.
16 if (active_states_[idx].load(std::memory_order_relaxed) != 0)
256 {
257 return true;
258 }
259 }
260 }
261 10 return false;
262 }
263
264 4 void InputPoller::set_require_focus(bool require_focus) noexcept
265 {
266 4 require_focus_.store(require_focus, std::memory_order_relaxed);
267 4 }
268
269 115 void InputPoller::shutdown() noexcept
270 {
271
2/2
✓ Branch 3 → 4 taken 63 times.
✓ Branch 3 → 5 taken 52 times.
115 if (!poll_thread_.joinable())
272 {
273 63 return;
274 }
275
276 52 poll_thread_.request_stop();
277 52 cv_.notify_all();
278 52 poll_thread_.join();
279
280 52 running_.store(false, std::memory_order_release);
281 52 release_active_holds();
282 }
283
284 52 void InputPoller::poll_loop(std::stop_token stop_token)
285 {
286 52 const size_t count = bindings_.size();
287 52 const int trigger_thresh = trigger_threshold_;
288 52 const int stick_thresh = stick_threshold_;
289 52 const auto &known_mods = known_modifiers_;
290
291 52 constexpr auto gamepad_reconnect_interval = std::chrono::seconds{2};
292 52 bool gamepad_was_connected = false;
293 52 auto last_gamepad_poll = std::chrono::steady_clock::time_point{};
294
295
2/2
✓ Branch 157 → 4 taken 60 times.
✓ Branch 157 → 158 taken 52 times.
112 while (!stop_token.stop_requested())
296 {
297 const bool process_focused =
298
4/6
✓ Branch 5 → 6 taken 54 times.
✓ Branch 5 → 8 taken 6 times.
✓ Branch 6 → 7 taken 54 times.
✗ Branch 6 → 201 not taken.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 54 times.
60 !require_focus_.load(std::memory_order_relaxed) || is_process_foreground();
299
300 // Poll gamepad state once per cycle when connected.
301 // When disconnected, throttle reconnection attempts to avoid
302 // the per-cycle overhead of XInputGetState on empty slots.
303 60 XINPUT_STATE gamepad_state{};
304 60 bool gamepad_connected = false;
305
3/4
✓ Branch 10 → 11 taken 15 times.
✓ Branch 10 → 24 taken 45 times.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 24 taken 15 times.
60 if (has_gamepad_bindings_ && process_focused)
306 {
307 const auto now = std::chrono::steady_clock::now();
308 if (gamepad_was_connected ||
309 (now - last_gamepad_poll) >= gamepad_reconnect_interval)
310 {
311 last_gamepad_poll = now;
312 gamepad_was_connected =
313 XInputGetState(static_cast<DWORD>(gamepad_index_),
314 &gamepad_state) == ERROR_SUCCESS;
315 }
316 gamepad_connected = gamepad_was_connected;
317 }
318
319
2/2
✓ Branch 149 → 25 taken 80 times.
✓ Branch 149 → 150 taken 60 times.
140 for (size_t i = 0; i < count; ++i)
320 {
321 80 const auto &binding = bindings_[i];
322
2/2
✓ Branch 27 → 28 taken 2 times.
✓ Branch 27 → 29 taken 78 times.
80 if (binding.keys.empty())
323 {
324 2 continue;
325 }
326
327 78 bool any_pressed = false;
328
329
2/2
✓ Branch 29 → 30 taken 3 times.
✓ Branch 29 → 104 taken 75 times.
78 if (process_focused)
330 {
331 3 bool modifiers_held = true;
332
1/2
✗ Branch 46 → 32 not taken.
✓ Branch 46 → 47 taken 3 times.
6 for (const auto &mod : binding.modifiers)
333 {
334 if (!is_code_pressed(mod, gamepad_state, gamepad_connected, trigger_thresh, stick_thresh))
335 {
336 modifiers_held = false;
337 break;
338 }
339 }
340
341
1/2
✓ Branch 47 → 48 taken 3 times.
✗ Branch 47 → 85 not taken.
3 if (modifiers_held)
342 {
343 // Strict matching: reject if any known modifier that is
344 // NOT in this binding's required set is currently held.
345
1/2
✗ Branch 83 → 50 not taken.
✓ Branch 83 → 84 taken 3 times.
6 for (const auto &km : known_mods)
346 {
347 if (!is_code_pressed(km, gamepad_state, gamepad_connected, trigger_thresh, stick_thresh))
348 {
349 continue;
350 }
351 bool is_required = false;
352 for (const auto &mod : binding.modifiers)
353 {
354 if (modifier_satisfies(mod, km))
355 {
356 is_required = true;
357 break;
358 }
359 }
360 if (!is_required)
361 {
362 modifiers_held = false;
363 break;
364 }
365 }
366 }
367
368
1/2
✓ Branch 85 → 86 taken 3 times.
✗ Branch 85 → 104 not taken.
3 if (modifiers_held)
369 {
370
2/2
✓ Branch 102 → 88 taken 3 times.
✓ Branch 102 → 103 taken 3 times.
9 for (const auto &key : binding.keys)
371 {
372
1/2
✗ Branch 91 → 92 not taken.
✓ Branch 91 → 93 taken 3 times.
3 if (is_code_pressed(key, gamepad_state, gamepad_connected, trigger_thresh, stick_thresh))
373 {
374 any_pressed = true;
375 break;
376 }
377 }
378 }
379 }
380
381 const bool was_active =
382 78 active_states_[i].load(std::memory_order_relaxed) != 0;
383
384
2/3
✓ Branch 112 → 113 taken 61 times.
✓ Branch 112 → 131 taken 17 times.
✗ Branch 112 → 148 not taken.
78 switch (binding.mode)
385 {
386 61 case InputMode::Press:
387 {
388
1/4
✗ Branch 113 → 114 not taken.
✓ Branch 113 → 118 taken 61 times.
✗ Branch 114 → 115 not taken.
✗ Branch 114 → 118 not taken.
61 if (any_pressed && !was_active)
389 {
390 if (binding.on_press)
391 {
392 try
393 {
394 binding.on_press();
395 }
396 catch (const std::exception &e)
397 {
398 Logger::get_instance().error(
399 "InputPoller: Exception in press callback \"{}\": {}",
400 binding.name, e.what());
401 }
402 catch (...)
403 {
404 Logger::get_instance().error(
405 "InputPoller: Unknown exception in press callback \"{}\"",
406 binding.name);
407 }
408 }
409 }
410
1/2
✗ Branch 119 → 120 not taken.
✓ Branch 119 → 121 taken 61 times.
61 active_states_[i].store(any_pressed ? 1 : 0, std::memory_order_relaxed);
411 61 break;
412 }
413 17 case InputMode::Hold:
414 {
415
1/2
✗ Branch 131 → 132 not taken.
✓ Branch 131 → 135 taken 17 times.
17 if (any_pressed != was_active)
416 {
417 if (binding.on_state_change)
418 {
419 try
420 {
421 binding.on_state_change(any_pressed);
422 }
423 catch (const std::exception &e)
424 {
425 Logger::get_instance().error(
426 "InputPoller: Exception in hold callback \"{}\": {}",
427 binding.name, e.what());
428 }
429 catch (...)
430 {
431 Logger::get_instance().error(
432 "InputPoller: Unknown exception in hold callback \"{}\"",
433 binding.name);
434 }
435 }
436 }
437
1/2
✗ Branch 136 → 137 not taken.
✓ Branch 136 → 138 taken 17 times.
17 active_states_[i].store(any_pressed ? 1 : 0, std::memory_order_relaxed);
438 17 break;
439 }
440 }
441 }
442
443
1/2
✓ Branch 150 → 151 taken 60 times.
✗ Branch 150 → 201 not taken.
60 std::unique_lock lock(cv_mutex_);
444
1/2
✓ Branch 152 → 153 taken 60 times.
✗ Branch 152 → 196 not taken.
60 cv_.wait_for(lock, stop_token, poll_interval_, [&stop_token]()
445 117 { return stop_token.stop_requested(); });
446 60 }
447 52 }
448
449 52 void InputPoller::release_active_holds() noexcept
450 {
451
2/2
✓ Branch 31 → 3 taken 67 times.
✓ Branch 31 → 32 taken 52 times.
119 for (size_t i = 0; i < bindings_.size(); ++i)
452 {
453
1/2
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 29 taken 67 times.
134 if (active_states_[i].load(std::memory_order_relaxed) != 0)
454 {
455 active_states_[i].store(0, std::memory_order_relaxed);
456
457 const auto &binding = bindings_[i];
458 if (binding.mode == InputMode::Hold && binding.on_state_change)
459 {
460 try
461 {
462 binding.on_state_change(false);
463 }
464 catch (const std::exception &e)
465 {
466 Logger::get_instance().error(
467 "InputPoller: Exception in hold release callback \"{}\": {}",
468 binding.name, e.what());
469 }
470 catch (...)
471 {
472 Logger::get_instance().error(
473 "InputPoller: Unknown exception in hold release callback \"{}\"",
474 binding.name);
475 }
476 }
477 }
478 }
479 52 }
480
481 54 bool InputPoller::is_process_foreground() const
482 {
483
1/2
✓ Branch 2 → 3 taken 54 times.
✗ Branch 2 → 10 not taken.
54 HWND foreground = GetForegroundWindow();
484
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 54 times.
54 if (!foreground)
485 {
486 return false;
487 }
488 54 DWORD foreground_pid = 0;
489
1/2
✓ Branch 5 → 6 taken 54 times.
✗ Branch 5 → 10 not taken.
54 GetWindowThreadProcessId(foreground, &foreground_pid);
490
1/2
✓ Branch 6 → 7 taken 54 times.
✗ Branch 6 → 10 not taken.
54 return foreground_pid == GetCurrentProcessId();
491 }
492
493 // --- InputManager ---
494
495 223 void InputManager::register_press(std::string_view name, const std::vector<InputCode> &keys,
496 std::function<void()> callback)
497 {
498
1/2
✓ Branch 6 → 7 taken 223 times.
✗ Branch 6 → 10 not taken.
223 register_press(name, keys, {}, std::move(callback));
499 223 }
500
501 236 void InputManager::register_press(std::string_view name, const std::vector<InputCode> &keys,
502 const std::vector<InputCode> &modifiers,
503 std::function<void()> callback)
504 {
505
1/2
✓ Branch 2 → 3 taken 236 times.
✗ Branch 2 → 40 not taken.
236 std::lock_guard lock(mutex_);
506
507
2/2
✓ Branch 4 → 5 taken 3 times.
✓ Branch 4 → 8 taken 233 times.
236 if (poller_)
508 {
509
2/4
✓ Branch 5 → 6 taken 3 times.
✗ Branch 5 → 38 not taken.
✓ Branch 6 → 7 taken 3 times.
✗ Branch 6 → 31 not taken.
3 Logger::get_instance().warning(
510 "InputManager: Cannot register binding \"{}\" while poller is running", name);
511 3 return;
512 }
513
514 233 InputBinding binding;
515
1/2
✓ Branch 11 → 12 taken 233 times.
✗ Branch 11 → 32 not taken.
233 binding.name = std::string{name};
516
1/2
✓ Branch 15 → 16 taken 233 times.
✗ Branch 15 → 36 not taken.
233 binding.keys = keys;
517
1/2
✓ Branch 16 → 17 taken 233 times.
✗ Branch 16 → 36 not taken.
233 binding.modifiers = modifiers;
518 233 binding.mode = InputMode::Press;
519 233 binding.on_press = std::move(callback);
520
1/2
✓ Branch 22 → 23 taken 233 times.
✗ Branch 22 → 36 not taken.
466 pending_bindings_.push_back(std::move(binding));
521
2/2
✓ Branch 26 → 27 taken 233 times.
✓ Branch 26 → 29 taken 3 times.
236 }
522
523 8 void InputManager::register_hold(std::string_view name, const std::vector<InputCode> &keys,
524 std::function<void(bool)> callback)
525 {
526
1/2
✓ Branch 6 → 7 taken 8 times.
✗ Branch 6 → 10 not taken.
8 register_hold(name, keys, {}, std::move(callback));
527 8 }
528
529 16 void InputManager::register_hold(std::string_view name, const std::vector<InputCode> &keys,
530 const std::vector<InputCode> &modifiers,
531 std::function<void(bool)> callback)
532 {
533
1/2
✓ Branch 2 → 3 taken 16 times.
✗ Branch 2 → 40 not taken.
16 std::lock_guard lock(mutex_);
534
535
2/2
✓ Branch 4 → 5 taken 2 times.
✓ Branch 4 → 8 taken 14 times.
16 if (poller_)
536 {
537
2/4
✓ Branch 5 → 6 taken 2 times.
✗ Branch 5 → 38 not taken.
✓ Branch 6 → 7 taken 2 times.
✗ Branch 6 → 31 not taken.
2 Logger::get_instance().warning(
538 "InputManager: Cannot register binding \"{}\" while poller is running", name);
539 2 return;
540 }
541
542 14 InputBinding binding;
543
1/2
✓ Branch 11 → 12 taken 14 times.
✗ Branch 11 → 32 not taken.
14 binding.name = std::string{name};
544
1/2
✓ Branch 15 → 16 taken 14 times.
✗ Branch 15 → 36 not taken.
14 binding.keys = keys;
545
1/2
✓ Branch 16 → 17 taken 14 times.
✗ Branch 16 → 36 not taken.
14 binding.modifiers = modifiers;
546 14 binding.mode = InputMode::Hold;
547 14 binding.on_state_change = std::move(callback);
548
1/2
✓ Branch 22 → 23 taken 14 times.
✗ Branch 22 → 36 not taken.
28 pending_bindings_.push_back(std::move(binding));
549
2/2
✓ Branch 26 → 27 taken 14 times.
✓ Branch 26 → 29 taken 2 times.
16 }
550
551 5 void InputManager::register_press(std::string_view name, const Config::KeyComboList &combos,
552 std::function<void()> callback)
553 {
554
2/2
✓ Branch 18 → 4 taken 6 times.
✓ Branch 18 → 19 taken 5 times.
16 for (const auto &combo : combos)
555 {
556
2/4
✓ Branch 6 → 7 taken 6 times.
✗ Branch 6 → 22 not taken.
✓ Branch 7 → 8 taken 6 times.
✗ Branch 7 → 20 not taken.
6 register_press(name, combo.keys, combo.modifiers, callback);
557 }
558 5 }
559
560 4 void InputManager::register_hold(std::string_view name, const Config::KeyComboList &combos,
561 std::function<void(bool)> callback)
562 {
563
2/2
✓ Branch 18 → 4 taken 5 times.
✓ Branch 18 → 19 taken 4 times.
13 for (const auto &combo : combos)
564 {
565
2/4
✓ Branch 6 → 7 taken 5 times.
✗ Branch 6 → 22 not taken.
✓ Branch 7 → 8 taken 5 times.
✗ Branch 7 → 20 not taken.
5 register_hold(name, combo.keys, combo.modifiers, callback);
566 }
567 4 }
568
569 3 void InputManager::set_require_focus(bool require_focus)
570 {
571
1/2
✓ Branch 2 → 3 taken 3 times.
✗ Branch 2 → 9 not taken.
3 std::lock_guard lock(mutex_);
572 3 require_focus_ = require_focus;
573
2/2
✓ Branch 4 → 5 taken 2 times.
✓ Branch 4 → 7 taken 1 time.
3 if (poller_)
574 {
575 2 poller_->set_require_focus(require_focus);
576 }
577 3 }
578
579 1 void InputManager::set_gamepad_index(int index)
580 {
581
1/2
✓ Branch 2 → 3 taken 1 time.
✗ Branch 2 → 10 not taken.
1 std::lock_guard lock(mutex_);
582
1/2
✓ Branch 3 → 4 taken 1 time.
✗ Branch 3 → 6 not taken.
1 gamepad_index_ = std::clamp(index, 0, 3);
583 1 }
584
585 1 void InputManager::set_trigger_threshold(int threshold)
586 {
587
1/2
✓ Branch 2 → 3 taken 1 time.
✗ Branch 2 → 10 not taken.
1 std::lock_guard lock(mutex_);
588
1/2
✓ Branch 3 → 4 taken 1 time.
✗ Branch 3 → 6 not taken.
1 trigger_threshold_ = std::clamp(threshold, 0, 255);
589 1 }
590
591 1 void InputManager::set_stick_threshold(int threshold)
592 {
593
1/2
✓ Branch 2 → 3 taken 1 time.
✗ Branch 2 → 10 not taken.
1 std::lock_guard lock(mutex_);
594
1/2
✓ Branch 3 → 4 taken 1 time.
✗ Branch 3 → 6 not taken.
1 stick_threshold_ = std::clamp(threshold, 0, 32767);
595 1 }
596
597 22 void InputManager::start(std::chrono::milliseconds poll_interval)
598 {
599
1/2
✓ Branch 2 → 3 taken 22 times.
✗ Branch 2 → 62 not taken.
22 std::lock_guard lock(mutex_);
600
601
2/2
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 8 taken 21 times.
22 if (poller_)
602 {
603
2/4
✓ Branch 5 → 6 taken 1 time.
✗ Branch 5 → 60 not taken.
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 51 not taken.
1 Logger::get_instance().warning("InputManager: start() called while already running");
604 1 return;
605 }
606
607
2/2
✓ Branch 9 → 10 taken 1 time.
✓ Branch 9 → 11 taken 20 times.
21 if (pending_bindings_.empty())
608 {
609 1 return;
610 }
611
612
1/2
✓ Branch 11 → 12 taken 20 times.
✗ Branch 11 → 60 not taken.
20 Logger &logger = Logger::get_instance();
613 logger.info("InputManager: Starting with {} binding(s), poll interval {}ms",
614
1/2
✓ Branch 14 → 15 taken 20 times.
✗ Branch 14 → 52 not taken.
20 pending_bindings_.size(), poll_interval.count());
615
616
2/2
✓ Branch 31 → 17 taken 26 times.
✓ Branch 31 → 32 taken 20 times.
66 for (const auto &binding : pending_bindings_)
617 {
618
1/2
✓ Branch 21 → 22 taken 26 times.
✗ Branch 21 → 55 not taken.
26 logger.debug("InputManager: Registered {} binding \"{}\" with {} key(s)",
619 52 input_mode_to_string(binding.mode), binding.name, binding.keys.size());
620 }
621
622
1/2
✓ Branch 34 → 35 taken 20 times.
✗ Branch 34 → 59 not taken.
40 poller_ = std::make_shared<InputPoller>(std::move(pending_bindings_), poll_interval,
623 20 require_focus_, gamepad_index_, trigger_threshold_,
624 40 stick_threshold_);
625 20 pending_bindings_.clear();
626
1/2
✓ Branch 39 → 40 taken 20 times.
✗ Branch 39 → 60 not taken.
20 poller_->start();
627 20 active_poller_.store(poller_, std::memory_order_release);
628 20 running_.store(true, std::memory_order_release);
629
2/2
✓ Branch 46 → 47 taken 20 times.
✓ Branch 46 → 49 taken 2 times.
22 }
630
631 24 bool InputManager::is_running() const noexcept
632 {
633 24 return running_.load(std::memory_order_acquire);
634 }
635
636 31 size_t InputManager::binding_count() const noexcept
637 {
638 31 std::lock_guard lock(mutex_);
639
2/2
✓ Branch 4 → 5 taken 12 times.
✓ Branch 4 → 7 taken 19 times.
31 if (poller_)
640 {
641 12 return poller_->binding_count();
642 }
643 19 return pending_bindings_.size();
644 31 }
645
646 7 bool InputManager::is_binding_active(std::string_view name) const noexcept
647 {
648 7 auto p = active_poller_.load(std::memory_order_acquire);
649
2/2
✓ Branch 4 → 5 taken 4 times.
✓ Branch 4 → 7 taken 3 times.
7 if (p)
650 {
651 4 return p->is_binding_active(name);
652 }
653 3 return false;
654 7 }
655
656 97 void InputManager::shutdown() noexcept
657 {
658 97 std::shared_ptr<InputPoller> local_poller;
659
660 {
661 97 std::lock_guard lock(mutex_);
662 // Clear atomic shared_ptr before releasing the poller to ensure
663 // concurrent is_binding_active() callers hold a valid shared_ptr.
664 97 active_poller_.store(nullptr, std::memory_order_release);
665 97 running_.store(false, std::memory_order_release);
666 194 local_poller = std::move(poller_);
667 97 pending_bindings_.clear();
668 97 }
669
670
2/2
✓ Branch 13 → 14 taken 20 times.
✓ Branch 13 → 16 taken 77 times.
97 if (local_poller)
671 {
672 20 local_poller->shutdown();
673 }
674 97 }
675 } // namespace DetourModKit
676