From 9388c483ecfeb78e07d0a0b037f4884ec39181f7 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Sat, 17 Dec 2022 22:10:33 +1000 Subject: [PATCH] InputManager: Support for inverted bindings (i.e. pedals) --- pcsx2-qt/Settings/InputBindingDialog.cpp | 34 ++++++++++++++++--- pcsx2-qt/Settings/InputBindingDialog.h | 2 ++ pcsx2-qt/Settings/InputBindingWidget.cpp | 35 ++++++++++++++++--- pcsx2-qt/Settings/InputBindingWidget.h | 3 ++ pcsx2-qt/pcsx2-qt.vcxproj | 2 +- pcsx2/Frontend/DInputSource.cpp | 21 ++++++++++-- pcsx2/Frontend/FullscreenUI.cpp | 43 ++++++++++++++++++++---- pcsx2/Frontend/InputManager.cpp | 3 ++ pcsx2/Frontend/InputManager.h | 4 ++- pcsx2/Frontend/SDLInputSource.cpp | 18 ++++------ 10 files changed, 132 insertions(+), 33 deletions(-) diff --git a/pcsx2-qt/Settings/InputBindingDialog.cpp b/pcsx2-qt/Settings/InputBindingDialog.cpp index af336b3416..6cfff55b69 100644 --- a/pcsx2-qt/Settings/InputBindingDialog.cpp +++ b/pcsx2-qt/Settings/InputBindingDialog.cpp @@ -152,6 +152,7 @@ void InputBindingDialog::onInputListenTimerTimeout() void InputBindingDialog::startListeningForInput(u32 timeout_in_seconds) { + m_value_ranges.clear(); m_new_bindings.clear(); m_mouse_mapping_enabled = InputBindingWidget::isMouseMappingEnabled(); m_input_listen_start_position = QCursor::pos(); @@ -266,14 +267,36 @@ void InputBindingDialog::saveListToSettings() void InputBindingDialog::inputManagerHookCallback(InputBindingKey key, float value) { - const float abs_value = std::abs(value); + if (!isListeningForInput()) + return; - for (InputBindingKey other_key : m_new_bindings) + float initial_value = value; + float min_value = value; + auto it = std::find_if(m_value_ranges.begin(), m_value_ranges.end(), [key](const auto& it) { return it.first.bits == key.bits; }); + if (it != m_value_ranges.end()) + { + initial_value = it->second.first; + min_value = it->second.second = std::min(it->second.second, value); + } + else + { + m_value_ranges.emplace_back(key, std::make_pair(initial_value, min_value)); + } + + const float abs_value = std::abs(value); + const bool reverse_threshold = (key.source_subtype == InputSubclass::ControllerAxis && initial_value > 0.5f); + + for (InputBindingKey& other_key : m_new_bindings) { if (other_key.MaskDirection() == key.MaskDirection()) { - if (abs_value < 0.5f) + // for pedals, we wait for it to go back to near its starting point to commit the binding + if ((reverse_threshold ? ((initial_value - value) <= 0.25f) : (abs_value < 0.5f))) { + // did we go the full range? + if (reverse_threshold && initial_value > 0.5f && min_value <= -0.5f) + other_key.modifier = InputModifier::FullAxis; + // if this key is in our new binding list, it's a "release", and we're done addNewBinding(); stopListeningForInput(); @@ -286,10 +309,11 @@ void InputBindingDialog::inputManagerHookCallback(InputBindingKey key, float val } // new binding, add it to the list, but wait for a decent distance first, and then wait for release - if (abs_value >= 0.5f) + if ((reverse_threshold ? (abs_value < 0.5f) : (abs_value >= 0.5f))) { InputBindingKey key_to_add = key; - key_to_add.modifier = value < 0.0f ? InputModifier::Negate : InputModifier::None; + key_to_add.modifier = (value < 0.0f && !reverse_threshold) ? InputModifier::Negate : InputModifier::None; + key_to_add.invert = reverse_threshold; m_new_bindings.push_back(key_to_add); } } diff --git a/pcsx2-qt/Settings/InputBindingDialog.h b/pcsx2-qt/Settings/InputBindingDialog.h index c50dd08033..e0133e98f4 100644 --- a/pcsx2-qt/Settings/InputBindingDialog.h +++ b/pcsx2-qt/Settings/InputBindingDialog.h @@ -20,6 +20,7 @@ #include #include #include +#include #include class SettingsInterface; @@ -68,6 +69,7 @@ protected: std::string m_key_name; std::vector m_bindings; std::vector m_new_bindings; + std::vector>> m_value_ranges; QTimer* m_input_listen_timer = nullptr; u32 m_input_listen_remaining_seconds = 0; diff --git a/pcsx2-qt/Settings/InputBindingWidget.cpp b/pcsx2-qt/Settings/InputBindingWidget.cpp index 4832783710..4b3da62632 100644 --- a/pcsx2-qt/Settings/InputBindingWidget.cpp +++ b/pcsx2-qt/Settings/InputBindingWidget.cpp @@ -300,6 +300,7 @@ void InputBindingWidget::onInputListenTimerTimeout() void InputBindingWidget::startListeningForInput(u32 timeout_in_seconds) { + m_value_ranges.clear(); m_new_bindings.clear(); m_mouse_mapping_enabled = isMouseMappingEnabled(); m_input_listen_start_position = QCursor::pos(); @@ -334,14 +335,36 @@ void InputBindingWidget::stopListeningForInput() void InputBindingWidget::inputManagerHookCallback(InputBindingKey key, float value) { - const float abs_value = std::abs(value); + if (!isListeningForInput()) + return; - for (InputBindingKey other_key : m_new_bindings) + float initial_value = value; + float min_value = value; + auto it = std::find_if(m_value_ranges.begin(), m_value_ranges.end(), [key](const auto& it) { return it.first.bits == key.bits; }); + if (it != m_value_ranges.end()) + { + initial_value = it->second.first; + min_value = it->second.second = std::min(it->second.second, value); + } + else + { + m_value_ranges.emplace_back(key, std::make_pair(initial_value, min_value)); + } + + const float abs_value = std::abs(value); + const bool reverse_threshold = (key.source_subtype == InputSubclass::ControllerAxis && initial_value > 0.5f); + + for (InputBindingKey& other_key : m_new_bindings) { if (other_key.MaskDirection() == key.MaskDirection()) { - if (abs_value < 0.5f) + // for pedals, we wait for it to go back to near its starting point to commit the binding + if ((reverse_threshold ? ((initial_value - value) <= 0.25f) : (abs_value < 0.5f))) { + // did we go the full range? + if (reverse_threshold && initial_value > 0.5f && min_value <= -0.5f) + other_key.modifier = InputModifier::FullAxis; + // if this key is in our new binding list, it's a "release", and we're done setNewBinding(); stopListeningForInput(); @@ -353,11 +376,13 @@ void InputBindingWidget::inputManagerHookCallback(InputBindingKey key, float val } } + // new binding, add it to the list, but wait for a decent distance first, and then wait for release - if (abs_value >= 0.5f) + if ((reverse_threshold ? (abs_value < 0.5f) : (abs_value >= 0.5f))) { InputBindingKey key_to_add = key; - key_to_add.modifier = value < 0.0f ? InputModifier::Negate : InputModifier::None; + key_to_add.modifier = (value < 0.0f && !reverse_threshold) ? InputModifier::Negate : InputModifier::None; + key_to_add.invert = reverse_threshold; m_new_bindings.push_back(key_to_add); } } diff --git a/pcsx2-qt/Settings/InputBindingWidget.h b/pcsx2-qt/Settings/InputBindingWidget.h index 826b75f120..b19ee09202 100644 --- a/pcsx2-qt/Settings/InputBindingWidget.h +++ b/pcsx2-qt/Settings/InputBindingWidget.h @@ -19,6 +19,8 @@ #include "pcsx2/Config.h" #include #include +#include +#include class QTimer; @@ -76,6 +78,7 @@ protected: std::string m_key_name; std::vector m_bindings; std::vector m_new_bindings; + std::vector>> m_value_ranges; QTimer* m_input_listen_timer = nullptr; u32 m_input_listen_remaining_seconds = 0; QPoint m_input_listen_start_position{}; diff --git a/pcsx2-qt/pcsx2-qt.vcxproj b/pcsx2-qt/pcsx2-qt.vcxproj index 54ef848b3c..dd94733a1e 100644 --- a/pcsx2-qt/pcsx2-qt.vcxproj +++ b/pcsx2-qt/pcsx2-qt.vcxproj @@ -51,7 +51,7 @@ Use PrecompiledHeader.h NoExtensions - WIN32_LEAN_AND_MEAN;LZMA_API_STATIC;BUILD_DX=1;ENABLE_RAINTEGRATION;ENABLE_ACHIEVEMENTS;ENABLE_DISCORD_PRESENCE;ENABLE_OPENGL;ENABLE_VULKAN;DIRECTINPUT_VERSION=0x0800;PCSX2_CORE;%(PreprocessorDefinitions) + WIN32_LEAN_AND_MEAN;LZMA_API_STATIC;BUILD_DX=1;ENABLE_RAINTEGRATION;ENABLE_ACHIEVEMENTS;ENABLE_DISCORD_PRESENCE;ENABLE_OPENGL;ENABLE_VULKAN;DIRECTINPUT_VERSION=0x0800;SDL_BUILD;PCSX2_CORE;%(PreprocessorDefinitions) PCSX2_DEBUG;PCSX2_DEVBUILD;_SECURE_SCL_=1;%(PreprocessorDefinitions) PCSX2_DEVEL;PCSX2_DEVBUILD;NDEBUG;_SECURE_SCL_=1;%(PreprocessorDefinitions) NDEBUG;_SECURE_SCL_=0;%(PreprocessorDefinitions) diff --git a/pcsx2/Frontend/DInputSource.cpp b/pcsx2/Frontend/DInputSource.cpp index e71da9fe6d..1233b85449 100644 --- a/pcsx2/Frontend/DInputSource.cpp +++ b/pcsx2/Frontend/DInputSource.cpp @@ -339,13 +339,28 @@ std::optional DInputSource::ParseKeyString(const std::string_vi if (StringUtil::StartsWith(binding, "+Axis") || StringUtil::StartsWith(binding, "-Axis")) { - const std::optional axis_index = StringUtil::FromChars(binding.substr(5)); + std::string_view end; + const std::optional axis_index = StringUtil::FromChars(binding.substr(5), 10, &end); if (!axis_index.has_value()) return std::nullopt; key.source_subtype = InputSubclass::ControllerAxis; key.data = axis_index.value(); key.modifier = (binding[0] == '-') ? InputModifier::Negate : InputModifier::None; + key.invert = (end == "~"); + return key; + } + else if (StringUtil::StartsWith(binding, "FullAxis")) + { + std::string_view end; + const std::optional axis_index = StringUtil::FromChars(binding.substr(8), 10, &end); + if (!axis_index.has_value()) + return std::nullopt; + + key.source_subtype = InputSubclass::ControllerAxis; + key.data = axis_index.value(); + key.modifier = InputModifier::FullAxis; + key.invert = (end == "~"); return key; } else if (StringUtil::StartsWith(binding, "Hat")) @@ -391,8 +406,8 @@ std::string DInputSource::ConvertKeyToString(InputBindingKey key) { if (key.source_subtype == InputSubclass::ControllerAxis) { - ret = - fmt::format("DInput-{}/{}Axis{}", u32(key.source_index), key.modifier == InputModifier::Negate ? '-' : '+', u32(key.data)); + const char* modifier = (key.modifier == InputModifier::FullAxis ? "Full" : (key.modifier == InputModifier::Negate ? "-" : "+")); + ret = fmt::format("DInput-{}/{}Axis{}{}", u32(key.source_index), modifier, u32(key.data), key.invert ? "~" : ""); } else if (key.source_subtype == InputSubclass::ControllerButton && key.data >= MAX_NUM_BUTTONS) { diff --git a/pcsx2/Frontend/FullscreenUI.cpp b/pcsx2/Frontend/FullscreenUI.cpp index f0e4d336e3..e60dce37f4 100644 --- a/pcsx2/Frontend/FullscreenUI.cpp +++ b/pcsx2/Frontend/FullscreenUI.cpp @@ -57,6 +57,8 @@ #include #include #include +#include +#include #ifdef ENABLE_ACHIEVEMENTS #include "Frontend/Achievements.h" @@ -372,6 +374,7 @@ namespace FullscreenUI static std::string s_input_binding_key; static std::string s_input_binding_display_name; static std::vector s_input_binding_new_bindings; + static std::vector>> s_input_binding_value_ranges; static Common::Timer s_input_binding_timer; ////////////////////////////////////////////////////////////////////////// @@ -1266,6 +1269,7 @@ void FullscreenUI::ClearInputBindingVariables() s_input_binding_key = {}; s_input_binding_display_name = {}; s_input_binding_new_bindings = {}; + s_input_binding_value_ranges = {}; } void FullscreenUI::BeginInputBinding(SettingsInterface* bsi, InputBindingInfo::Type type, const std::string_view& section, @@ -1282,23 +1286,47 @@ void FullscreenUI::BeginInputBinding(SettingsInterface* bsi, InputBindingInfo::T s_input_binding_key = key; s_input_binding_display_name = display_name; s_input_binding_new_bindings = {}; + s_input_binding_value_ranges = {}; s_input_binding_timer.Reset(); const bool game_settings = IsEditingGameSettings(bsi); InputManager::SetHook([game_settings](InputBindingKey key, float value) -> InputInterceptHook::CallbackResult { + if (s_input_binding_type == InputBindingInfo::Type::Unknown) + return InputInterceptHook::CallbackResult::StopProcessingEvent; + // holding the settings lock here will protect the input binding list auto lock = Host::GetSettingsLock(); - const float abs_value = std::abs(value); - - for (InputBindingKey other_key : s_input_binding_new_bindings) + float initial_value = value; + float min_value = value; + auto it = std::find_if(s_input_binding_value_ranges.begin(), s_input_binding_value_ranges.end(), + [key](const auto& it) { return it.first.bits == key.bits; }); + if (it != s_input_binding_value_ranges.end()) { + initial_value = it->second.first; + min_value = it->second.second = std::min(it->second.second, value); + } + else + { + s_input_binding_value_ranges.emplace_back(key, std::make_pair(initial_value, min_value)); + } + + const float abs_value = std::abs(value); + const bool reverse_threshold = (key.source_subtype == InputSubclass::ControllerAxis && initial_value > 0.5f); + + for (InputBindingKey& other_key : s_input_binding_new_bindings) + { + // if this key is in our new binding list, it's a "release", and we're done if (other_key.MaskDirection() == key.MaskDirection()) { - if (abs_value < 0.5f) + // for pedals, we wait for it to go back to near its starting point to commit the binding + if ((reverse_threshold ? ((initial_value - value) <= 0.25f) : (abs_value < 0.5f))) { - // if this key is in our new binding list, it's a "release", and we're done + // did we go the full range? + if (reverse_threshold && initial_value > 0.5f && min_value <= -0.5f) + other_key.modifier = InputModifier::FullAxis; + SettingsInterface* bsi = GetEditingSettingsInterface(game_settings); const std::string new_binding(InputManager::ConvertInputBindingKeysToString( s_input_binding_type, s_input_binding_new_bindings.data(), s_input_binding_new_bindings.size())); @@ -1314,10 +1342,11 @@ void FullscreenUI::BeginInputBinding(SettingsInterface* bsi, InputBindingInfo::T } // new binding, add it to the list, but wait for a decent distance first, and then wait for release - if (abs_value >= 0.5f) + if ((reverse_threshold ? (abs_value < 0.5f) : (abs_value >= 0.5f))) { InputBindingKey key_to_add = key; - key_to_add.modifier = (value < 0.0f) ? InputModifier::Negate : InputModifier::None; + key_to_add.modifier = (value < 0.0f && !reverse_threshold) ? InputModifier::Negate : InputModifier::None; + key_to_add.invert = reverse_threshold; s_input_binding_new_bindings.push_back(key_to_add); } diff --git a/pcsx2/Frontend/InputManager.cpp b/pcsx2/Frontend/InputManager.cpp index 4745e2f57b..0875209d4c 100644 --- a/pcsx2/Frontend/InputManager.cpp +++ b/pcsx2/Frontend/InputManager.cpp @@ -834,6 +834,9 @@ bool InputManager::ProcessEvent(InputBindingKey key, float value, bool skip_butt break; } + // handle inverting, needed for some wheels. + value_to_pass = binding->keys[i].invert ? (1.0f - value_to_pass) : value_to_pass; + // axes are fired regardless of a state change, unless they're zero // (but going from not-zero to zero will still fire, because of the full state) // for buttons, we can use the state of the last chord key, because it'll be 1 on press, diff --git a/pcsx2/Frontend/InputManager.h b/pcsx2/Frontend/InputManager.h index 5cc9dbf116..2feb6364e5 100644 --- a/pcsx2/Frontend/InputManager.h +++ b/pcsx2/Frontend/InputManager.h @@ -74,7 +74,8 @@ union InputBindingKey u32 source_index : 8; ///< controller number InputSubclass source_subtype : 2; ///< if 1, binding is for an axis and not a button (used for controllers) InputModifier modifier : 2; - u32 unused : 16; + u32 invert : 1; ///< if 1, value is inverted prior to being sent to the sink + u32 unused : 15; u32 data; }; @@ -90,6 +91,7 @@ union InputBindingKey InputBindingKey r; r.bits = bits; r.modifier = InputModifier::None; + r.invert = 0; return r; } }; diff --git a/pcsx2/Frontend/SDLInputSource.cpp b/pcsx2/Frontend/SDLInputSource.cpp index 9e3b4d4023..00dbe81c0e 100644 --- a/pcsx2/Frontend/SDLInputSource.cpp +++ b/pcsx2/Frontend/SDLInputSource.cpp @@ -265,11 +265,13 @@ std::optional SDLInputSource::ParseKeyString(const std::string_ if (StringUtil::StartsWith(axis_name, "Axis")) { - if (auto value = StringUtil::FromChars(axis_name.substr(4))) + std::string_view end; + if (auto value = StringUtil::FromChars(axis_name.substr(4), 10, &end)) { key.source_subtype = InputSubclass::ControllerAxis; key.data = *value; key.modifier = (binding[0] == '-') ? InputModifier::Negate : InputModifier::None; + key.invert = (end == "~"); return key; } } @@ -348,17 +350,11 @@ std::string SDLInputSource::ConvertKeyToString(InputBindingKey key) { if (key.source_subtype == InputSubclass::ControllerAxis) { - const char* modifier = key.modifier == InputModifier::Negate ? "-" : "+"; + const char* modifier = (key.modifier == InputModifier::FullAxis ? "Full" : (key.modifier == InputModifier::Negate ? "-" : "+")); if (key.data < std::size(s_sdl_axis_names)) - { ret = StringUtil::StdStringFromFormat("SDL-%u/%s%s", key.source_index, modifier, s_sdl_axis_names[key.data]); - } else - { - if (key.modifier == InputModifier::FullAxis) - modifier = "Full"; - ret = StringUtil::StdStringFromFormat("SDL-%u/%sAxis%u", key.source_index, modifier, key.data); - } + ret = StringUtil::StdStringFromFormat("SDL-%u/%sAxis%u%s", key.source_index, modifier, key.data, key.invert ? "~" : ""); } else if (key.source_subtype == InputSubclass::ControllerButton) { @@ -685,7 +681,7 @@ bool SDLInputSource::HandleJoystickButtonEvent(const SDL_JoyButtonEvent* ev) if (ev->button < it->joy_button_used_in_gc.size() && it->joy_button_used_in_gc[ev->button]) return false; // Will get handled by GC event const u32 button = ev->button + std::size(s_sdl_button_names); // Ensure we don't conflict with GC buttons - const InputBindingKey key(MakeGenericControllerAxisKey(InputSourceType::SDL, it->player_id, button)); + const InputBindingKey key(MakeGenericControllerButtonKey(InputSourceType::SDL, it->player_id, button)); InputManager::InvokeEvents(key, (ev->state == SDL_PRESSED) ? 1.0f : 0.0f); return true; } @@ -797,7 +793,7 @@ bool SDLInputSource::GetGenericBindingMapping(const std::string_view& device, In } else { - // joysticks, which we haven't implemented yet anyway. + // joysticks have arbitrary axis numbers, so automapping isn't going to work here. return false; } }