GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 92.9% 13 / 0 / 14
Functions: 100.0% 5 / 0 / 5
Branches: 71.4% 5 / 0 / 7

include/DetourModKit/input.hpp
Line Branch Exec Source
1 #ifndef DETOURMODKIT_INPUT_HPP
2 #define DETOURMODKIT_INPUT_HPP
3
4 #include "DetourModKit/input_codes.hpp"
5 #include "DetourModKit/config.hpp"
6
7 #include <atomic>
8 #include <chrono>
9 #include <condition_variable>
10 #include <cstdint>
11 #include <functional>
12 #include <memory>
13 #include <mutex>
14 #include <string>
15 #include <string_view>
16 #include <thread>
17 #include <unordered_map>
18 #include <unordered_set>
19 #include <vector>
20
21 namespace DetourModKit
22 {
23 /**
24 * @enum InputMode
25 * @brief Defines how a registered key binding is triggered.
26 */
27 enum class InputMode
28 {
29 Press,
30 Hold
31 };
32
33 /**
34 * @brief Converts an InputMode enum to its string representation.
35 * @param mode The InputMode enum value.
36 * @return std::string_view String representation of the mode.
37 */
38 28 constexpr std::string_view input_mode_to_string(InputMode mode)
39 {
40
2/3
✓ Branch 2 → 3 taken 21 times.
✓ Branch 2 → 4 taken 7 times.
✗ Branch 2 → 5 not taken.
28 switch (mode)
41 {
42 21 case InputMode::Press:
43 21 return "Press";
44 7 case InputMode::Hold:
45 7 return "Hold";
46 }
47 return "Unknown";
48 }
49
50 // Input system configuration defaults
51 inline constexpr std::chrono::milliseconds DEFAULT_POLL_INTERVAL{16};
52 inline constexpr std::chrono::milliseconds MIN_POLL_INTERVAL{1};
53 inline constexpr std::chrono::milliseconds MAX_POLL_INTERVAL{1000};
54
55 /**
56 * @struct InputBinding
57 * @brief Describes a single input-to-action binding.
58 * @details Holds the action name, input codes, modifier codes, input mode,
59 * and callbacks. For Press mode, the press callback fires on key-down edge.
60 * For Hold mode, the state callback fires with true on press and false on
61 * release (including during shutdown for active holds).
62 *
63 * The keys vector uses OR logic: any single input triggers the binding.
64 * The modifiers vector uses AND logic: all modifiers must be held
65 * simultaneously for the binding to activate. Modifier matching is
66 * strict: any key that appears as a modifier in *any* registered
67 * binding will block bindings that do not list it as a required
68 * modifier. This prevents "V" from firing when "Shift+V" is pressed.
69 *
70 * Each InputCode identifies both the device source (keyboard, mouse,
71 * gamepad) and the button/key code. All codes within a binding should
72 * be from the same device group (keyboard/mouse or gamepad).
73 *
74 * @warning Callbacks are invoked on the polling thread. They must not capture references
75 * or pointers to objects whose lifetime may end before shutdown() completes.
76 * Callbacks should execute quickly to avoid degrading the effective poll rate.
77 */
78 struct InputBinding
79 {
80 std::string name;
81 std::vector<InputCode> keys;
82 std::vector<InputCode> modifiers;
83 InputMode mode = InputMode::Press;
84 std::function<void()> on_press;
85 std::function<void(bool)> on_state_change;
86 };
87
88 /**
89 * @class InputPoller
90 * @brief RAII input polling engine that monitors key states on a background thread.
91 * @details Manages a dedicated polling thread that checks virtual key states via
92 * GetAsyncKeyState. Supports both press (edge-triggered) and hold
93 * (level-triggered) input modes with optional modifier key combinations.
94 * When require_focus is enabled (default), key events are only processed
95 * when the current process owns the foreground window.
96 *
97 * On shutdown, active hold bindings receive an on_state_change(false)
98 * callback to ensure consumers are notified of the release.
99 *
100 * @note Non-copyable, non-movable. Callbacks are invoked on the polling thread.
101 * @note This class is the building block for the InputManager singleton.
102 *
103 * @warning When used inside a DLL, shutdown() must be called before DLL_PROCESS_DETACH.
104 * Calling join() on a thread during DllMain can deadlock due to the loader lock.
105 * Use DMK_Shutdown() to ensure proper teardown ordering.
106 */
107 class InputPoller
108 {
109 public:
110 /**
111 * @brief Constructs an InputPoller with the given bindings and poll interval.
112 * @param bindings Vector of input bindings to monitor.
113 * @param poll_interval Time between polling cycles.
114 * @param require_focus When true, key events are ignored unless the current
115 * process owns the foreground window.
116 * @param gamepad_index XInput controller index (0-3) to poll for gamepad bindings.
117 * @param trigger_threshold Analog trigger deadzone threshold (0-255). Trigger values
118 * above this threshold are considered "pressed".
119 * @param stick_threshold Thumbstick deadzone threshold (0-32767). Axis values
120 * exceeding this threshold in any direction are "pressed".
121 * @note The polling thread does not start until start() is called.
122 */
123 explicit InputPoller(std::vector<InputBinding> bindings,
124 std::chrono::milliseconds poll_interval = DEFAULT_POLL_INTERVAL,
125 bool require_focus = true,
126 int gamepad_index = 0,
127 int trigger_threshold = GamepadCode::TriggerThreshold,
128 int stick_threshold = GamepadCode::StickThreshold);
129
130 ~InputPoller() noexcept;
131
132 InputPoller(const InputPoller &) = delete;
133 InputPoller &operator=(const InputPoller &) = delete;
134 InputPoller(InputPoller &&) = delete;
135 InputPoller &operator=(InputPoller &&) = delete;
136
137 /**
138 * @brief Starts the polling thread.
139 * @details Safe to call only once. Subsequent calls are ignored with a warning.
140 * @note Not thread-safe. Must be called from a single thread. Use
141 * InputManager::start() for a thread-safe wrapper.
142 */
143 void start();
144
145 /**
146 * @brief Checks if the polling thread is currently running.
147 * @return true if the poller is active and monitoring keys.
148 */
149 [[nodiscard]] bool is_running() const noexcept;
150
151 /**
152 * @brief Returns the number of registered bindings.
153 * @return size_t Number of bindings.
154 */
155 [[nodiscard]] size_t binding_count() const noexcept;
156
157 /**
158 * @brief Returns the configured poll interval.
159 * @return std::chrono::milliseconds The poll interval.
160 */
161 [[nodiscard]] std::chrono::milliseconds poll_interval() const noexcept;
162
163 /**
164 * @brief Returns the configured gamepad controller index.
165 * @return int The XInput controller index (0-3).
166 */
167 [[nodiscard]] int gamepad_index() const noexcept;
168
169 /**
170 * @brief Queries whether a binding is currently active by index.
171 * @param index Zero-based index into the bindings vector.
172 * @return true if the binding's key(s) are currently pressed.
173 * Returns false for out-of-range indices.
174 * @note Thread-safe. Can be called from any thread.
175 */
176 [[nodiscard]] bool is_binding_active(size_t index) const noexcept;
177
178 /**
179 * @brief Queries whether a binding is currently active by name.
180 * @param name The binding name to look up.
181 * @return true if the named binding's key(s) are currently pressed.
182 * Returns false if no binding with the given name exists.
183 * @note Thread-safe. Can be called from any thread.
184 */
185 [[nodiscard]] bool is_binding_active(std::string_view name) const noexcept;
186
187 /**
188 * @brief Sets whether the poller requires the current process to own the
189 * foreground window before processing key events.
190 * @param require_focus true to enable focus checking (default), false to disable.
191 * @note Thread-safe. Can be called while the poller is running.
192 */
193 void set_require_focus(bool require_focus) noexcept;
194
195 /**
196 * @brief Stops the polling thread.
197 * @details Signals the thread to stop and waits for it to join. After the
198 * thread has joined, fires on_state_change(false) for any hold
199 * bindings that were active at the time of shutdown. Safe to call
200 * multiple times.
201 */
202 void shutdown() noexcept;
203
204 private:
205 void poll_loop(std::stop_token stop_token);
206 void release_active_holds() noexcept;
207 [[nodiscard]] bool is_process_foreground() const;
208
209 /// Transparent hasher enabling std::string_view lookup without allocation.
210 struct StringHash
211 {
212 using is_transparent = void;
213 94 size_t operator()(std::string_view sv) const noexcept
214 {
215 94 return std::hash<std::string_view>{}(sv);
216 }
217 };
218
219 std::vector<InputBinding> bindings_;
220 std::unordered_map<std::string, std::vector<size_t>, StringHash, std::equal_to<>> name_index_;
221 std::vector<InputCode> known_modifiers_;
222 std::chrono::milliseconds poll_interval_;
223 std::atomic<bool> require_focus_;
224 std::atomic<bool> running_{false};
225 std::jthread poll_thread_;
226 std::mutex cv_mutex_;
227 std::condition_variable_any cv_;
228
229 // Per-binding active state, indexed parallel to bindings_.
230 // Atomic for cross-thread reads via is_binding_active().
231 std::unique_ptr<std::atomic<uint8_t>[]> active_states_;
232
233 int gamepad_index_;
234 int trigger_threshold_;
235 int stick_threshold_;
236 bool has_gamepad_bindings_;
237 };
238
239 /**
240 * @class InputManager
241 * @brief Singleton that provides a convenient interface for registering and
242 * monitoring hotkey bindings.
243 * @details Wraps an InputPoller internally. Bindings are registered before
244 * calling start(), which constructs and starts the poller. Integrates
245 * with DMK_Shutdown() for automatic cleanup.
246 *
247 * @note Thread-safe. For advanced use cases requiring multiple independent
248 * pollers or custom lifetime management, use InputPoller directly.
249 *
250 * @warning When used inside a DLL, shutdown() must be called before DLL_PROCESS_DETACH.
251 * Calling join() on a thread during DllMain can deadlock due to the loader lock.
252 */
253 class InputManager
254 {
255 public:
256 /**
257 * @brief Retrieves the singleton instance of the InputManager.
258 * @return InputManager& Reference to the single InputManager instance.
259 */
260 115 static InputManager &get_instance()
261 {
262
3/4
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 8 taken 114 times.
✓ Branch 4 → 5 taken 1 time.
✗ Branch 4 → 8 not taken.
115 static InputManager instance;
263 115 return instance;
264 }
265
266 /**
267 * @brief Registers a press-mode binding.
268 * @details The callback fires once per key-down edge for any key in the list.
269 * Must be called before start(). Ignored if the poller is already running.
270 * @param name Unique, descriptive name for the binding.
271 * @param keys Vector of input codes (any triggers the action).
272 * @param callback Function to invoke on key press.
273 */
274 void register_press(std::string_view name, const std::vector<InputCode> &keys,
275 std::function<void()> callback);
276
277 /**
278 * @brief Registers a press-mode binding with modifier keys.
279 * @details The callback fires once per key-down edge for any key in the list,
280 * but only when all modifier inputs are simultaneously held.
281 * @param name Unique, descriptive name for the binding.
282 * @param keys Vector of input codes (any triggers the action).
283 * @param modifiers Vector of modifier input codes (all must be held).
284 * @param callback Function to invoke on key press.
285 */
286 void register_press(std::string_view name, const std::vector<InputCode> &keys,
287 const std::vector<InputCode> &modifiers,
288 std::function<void()> callback);
289
290 /**
291 * @brief Registers press-mode bindings from a KeyComboList.
292 * @details Registers one binding per combo in the list. All bindings share
293 * the same name, enabling OR-logic via is_binding_active().
294 * Must be called before start(). Ignored if the poller is already running.
295 * @param name Shared binding name for all combos.
296 * @param combos List of key combinations (each combo is registered independently).
297 * @param callback Function to invoke on key press.
298 */
299 void register_press(std::string_view name, const Config::KeyComboList &combos,
300 std::function<void()> callback);
301
302 /**
303 * @brief Registers a hold-mode binding.
304 * @details The callback fires with true when any input in the list is pressed,
305 * and false when all are released. Must be called before start().
306 * Ignored if the poller is already running.
307 * @param name Unique, descriptive name for the binding.
308 * @param keys Vector of input codes (any activates the hold).
309 * @param callback Function invoked with the hold state (true = held, false = released).
310 */
311 void register_hold(std::string_view name, const std::vector<InputCode> &keys,
312 std::function<void(bool)> callback);
313
314 /**
315 * @brief Registers a hold-mode binding with modifier keys.
316 * @details The callback fires with true when any input in the list is pressed
317 * and all modifier inputs are simultaneously held, and false when the
318 * condition is no longer met.
319 * @param name Unique, descriptive name for the binding.
320 * @param keys Vector of input codes (any activates the hold).
321 * @param modifiers Vector of modifier input codes (all must be held).
322 * @param callback Function invoked with the hold state (true = held, false = released).
323 */
324 void register_hold(std::string_view name, const std::vector<InputCode> &keys,
325 const std::vector<InputCode> &modifiers,
326 std::function<void(bool)> callback);
327
328 /**
329 * @brief Registers hold-mode bindings from a KeyComboList.
330 * @details Registers one binding per combo in the list. All bindings share
331 * the same name, enabling OR-logic via is_binding_active().
332 * Must be called before start(). Ignored if the poller is already running.
333 * @param name Shared binding name for all combos.
334 * @param combos List of key combinations (each combo is registered independently).
335 * @param callback Function invoked with the hold state (true = held, false = released).
336 */
337 void register_hold(std::string_view name, const Config::KeyComboList &combos,
338 std::function<void(bool)> callback);
339
340 /**
341 * @brief Sets whether the poller requires the current process to own the
342 * foreground window before processing key events.
343 * @param require_focus true to enable focus checking (default), false to disable.
344 * @note Can be called before or after start(). Changes take effect immediately.
345 */
346 void set_require_focus(bool require_focus);
347
348 /**
349 * @brief Sets the XInput controller index to poll for gamepad bindings.
350 * @param index Controller index (0-3). Clamped to valid range.
351 * @note Must be called before start(). Has no effect while the poller is running.
352 */
353 void set_gamepad_index(int index);
354
355 /**
356 * @brief Sets the analog trigger deadzone threshold for gamepad bindings.
357 * @param threshold Trigger values above this threshold (0-255) are "pressed".
358 * @note Must be called before start(). Has no effect while the poller is running.
359 */
360 void set_trigger_threshold(int threshold);
361
362 /**
363 * @brief Sets the thumbstick deadzone threshold for gamepad bindings.
364 * @param threshold Axis values exceeding this threshold (0-32767) are "pressed".
365 * @note Must be called before start(). Has no effect while the poller is running.
366 */
367 void set_stick_threshold(int threshold);
368
369 /**
370 * @brief Starts the input polling thread with all registered bindings.
371 * @details Constructs an internal InputPoller with the current bindings
372 * and begins monitoring. Subsequent register calls are ignored
373 * until the poller is stopped and bindings are cleared.
374 * @param poll_interval Time between polling cycles.
375 */
376 void start(std::chrono::milliseconds poll_interval = DEFAULT_POLL_INTERVAL);
377
378 /**
379 * @brief Checks if the input polling thread is currently running.
380 * @return true if the poller is active.
381 */
382 [[nodiscard]] bool is_running() const noexcept;
383
384 /**
385 * @brief Returns the number of registered bindings.
386 * @return size_t Number of bindings (pending or active).
387 */
388 [[nodiscard]] size_t binding_count() const noexcept;
389
390 /**
391 * @brief Queries whether a named binding is currently active.
392 * @param name The binding name to look up.
393 * @return true if the named binding's key(s) are currently pressed.
394 * Returns false if the poller is not running or the name is unknown.
395 * @note Thread-safe. Can be called from any thread (e.g., render thread).
396 */
397 [[nodiscard]] bool is_binding_active(std::string_view name) const noexcept;
398
399 /**
400 * @brief Stops the polling thread and clears all registered bindings.
401 * @details Safe to call multiple times. After shutdown, new bindings
402 * can be registered and start() called again.
403 */
404 void shutdown() noexcept;
405
406 private:
407 1 InputManager() = default;
408 1 ~InputManager() = default;
409
410 InputManager(const InputManager &) = delete;
411 InputManager &operator=(const InputManager &) = delete;
412 InputManager(InputManager &&) = delete;
413 InputManager &operator=(InputManager &&) = delete;
414
415 mutable std::mutex mutex_;
416 std::vector<InputBinding> pending_bindings_;
417 std::shared_ptr<InputPoller> poller_;
418 std::atomic<std::shared_ptr<InputPoller>> active_poller_{};
419 std::atomic<bool> running_{false};
420 bool require_focus_ = true;
421 int gamepad_index_ = 0;
422 int trigger_threshold_ = GamepadCode::TriggerThreshold;
423 int stick_threshold_ = GamepadCode::StickThreshold;
424 };
425 } // namespace DetourModKit
426
427 #endif // DETOURMODKIT_INPUT_HPP
428