InputManager: Support for inverted bindings (i.e. pedals)

This commit is contained in:
Connor McLaughlin 2022-12-17 22:10:33 +10:00 committed by refractionpcsx2
parent 7cbede9190
commit 9388c483ec
10 changed files with 132 additions and 33 deletions

View File

@ -152,6 +152,7 @@ void InputBindingDialog::onInputListenTimerTimeout()
void InputBindingDialog::startListeningForInput(u32 timeout_in_seconds) void InputBindingDialog::startListeningForInput(u32 timeout_in_seconds)
{ {
m_value_ranges.clear();
m_new_bindings.clear(); m_new_bindings.clear();
m_mouse_mapping_enabled = InputBindingWidget::isMouseMappingEnabled(); m_mouse_mapping_enabled = InputBindingWidget::isMouseMappingEnabled();
m_input_listen_start_position = QCursor::pos(); m_input_listen_start_position = QCursor::pos();
@ -266,14 +267,36 @@ void InputBindingDialog::saveListToSettings()
void InputBindingDialog::inputManagerHookCallback(InputBindingKey key, float value) 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 (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 // if this key is in our new binding list, it's a "release", and we're done
addNewBinding(); addNewBinding();
stopListeningForInput(); 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 // 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; 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); m_new_bindings.push_back(key_to_add);
} }
} }

View File

@ -20,6 +20,7 @@
#include <QtWidgets/QDialog> #include <QtWidgets/QDialog>
#include <optional> #include <optional>
#include <string> #include <string>
#include <utility>
#include <vector> #include <vector>
class SettingsInterface; class SettingsInterface;
@ -68,6 +69,7 @@ protected:
std::string m_key_name; std::string m_key_name;
std::vector<std::string> m_bindings; std::vector<std::string> m_bindings;
std::vector<InputBindingKey> m_new_bindings; std::vector<InputBindingKey> m_new_bindings;
std::vector<std::pair<InputBindingKey, std::pair<float, float>>> m_value_ranges;
QTimer* m_input_listen_timer = nullptr; QTimer* m_input_listen_timer = nullptr;
u32 m_input_listen_remaining_seconds = 0; u32 m_input_listen_remaining_seconds = 0;

View File

@ -300,6 +300,7 @@ void InputBindingWidget::onInputListenTimerTimeout()
void InputBindingWidget::startListeningForInput(u32 timeout_in_seconds) void InputBindingWidget::startListeningForInput(u32 timeout_in_seconds)
{ {
m_value_ranges.clear();
m_new_bindings.clear(); m_new_bindings.clear();
m_mouse_mapping_enabled = isMouseMappingEnabled(); m_mouse_mapping_enabled = isMouseMappingEnabled();
m_input_listen_start_position = QCursor::pos(); m_input_listen_start_position = QCursor::pos();
@ -334,14 +335,36 @@ void InputBindingWidget::stopListeningForInput()
void InputBindingWidget::inputManagerHookCallback(InputBindingKey key, float value) 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 (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 // if this key is in our new binding list, it's a "release", and we're done
setNewBinding(); setNewBinding();
stopListeningForInput(); 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 // 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; 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); m_new_bindings.push_back(key_to_add);
} }
} }

View File

@ -19,6 +19,8 @@
#include "pcsx2/Config.h" #include "pcsx2/Config.h"
#include <QtWidgets/QPushButton> #include <QtWidgets/QPushButton>
#include <optional> #include <optional>
#include <utility>
#include <vector>
class QTimer; class QTimer;
@ -76,6 +78,7 @@ protected:
std::string m_key_name; std::string m_key_name;
std::vector<std::string> m_bindings; std::vector<std::string> m_bindings;
std::vector<InputBindingKey> m_new_bindings; std::vector<InputBindingKey> m_new_bindings;
std::vector<std::pair<InputBindingKey, std::pair<float, float>>> m_value_ranges;
QTimer* m_input_listen_timer = nullptr; QTimer* m_input_listen_timer = nullptr;
u32 m_input_listen_remaining_seconds = 0; u32 m_input_listen_remaining_seconds = 0;
QPoint m_input_listen_start_position{}; QPoint m_input_listen_start_position{};

View File

