GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 100.0% 22 / 0 / 22
Functions: 100.0% 13 / 0 / 13
Branches: 85.7% 6 / 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 #include "DetourModKit/srw_shared_mutex.hpp"
7
8 #include <atomic>
9 #include <chrono>
10 #include <condition_variable>
11 #include <cstdint>
12 #include <functional>
13 #include <memory>
14 #include <mutex>
15 #include <string>
16 #include <string_view>
17 #include <thread>
18 #include <unordered_map>
19 #include <unordered_set>
20 #include <vector>
21
22 namespace DetourModKit
23 {
24 /**
25 * @enum InputMode
26 * @brief Defines how a registered key binding is triggered.
27 */
28 enum class InputMode
29 {
30 Press,
31 Hold
32 };
33
34 /**
35 * @brief Converts an InputMode enum to its string representation.
36 * @param mode The InputMode enum value.
37 * @return std::string_view String representation of the mode.
38 */
39 55 [[nodiscard]] constexpr std::string_view input_mode_to_string(InputMode mode) noexcept
40 {
41
3/3
✓ Branch 2 → 3 taken 43 times.
✓ Branch 2 → 4 taken 11 times.
✓ Branch 2 → 5 taken 1 time.
55 switch (mode)
42 {
43 43 case InputMode::Press:
44 43 return "Press";
45 11 case InputMode::Hold:
46 11 return "Hold";
47 }
48 1 return "Unknown";
49 }
50
51 // Input system configuration defaults
52 inline constexpr std::chrono::milliseconds DEFAULT_POLL_INTERVAL{16};
53 inline constexpr std::chrono::milliseconds MIN_POLL_INTERVAL{1};
54 inline constexpr std::chrono::milliseconds MAX_POLL_INTERVAL{1000};
55
56 /**
57 * @struct InputBinding
58 * @brief Describes a single input-to-action binding.
59 * @details Holds the action name, input codes, modifier codes, input mode, and callbacks. For Press mode, the press
60 * callback fires on key-down edge. 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. The modifiers vector uses AND
64 * logic: all modifiers must be held simultaneously for the binding to activate. Modifier matching is
65 * strict: any key that appears as a modifier in *any* registered
66 * binding will block bindings that do not list it as a required modifier. This prevents "V" from firing
67 * when "Shift+V" is pressed.
68 *
69 * Each InputCode identifies both the device source (keyboard, mouse, gamepad, mouse wheel) and the
70 * button/key code. All codes within a binding should be from the same device group (keyboard/mouse or
71 * gamepad); mouse-wheel codes are a standalone source and should not be mixed with other devices in one
72 * binding. Mouse-wheel codes are trigger-only and Press-mode: the wheel has no held state, so a single
73 * notch reads as one Press edge.
74 *
75 * When @c consume is set, the binding's trigger is additionally hidden from the game (see the @c consume
76 * field).
77 *
78 * @warning Callbacks are invoked on the polling thread. They must not capture references or pointers to objects
79 * whose lifetime may end before shutdown() completes. Callbacks should execute quickly to avoid degrading
80 * the effective poll rate.
81 */
82 struct InputBinding
83 {
84 std::string name;
85 std::vector<InputCode> keys;
86 std::vector<InputCode> modifiers;
87 InputMode mode = InputMode::Press;
88
89 /**
90 * Opt-in input suppression. When true, the binding's trigger input is hidden from the game so it does not also
91 * act on it (for example an "LB + D-pad" zoom that must not open the map when released). Honored for digital
92 * gamepad buttons (D-pad, face buttons, bumpers, stick clicks) via an XInputGetState hook, and for the mouse
93 * wheel via the window-procedure hook. Analog triggers and stick directions cannot be masked (the detour clears
94 * only the digital button bitmask), and keyboard/mouse-button suppression is not provided. Default off keeps
95 * the input system purely observational.
96 */
97 bool consume = false;
98
99 std::function<void()> on_press;
100 std::function<void(bool)> on_state_change;
101 };
102
103 /**
104 * @class BindingToken
105 * @brief Generation-checked handle to a named binding's resolved entry set.
106 * @details A high-frequency consumer (a render thread polling a hotkey every frame) can resolve a binding name to a
107 * token once with InputManager::acquire_binding_token / InputPoller::acquire_binding_token, then query it
108 * every frame with the BindingToken overload of is_binding_active. The token caches the name's resolved
109 * entry indices, so a query skips the per-call name hash lookup the string_view overload performs.
110 *
111 * The token is stamped with the binding generation it was minted at. Any reshape of the binding set
112 * (register / remove / clear / combo update / consume change) advances the generation, so a query through
113 * a stale token fails closed -- it returns false without dereferencing the now-meaningless cached indices,
114 * rather than reading a different binding's state. The generation is drawn from a process-wide monotonic
115 * counter, so a token minted by one poller can never alias a different poller (for example after an
116 * InputManager::shutdown / start cycle replaces the underlying poller): its generation simply never
117 * matches again.
118 *
119 * A consumer detects staleness with binding_token_current() (or by re-acquiring after a known reshape such
120 * as an INI hot-reload) and re-acquires to recover. A default-constructed token, a token for an unknown
121 * name, and a token whose resolution ran out of memory are all invalid (valid() == false) and always read
122 * inactive.
123 * @note The token is only meaningful to the poller (or the InputManager wrapping it) that minted it.
124 */
125 class BindingToken
126 {
127 public:
128 1 BindingToken() = default;
129
130 /**
131 * @brief Reports whether the token resolved a name at acquisition time.
132 * @details true only when acquire_binding_token found the name and resolved its entries. It does NOT imply the
133 * token is still current: a valid token can be stale after a reshape. Use binding_token_current(), or
134 * is_binding_active which fails closed, to test currency.
135 * @return true when the token names a resolved binding set; false for a default, unknown-name, or
136 * allocation-failed token.
137 */
138 48 [[nodiscard]] bool valid() const noexcept { return m_generation != 0; }
139
140 private:
141 friend class InputPoller;
142
143 // Binding generation this token was minted at; 0 marks an unresolved (invalid) token. A live generation is
144 // always >= 1 (drawn from the process-wide counter that starts at 1), so 0 can never collide with a real one.
145 std::uint64_t m_generation{0};
146
147 // The name's resolved entry indices into the poller's binding array, captured at acquire time. Read only while
148 // m_generation still matches the poller's live generation, which guarantees these indices remain in bounds and
149 // address the same bindings.
150 std::vector<std::size_t> m_indices;
151 };
152
153 /**
154 * @class InputPoller
155 * @brief RAII input polling engine that monitors key states on a background thread.
156 * @details Manages a dedicated polling thread that checks virtual key states via
157 * GetAsyncKeyState. Supports both press (edge-triggered) and hold (level-triggered) input modes with
158 * optional modifier key combinations. When require_focus is enabled (default), key events are only
159 * processed when the current process owns the foreground window.
160 *
161 * On shutdown, active hold bindings receive an on_state_change(false) callback to ensure consumers are
162 * notified of the release.
163 *
164 * @note Non-copyable, non-movable. Callbacks are invoked on the polling thread.
165 * @note This class is the building block for the InputManager singleton.
166 *
167 * @warning When used inside a DLL, shutdown() must be called before DLL_PROCESS_DETACH. Calling join() on a thread
168 * during DllMain can deadlock due to the loader lock. Use DMK_Shutdown() to ensure proper teardown
169 * ordering.
170 * @warning The opt-in interception layer (mouse-wheel capture and gamepad passthrough suppression) is backed by
171 * process-global state and a single set of hooks: one XInput hook bound to a single gamepad index, one
172 * window-procedure subclass, and one suppression mask. At most one
173 * InputPoller may therefore use those features at a time; running two pollers that both register consume
174 * or mouse-wheel bindings is unsupported and would have them fight over the shared mask and hooks. The
175 * InputManager singleton is the intended single-instance owner. Purely observational pollers (no consume,
176 * no wheel bindings) install nothing and are unaffected.
177 */
178 class InputPoller
179 {
180 public:
181 /**
182 * @brief Constructs an InputPoller with the given bindings and poll interval.
183 * @param bindings Vector of input bindings to monitor.
184 * @param poll_interval Time between polling cycles.
185 * @param require_focus When true, key events are ignored unless the current process owns the foreground window.
186 * @param gamepad_index XInput controller index (0-3) to poll for gamepad bindings.
187 * @param trigger_threshold Analog trigger deadzone threshold (0-255). Trigger values above this threshold are
188 * considered "pressed".
189 * @param stick_threshold Thumbstick deadzone threshold (0-32767). Axis values exceeding this threshold in any
190 * direction are "pressed".
191 * @note The polling thread does not start until start() is called.
192 */
193 explicit InputPoller(std::vector<InputBinding> bindings,
194 std::chrono::milliseconds poll_interval = DEFAULT_POLL_INTERVAL, bool require_focus = true,
195 int gamepad_index = 0, int trigger_threshold = GamepadCode::TriggerThreshold,
196 int stick_threshold = GamepadCode::StickThreshold);
197
198 ~InputPoller() noexcept;
199
200 InputPoller(const InputPoller &) = delete;
201 InputPoller &operator=(const InputPoller &) = delete;
202 InputPoller(InputPoller &&) = delete;
203 InputPoller &operator=(InputPoller &&) = delete;
204
205 /**
206 * @brief Starts the polling thread.
207 * @details Safe to call only once. Subsequent calls are ignored with a warning.
208 * @note Not thread-safe. Must be called from a single thread. Use
209 * InputManager::start() for a thread-safe wrapper.
210 */
211 void start();
212
213 /**
214 * @brief Checks if the polling thread is currently running.
215 * @return true if the poller is active and monitoring keys.
216 */
217 [[nodiscard]] bool is_running() const noexcept;
218
219 /**
220 * @brief Returns the number of registered bindings under the binding reader lock.
221 *
222 * @return size_t Number of bindings.
223 */
224 [[nodiscard]] size_t binding_count() const noexcept;
225
226 /**
227 * @brief Returns the configured poll interval.
228 * @return std::chrono::milliseconds The poll interval.
229 */
230 [[nodiscard]] std::chrono::milliseconds poll_interval() const noexcept;
231
232 /**
233 * @brief Returns the configured gamepad controller index.
234 * @return int The XInput controller index (0-3).
235 */
236 [[nodiscard]] int gamepad_index() const noexcept;
237
238 /**
239 * @brief Queries whether a binding is currently active by index.
240 * @param index Zero-based index into the bindings vector.
241 * @return true if the binding's key(s) are currently pressed. Returns false for out-of-range indices.
242 * @note Thread-safe. Acquires m_bindings_rw_mutex as a reader so the index/array pair stays consistent across
243 * reshape calls (add_binding, remove_bindings_by_name, update_combos). The fast path is the cheap
244 * shared_lock acquire when no writer is in flight.
245 */
246 [[nodiscard]] bool is_binding_active(size_t index) const noexcept;
247
248 /**
249 * @brief Queries whether a binding is currently active by name.
250 * @param name The binding name to look up.
251 * @return true if the named binding's key(s) are currently pressed. Returns false if no binding with the given
252 * name exists.
253 * @note Thread-safe. Can be called from any thread.
254 */
255 [[nodiscard]] bool is_binding_active(std::string_view name) const noexcept;
256
257 /**
258 * @brief Resolves a binding name to a generation-checked token for repeated low-overhead queries.
259 * @details Looks the name up once under the binding reader lock and captures its resolved entry indices plus
260 * the current binding generation. A high-frequency consumer holds the returned token and queries it
261 * with is_binding_active(const BindingToken &), skipping the name hash lookup the string_view overload
262 * repeats per call. The token fails closed after any reshape (see BindingToken).
263 * @param name The binding name to resolve.
264 * @return A valid token when the name is registered; an invalid token (BindingToken::valid() == false) when the
265 * name is unknown or resolution runs out of memory.
266 * @note Thread-safe. Setup/control-plane: resolving a token copies the name's index set and may allocate. Mint
267 * the token once (or after a reshape), not every frame; the per-frame query path is the BindingToken
268 * overload of is_binding_active.
269 */
270 [[nodiscard]] BindingToken acquire_binding_token(std::string_view name) const noexcept;
271
272 /**
273 * @brief Queries whether a binding is currently active through a previously acquired token.
274 * @details Callback-safe hot-path query: acquires the binding reader lock, verifies the token's generation
275 * still matches the live binding generation, and ORs the cached entry indices against the active-state
276 * array. A stale token (any reshape since acquisition) or an invalid token returns false without
277 * dereferencing its indices.
278 * @param token A token from acquire_binding_token().
279 * @return true if any entry of the token's binding is currently pressed; false if inactive, stale, or invalid.
280 * @note Thread-safe. Callback-safe: a shared_lock acquire plus a relaxed atomic load per cached entry, no name
281 * hash and no allocation.
282 */
283 [[nodiscard]] bool is_binding_active(const BindingToken &token) const noexcept;
284
285 /**
286 * @brief Reports whether a token still matches the live binding generation.
287 * @details Lets a consumer detect a reshape and re-acquire instead of silently reading inactive. Equivalent to
288 * asking whether is_binding_active(token) would evaluate the cached indices rather than fail closed.
289 * @param token A token from acquire_binding_token().
290 * @return true when the token is valid and its generation matches the current binding set; false otherwise.
291 * @note Thread-safe. Callback-safe: a shared_lock acquire and a single integer comparison.
292 */
293 [[nodiscard]] bool binding_token_current(const BindingToken &token) const noexcept;
294
295 /**
296 * @brief Sets whether the poller requires the current process to own the foreground window before processing
297 * key events.
298 * @param require_focus true to enable focus checking (default), false to disable.
299 * @note Thread-safe. Can be called while the poller is running.
300 */
301 void set_require_focus(bool require_focus) noexcept;
302
303 /**
304 * @brief Sets the input-suppression flag on every binding sharing @p name.
305 * @details Updates the @c consume flag on all matching bindings and refreshes the interception gates so the
306 * XInput / window-procedure hooks are installed (or left uninstalled) on the next poll cycle. A no-op
307 * if the name was never registered. Thread-safe; may be called while the poller is running.
308 * @param name Binding name previously registered.
309 * @param consume true to hide the binding's trigger from the game.
310 */
311 void set_consume(std::string_view name, bool consume) noexcept;
312
313 /**
314 * @brief Stops the polling thread.
315 * @details Signals the thread to stop and waits for it to join. After the thread has joined, fires
316 * on_state_change(false) for any hold bindings that were active at the time of shutdown. Safe to call
317 * multiple times.
318 */
319 void shutdown() noexcept;
320
321 /**
322 * @brief Replaces the trigger combos of all bindings sharing @p name.
323 * @details The poller maps each combo passed to register_press/register_hold to an independent binding entry
324 * with a shared name. When the replacement count matches the existing entry count, keys and modifiers
325 * are overwritten in place. When the count differs, the existing entries are erased and one entry per
326 * replacement combo is appended; callbacks, binding mode, and binding name inherit from the first
327 * existing entry.
328 *
329 * An empty replacement list is a valid binding state meaning "no keys bound": the existing entries are
330 * erased and a single inert sentinel entry takes their place so the binding name remains addressable
331 * for a later non-empty update. Held bindings receive an on_state_change(false) callback before the
332 * swap completes. Safe to call while the poll thread is running.
333 * @param name Binding name previously registered.
334 * @param combos Replacement combos. May be empty to unbind.
335 * @return true on successful swap (including the unbind case), false only if the name was never registered.
336 */
337 [[nodiscard]] bool update_combos(std::string_view name, const Config::KeyComboList &combos) noexcept;
338
339 /**
340 * @brief Appends a binding to the running poller.
341 * @details Thread-safe. Takes the bindings rw mutex exclusively, so a concurrent poll cycle blocks for at most
342 * the duration of its current tick. The m_active_states array is rebuilt to match the new binding
343 * count, with the previous atomic value carried forward for every existing entry so a held binding
344 * does not flicker through one inactive tick.
345 * @param binding Binding to append.
346 */
347 void add_binding(InputBinding binding) noexcept;
348
349 /**
350 * @brief Removes every binding whose name matches @p name.
351 * @details Thread-safe. Active hold bindings receive an on_state_change(false) callback before erasure. The
352 * m_active_states array is rebuilt to match the new binding count, with the previous atomic value
353 * carried forward for every surviving entry.
354 * @param name Binding name to remove.
355 * @return Number of bindings removed (zero if the name was not registered).
356 */
357 1 size_t remove_bindings_by_name(std::string_view name) noexcept { return remove_bindings_by_name(name, true); }
358
359 /**
360 * @brief Drops every binding without stopping the poll thread.
361 * @details Active hold bindings receive an on_state_change(false) callback before erasure. After the call the
362 * poller has zero bindings and the poll thread keeps running idle. Thread-safe.
363 */
364 1 void clear_bindings() noexcept { clear_bindings(true); }
365
366 /**
367 * @brief Variant of remove_bindings_by_name that suppresses the on_state_change(false) release callbacks for
368 * active holds.
369 * @details Used by the loader-lock-safe Bootstrap unload path: user callbacks live in a Logic DLL whose code
370 * pages may be about to be unmapped, so invoking them under the loader lock would risk a deadlock or a
371 * use-after-unload.
372 * @param name Binding name to remove.
373 * @param invoke_callbacks When true (default for the public API), active hold bindings receive
374 * on_state_change(false) before erasure. When false, the release callbacks are dropped
375 * on the floor.
376 * @return Number of bindings removed.
377 */
378 size_t remove_bindings_by_name(std::string_view name, bool invoke_callbacks) noexcept;
379
380 /**
381 * @brief Variant of clear_bindings that suppresses the on_state_change(false) release callbacks for active
382 * holds.
383 * @details See the single-argument overload of remove_bindings_by_name for the rationale; both overloads serve
384 * the same loader-lock-safe teardown path.
385 * @param invoke_callbacks When true (default for the public API), active hold bindings receive
386 * on_state_change(false) before erasure. When false, the release callbacks are dropped.
387 */
388 void clear_bindings(bool invoke_callbacks) noexcept;
389
390 private:
391 void poll_loop(std::stop_token stop_token);
392 void release_active_holds() noexcept;
393 [[nodiscard]] bool is_process_foreground() const noexcept;
394 void recompute_modifier_caches_locked() noexcept;
395
396 /// Transparent hasher enabling std::string_view lookup without allocation.
397 struct StringHash
398 {
399 using is_transparent = void;
400 232954 size_t operator()(std::string_view sv) const noexcept { return std::hash<std::string_view>{}(sv); }
401 };
402
403 // m_bindings_rw_mutex protects m_bindings, m_name_index, m_known_modifiers, m_binding_generation, and
404 // m_has_gamepad_bindings when a live update is in flight. The poll loop holds a shared lock across the
405 // binding-evaluation pass of each cycle and releases it before dispatching user callbacks, so callbacks may
406 // call binding_count(), is_binding_active(), or update_binding_combos() without re-acquiring the non-recursive
407 // lock; update_combos() holds an exclusive lock across the swap. m_active_states entries are always accessed
408 // via atomic ops and need no further guard.
409 mutable detail::SrwSharedMutex m_bindings_rw_mutex;
410 std::vector<InputBinding> m_bindings;
411 std::unordered_map<std::string, std::vector<size_t>, StringHash, std::equal_to<>> m_name_index;
412 std::vector<InputCode> m_known_modifiers;
413 // Advances on every binding-set reshape (each rebuild of m_name_index, plus clear_bindings). A BindingToken
414 // captures this value at acquire time; a query whose token generation no longer matches fails closed. Guarded
415 // by m_bindings_rw_mutex, like the binding array it tracks.
416 std::uint64_t m_binding_generation{0};
417 std::chrono::milliseconds m_poll_interval;
418 std::atomic<bool> m_require_focus;
419 std::atomic<bool> m_running{false};
420 std::jthread m_poll_thread;
421 std::mutex m_cv_mutex;
422 std::condition_variable_any m_cv;
423
424 // Per-binding active state, indexed parallel to m_bindings. Atomic for cross-thread reads via
425 // is_binding_active().
426 std::unique_ptr<std::atomic<uint8_t>[]> m_active_states;
427
428 int m_gamepad_index;
429 int m_trigger_threshold;
430 int m_stick_threshold;
431 std::atomic<bool> m_has_gamepad_bindings{false};
432
433 // Interception gates, recomputed alongside the modifier caches. Each decides whether an active-input hook is
434 // installed lazily by the poll loop, so a mod that never opts in pays no interception cost.
435 // any MouseWheel trigger -> WndProc hook
436 std::atomic<bool> m_has_wheel_bindings{false};
437 // any consume gamepad binding -> XInput hook
438 std::atomic<bool> m_has_consume_gamepad_bindings{false};
439 // any consume wheel binding -> swallow wheel messages
440 std::atomic<bool> m_has_wheel_consume_bindings{false};
441 };
442
443 /**
444 * @class InputManager
445 * @brief Singleton that provides a convenient interface for registering and monitoring hotkey bindings.
446 * @details Wraps an InputPoller internally. Bindings are registered before calling start(), which constructs and
447 * starts the poller. Integrates with DMK_Shutdown() for automatic cleanup.
448 *
449 * @note Thread-safe. For advanced use cases requiring multiple independent pollers or custom lifetime management,
450 * use InputPoller directly.
451 *
452 * @warning When used inside a DLL, shutdown() must be called before DLL_PROCESS_DETACH. Calling join() on a thread
453 * during DllMain can deadlock due to the loader lock.
454 */
455 class InputManager
456 {
457 public:
458 /**
459 * @brief Retrieves the singleton instance of the InputManager.
460 * @return InputManager& Reference to the single InputManager instance.
461 */
462 348 static InputManager &get_instance()
463 {
464
3/4
✓ Branch 2 → 3 taken 105 times.
✓ Branch 2 → 8 taken 243 times.
✓ Branch 4 → 5 taken 105 times.
✗ Branch 4 → 8 not taken.
348 static InputManager instance;
465 348 return instance;
466 }
467
468 /**
469 * @brief Registers a press-mode binding.
470 * @details The callback fires once per key-down edge for any key in the list. Can be called either before or
471 * after start(); a binding registered while the poller is running is appended to the live binding set
472 * and starts firing on the next poll cycle.
473 * @param name Unique, descriptive name for the binding.
474 * @param keys Vector of input codes (any triggers the action).
475 * @param callback Function to invoke on key press.
476 */
477 void register_press(std::string_view name, const std::vector<InputCode> &keys, std::function<void()> callback);
478
479 /**
480 * @brief Registers a press-mode binding with modifier keys.
481 * @details The callback fires once per key-down edge for any key in the list, but only when all modifier inputs
482 * are simultaneously held. Live registration is supported (see the no-modifier overload).
483 * @param name Unique, descriptive name for the binding.
484 * @param keys Vector of input codes (any triggers the action).
485 * @param modifiers Vector of modifier input codes (all must be held).
486 * @param callback Function to invoke on key press.
487 */
488 void register_press(std::string_view name, const std::vector<InputCode> &keys,
489 const std::vector<InputCode> &modifiers, std::function<void()> callback);
490
491 /**
492 * @brief Registers press-mode bindings from a KeyComboList.
493 * @details Registers one binding per combo in the list. All bindings share the same name, enabling OR-logic via
494 * is_binding_active(). When @p combos is empty, a single sentinel binding with no keys is registered
495 * so the name is reachable by update_binding_combos(). Live registration is supported.
496 * @param name Shared binding name for all combos.
497 * @param combos List of key combinations (each combo is registered independently).
498 * @param callback Function to invoke on key press.
499 */
500 void register_press(std::string_view name, const Config::KeyComboList &combos, std::function<void()> callback);
501
502 /**
503 * @brief Registers a hold-mode binding.
504 * @details The callback fires with true when any input in the list is pressed, and false when all are released.
505 * Live registration is supported (see register_press for semantics).
506 * @param name Unique, descriptive name for the binding.
507 * @param keys Vector of input codes (any activates the hold).
508 * @param callback Function invoked with the hold state (true = held, false = released).
509 */
510 void register_hold(std::string_view name, const std::vector<InputCode> &keys,
511 std::function<void(bool)> callback);
512
513 /**
514 * @brief Registers a hold-mode binding with modifier keys.
515 * @details The callback fires with true when any input in the list is pressed and all modifier inputs are
516 * simultaneously held, and false when the condition is no longer met. Live registration is supported.
517 * @param name Unique, descriptive name for the binding.
518 * @param keys Vector of input codes (any activates the hold).
519 * @param modifiers Vector of modifier input codes (all must be held).
520 * @param callback Function invoked with the hold state (true = held, false = released).
521 */
522 void register_hold(std::string_view name, const std::vector<InputCode> &keys,
523 const std::vector<InputCode> &modifiers, std::function<void(bool)> callback);
524
525 /**
526 * @brief Registers hold-mode bindings from a KeyComboList.
527 * @details Registers one binding per combo in the list. All bindings share the same name, enabling OR-logic via
528 * is_binding_active(). When @p combos is empty, a single sentinel binding with no keys is registered
529 * so the name is reachable by update_binding_combos(). Live registration is supported.
530 * @param name Shared binding name for all combos.
531 * @param combos List of key combinations (each combo is registered independently).
532 * @param callback Function invoked with the hold state (true = held, false = released).
533 */
534 void register_hold(std::string_view name, const Config::KeyComboList &combos,
535 std::function<void(bool)> callback);
536
537 /**
538 * @brief Sets whether the poller requires the current process to own the foreground window before processing
539 * key events.
540 * @param require_focus true to enable focus checking (default), false to disable.
541 * @note Can be called before or after start(). Changes take effect immediately.
542 */
543 void set_require_focus(bool require_focus);
544
545 /**
546 * @brief Enables or disables input suppression for a named binding.
547 * @details Sets the @c consume flag on every binding sharing @p name, forwarding to the active poller when
548 * running or updating pending bindings before start(). When enabled, the binding's trigger is hidden
549 * from the game: digital gamepad buttons via an XInputGetState hook (analog triggers and stick
550 * directions cannot be masked) and the mouse wheel via the window-procedure hook (keyboard and
551 * mouse-button suppression are not provided). A no-op if the name is unknown. Thread-safe.
552 * @param name Binding name previously registered.
553 * @param consume true to hide the binding's trigger from the game.
554 */
555 void set_consume(std::string_view name, bool consume) noexcept;
556
557 /**
558 * @brief Sets the XInput controller index to poll for gamepad bindings.
559 * @param index Controller index (0-3). Clamped to valid range.
560 * @note Must be called before start(). Has no effect while the poller is running.
561 */
562 void set_gamepad_index(int index);
563
564 /**
565 * @brief Sets the analog trigger deadzone threshold for gamepad bindings.
566 * @param threshold Trigger values above this threshold (0-255) are "pressed".
567 * @note Must be called before start(). Has no effect while the poller is running.
568 */
569 void set_trigger_threshold(int threshold);
570
571 /**
572 * @brief Sets the thumbstick deadzone threshold for gamepad bindings.
573 * @param threshold Axis values exceeding this threshold (0-32767) are "pressed".
574 * @note Must be called before start(). Has no effect while the poller is running.
575 */
576 void set_stick_threshold(int threshold);
577
578 /**
579 * @brief Starts the input polling thread with all registered bindings.
580 * @details Constructs an internal InputPoller with the current bindings and begins monitoring. Registrations
581 * made after start() are forwarded live to the active poller and take effect on the next poll cycle;
582 * no stop or restart is required.
583 * @param poll_interval Time between polling cycles.
584 */
585 void start(std::chrono::milliseconds poll_interval = DEFAULT_POLL_INTERVAL);
586
587 /**
588 * @brief Checks if the input polling thread is currently running.
589 * @return true if the poller is active.
590 */
591 [[nodiscard]] bool is_running() const noexcept;
592
593 /**
594 * @brief Returns the number of registered bindings.
595 * @return size_t Number of bindings (pending or active).
596 */
597 [[nodiscard]] size_t binding_count() const noexcept;
598
599 /**
600 * @brief Queries whether a named binding is currently active.
601 * @param name The binding name to look up.
602 * @return true if the named binding's key(s) are currently pressed. Returns false if the poller is not running
603 * or the name is unknown.
604 * @note Thread-safe. Can be called from any thread (e.g., render thread).
605 */
606 [[nodiscard]] bool is_binding_active(std::string_view name) const noexcept;
607
608 /**
609 * @brief Resolves a binding name to a generation-checked token against the running poller.
610 * @details Forwards to InputPoller::acquire_binding_token() on the active poller. Returns an invalid token when
611 * the poller is not running or the name is unknown, so a token acquired before start() (or after
612 * shutdown()) is simply invalid.
613 * @param name The binding name to resolve.
614 * @return A valid token when running and the name is registered; an invalid token otherwise.
615 * @note Thread-safe. Setup/control-plane: acquire once, then query with the BindingToken overload of
616 * is_binding_active. See BindingToken for the staleness contract.
617 */
618 [[nodiscard]] BindingToken acquire_binding_token(std::string_view name) const noexcept;
619
620 /**
621 * @brief Queries whether a binding is currently active through a previously acquired token.
622 * @details Forwards to InputPoller::is_binding_active(const BindingToken &) on the active poller. Returns false
623 * when the poller is not running, or when the token is stale or invalid.
624 * @param token A token from acquire_binding_token().
625 * @return true if the token's binding is currently pressed; false if inactive, stale, invalid, or not running.
626 * @note Thread-safe. Callback-safe: no name hash and no allocation on the query path.
627 */
628 [[nodiscard]] bool is_binding_active(const BindingToken &token) const noexcept;
629
630 /**
631 * @brief Reports whether a token still matches the running poller's binding generation.
632 * @details Forwards to InputPoller::binding_token_current(). Returns false when the poller is not running.
633 * @param token A token from acquire_binding_token().
634 * @return true when the token is valid and current against the active poller; false otherwise.
635 * @note Thread-safe. Lets a consumer re-acquire only when a reshape has invalidated its token.
636 */
637 [[nodiscard]] bool binding_token_current(const BindingToken &token) const noexcept;
638
639 /**
640 * @brief Replaces the trigger combos of all bindings sharing @p name.
641 * @details Forwards to the active InputPoller when running, or updates pending bindings before start(). The
642 * binding's name, callback, and mode are preserved; only keys and modifiers are swapped. Any
643 * cardinality is accepted: matching counts rewrite in place, differing counts rebuild the entry set
644 * carrying callback identity forward.
645 *
646 * An empty replacement list unbinds the named binding while keeping a single inert sentinel entry so a
647 * subsequent non-empty update can rebind it. Held bindings receive an on_state_change(false) callback
648 * before the swap completes. If the name is unknown the call is a no-op logged at
649 * Debug level. Thread-safe.
650 * @param name Binding name previously registered.
651 * @param combos Replacement combos. May be empty to unbind.
652 */
653 void update_binding_combos(std::string_view name, const Config::KeyComboList &combos) noexcept;
654
655 /**
656 * @brief Removes every binding whose name matches @p name.
657 * @details Forwards to the active InputPoller when running, or erases matching entries from pending bindings
658 * before start(). Thread-safe.
659 * @param name Binding name to remove.
660 * @return Number of bindings removed.
661 */
662 2 size_t remove_binding_by_name(std::string_view name) noexcept { return remove_binding_by_name(name, true); }
663
664 /**
665 * @brief Plural alias for remove_binding_by_name(name); removes every binding sharing @p name.
666 * @details Naming parity with InputPoller::remove_bindings_by_name. Delegates to the singular overload with
667 * identical behavior. Prefer this spelling: one name maps to many combos, so the call removes all
668 * bindings registered under the shared name.
669 * @param name Binding name to remove.
670 * @return Number of bindings removed.
671 */
672 2 size_t remove_bindings_by_name(std::string_view name) noexcept { return remove_binding_by_name(name, true); }
673
674 /**
675 * @brief Drops every registered binding without stopping the poller.
676 * @details Forwards to the active InputPoller when running and clears pending bindings. Active hold bindings
677 * receive an on_state_change(false) callback before erasure. The poll thread keeps running and can be
678 * reseeded via subsequent register_press / register_hold calls. Thread-safe.
679 */
680 1 void clear_bindings() noexcept { clear_bindings(true); }
681
682 /**
683 * @brief Variant of remove_binding_by_name that suppresses the on_state_change(false) release callbacks for
684 * active holds.
685 * @details Forwarded straight to the underlying InputPoller. Loader-lock callers use this overload because user
686 * callbacks live in a
687 * Logic DLL whose code pages may be about to be unmapped.
688 * @param name Binding name to remove.
689 * @param invoke_callbacks When true, behaves identically to the public single-argument overload. When false,
690 * drops release callbacks.
691 * @return Number of bindings removed.
692 */
693 size_t remove_binding_by_name(std::string_view name, bool invoke_callbacks) noexcept;
694
695 /**
696 * @brief Plural alias for remove_binding_by_name(name, invoke_callbacks).
697 * @param name Binding name to remove.
698 * @param invoke_callbacks When false, drops the on_state_change(false) release callbacks (loader-lock path).
699 * @return Number of bindings removed.
700 */
701 1 size_t remove_bindings_by_name(std::string_view name, bool invoke_callbacks) noexcept
702 {
703 1 return remove_binding_by_name(name, invoke_callbacks);
704 }
705
706 /**
707 * @brief Variant of clear_bindings that suppresses the on_state_change(false) release callbacks for active
708 * holds.
709 * @param invoke_callbacks When true, behaves identically to the public zero-argument overload. When false,
710 * drops release callbacks.
711 */
712 void clear_bindings(bool invoke_callbacks) noexcept;
713
714 /**
715 * @brief Stops the polling thread and clears all registered bindings.
716 * @details Safe to call multiple times. After shutdown, new bindings can be registered and start() called
717 * again.
718 */
719 void shutdown() noexcept;
720
721 private:
722 105 InputManager() = default;
723 105 ~InputManager() noexcept = default;
724
725 InputManager(const InputManager &) = delete;
726 InputManager &operator=(const InputManager &) = delete;
727 InputManager(InputManager &&) = delete;
728 InputManager &operator=(InputManager &&) = delete;
729
730 mutable std::mutex m_mutex;
731 std::vector<InputBinding> m_pending_bindings;
732 std::shared_ptr<InputPoller> m_poller;
733 std::atomic<std::shared_ptr<InputPoller>> m_active_poller{};
734 std::atomic<bool> m_running{false};
735 bool m_require_focus{true};
736 int m_gamepad_index{0};
737 int m_trigger_threshold{GamepadCode::TriggerThreshold};
738 int m_stick_threshold{GamepadCode::StickThreshold};
739 };
740 } // namespace DetourModKit
741
742 #endif // DETOURMODKIT_INPUT_HPP
743