#include "graphics/presentation/window/hostInput.h" #include "SDL_error.h" #include "SDL_events.h" #include "SDL_keyboard.h" #include "SDL_keycode.h" #include "SDL_mouse.h" #include "SDL_timer.h" #include "SDL_video.h" #include "common/assert.h" #include "common/emulatorConfig.h" #include "common/logging/log.h" #include "libs/controller.h" #include #include #include #include #include #include #include namespace Libs::Graphics { namespace { struct ControlInfo { std::string_view name; uint32_t button = 0; Controller::Axis axis = Controller::Axis::AxisMax; bool positive = false; float touch_x = 0.0f; }; static constexpr std::array CONTROL_INFO = { ControlInfo {"L3", Controller::PAD_BUTTON_L3}, ControlInfo {"R3", Controller::PAD_BUTTON_R3}, ControlInfo {"Options", Controller::PAD_BUTTON_OPTIONS}, ControlInfo {"Up", Controller::PAD_BUTTON_UP}, ControlInfo {"Right", Controller::PAD_BUTTON_RIGHT}, ControlInfo {"Down", Controller::PAD_BUTTON_DOWN}, ControlInfo {"Left", Controller::PAD_BUTTON_LEFT}, ControlInfo {"L2", Controller::PAD_BUTTON_L2}, ControlInfo {"R2", Controller::PAD_BUTTON_R2}, ControlInfo {"L1", Controller::PAD_BUTTON_L1}, ControlInfo {"R1", Controller::PAD_BUTTON_R1}, ControlInfo {"Triangle", Controller::PAD_BUTTON_TRIANGLE}, ControlInfo {"Circle", Controller::PAD_BUTTON_CIRCLE}, ControlInfo {"Cross", Controller::PAD_BUTTON_CROSS}, ControlInfo {"Square", Controller::PAD_BUTTON_SQUARE}, ControlInfo {"TouchPad", Controller::PAD_BUTTON_TOUCH_PAD, Controller::Axis::AxisMax, false, 0.25f}, ControlInfo {"TouchPadRight", Controller::PAD_BUTTON_TOUCH_PAD, Controller::Axis::AxisMax, false, 0.75f}, ControlInfo {"LeftStickLeft", 0, Controller::Axis::LeftX, false}, ControlInfo {"LeftStickRight", 0, Controller::Axis::LeftX, true}, ControlInfo {"LeftStickUp", 0, Controller::Axis::LeftY, false}, ControlInfo {"LeftStickDown", 0, Controller::Axis::LeftY, true}, ControlInfo {"RightStickLeft", 0, Controller::Axis::RightX, false}, ControlInfo {"RightStickRight", 0, Controller::Axis::RightX, true}, ControlInfo {"RightStickUp", 0, Controller::Axis::RightY, false}, ControlInfo {"RightStickDown", 0, Controller::Axis::RightY, true}, }; constexpr std::size_t INVALID_CONTROL = CONTROL_INFO.size(); struct Binding { SDL_Keycode key = SDLK_UNKNOWN; uint8_t mouse_button = 0; std::size_t control = INVALID_CONTROL; }; constexpr int MOUSE_POLL_INTERVAL_MS = 33; constexpr std::string_view MOUSE_SENSITIVITY = "MouseSensitivity="; struct MouseJoystickState { bool enabled = false; bool output = false; uint64_t next_poll = 0; }; MouseJoystickState g_mouse; std::size_t ControlFromName(std::string_view name) { const auto info = std::find_if(CONTROL_INFO.begin(), CONTROL_INFO.end(), [name](const auto& item) { return item.name == name; }); return static_cast(std::distance(CONTROL_INFO.begin(), info)); } SDL_Keycode NormalizeKey(SDL_Keycode key) { switch (key) { case SDLK_RSHIFT: return SDLK_LSHIFT; case SDLK_RCTRL: return SDLK_LCTRL; case SDLK_RALT: return SDLK_LALT; case SDLK_RGUI: return SDLK_LGUI; default: return key; } } uint8_t MouseButtonFromName(std::string_view name) { static constexpr std::array buttons = { std::pair {std::string_view("Mouse:Left"), uint8_t {SDL_BUTTON_LEFT}}, std::pair {std::string_view("Mouse:Middle"), uint8_t {SDL_BUTTON_MIDDLE}}, std::pair {std::string_view("Mouse:Right"), uint8_t {SDL_BUTTON_RIGHT}}, std::pair {std::string_view("Mouse:X1"), uint8_t {SDL_BUTTON_X1}}, std::pair {std::string_view("Mouse:X2"), uint8_t {SDL_BUTTON_X2}}, }; const auto button = std::find_if(buttons.begin(), buttons.end(), [name](const auto& item) { return item.first == name; }); return button != buttons.end() ? button->second : 0; } bool Conflicts(const Binding& first, const Binding& second) { return first.control == second.control || (first.key != SDLK_UNKNOWN && first.key == second.key) || (first.mouse_button != 0 && first.mouse_button == second.mouse_button); } class InputMap { public: InputMap() { for (const auto& value: Config::GetKeymap()) { const std::string_view entry = value; if (entry.starts_with(MOUSE_SENSITIVITY)) { const float sensitivity = std::strtof(value.c_str() + MOUSE_SENSITIVITY.size(), nullptr); m_mouse_sensitivity = std::clamp(std::isfinite(sensitivity) ? sensitivity : 1.0f, 0.1f, 5.0f); continue; } const auto split = entry.find('='); Binding binding; if (split != std::string_view::npos) { binding.control = ControlFromName(entry.substr(0, split)); const auto host_input = entry.substr(split + 1); binding.mouse_button = MouseButtonFromName(host_input); if (binding.mouse_button == 0) { binding.key = NormalizeKey(SDL_GetKeyFromName(std::string(host_input).c_str())); } } const bool reserved = binding.key == SDLK_ESCAPE || binding.key == SDLK_SPACE || binding.key == SDLK_F1 || binding.key == SDLK_F7 || binding.key == SDLK_F11; if (binding.control == INVALID_CONTROL || reserved || (binding.key == SDLK_UNKNOWN && binding.mouse_button == 0)) { EXIT("Invalid input mapping: %s\n", value.c_str()); } Add(binding); } } [[nodiscard]] bool Custom() const { return m_size != 0; } [[nodiscard]] float MouseSensitivity() const { return m_mouse_sensitivity; } [[nodiscard]] std::size_t FindKey(int key_code) const { key_code = NormalizeKey(static_cast(key_code)); const auto binding = std::find_if(m_bindings.begin(), m_bindings.begin() + m_size, [key_code](const auto& item) { return item.key == key_code; }); return binding != m_bindings.begin() + m_size ? binding->control : INVALID_CONTROL; } [[nodiscard]] std::size_t FindMouseButton(uint8_t mouse_button) const { const auto binding = std::find_if( m_bindings.begin(), m_bindings.begin() + m_size, [mouse_button](const auto& item) { return item.mouse_button == mouse_button; }); return binding != m_bindings.begin() + m_size ? binding->control : INVALID_CONTROL; } private: void Add(const Binding& binding) { for (std::size_t index = 0; index < m_size;) { if (Conflicts(m_bindings[index], binding)) { m_bindings[index] = m_bindings[--m_size]; } else { index++; } } EXIT_IF(m_size >= m_bindings.size()); m_bindings[m_size++] = binding; } std::array m_bindings {}; std::size_t m_size = 0; float m_mouse_sensitivity = 1.0f; }; const InputMap& GetInputMap() { static const InputMap map; return map; } void SetButton(uint32_t button, bool down) { if (button == Controller::PAD_BUTTON_L2) { Controller::SetAxis(Controller::HOST_INPUT_CONTROLLER_ID, Controller::Axis::TriggerLeft, down ? 255 : 0); } else if (button == Controller::PAD_BUTTON_R2) { Controller::SetAxis(Controller::HOST_INPUT_CONTROLLER_ID, Controller::Axis::TriggerRight, down ? 255 : 0); } else if (button != 0) { Controller::SetButton(Controller::HOST_INPUT_CONTROLLER_ID, button, down); } } void SetTouchPad(float x, bool down) { Controller::SetTouchPad(Controller::HOST_INPUT_CONTROLLER_ID, 0, down, x, 0.5f); } uint32_t DefaultKeyboardButton(int key_code) { switch (NormalizeKey(static_cast(key_code))) { case SDLK_UP: return Controller::PAD_BUTTON_UP; case SDLK_LEFT: return Controller::PAD_BUTTON_LEFT; case SDLK_DOWN: return Controller::PAD_BUTTON_DOWN; case SDLK_RIGHT: return Controller::PAD_BUTTON_RIGHT; case SDLK_j: return Controller::PAD_BUTTON_CROSS; case SDLK_i: return Controller::PAD_BUTTON_TRIANGLE; case SDLK_k: return Controller::PAD_BUTTON_SQUARE; case SDLK_l: return Controller::PAD_BUTTON_CIRCLE; case SDLK_q: return Controller::PAD_BUTTON_L1; case SDLK_e: return Controller::PAD_BUTTON_R1; case SDLK_z: return Controller::PAD_BUTTON_L2; case SDLK_c: return Controller::PAD_BUTTON_R2; case SDLK_LSHIFT: return Controller::PAD_BUTTON_L3; case SDLK_LCTRL: return Controller::PAD_BUTTON_R3; case SDLK_RETURN: case SDLK_RETURN2: return Controller::PAD_BUTTON_OPTIONS; default: return 0; } } struct StickKeys { bool left = false; bool right = false; bool up = false; bool down = false; }; void SetStickAxis(Controller::Axis axis, bool negative, bool positive) { int value = 128; if (negative && !positive) { value = 0; } else if (positive && !negative) { value = 255; } Controller::SetAxis(Controller::HOST_INPUT_CONTROLLER_ID, axis, value); } void SetControl(std::size_t control, bool down) { if (control == INVALID_CONTROL) { return; } const auto& info = CONTROL_INFO[control]; if (info.button == Controller::PAD_BUTTON_TOUCH_PAD) { SetTouchPad(info.touch_x, down); return; } if (info.button != 0) { SetButton(info.button, down); return; } struct AxisKeys { bool negative = false; bool positive = false; }; static std::array axes; const auto axis = static_cast(info.axis); EXIT_IF(axis >= axes.size()); if (info.positive) { axes[axis].positive = down; } else { axes[axis].negative = down; } SetStickAxis(info.axis, axes[axis].negative, axes[axis].positive); } void DefaultKeyboardInput(int key_code, bool down) { static StickKeys left; static StickKeys right; switch (NormalizeKey(static_cast(key_code))) { case SDLK_BACKSPACE: SetTouchPad(0.25f, down); return; case SDLK_TAB: SetTouchPad(0.75f, down); return; case SDLK_a: left.left = down; SetStickAxis(Controller::Axis::LeftX, left.left, left.right); return; case SDLK_d: left.right = down; SetStickAxis(Controller::Axis::LeftX, left.left, left.right); return; case SDLK_w: left.up = down; SetStickAxis(Controller::Axis::LeftY, left.up, left.down); return; case SDLK_s: left.down = down; SetStickAxis(Controller::Axis::LeftY, left.up, left.down); return; case SDLK_f: right.left = down; SetStickAxis(Controller::Axis::RightX, right.left, right.right); return; case SDLK_h: right.right = down; SetStickAxis(Controller::Axis::RightX, right.left, right.right); return; case SDLK_t: right.up = down; SetStickAxis(Controller::Axis::RightY, right.up, right.down); return; case SDLK_g: right.down = down; SetStickAxis(Controller::Axis::RightY, right.up, right.down); return; default: SetButton(DefaultKeyboardButton(key_code), down); return; } } void MouseToJoystick(int delta_x, int delta_y) { const double distance = std::hypot(delta_x, delta_y); const double scale = std::clamp(distance * GetInputMap().MouseSensitivity() + 16.0, 64.0, 128.0) / distance; const auto map_axis = [scale](int delta) { return std::clamp(128 + static_cast(std::lround(delta * scale)), 0, 255); }; Controller::SetRightStick(Controller::HOST_INPUT_CONTROLLER_ID, map_axis(delta_x), map_axis(delta_y)); } void CenterMouseStick() { if (!g_mouse.output) { return; } Controller::SetRightStick(Controller::HOST_INPUT_CONTROLLER_ID, 128, 128); g_mouse.output = false; } bool SetRelativeMouseMode(bool enabled) { if (SDL_SetRelativeMouseMode(enabled ? SDL_TRUE : SDL_FALSE) == 0) { return true; } LOGF("Mouse-to-joystick relative mode failed: %s\n", SDL_GetError()); return false; } } // namespace void HostInputInit() { GetInputMap(); } void HostInputKey(int key_code, bool down) { const auto& map = GetInputMap(); if (map.Custom()) { SetControl(map.FindKey(key_code), down); } else { DefaultKeyboardInput(key_code, down); } } void HostInputMouseButton(uint8_t mouse_button, bool down) { const auto& map = GetInputMap(); if (map.Custom() && mouse_button != 0) { SetControl(map.FindMouseButton(mouse_button), down); } } void HostInputToggleMouseToJoystick() { if (g_mouse.enabled) { SetRelativeMouseMode(false); CenterMouseStick(); g_mouse = {}; LOGF("Mouse to right stick: disabled\n"); return; } if (!SetRelativeMouseMode(true)) { return; } int ignored_x = 0; int ignored_y = 0; SDL_GetRelativeMouseState(&ignored_x, &ignored_y); g_mouse.enabled = true; g_mouse.next_poll = SDL_GetTicks64() + MOUSE_POLL_INTERVAL_MS; LOGF("Mouse to right stick: enabled (F7 to release)\n"); } int PollMouse(uint64_t now_ms) { if (now_ms < g_mouse.next_poll) { return static_cast(g_mouse.next_poll - now_ms); } g_mouse.next_poll = now_ms + MOUSE_POLL_INTERVAL_MS; int delta_x = 0; int delta_y = 0; SDL_GetRelativeMouseState(&delta_x, &delta_y); if (delta_x == 0 && delta_y == 0) { CenterMouseStick(); return MOUSE_POLL_INTERVAL_MS; } MouseToJoystick(delta_x, delta_y); g_mouse.output = true; return MOUSE_POLL_INTERVAL_MS; } bool HostInputWaitEvent(SDL_Event* event) { if (!g_mouse.enabled || SDL_GetKeyboardFocus() == nullptr) { CenterMouseStick(); if (SDL_WaitEvent(event) == 0) { EXIT("%s\n", SDL_GetError()); } return true; } const int timeout_ms = PollMouse(SDL_GetTicks64()); SDL_ClearError(); if (SDL_WaitEventTimeout(event, timeout_ms) != 0) { return true; } if (SDL_GetError()[0] != '\0') { EXIT("%s\n", SDL_GetError()); } return false; } } // namespace Libs::Graphics