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 |