rexglue-skate3/src/input/mnk/mnk_input_driver.cpp
2026-06-04 18:36:22 +01:00

421 lines
14 KiB
C++

/**
* @file input/mnk/mnk_input_driver.cpp
* @brief Keyboard/mouse input driver implementation.
*
* @copyright Copyright (c) 2026 Tom Clay <tomc@tctechstuff.com>
* All rights reserved.
*
* @license BSD 3-Clause License
* See LICENSE file in the project root for full license text.
*/
#include <rex/input/mnk/mnk_input_driver.h>
#include <rex/cvar.h>
#include <rex/input/input.h>
#include <rex/logging.h>
#include <rex/ui/keybinds.h>
#include <rex/ui/virtual_key.h>
#include <rex/ui/window.h>
#include <algorithm>
#include <cmath>
#include <cstring>
#if REX_PLATFORM_WIN32
#include <rex/ui/window_win.h>
#include <Windows.h>
#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<uint32_t>(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<uint16_t>(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<int16_t>(0x7FFF);
out_caps->gamepad.thumb_ly = static_cast<int16_t>(0x7FFF);
out_caps->gamepad.thumb_rx = static_cast<int16_t>(0x7FFF);
out_caps->gamepad.thumb_ry = static_cast<int16_t>(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<int32_t>(mouse_dx_ * sensitivity * kBaseScale);
int32_t ry = static_cast<int32_t>(-mouse_dy_ * sensitivity * kBaseScale);
mouse_dx_ = 0;
mouse_dy_ = 0;
auto clamp16 = [](int32_t v) -> int16_t {
return static_cast<int16_t>(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<uint8_t>(UserIndex());
ks.hid_code = 0;
keystroke_queue_.push(ks);
}
void MnkInputDriver::CenterCursor() {
if (!attached_window_)
return;
int32_t cx = static_cast<int32_t>(attached_window_->GetActualLogicalWidth() / 2);
int32_t cy = static_cast<int32_t>(attached_window_->GetActualLogicalHeight() / 2);
prev_mouse_x_ = cx;
prev_mouse_y_ = cy;
#if REX_PLATFORM_WIN32
auto* win32_window = dynamic_cast<rex::ui::Win32Window*>(attached_window_);
if (win32_window && win32_window->hwnd()) {
POINT pt = {static_cast<LONG>(cx), static_cast<LONG>(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<uint16_t>(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<uint16_t>(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<uint16_t>(VirtualKey::kLButton), true);
break;
case rex::ui::MouseEvent::Button::kRight:
SetKeyState(static_cast<uint16_t>(VirtualKey::kRButton), true);
break;
case rex::ui::MouseEvent::Button::kMiddle:
SetKeyState(static_cast<uint16_t>(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<uint16_t>(VirtualKey::kLButton), false);
break;
case rex::ui::MouseEvent::Button::kRight:
SetKeyState(static_cast<uint16_t>(VirtualKey::kRButton), false);
break;
case rex::ui::MouseEvent::Button::kMiddle:
SetKeyState(static_cast<uint16_t>(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