@ -51,7 +51,7 @@
<PrecompiledHeader>Use</PrecompiledHeader> <PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>PrecompiledHeader.h</PrecompiledHeaderFile> <PrecompiledHeaderFile>PrecompiledHeader.h</PrecompiledHeaderFile>
<EnableEnhancedInstructionSet>NoExtensions</EnableEnhancedInstructionSet> <EnableEnhancedInstructionSet>NoExtensions</EnableEnhancedInstructionSet>
<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;PCSX2_CORE;%(PreprocessorDefinitions)</PreprocessorDefinitions> <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)</PreprocessorDefinitions>
<PreprocessorDefinitions Condition="$(Configuration.Contains(Debug))">PCSX2_DEBUG;PCSX2_DEVBUILD;_SECURE_SCL_=1;%(PreprocessorDefinitions)</PreprocessorDefinitions> <PreprocessorDefinitions Condition="$(Configuration.Contains(Debug))">PCSX2_DEBUG;PCSX2_DEVBUILD;_SECURE_SCL_=1;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PreprocessorDefinitions Condition="$(Configuration.Contains(Devel))">PCSX2_DEVEL;PCSX2_DEVBUILD;NDEBUG;_SECURE_SCL_=1;%(PreprocessorDefinitions)</PreprocessorDefinitions> <PreprocessorDefinitions Condition="$(Configuration.Contains(Devel))">PCSX2_DEVEL;PCSX2_DEVBUILD;NDEBUG;_SECURE_SCL_=1;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PreprocessorDefinitions Condition="$(Configuration.Contains(Release))">NDEBUG;_SECURE_SCL_=0;%(PreprocessorDefinitions)</PreprocessorDefinitions> <PreprocessorDefinitions Condition="$(Configuration.Contains(Release))">NDEBUG;_SECURE_SCL_=0;%(PreprocessorDefinitions)</PreprocessorDefinitions>

View File

