From 2880b71b485adbe33657d88d49ea104c51df144b Mon Sep 17 00:00:00 2001 From: Silent Date: Sun, 15 Nov 2020 14:56:52 +0100 Subject: [PATCH] Support for SDL Joysticks This enables use of non-controller peripherals, such as DirectInput steering wheels or flight sticks --- src/core/negcon.cpp | 5 +- src/duckstation-qt/inputbindingdialog.cpp | 18 +- src/duckstation-qt/inputbindingdialog.h | 4 +- src/duckstation-qt/inputbindingmonitor.cpp | 104 +++++- src/duckstation-qt/inputbindingmonitor.h | 19 + src/duckstation-qt/inputbindingwidgets.cpp | 18 +- src/duckstation-qt/inputbindingwidgets.h | 4 +- src/frontend-common/common_host_interface.cpp | 67 +++- src/frontend-common/controller_interface.cpp | 8 +- src/frontend-common/controller_interface.h | 21 +- .../sdl_controller_interface.cpp | 330 +++++++++++++++++- .../sdl_controller_interface.h | 17 +- .../xinput_controller_interface.cpp | 22 +- .../xinput_controller_interface.h | 6 +- 14 files changed, 570 insertions(+), 73 deletions(-) diff --git a/src/core/negcon.cpp b/src/core/negcon.cpp index a12367633..031e85f50 100644 --- a/src/core/negcon.cpp +++ b/src/core/negcon.cpp @@ -63,9 +63,8 @@ void NeGcon::SetAxisState(s32 axis_code, float value) return; } - // I, II, L: 0..1 -> 0..255 or -1..0 -> 0..255 to support negative axis ranges, - // e.g. if bound to analog stick instead of trigger - const u8 u8_value = static_cast(std::clamp(std::abs(value) * 255.0f, 0.0f, 255.0f)); + // I, II, L: -1..1 -> 0..255 + const u8 u8_value = static_cast(std::clamp(((value + 1.0f) / 2.0f) * 255.0f, 0.0f, 255.0f)); SetAxisState(static_cast(axis_code), u8_value); } diff --git a/src/duckstation-qt/inputbindingdialog.cpp b/src/duckstation-qt/inputbindingdialog.cpp index 294a0af19..053245848 100644 --- a/src/duckstation-qt/inputbindingdialog.cpp +++ b/src/duckstation-qt/inputbindingdialog.cpp @@ -128,16 +128,18 @@ void InputBindingDialog::addNewBinding(std::string new_binding) saveListToSettings(); } -void InputBindingDialog::bindToControllerAxis(int controller_index, int axis_index, std::optional positive) +void InputBindingDialog::bindToControllerAxis(int controller_index, int axis_index, bool inverted, + std::optional half_axis_positive) { + const char* invert_char = inverted ? "-" : ""; const char* sign_char = ""; - if (positive) + if (half_axis_positive) { - sign_char = *positive ? "+" : "-"; + sign_char = *half_axis_positive ? "+" : "-"; } std::string binding = - StringUtil::StdStringFromFormat("Controller%d/%sAxis%d", controller_index, sign_char, axis_index); + StringUtil::StdStringFromFormat("Controller%d/%sAxis%d%s", controller_index, sign_char, axis_index, invert_char); addNewBinding(std::move(binding)); stopListeningForInput(); } @@ -149,6 +151,14 @@ void InputBindingDialog::bindToControllerButton(int controller_index, int button stopListeningForInput(); } +void InputBindingDialog::bindToControllerHat(int controller_index, int hat_index, const QString& hat_direction) +{ + std::string binding = StringUtil::StdStringFromFormat("Controller%d/Hat%d %s", controller_index, hat_index, + hat_direction.toLatin1().constData()); + addNewBinding(std::move(binding)); + stopListeningForInput(); +} + void InputBindingDialog::onAddBindingButtonClicked() { if (isListeningForInput()) diff --git a/src/duckstation-qt/inputbindingdialog.h b/src/duckstation-qt/inputbindingdialog.h index ff26b3776..5e6b64982 100644 --- a/src/duckstation-qt/inputbindingdialog.h +++ b/src/duckstation-qt/inputbindingdialog.h @@ -19,8 +19,10 @@ public: ~InputBindingDialog(); protected Q_SLOTS: - void bindToControllerAxis(int controller_index, int axis_index, std::optional positive); + void bindToControllerAxis(int controller_index, int axis_index, bool inverted, + std::optional half_axis_positive); void bindToControllerButton(int controller_index, int button_index); + void bindToControllerHat(int controller_index, int hat_index, const QString& hat_direction); void onAddBindingButtonClicked(); void onRemoveBindingButtonClicked(); void onClearBindingsButtonClicked(); diff --git a/src/duckstation-qt/inputbindingmonitor.cpp b/src/duckstation-qt/inputbindingmonitor.cpp index 906ed236d..e0e0ed7f1 100644 --- a/src/duckstation-qt/inputbindingmonitor.cpp +++ b/src/duckstation-qt/inputbindingmonitor.cpp @@ -1,4 +1,5 @@ #include "inputbindingmonitor.h" +#include ControllerInterface::Hook::CallbackResult InputButtonBindingMonitor::operator()(const ControllerInterface::Hook& ei) const @@ -6,20 +7,32 @@ InputButtonBindingMonitor::operator()(const ControllerInterface::Hook& ei) const if (ei.type == ControllerInterface::Hook::Type::Axis) { // wait until it's at least half pushed so we don't get confused between axises with small movement - if (std::abs(ei.value) < 0.5f) + if (std::abs(std::get(ei.value)) < 0.5f) return ControllerInterface::Hook::CallbackResult::ContinueMonitoring; // TODO: this probably should consider the "last value" QMetaObject::invokeMethod(m_parent, "bindToControllerAxis", Q_ARG(int, ei.controller_index), - Q_ARG(int, ei.button_or_axis_number), Q_ARG(std::optional, ei.value > 0)); + Q_ARG(int, ei.button_or_axis_number), Q_ARG(bool, false), + Q_ARG(std::optional, std::get(ei.value) > 0)); return ControllerInterface::Hook::CallbackResult::StopMonitoring; } - else if (ei.type == ControllerInterface::Hook::Type::Button && ei.value > 0.0f) + else if (ei.type == ControllerInterface::Hook::Type::Button && std::get(ei.value) > 0.0f) { QMetaObject::invokeMethod(m_parent, "bindToControllerButton", Q_ARG(int, ei.controller_index), Q_ARG(int, ei.button_or_axis_number)); return ControllerInterface::Hook::CallbackResult::StopMonitoring; } + else if (ei.type == ControllerInterface::Hook::Type::Hat) + { + const std::string_view hat_position = std::get(ei.value); + if (!hat_position.empty()) + { + QString str = QString::fromLatin1(hat_position.data(), static_cast(hat_position.size())); + QMetaObject::invokeMethod(m_parent, "bindToControllerHat", Q_ARG(int, ei.controller_index), + Q_ARG(int, ei.button_or_axis_number), Q_ARG(QString, std::move(str))); + return ControllerInterface::Hook::CallbackResult::StopMonitoring; + } + } return ControllerInterface::Hook::CallbackResult::ContinueMonitoring; } @@ -28,16 +41,17 @@ ControllerInterface::Hook::CallbackResult InputAxisBindingMonitor::operator()(co { if (ei.type == ControllerInterface::Hook::Type::Axis) { - // wait until it's at least half pushed so we don't get confused between axises with small movement - if (std::abs(ei.value) < 0.5f) + std::optional half_axis_positive, inverted; + if (!ProcessAxisInput(ei, half_axis_positive, inverted)) return ControllerInterface::Hook::CallbackResult::ContinueMonitoring; QMetaObject::invokeMethod(m_parent, "bindToControllerAxis", Q_ARG(int, ei.controller_index), - Q_ARG(int, ei.button_or_axis_number), Q_ARG(std::optional, std::nullopt)); + Q_ARG(int, ei.button_or_axis_number), Q_ARG(bool, inverted.value_or(false)), + Q_ARG(std::optional, half_axis_positive)); return ControllerInterface::Hook::CallbackResult::StopMonitoring; } else if (ei.type == ControllerInterface::Hook::Type::Button && m_axis_type == Controller::AxisType::Half && - ei.value > 0.0f) + std::get(ei.value) > 0.0f) { QMetaObject::invokeMethod(m_parent, "bindToControllerButton", Q_ARG(int, ei.controller_index), Q_ARG(int, ei.button_or_axis_number)); @@ -47,10 +61,84 @@ ControllerInterface::Hook::CallbackResult InputAxisBindingMonitor::operator()(co return ControllerInterface::Hook::CallbackResult::ContinueMonitoring; } +bool InputAxisBindingMonitor::ProcessAxisInput(const ControllerInterface::Hook& ei, + std::optional& half_axis_positive, + std::optional& inverted) const +{ + const float value = std::get(ei.value); + + if (!ei.track_history) // Keyboard, mouse, game controller + { + // wait until it's at least half pushed so we don't get confused between axises with small movement + if (std::abs(value) < 0.5f) + return false; + + return true; + } + else // Joystick + { + auto& history = m_context->m_inputs_history; + // Reject inputs coming from multiple sources + if (!history.empty()) + { + const auto& item = history.front(); + if (ei.controller_index != item.controller_index || ei.button_or_axis_number != item.axis_number) + return false; + } + history.push_back({ei.controller_index, ei.button_or_axis_number, value}); + return AnalyzeInputHistory(half_axis_positive, inverted); + } +} + +bool InputAxisBindingMonitor::AnalyzeInputHistory(std::optional& half_axis_positive, + std::optional& inverted) const +{ + const auto& history = m_context->m_inputs_history; + const auto [min, max] = std::minmax_element( + history.begin(), history.end(), [](const auto& left, const auto& right) { return left.value < right.value; }); + + // Ignore small input magnitudes + if (std::abs(max->value - min->value) < 0.5f) + return false; + + // Used heuristics: + // * If history contains inputs with both - and + sign (ignoring 0), bind a full axis + // * If history contains only 0 and inputs of the same sign AND maxes out at 1.0/-1.0, bind a half axis + // * Use the direction of input changes to determine whether the axis is inverted or not + if (std::signbit(min->value) != std::signbit(max->value)) + { + if (min->value != 0.0f && max->value != 0.0f) + { + // If max value comes before the min value, invert the half axis + if (m_axis_type == Controller::AxisType::Half) + { + inverted = std::distance(min, max) < 0; + } + + return true; + } + } + else + { + if ((std::abs(min->value) > 0.99f || std::abs(max->value) > 0.99f) && + (std::abs(min->value) < 0.01f || std::abs(max->value) < 0.01f)) + { + + if (m_axis_type == Controller::AxisType::Half) + { + half_axis_positive = max->value > 0.0f; + } + return true; + } + } + + return false; +} + ControllerInterface::Hook::CallbackResult InputRumbleBindingMonitor::operator()(const ControllerInterface::Hook& ei) const { - if (ei.type == ControllerInterface::Hook::Type::Button && ei.value > 0.0f) + if (ei.type == ControllerInterface::Hook::Type::Button && std::get(ei.value) > 0.0f) { QMetaObject::invokeMethod(m_parent, "bindToControllerRumble", Q_ARG(int, ei.controller_index)); return ControllerInterface::Hook::CallbackResult::StopMonitoring; diff --git a/src/duckstation-qt/inputbindingmonitor.h b/src/duckstation-qt/inputbindingmonitor.h index d22935535..89e90c54c 100644 --- a/src/duckstation-qt/inputbindingmonitor.h +++ b/src/duckstation-qt/inputbindingmonitor.h @@ -2,6 +2,8 @@ #include "frontend-common/controller_interface.h" #include +#include +#include // NOTE: Those Monitor classes must be copyable to meet the requirements of std::function, but at the same time we want // copies to be opaque to the caling code and share context. Therefore, all mutable context of the monitor (if required) @@ -29,8 +31,25 @@ public: ControllerInterface::Hook::CallbackResult operator()(const ControllerInterface::Hook& ei) const; private: + bool ProcessAxisInput(const ControllerInterface::Hook& ei, std::optional& half_axis_positive, + std::optional& inverted) const; + bool AnalyzeInputHistory(std::optional& half_axis_positive, std::optional& inverted) const; + + struct Context + { + struct History + { + int controller_index; + int axis_number; + float value; + }; + + std::vector m_inputs_history; + }; + QObject* m_parent; Controller::AxisType m_axis_type; + std::shared_ptr m_context = std::make_shared(); }; class InputRumbleBindingMonitor diff --git a/src/duckstation-qt/inputbindingwidgets.cpp b/src/duckstation-qt/inputbindingwidgets.cpp index 4c9050962..98a84470e 100644 --- a/src/duckstation-qt/inputbindingwidgets.cpp +++ b/src/duckstation-qt/inputbindingwidgets.cpp @@ -41,16 +41,18 @@ void InputBindingWidget::updateText() setText(QString::fromStdString(m_bindings[0])); } -void InputBindingWidget::bindToControllerAxis(int controller_index, int axis_index, std::optional positive) +void InputBindingWidget::bindToControllerAxis(int controller_index, int axis_index, bool inverted, + std::optional half_axis_positive) { + const char* invert_char = inverted ? "-" : ""; const char* sign_char = ""; - if (positive) + if (half_axis_positive) { - sign_char = *positive ? "+" : "-"; + sign_char = *half_axis_positive ? "+" : "-"; } m_new_binding_value = - StringUtil::StdStringFromFormat("Controller%d/%sAxis%d", controller_index, sign_char, axis_index); + StringUtil::StdStringFromFormat("Controller%d/%sAxis%d%s", controller_index, sign_char, axis_index, invert_char); setNewBinding(); stopListeningForInput(); } @@ -62,6 +64,14 @@ void InputBindingWidget::bindToControllerButton(int controller_index, int button stopListeningForInput(); } +void InputBindingWidget::bindToControllerHat(int controller_index, int hat_index, const QString& hat_direction) +{ + m_new_binding_value = StringUtil::StdStringFromFormat("Controller%d/Hat%d %s", controller_index, hat_index, + hat_direction.toLatin1().constData()); + setNewBinding(); + stopListeningForInput(); +} + void InputBindingWidget::beginRebindAll() { m_is_binding_all = true; diff --git a/src/duckstation-qt/inputbindingwidgets.h b/src/duckstation-qt/inputbindingwidgets.h index e34f44d8d..34113dfcd 100644 --- a/src/duckstation-qt/inputbindingwidgets.h +++ b/src/duckstation-qt/inputbindingwidgets.h @@ -20,8 +20,10 @@ public: ALWAYS_INLINE void setNextWidget(InputBindingWidget* widget) { m_next_widget = widget; } public Q_SLOTS: - void bindToControllerAxis(int controller_index, int axis_index, std::optional positive); + void bindToControllerAxis(int controller_index, int axis_index, bool inverted, + std::optional half_axis_positive); void bindToControllerButton(int controller_index, int button_index); + void bindToControllerHat(int controller_index, int hat_index, const QString& hat_direction); void beginRebindAll(); void clearBinding(); void reloadBinding(); diff --git a/src/frontend-common/common_host_interface.cpp b/src/frontend-common/common_host_interface.cpp index 75c130c40..8dc7ef0fa 100644 --- a/src/frontend-common/common_host_interface.cpp +++ b/src/frontend-common/common_host_interface.cpp @@ -1242,6 +1242,30 @@ bool CommonHostInterface::AddButtonToInputMap(const std::string& binding, const return true; } + else if (StringUtil::StartsWith(button, "Hat")) + { + const std::optional hat_index = StringUtil::FromChars(button.substr(3)); + const std::optional hat_direction = [](const auto& button) { + std::optional result; + + const size_t pos = button.find(' '); + if (pos != button.npos) + { + result = button.substr(pos + 1); + } + return result; + }(button); + + if (!hat_index || !hat_direction || + !m_controller_interface->BindControllerHatToButton(*controller_index, *hat_index, *hat_direction, + std::move(handler))) + { + Log_WarningPrintf("Failed to bind controller hat '%s' to button", binding.c_str()); + return false; + } + + return true; + } Log_WarningPrintf("Malformed controller binding '%s' in button", binding.c_str()); return false; @@ -1266,7 +1290,8 @@ bool CommonHostInterface::AddAxisToInputMap(const std::string& binding, const st return false; } - m_keyboard_input_handlers.emplace(key_id.value(), std::move(handler)); + m_keyboard_input_handlers.emplace(key_id.value(), + [cb = std::move(handler)](bool pressed) { cb(pressed ? 1.0f : -1.0f); }); return true; } @@ -1281,7 +1306,8 @@ bool CommonHostInterface::AddAxisToInputMap(const std::string& binding, const st return false; } - m_mouse_input_handlers.emplace(static_cast(button_index.value()), std::move(handler)); + m_mouse_input_handlers.emplace(static_cast(button_index.value()), + [cb = std::move(handler)](bool pressed) { cb(pressed ? 1.0f : -1.0f); }); return true; } @@ -1305,17 +1331,38 @@ bool CommonHostInterface::AddAxisToInputMap(const std::string& binding, const st return false; } - if (StringUtil::StartsWith(axis, "Axis")) + if (StringUtil::StartsWith(axis, "Axis") || StringUtil::StartsWith(axis, "+Axis") || + StringUtil::StartsWith(axis, "-Axis")) { - const std::optional axis_index = StringUtil::FromChars(axis.substr(4)); - if (!axis_index || - !m_controller_interface->BindControllerAxis(*controller_index, *axis_index, std::move(handler))) + const std::optional axis_index = + StringUtil::FromChars(axis.substr(axis[0] == '+' || axis[0] == '-' ? 5 : 4)); + if (axis_index) { - Log_WarningPrintf("Failed to bind controller axis '%s' to axis", binding.c_str()); - return false; - } + ControllerInterface::AxisSide axis_side = ControllerInterface::AxisSide::Full; + if (axis[0] == '+') + axis_side = ControllerInterface::AxisSide::Positive; + else if (axis[0] == '-') + axis_side = ControllerInterface::AxisSide::Negative; - return true; + const bool inverted = StringUtil::EndsWith(axis, "-"); + if (!inverted) + { + if (m_controller_interface->BindControllerAxis(*controller_index, *axis_index, axis_side, std::move(handler))) + { + return true; + } + } + else + { + if (m_controller_interface->BindControllerAxis(*controller_index, *axis_index, axis_side, + [cb = std::move(handler)](float value) { cb(-value); })) + { + return true; + } + } + } + Log_WarningPrintf("Failed to bind controller axis '%s' to axis", binding.c_str()); + return false; } else if (StringUtil::StartsWith(axis, "Button") && axis_type == Controller::AxisType::Half) { diff --git a/src/frontend-common/controller_interface.cpp b/src/frontend-common/controller_interface.cpp index 0cfc58e8f..c27f093fc 100644 --- a/src/frontend-common/controller_interface.cpp +++ b/src/frontend-common/controller_interface.cpp @@ -36,13 +36,14 @@ void ControllerInterface::ClearHook() m_event_intercept_callback = {}; } -bool ControllerInterface::DoEventHook(Hook::Type type, int controller_index, int button_or_axis_number, float value) +bool ControllerInterface::DoEventHook(Hook::Type type, int controller_index, int button_or_axis_number, + std::variant value, bool track_history) { std::unique_lock lock(m_event_intercept_mutex); if (!m_event_intercept_callback) return false; - const Hook ei{type, controller_index, button_or_axis_number, value}; + const Hook ei{type, controller_index, button_or_axis_number, std::move(value), track_history}; const Hook::CallbackResult action = m_event_intercept_callback(ei); if (action == Hook::CallbackResult::StopMonitoring) m_event_intercept_callback = {}; @@ -64,7 +65,8 @@ void ControllerInterface::OnControllerDisconnected(int host_id) void ControllerInterface::ClearBindings() {} -bool ControllerInterface::BindControllerAxis(int controller_index, int axis_number, AxisCallback callback) +bool ControllerInterface::BindControllerAxis(int controller_index, int axis_number, AxisSide axis_side, + AxisCallback callback) { return false; } diff --git a/src/frontend-common/controller_interface.h b/src/frontend-common/controller_interface.h index 952f6a2b6..d40c167e5 100644 --- a/src/frontend-common/controller_interface.h +++ b/src/frontend-common/controller_interface.h @@ -6,6 +6,7 @@ #include #include #include +#include class HostInterface; class Controller; @@ -31,6 +32,13 @@ public: MAX_NUM_BUTTONS = 15 }; + enum AxisSide + { + Full, + Positive, + Negative + }; + using AxisCallback = CommonHostInterface::InputAxisHandler; using ButtonCallback = CommonHostInterface::InputButtonHandler; @@ -50,10 +58,12 @@ public: virtual void ClearBindings() = 0; // Binding to events. If a binding for this axis/button already exists, returns false. - virtual bool BindControllerAxis(int controller_index, int axis_number, AxisCallback callback) = 0; + virtual bool BindControllerAxis(int controller_index, int axis_number, AxisSide axis_side, AxisCallback callback) = 0; virtual bool BindControllerButton(int controller_index, int button_number, ButtonCallback callback) = 0; virtual bool BindControllerAxisToButton(int controller_index, int axis_number, bool direction, ButtonCallback callback) = 0; + virtual bool BindControllerHatToButton(int controller_index, int hat_number, std::string_view hat_position, + ButtonCallback callback) = 0; virtual bool BindControllerButtonToAxis(int controller_index, int button_number, AxisCallback callback) = 0; virtual void PollEvents() = 0; @@ -71,7 +81,8 @@ public: enum class Type { Axis, - Button + Button, + Hat // Only for joysticks }; enum class CallbackResult @@ -85,13 +96,15 @@ public: Type type; int controller_index; int button_or_axis_number; - float value; // 0/1 for buttons, -1..1 for axises + std::variant value; // 0/1 for buttons, -1..1 for axes, hat direction name for hats + bool track_history; // Track axis movement to spot inversion/half axes }; void SetHook(Hook::Callback callback); void ClearHook(); protected: - bool DoEventHook(Hook::Type type, int controller_index, int button_or_axis_number, float value); + bool DoEventHook(Hook::Type type, int controller_index, int button_or_axis_number, + std::variant value, bool track_history = false); void OnControllerConnected(int host_id); void OnControllerDisconnected(int host_id); diff --git a/src/frontend-common/sdl_controller_interface.cpp b/src/frontend-common/sdl_controller_interface.cpp index d9852841c..10d299446 100644 --- a/src/frontend-common/sdl_controller_interface.cpp +++ b/src/frontend-common/sdl_controller_interface.cpp @@ -106,18 +106,29 @@ bool SDLControllerInterface::ProcessSDLEvent(const SDL_Event* event) case SDL_CONTROLLERBUTTONUP: return HandleControllerButtonEvent(event); + case SDL_JOYDEVICEADDED: + if (SDL_IsGameController(event->jdevice.which)) + return true; + + Log_InfoPrintf("Joystick %d inserted", event->jdevice.which); + OpenJoystick(event->jdevice.which); + return true; + + case SDL_JOYAXISMOTION: + return HandleJoystickAxisEvent(&event->jaxis); + + case SDL_JOYHATMOTION: + return HandleJoystickHatEvent(&event->jhat); + + case SDL_JOYBUTTONDOWN: + case SDL_JOYBUTTONUP: + return HandleJoystickButtonEvent(&event->jbutton); + default: return false; } } -SDLControllerInterface::ControllerDataVector::iterator -SDLControllerInterface::GetControllerDataForController(void* controller) -{ - return std::find_if(m_controllers.begin(), m_controllers.end(), - [controller](const ControllerData& cd) { return cd.controller == controller; }); -} - SDLControllerInterface::ControllerDataVector::iterator SDLControllerInterface::GetControllerDataForJoystickId(int id) { return std::find_if(m_controllers.begin(), m_controllers.end(), @@ -179,10 +190,10 @@ bool SDLControllerInterface::OpenGameController(int index) SDL_GameControllerName(gcontroller)); ControllerData cd = {}; - cd.controller = gcontroller; cd.player_id = player_id; cd.joystick_id = joystick_id; cd.haptic_left_right_effect = -1; + cd.is_game_controller = true; SDL_Haptic* haptic = SDL_HapticOpenFromJoystick(joystick); if (haptic) @@ -233,7 +244,7 @@ bool SDLControllerInterface::CloseGameController(int joystick_index, bool notify if (it->haptic) SDL_HapticClose(static_cast(it->haptic)); - SDL_GameControllerClose(static_cast(it->controller)); + SDL_GameControllerClose(SDL_GameControllerFromInstanceID(joystick_index)); m_controllers.erase(it); if (notify) @@ -241,18 +252,264 @@ bool SDLControllerInterface::CloseGameController(int joystick_index, bool notify return true; } +bool SDLControllerInterface::OpenJoystick(int index) +{ + SDL_Joystick* joystick = SDL_JoystickOpen(index); + if (!joystick) + { + Log_WarningPrintf("Failed to open joystick %d", index); + + return false; + } + + int joystick_id = SDL_JoystickInstanceID(joystick); +#if SDL_VERSION_ATLEAST(2, 0, 9) + int player_id = SDL_JoystickGetDevicePlayerIndex(index); +#else + int player_id = -1; +#endif + if (player_id < 0 || GetControllerDataForPlayerId(player_id) != m_controllers.end()) + { + const int free_player_id = GetFreePlayerId(); + Log_WarningPrintf( + "Controller %d (joystick %d) returned player ID %d, which is invalid or in use. Using ID %d instead.", index, + joystick_id, player_id, free_player_id); + player_id = free_player_id; + } + + const char* name = SDL_JoystickName(joystick); + + Log_InfoPrintf("Opened controller %d (instance id %d, player id %d): %s", index, joystick_id, player_id, name); + + ControllerData cd = {}; + cd.player_id = player_id; + cd.joystick_id = joystick_id; + cd.haptic_left_right_effect = -1; + cd.is_game_controller = false; + + SDL_Haptic* haptic = SDL_HapticOpenFromJoystick(joystick); + if (haptic) + { + SDL_HapticEffect ef = {}; + ef.leftright.type = SDL_HAPTIC_LEFTRIGHT; + ef.leftright.length = 1000; + + int ef_id = SDL_HapticNewEffect(haptic, &ef); + if (ef_id >= 0) + { + cd.haptic = haptic; + cd.haptic_left_right_effect = ef_id; + } + else + { + Log_ErrorPrintf("Failed to create haptic left/right effect: %s", SDL_GetError()); + if (SDL_HapticRumbleSupported(haptic) && SDL_HapticRumbleInit(haptic) != 0) + { + cd.haptic = haptic; + } + else + { + Log_ErrorPrintf("No haptic rumble supported: %s", SDL_GetError()); + SDL_HapticClose(haptic); + } + } + } + + if (cd.haptic) + Log_InfoPrintf("Rumble is supported on '%s'", name); + else + Log_WarningPrintf("Rumble is not supported on '%s'", name); + + m_controllers.push_back(std::move(cd)); + OnControllerConnected(player_id); + return true; +} + +bool SDLControllerInterface::HandleJoystickAxisEvent(const SDL_JoyAxisEvent* event) +{ + const float value = static_cast(event->value) / (event->value < 0 ? 32768.0f : 32767.0f); + Log_DebugPrintf("controller %d axis %d %d %f", event->which, event->axis, event->value, value); + + auto it = GetControllerDataForJoystickId(event->which); + if (it == m_controllers.end() || it->is_game_controller) + return false; + + if (DoEventHook(Hook::Type::Axis, it->player_id, event->axis, value, true)) + return true; + + bool processed = false; + + const AxisCallback& cb = it->axis_mapping[event->axis][AxisSide::Full]; + if (cb) + { + cb(value); + processed = true; + } + + if (value > 0.0f) + { + const AxisCallback& cb = it->axis_mapping[event->axis][AxisSide::Positive]; + if (cb) + { + // Expand 0..1 - -1..1 + cb(value * 2.0f - 1.0f); + processed = true; + } + } + else if (value < 0.0f) + { + const AxisCallback& cb = it->axis_mapping[event->axis][AxisSide::Negative]; + if (cb) + { + // Expand 0..-1 - -1..1 + cb(value * -2.0f - 1.0f); + processed = true; + } + } + + if (processed) + return true; + + // set the other direction to false so large movements don't leave the opposite on + const bool outside_deadzone = (std::abs(value) >= it->deadzone); + const bool positive = (value >= 0.0f); + const ButtonCallback& other_button_cb = it->axis_button_mapping[event->axis][BoolToUInt8(!positive)]; + const ButtonCallback& button_cb = it->axis_button_mapping[event->axis][BoolToUInt8(positive)]; + if (button_cb) + { + button_cb(outside_deadzone); + if (other_button_cb) + other_button_cb(false); + return true; + } + else if (other_button_cb) + { + other_button_cb(false); + return true; + } + else + { + return false; + } +} + +bool SDLControllerInterface::HandleJoystickButtonEvent(const SDL_JoyButtonEvent* event) +{ + Log_DebugPrintf("controller %d button %d %s", event->which, event->button, + event->state == SDL_PRESSED ? "pressed" : "released"); + + auto it = GetControllerDataForJoystickId(event->which); + if (it == m_controllers.end() || it->is_game_controller) + return false; + + const bool pressed = (event->state == SDL_PRESSED); + if (DoEventHook(Hook::Type::Button, it->player_id, event->button, pressed ? 1.0f : 0.0f)) + return true; + + const ButtonCallback& cb = it->button_mapping[event->button]; + if (cb) + { + cb(pressed); + return true; + } + + const AxisCallback& axis_cb = it->button_axis_mapping[event->button]; + if (axis_cb) + { + axis_cb(pressed ? 1.0f : -1.0f); + return true; + } + + return false; +} + +bool SDLControllerInterface::HandleJoystickHatEvent(const SDL_JoyHatEvent* event) +{ + Log_DebugPrintf("controller %d hat %d %d", event->which, event->hat, event->value); + + auto it = GetControllerDataForJoystickId(event->which); + if (it == m_controllers.end() || it->is_game_controller) + return false; + + auto HatEventHook = [hat = event->hat, value = event->value, player_id = it->player_id, this](int hat_position) { + if ((value & hat_position) == 0) + return false; + + std::string_view position_str; + switch (value) + { + case SDL_HAT_UP: + position_str = "Up"; + break; + case SDL_HAT_RIGHT: + position_str = "Right"; + break; + case SDL_HAT_DOWN: + position_str = "Down"; + break; + case SDL_HAT_LEFT: + position_str = "Left"; + break; + default: + return false; + } + + return DoEventHook(Hook::Type::Hat, player_id, hat, position_str); + }; + + if (event->value == SDL_HAT_CENTERED) + { + if (HatEventHook(SDL_HAT_CENTERED)) + return true; + } + else + { + // event->value can be a bitmask of multiple direction, so probe them all + if (HatEventHook(SDL_HAT_UP) || HatEventHook(SDL_HAT_RIGHT) || HatEventHook(SDL_HAT_DOWN) || + HatEventHook(SDL_HAT_LEFT)) + return true; + } + + bool processed = false; + + if (const ButtonCallback& cb = it->hat_button_mapping[event->hat][0]; cb) + { + cb(event->value & SDL_HAT_UP); + processed = true; + } + if (const ButtonCallback& cb = it->hat_button_mapping[event->hat][1]; cb) + { + cb(event->value & SDL_HAT_RIGHT); + processed = true; + } + if (const ButtonCallback& cb = it->hat_button_mapping[event->hat][2]; cb) + { + cb(event->value & SDL_HAT_DOWN); + processed = true; + } + if (const ButtonCallback& cb = it->hat_button_mapping[event->hat][3]; cb) + { + cb(event->value & SDL_HAT_LEFT); + processed = true; + } + + return processed; +} + void SDLControllerInterface::ClearBindings() { for (auto& it : m_controllers) { - for (AxisCallback& ac : it.axis_mapping) - ac = {}; - for (ButtonCallback& bc : it.button_mapping) - bc = {}; + it.axis_mapping.fill({}); + it.button_mapping.fill({}); + it.axis_button_mapping.fill({}); + it.button_axis_mapping.fill({}); + it.hat_button_mapping.clear(); } } -bool SDLControllerInterface::BindControllerAxis(int controller_index, int axis_number, AxisCallback callback) +bool SDLControllerInterface::BindControllerAxis(int controller_index, int axis_number, AxisSide axis_side, + AxisCallback callback) { auto it = GetControllerDataForPlayerId(controller_index); if (it == m_controllers.end()) @@ -261,7 +518,7 @@ bool SDLControllerInterface::BindControllerAxis(int controller_index, int axis_n if (axis_number < 0 || axis_number >= MAX_NUM_AXISES) return false; - it->axis_mapping[axis_number] = std::move(callback); + it->axis_mapping[axis_number][axis_side] = std::move(callback); return true; } @@ -292,6 +549,33 @@ bool SDLControllerInterface::BindControllerAxisToButton(int controller_index, in return true; } +bool SDLControllerInterface::BindControllerHatToButton(int controller_index, int hat_number, + std::string_view hat_position, ButtonCallback callback) +{ + auto it = GetControllerDataForPlayerId(controller_index); + if (it == m_controllers.end()) + return false; + + size_t index; + if (hat_position == "Up") + index = 0; + else if (hat_position == "Right") + index = 1; + else if (hat_position == "Down") + index = 2; + else if (hat_position == "Left") + index = 3; + else + return false; + + // We need 4 entries per hat_number + if (it->hat_button_mapping.size() < hat_number + 1) + it->hat_button_mapping.resize(hat_number + 1); + + it->hat_button_mapping[hat_number][index] = std::move(callback); + return true; +} + bool SDLControllerInterface::BindControllerButtonToAxis(int controller_index, int button_number, AxisCallback callback) { auto it = GetControllerDataForPlayerId(controller_index); @@ -317,10 +601,18 @@ bool SDLControllerInterface::HandleControllerAxisEvent(const SDL_Event* ev) if (DoEventHook(Hook::Type::Axis, it->player_id, ev->caxis.axis, value)) return true; - const AxisCallback& cb = it->axis_mapping[ev->caxis.axis]; + const AxisCallback& cb = it->axis_mapping[ev->caxis.axis][AxisSide::Full]; if (cb) { - cb(value); + // Extend triggers from a 0 - 1 range to a -1 - 1 range for consistency with other inputs + if (ev->caxis.axis == SDL_CONTROLLER_AXIS_TRIGGERLEFT || ev->caxis.axis == SDL_CONTROLLER_AXIS_TRIGGERRIGHT) + { + cb((value * 2.0f) - 1.0f); + } + else + { + cb(value); + } return true; } @@ -367,11 +659,11 @@ bool SDLControllerInterface::HandleControllerButtonEvent(const SDL_Event* ev) return true; } - // Assume a half-axis, i.e. in 0..1 range const AxisCallback& axis_cb = it->button_axis_mapping[ev->cbutton.button]; if (axis_cb) { - axis_cb(pressed ? 1.0f : 0.0f); + axis_cb(pressed ? 1.0f : -1.0f); + return true; } return false; diff --git a/src/frontend-common/sdl_controller_interface.h b/src/frontend-common/sdl_controller_interface.h index 8f5dbc212..fed97c021 100644 --- a/src/frontend-common/sdl_controller_interface.h +++ b/src/frontend-common/sdl_controller_interface.h @@ -25,10 +25,12 @@ public: void ClearBindings() override; // Binding to events. If a binding for this axis/button already exists, returns false. - bool BindControllerAxis(int controller_index, int axis_number, AxisCallback callback) override; + bool BindControllerAxis(int controller_index, int axis_number, AxisSide axis_side, AxisCallback callback) override; bool BindControllerButton(int controller_index, int button_number, ButtonCallback callback) override; bool BindControllerAxisToButton(int controller_index, int axis_number, bool direction, ButtonCallback callback) override; + bool BindControllerHatToButton(int controller_index, int hat_number, std::string_view hat_position, + ButtonCallback callback) override; bool BindControllerButtonToAxis(int controller_index, int button_number, AxisCallback callback) override; // Changing rumble strength. @@ -45,23 +47,25 @@ public: private: struct ControllerData { - void* controller; void* haptic; int haptic_left_right_effect; int joystick_id; int player_id; + bool is_game_controller; float deadzone = 0.25f; - std::array axis_mapping; + // TODO: Turn to vectors to support arbitrary amounts of buttons and axes (for Joysticks) + // Preferably implement a simple "flat map", an ordered view over a vector + std::array, MAX_NUM_AXISES> axis_mapping; std::array button_mapping; std::array, MAX_NUM_AXISES> axis_button_mapping; std::array button_axis_mapping; + std::vector> hat_button_mapping; }; using ControllerDataVector = std::vector; - ControllerDataVector::iterator GetControllerDataForController(void* controller); ControllerDataVector::iterator GetControllerDataForJoystickId(int id); ControllerDataVector::iterator GetControllerDataForPlayerId(int id); int GetFreePlayerId() const; @@ -71,6 +75,11 @@ private: bool HandleControllerAxisEvent(const SDL_Event* event); bool HandleControllerButtonEvent(const SDL_Event* event); + bool OpenJoystick(int index); + bool HandleJoystickAxisEvent(const struct SDL_JoyAxisEvent* event); + bool HandleJoystickButtonEvent(const struct SDL_JoyButtonEvent* event); + bool HandleJoystickHatEvent(const struct SDL_JoyHatEvent* event); + ControllerDataVector m_controllers; std::mutex m_event_intercept_mutex; diff --git a/src/frontend-common/xinput_controller_interface.cpp b/src/frontend-common/xinput_controller_interface.cpp index 575e18361..90f7b6f95 100644 --- a/src/frontend-common/xinput_controller_interface.cpp +++ b/src/frontend-common/xinput_controller_interface.cpp @@ -158,16 +158,11 @@ void XInputControllerInterface::CheckForStateChanges(u32 index, const XINPUT_STA void XInputControllerInterface::ClearBindings() { - for (auto& it : m_controllers) - { - for (AxisCallback& ac : it.axis_mapping) - ac = {}; - for (ButtonCallback& bc : it.button_mapping) - bc = {}; - } + m_controllers.fill({}); } -bool XInputControllerInterface::BindControllerAxis(int controller_index, int axis_number, AxisCallback callback) +bool XInputControllerInterface::BindControllerAxis(int controller_index, int axis_number, AxisSide axis_side, + AxisCallback callback) { if (static_cast(controller_index) >= m_controllers.size() || !m_controllers[controller_index].connected) return false; @@ -175,7 +170,7 @@ bool XInputControllerInterface::BindControllerAxis(int controller_index, int axi if (axis_number < 0 || axis_number >= NUM_AXISES) return false; - m_controllers[controller_index].axis_mapping[axis_number] = std::move(callback); + m_controllers[controller_index].axis_mapping[axis_number][axis_side] = std::move(callback); return true; } @@ -204,6 +199,13 @@ bool XInputControllerInterface::BindControllerAxisToButton(int controller_index, return true; } +bool XInputControllerInterface::BindControllerHatToButton(int controller_index, int hat_number, + std::string_view hat_position, ButtonCallback callback) +{ + // Hats don't exist in XInput + return false; +} + bool XInputControllerInterface::BindControllerButtonToAxis(int controller_index, int button_number, AxisCallback callback) { @@ -226,7 +228,7 @@ bool XInputControllerInterface::HandleAxisEvent(u32 index, Axis axis, s32 value) if (DoEventHook(Hook::Type::Axis, index, static_cast(axis), f_value)) return true; - const AxisCallback& cb = m_controllers[index].axis_mapping[static_cast(axis)]; + const AxisCallback& cb = m_controllers[index].axis_mapping[static_cast(axis)][AxisSide::Full]; if (cb) { cb(f_value); diff --git a/src/frontend-common/xinput_controller_interface.h b/src/frontend-common/xinput_controller_interface.h index 3c1996f84..8927ca3bf 100644 --- a/src/frontend-common/xinput_controller_interface.h +++ b/src/frontend-common/xinput_controller_interface.h @@ -22,10 +22,12 @@ public: void ClearBindings() override; // Binding to events. If a binding for this axis/button already exists, returns false. - bool BindControllerAxis(int controller_index, int axis_number, AxisCallback callback) override; + bool BindControllerAxis(int controller_index, int axis_number, AxisSide axis_side, AxisCallback callback) override; bool BindControllerButton(int controller_index, int button_number, ButtonCallback callback) override; bool BindControllerAxisToButton(int controller_index, int axis_number, bool direction, ButtonCallback callback) override; + bool BindControllerHatToButton(int controller_index, int hat_number, std::string_view hat_position, + ButtonCallback callback) override; bool BindControllerButtonToAxis(int controller_index, int button_number, AxisCallback callback) override; // Changing rumble strength. @@ -61,7 +63,7 @@ private: float deadzone = 0.25f; - std::array axis_mapping; + std::array, MAX_NUM_AXISES> axis_mapping; std::array button_mapping; std::array, MAX_NUM_AXISES> axis_button_mapping; std::array button_axis_mapping;