/** * @file input/mnk/mnk_input_driver.cpp * @brief Keyboard/mouse input driver implementation. * * @copyright Copyright (c) 2026 Tom Clay * All rights reserved. * * @license BSD 3-Clause License * See LICENSE file in the project root for full license text. */ #include #include #include #include #include #include #include #include #include #include #if REX_PLATFORM_WIN32 #include #include #endif REXCVAR_DEFINE_BOOL(mnk_mode, false, "Input", "Enable keyboard/mouse controller emulation"); REXCVAR_DEFINE_BOOL(mnk_capture_mouse, false, "Input", "Capture and hide mouse cursor while MnK is active"); REXCVAR_DEFINE_INT32(mnk_user_index, 0, "Input", "Controller slot (0-3) for MnK").range(0, 3); REXCVAR_DEFINE_DOUBLE(mnk_sensitivity, 1.0, "Input", "Mouse sensitivity for right stick") .range(0.01, 10.0); REXCVAR_DEFINE_STRING(keybind_a, "Space", "Input/Keybinds/Controller", "A button"); REXCVAR_DEFINE_STRING(keybind_b, "Shift", "Input/Keybinds/Controller", "B button"); REXCVAR_DEFINE_STRING(keybind_x, "R", "Input/Keybinds/Controller", "X button"); REXCVAR_DEFINE_STRING(keybind_y, "E", "Input/Keybinds/Controller", "Y button"); REXCVAR_DEFINE_STRING(keybind_left_trigger, "RMB", "Input/Keybinds/Controller", "Left trigger"); REXCVAR_DEFINE_STRING(keybind_right_trigger, "LMB", "Input/Keybinds/Controller", "Right trigger"); REXCVAR_DEFINE_STRING(keybind_left_shoulder, "Q", "Input/Keybinds/Controller", "Left shoulder"); REXCVAR_DEFINE_STRING(keybind_right_shoulder, "F", "Input/Keybinds/Controller", "Right shoulder"); REXCVAR_DEFINE_STRING(keybind_lstick_up, "W", "Input/Keybinds/Controller", "Left stick up"); REXCVAR_DEFINE_STRING(keybind_lstick_down, "S", "Input/Keybinds/Controller", "Left stick down"); REXCVAR_DEFINE_STRING(keybind_lstick_left, "A", "Input/Keybinds/Controller", "Left stick left"); REXCVAR_DEFINE_STRING(keybind_lstick_right, "D", "Input/Keybinds/Controller", "Left stick right"); REXCVAR_DEFINE_STRING(keybind_lstick_press, "C", "Input/Keybinds/Controller", "Left stick press"); REXCVAR_DEFINE_STRING(keybind_rstick_press, "MMB", "Input/Keybinds/Controller", "Right stick press"); REXCVAR_DEFINE_STRING(keybind_dpad_up, "Up", "Input/Keybinds/Controller", "D-pad up"); REXCVAR_DEFINE_STRING(keybind_dpad_down, "Down", "Input/Keybinds/Controller", "D-pad down"); REXCVAR_DEFINE_STRING(keybind_dpad_left, "Left", "Input/Keybinds/Controller", "D-pad left"); REXCVAR_DEFINE_STRING(keybind_dpad_right, "Right", "Input/Keybinds/Controller", "D-pad right"); REXCVAR_DEFINE_STRING(keybind_back, "Tab", "Input/Keybinds/Controller", "Back button"); REXCVAR_DEFINE_STRING(keybind_start, "Escape", "Input/Keybinds/Controller", "Start button"); REXCVAR_DEFINE_STRING(keybind_guide, "", "Input/Keybinds/Controller", "Guide button"); namespace rex::input::mnk { using rex::ui::VirtualKey; MnkInputDriver::MnkInputDriver(rex::ui::Window* window, size_t window_z_order) : InputDriver(window, window_z_order) {} MnkInputDriver::~MnkInputDriver() { // Detach handled by OnClosing; if window outlives the driver, clean up here. if (attached_window_) { attached_window_->RemoveInputListener(this); attached_window_->RemoveListener(this); attached_window_ = nullptr; } } X_STATUS MnkInputDriver::Setup() { REXLOG_INFO("MnK input driver initialized"); return X_STATUS_SUCCESS; } void MnkInputDriver::OnWindowAvailable(rex::ui::Window* window) { if (window) { attached_window_ = window; window->AddInputListener(this, window_z_order()); window->AddListener(this); } } void MnkInputDriver::OnClosing(rex::ui::UIEvent&) { if (attached_window_) { if (mouse_captured_) { mouse_captured_ = false; attached_window_->SetCursorVisibility(rex::ui::Window::CursorVisibility::kVisible); attached_window_->ReleaseMouse(); } attached_window_->RemoveInputListener(this); attached_window_->RemoveListener(this); attached_window_ = nullptr; } } uint32_t MnkInputDriver::UserIndex() const { return static_cast(REXCVAR_GET(mnk_user_index)); } bool MnkInputDriver::IsEnabled() const { return REXCVAR_GET(mnk_mode); } void MnkInputDriver::ClearStateLocked() { std::memset(key_down_, 0, sizeof(key_down_)); mouse_dx_ = 0; mouse_dy_ = 0; } static bool IsBindPressed(const bool (&key_down)[256], const std::string& cvar_val) { VirtualKey vk = rex::ui::ParseVirtualKey(cvar_val); if (vk == VirtualKey::kNone) return false; uint16_t idx = static_cast(vk); return idx < 256 && key_down[idx]; } X_RESULT MnkInputDriver::GetCapabilities(uint32_t user_index, uint32_t flags, X_INPUT_CAPABILITIES* out_caps) { if (!IsEnabled() || user_index != UserIndex()) { return X_ERROR_DEVICE_NOT_CONNECTED; } if (out_caps) { std::memset(out_caps, 0, sizeof(*out_caps)); out_caps->type = 0x01; out_caps->sub_type = 0x01; out_caps->flags = 0; out_caps->gamepad.buttons = 0xFFFF; out_caps->gamepad.left_trigger = 0xFF; out_caps->gamepad.right_trigger = 0xFF; out_caps->gamepad.thumb_lx = static_cast(0x7FFF); out_caps->gamepad.thumb_ly = static_cast(0x7FFF); out_caps->gamepad.thumb_rx = static_cast(0x7FFF); out_caps->gamepad.thumb_ry = static_cast(0x7FFF); out_caps->vibration.left_motor_speed = 0xFFFF; out_caps->vibration.right_motor_speed = 0xFFFF; } return X_ERROR_SUCCESS; } X_RESULT MnkInputDriver::GetState(uint32_t user_index, X_INPUT_STATE* out_state) { if (!IsEnabled()) { std::lock_guard lock(state_mutex_); ClearStateLocked(); return X_ERROR_DEVICE_NOT_CONNECTED; } if (user_index != UserIndex()) { return X_ERROR_DEVICE_NOT_CONNECTED; } UpdateMouseCapture(); if (!is_active() || !has_focus_) { std::lock_guard lock(state_mutex_); ClearStateLocked(); if (out_state) { std::memset(out_state, 0, sizeof(*out_state)); out_state->packet_number = packet_number_; } return X_ERROR_SUCCESS; } std::lock_guard lock(state_mutex_); uint16_t buttons = 0; if (IsBindPressed(key_down_, REXCVAR_GET(keybind_a))) buttons |= X_INPUT_GAMEPAD_A; if (IsBindPressed(key_down_, REXCVAR_GET(keybind_b))) buttons |= X_INPUT_GAMEPAD_B; if (IsBindPressed(key_down_, REXCVAR_GET(keybind_x))) buttons |= X_INPUT_GAMEPAD_X; if (IsBindPressed(key_down_, REXCVAR_GET(keybind_y))) buttons |= X_INPUT_GAMEPAD_Y; if (IsBindPressed(key_down_, REXCVAR_GET(keybind_left_shoulder))) buttons |= X_INPUT_GAMEPAD_LEFT_SHOULDER; if (IsBindPressed(key_down_, REXCVAR_GET(keybind_right_shoulder))) buttons |= X_INPUT_GAMEPAD_RIGHT_SHOULDER; if (IsBindPressed(key_down_, REXCVAR_GET(keybind_lstick_press))) buttons |= X_INPUT_GAMEPAD_LEFT_THUMB; if (IsBindPressed(key_down_, REXCVAR_GET(keybind_rstick_press))) buttons |= X_INPUT_GAMEPAD_RIGHT_THUMB; if (IsBindPressed(key_down_, REXCVAR_GET(keybind_back))) buttons |= X_INPUT_GAMEPAD_BACK; if (IsBindPressed(key_down_, REXCVAR_GET(keybind_start))) buttons |= X_INPUT_GAMEPAD_START; if (IsBindPressed(key_down_, REXCVAR_GET(keybind_guide))) buttons |= X_INPUT_GAMEPAD_GUIDE; if (IsBindPressed(key_down_, REXCVAR_GET(keybind_dpad_up))) buttons |= X_INPUT_GAMEPAD_DPAD_UP; if (IsBindPressed(key_down_, REXCVAR_GET(keybind_dpad_down))) buttons |= X_INPUT_GAMEPAD_DPAD_DOWN; if (IsBindPressed(key_down_, REXCVAR_GET(keybind_dpad_left))) buttons |= X_INPUT_GAMEPAD_DPAD_LEFT; if (IsBindPressed(key_down_, REXCVAR_GET(keybind_dpad_right))) buttons |= X_INPUT_GAMEPAD_DPAD_RIGHT; uint8_t lt = IsBindPressed(key_down_, REXCVAR_GET(keybind_left_trigger)) ? 0xFF : 0; uint8_t rt = IsBindPressed(key_down_, REXCVAR_GET(keybind_right_trigger)) ? 0xFF : 0; int32_t lx = 0; int32_t ly = 0; if (IsBindPressed(key_down_, REXCVAR_GET(keybind_lstick_left))) lx -= INT16_MAX; if (IsBindPressed(key_down_, REXCVAR_GET(keybind_lstick_right))) lx += INT16_MAX; if (IsBindPressed(key_down_, REXCVAR_GET(keybind_lstick_up))) ly += INT16_MAX; if (IsBindPressed(key_down_, REXCVAR_GET(keybind_lstick_down))) ly -= INT16_MAX; double sensitivity = REXCVAR_GET(mnk_sensitivity); constexpr double kBaseScale = 200.0; int32_t rx = static_cast(mouse_dx_ * sensitivity * kBaseScale); int32_t ry = static_cast(-mouse_dy_ * sensitivity * kBaseScale); mouse_dx_ = 0; mouse_dy_ = 0; auto clamp16 = [](int32_t v) -> int16_t { return static_cast(std::clamp(v, (int32_t)INT16_MIN, (int32_t)INT16_MAX)); }; packet_number_++; if (out_state) { out_state->packet_number = packet_number_; out_state->gamepad.buttons = buttons; out_state->gamepad.left_trigger = lt; out_state->gamepad.right_trigger = rt; out_state->gamepad.thumb_lx = clamp16(lx); out_state->gamepad.thumb_ly = clamp16(ly); out_state->gamepad.thumb_rx = clamp16(rx); out_state->gamepad.thumb_ry = clamp16(ry); } return X_ERROR_SUCCESS; } X_RESULT MnkInputDriver::SetState(uint32_t user_index, X_INPUT_VIBRATION* vibration) { if (!IsEnabled() || user_index != UserIndex()) { return X_ERROR_DEVICE_NOT_CONNECTED; } return X_ERROR_SUCCESS; } X_RESULT MnkInputDriver::GetKeystroke(uint32_t user_index, uint32_t flags, X_INPUT_KEYSTROKE* out_keystroke) { if (!IsEnabled()) { std::lock_guard lock(state_mutex_); ClearStateLocked(); while (!keystroke_queue_.empty()) { keystroke_queue_.pop(); } return X_ERROR_DEVICE_NOT_CONNECTED; } if (user_index != UserIndex()) { return X_ERROR_DEVICE_NOT_CONNECTED; } std::lock_guard lock(state_mutex_); if (!is_active() || !has_focus_) { ClearStateLocked(); while (!keystroke_queue_.empty()) { keystroke_queue_.pop(); } return X_ERROR_EMPTY; } if (keystroke_queue_.empty()) { return X_ERROR_EMPTY; } if (out_keystroke) { *out_keystroke = keystroke_queue_.front(); } keystroke_queue_.pop(); return X_ERROR_SUCCESS; } void MnkInputDriver::EnqueueKeystroke(uint16_t vk_pad, bool down) { X_INPUT_KEYSTROKE ks = {}; ks.virtual_key = vk_pad; ks.unicode = 0; ks.flags = down ? X_INPUT_KEYSTROKE_KEYDOWN : X_INPUT_KEYSTROKE_KEYUP; ks.user_index = static_cast(UserIndex()); ks.hid_code = 0; keystroke_queue_.push(ks); } void MnkInputDriver::CenterCursor() { if (!attached_window_) return; int32_t cx = static_cast(attached_window_->GetActualLogicalWidth() / 2); int32_t cy = static_cast(attached_window_->GetActualLogicalHeight() / 2); prev_mouse_x_ = cx; prev_mouse_y_ = cy; #if REX_PLATFORM_WIN32 auto* win32_window = dynamic_cast(attached_window_); if (win32_window && win32_window->hwnd()) { POINT pt = {static_cast(cx), static_cast(cy)}; ClientToScreen(win32_window->hwnd(), &pt); SetCursorPos(pt.x, pt.y); } #endif } void MnkInputDriver::UpdateMouseCapture() { if (!attached_window_) return; bool should_capture = IsEnabled() && has_focus_ && is_active() && REXCVAR_GET(mnk_capture_mouse); if (should_capture && !mouse_captured_) { mouse_captured_ = true; attached_window_->SetCursorVisibility(rex::ui::Window::CursorVisibility::kHidden); attached_window_->CaptureMouse(); // Reset deltas to avoid a spike on capture start mouse_dx_ = 0; mouse_dy_ = 0; } else if (!should_capture && mouse_captured_) { mouse_captured_ = false; attached_window_->SetCursorVisibility(rex::ui::Window::CursorVisibility::kVisible); attached_window_->ReleaseMouse(); } // Re-center cursor each frame while captured to prevent edge clamping if (mouse_captured_) { CenterCursor(); } } void MnkInputDriver::SetKeyState(uint16_t vk, bool down) { if (vk < 256) { key_down_[vk] = down; } } void MnkInputDriver::OnKeyDown(rex::ui::KeyEvent& e) { if (!IsEnabled() || !has_focus_ || !is_active()) { return; } std::lock_guard lock(state_mutex_); uint16_t vk = static_cast(e.virtual_key()); SetKeyState(vk, true); } void MnkInputDriver::OnKeyUp(rex::ui::KeyEvent& e) { if (!IsEnabled()) return; std::lock_guard lock(state_mutex_); uint16_t vk = static_cast(e.virtual_key()); SetKeyState(vk, false); } void MnkInputDriver::OnMouseDown(rex::ui::MouseEvent& e) { if (!IsEnabled() || !has_focus_ || !is_active()) return; std::lock_guard lock(state_mutex_); switch (e.button()) { case rex::ui::MouseEvent::Button::kLeft: SetKeyState(static_cast(VirtualKey::kLButton), true); break; case rex::ui::MouseEvent::Button::kRight: SetKeyState(static_cast(VirtualKey::kRButton), true); break; case rex::ui::MouseEvent::Button::kMiddle: SetKeyState(static_cast(VirtualKey::kMButton), true); break; default: break; } } void MnkInputDriver::OnMouseUp(rex::ui::MouseEvent& e) { if (!IsEnabled()) return; std::lock_guard lock(state_mutex_); switch (e.button()) { case rex::ui::MouseEvent::Button::kLeft: SetKeyState(static_cast(VirtualKey::kLButton), false); break; case rex::ui::MouseEvent::Button::kRight: SetKeyState(static_cast(VirtualKey::kRButton), false); break; case rex::ui::MouseEvent::Button::kMiddle: SetKeyState(static_cast(VirtualKey::kMButton), false); break; default: break; } } void MnkInputDriver::OnMouseMove(rex::ui::MouseEvent& e) { if (!IsEnabled() || !has_focus_ || !is_active()) return; std::lock_guard lock(state_mutex_); int32_t x = e.x(); int32_t y = e.y(); mouse_dx_ += x - prev_mouse_x_; mouse_dy_ += y - prev_mouse_y_; prev_mouse_x_ = x; prev_mouse_y_ = y; } void MnkInputDriver::OnLostFocus(rex::ui::UISetupEvent&) { std::lock_guard lock(state_mutex_); has_focus_ = false; ClearStateLocked(); if (mouse_captured_ && attached_window_) { mouse_captured_ = false; attached_window_->SetCursorVisibility(rex::ui::Window::CursorVisibility::kVisible); attached_window_->ReleaseMouse(); } } void MnkInputDriver::OnGotFocus(rex::ui::UISetupEvent&) { std::lock_guard lock(state_mutex_); has_focus_ = true; } } // namespace rex::input::mnk