@ -339,13 +339,28 @@ std::optional<InputBindingKey> DInputSource::ParseKeyString(const std::string_vi
if (StringUtil::StartsWith(binding, "+Axis") || StringUtil::StartsWith(binding, "-Axis")) if (StringUtil::StartsWith(binding, "+Axis") || StringUtil::StartsWith(binding, "-Axis"))
{ {
const std::optional<u32> axis_index = StringUtil::FromChars<u32>(binding.substr(5)); std::string_view end;
const std::optional<u32> axis_index = StringUtil::FromChars<u32>(binding.substr(5), 10, &end);
if (!axis_index.has_value()) if (!axis_index.has_value())
return std::nullopt; return std::nullopt;
key.source_subtype = InputSubclass::ControllerAxis; key.source_subtype = InputSubclass::ControllerAxis;
key.data = axis_index.value(); key.data = axis_index.value();
key.modifier = (binding[0] == '-') ? InputModifier::Negate : InputModifier::None; 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<u32> axis_index = StringUtil::FromChars<u32>(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; return key;
} }
else if (StringUtil::StartsWith(binding, "Hat")) else if (StringUtil::StartsWith(binding, "Hat"))
@ -391,8 +406,8 @@ std::string DInputSource::ConvertKeyToString(InputBindingKey key)
{ {
if (key.source_subtype == InputSubclass::ControllerAxis) if (key.source_subtype == InputSubclass::ControllerAxis)
{ {
ret = const char* modifier = (key.modifier == InputModifier::FullAxis ? "Full" : (key.modifier == InputModifier::Negate ? "-" : "+"));
fmt::format("DInput-{}/{}Axis{}", u32(key.source_index), key.modifier == InputModifier::Negate ? '-' : '+', u32(key.data)); 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) else if (key.source_subtype == InputSubclass::ControllerButton && key.data >= MAX_NUM_BUTTONS)
{ {

View File

@ -57,6 +57,8 @@
#include <array> #include <array>
#include <bitset> #include <bitset>
#include <thread> #include <thread>
#include <utility>
#include <vector>
#ifdef ENABLE_ACHIEVEMENTS #ifdef ENABLE_ACHIEVEMENTS
#include "Frontend/Achievements.h" #include "Frontend/Achievements.h"
@ -372,6 +374,7 @@ namespace FullscreenUI
static std::string s_input_binding_key; static std::string s_input_binding_key;
static std::string s_input_binding_display_name; static std::string s_input_binding_display_name;
static std::vector<InputBindingKey> s_input_binding_new_bindings; static std::vector<InputBindingKey> s_input_binding_new_bindings;
static std::vector<std::pair<InputBindingKey, std::pair<float, float>>> s_input_binding_value_ranges;
static Common::Timer s_input_binding_timer; static Common::Timer s_input_binding_timer;
////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////
@ -1266,6 +1269,7 @@ void FullscreenUI::ClearInputBindingVariables()
s_input_binding_key = {}; s_input_binding_key = {};
s_input_binding_display_name = {}; s_input_binding_display_name = {};
s_input_binding_new_bindings = {}; s_input_binding_new_bindings = {};
s_input_binding_value_ranges = {};
} }
void FullscreenUI::BeginInputBinding(SettingsInterface* bsi, InputBindingInfo::Type type, const std::string_view& section, 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_key = key;
s_input_binding_display_name = display_name; s_input_binding_display_name = display_name;
s_input_binding_new_bindings = {}; s_input_binding_new_bindings = {};
s_input_binding_value_ranges = {};
s_input_binding_timer.Reset(); s_input_binding_timer.Reset();
const bool game_settings = IsEditingGameSettings(bsi); const bool game_settings = IsEditingGameSettings(bsi);
InputManager::SetHook([game_settings](InputBindingKey key, float value) -> InputInterceptHook::CallbackResult { 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 // holding the settings lock here will protect the input binding list
auto lock = Host::GetSettingsLock(); auto lock = Host::GetSettingsLock();
const float abs_value = std::abs(value); float initial_value = value;
float min_value = value;
for (InputBindingKey other_key : s_input_binding_new_bindings) 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 (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); SettingsInterface* bsi = GetEditingSettingsInterface(game_settings);
const std::string new_binding(InputManager::ConvertInputBindingKeysToString( const std::string new_binding(InputManager::ConvertInputBindingKeysToString(
s_input_binding_type, s_input_binding_new_bindings.data(), s_input_binding_new_bindings.size())); 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 // 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; 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); s_input_binding_new_bindings.push_back(key_to_add);
} }

View File

@ -834,6 +834,9 @@ bool InputManager::ProcessEvent(InputBindingKey key, float value, bool skip_butt
break; 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 // 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) // (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, // for buttons, we can use the state of the last chord key, because it'll be 1 on press,

View File

@ -74,7 +74,8 @@ union InputBindingKey
u32 source_index : 8; ///< controller number u32 source_index : 8; ///< controller number
InputSubclass source_subtype : 2; ///< if 1, binding is for an axis and not a button (used for controllers) InputSubclass source_subtype : 2; ///< if 1, binding is for an axis and not a button (used for controllers)
InputModifier modifier : 2; 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; u32 data;
}; };
@ -90,6 +91,7 @@ union InputBindingKey
InputBindingKey r; InputBindingKey r;
r.bits = bits; r.bits = bits;
r.modifier = InputModifier::None; r.modifier = InputModifier::None;
r.invert = 0;
return r; return r;
} }
}; };

View File

@ -265,11 +265,13 @@ std::optional<InputBindingKey> SDLInputSource::ParseKeyString(const std::string_
if (StringUtil::StartsWith(axis_name, "Axis")) if (StringUtil::StartsWith(axis_name, "Axis"))
{ {
if (auto value = StringUtil::FromChars<u32>(axis_name.substr(4))) std::string_view end;
if (auto value = StringUtil::FromChars<u32>(axis_name.substr(4), 10, &end))
{ {
key.source_subtype = InputSubclass::ControllerAxis; key.source_subtype = InputSubclass::ControllerAxis;
key.data = *value; key.data = *value;
key.modifier = (binding[0] == '-') ? InputModifier::Negate : InputModifier::None; key.modifier = (binding[0] == '-') ? InputModifier::Negate : InputModifier::None;
key.invert = (end == "~");
return key; return key;
} }
} }
@ -348,17 +350,11 @@ std::string SDLInputSource::ConvertKeyToString(InputBindingKey key)
{ {
if (key.source_subtype == InputSubclass::ControllerAxis) 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)) 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]); ret = StringUtil::StdStringFromFormat("SDL-%u/%s%s", key.source_index, modifier, s_sdl_axis_names[key.data]);
}
else else
{ ret = StringUtil::StdStringFromFormat("SDL-%u/%sAxis%u%s", key.source_index, modifier, key.data, key.invert ? "~" : "");
if (key.modifier == InputModifier::FullAxis)
modifier = "Full";
ret = StringUtil::StdStringFromFormat("SDL-%u/%sAxis%u", key.source_index, modifier, key.data);
}
} }
else if (key.source_subtype == InputSubclass::ControllerButton) 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]) 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 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 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); InputManager::InvokeEvents(key, (ev->state == SDL_PRESSED) ? 1.0f : 0.0f);
return true; return true;
} }
@ -797,7 +793,7 @@ bool SDLInputSource::GetGenericBindingMapping(const std::string_view& device, In
} }
else else
{ {
// joysticks, which we haven't implemented yet anyway. // joysticks have arbitrary axis numbers, so automapping isn't going to work here.
return false; return false;
} }
} }