2023-11-26 11:34:00 +00:00
|
|
|
// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin <stenzek@gmail.com>
|
2022-12-04 11:03:45 +00:00
|
|
|
// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
|
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
#include "xinput_source.h"
|
2023-11-26 11:34:00 +00:00
|
|
|
#include "input_manager.h"
|
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
#include "common/assert.h"
|
|
|
|
#include "common/log.h"
|
|
|
|
#include "common/string_util.h"
|
2023-11-26 11:34:00 +00:00
|
|
|
|
|
|
|
#include "IconsPromptFont.h"
|
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
#include <cmath>
|
2023-11-26 11:34:00 +00:00
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
Log_SetChannel(XInputSource);
|
|
|
|
|
2023-11-26 11:34:00 +00:00
|
|
|
static const char* s_axis_names[XInputSource::NUM_AXES] = {
|
2022-07-11 13:03:29 +00:00
|
|
|
"LeftX", // AXIS_LEFTX
|
|
|
|
"LeftY", // AXIS_LEFTY
|
|
|
|
"RightX", // AXIS_RIGHTX
|
|
|
|
"RightY", // AXIS_RIGHTY
|
|
|
|
"LeftTrigger", // AXIS_TRIGGERLEFT
|
|
|
|
"RightTrigger", // AXIS_TRIGGERRIGHT
|
|
|
|
};
|
2023-11-26 11:34:00 +00:00
|
|
|
static constexpr const char* s_axis_icons[][2] = {
|
|
|
|
{ICON_PF_LEFT_ANALOG_LEFT, ICON_PF_LEFT_ANALOG_RIGHT}, // AXIS_LEFTX
|
|
|
|
{ICON_PF_LEFT_ANALOG_UP, ICON_PF_LEFT_ANALOG_DOWN}, // AXIS_LEFTY
|
|
|
|
{ICON_PF_RIGHT_ANALOG_LEFT, ICON_PF_RIGHT_ANALOG_RIGHT}, // AXIS_RIGHTX
|
|
|
|
{ICON_PF_RIGHT_ANALOG_UP, ICON_PF_RIGHT_ANALOG_DOWN}, // AXIS_RIGHTY
|
|
|
|
{nullptr, ICON_PF_LEFT_TRIGGER_PULL}, // AXIS_TRIGGERLEFT
|
|
|
|
{nullptr, ICON_PF_RIGHT_TRIGGER_PULL}, // AXIS_TRIGGERRIGHT
|
|
|
|
};
|
2022-07-11 13:03:29 +00:00
|
|
|
static const GenericInputBinding s_xinput_generic_binding_axis_mapping[][2] = {
|
|
|
|
{GenericInputBinding::LeftStickLeft, GenericInputBinding::LeftStickRight}, // AXIS_LEFTX
|
|
|
|
{GenericInputBinding::LeftStickUp, GenericInputBinding::LeftStickDown}, // AXIS_LEFTY
|
|
|
|
{GenericInputBinding::RightStickLeft, GenericInputBinding::RightStickRight}, // AXIS_RIGHTX
|
|
|
|
{GenericInputBinding::RightStickUp, GenericInputBinding::RightStickDown}, // AXIS_RIGHTY
|
|
|
|
{GenericInputBinding::Unknown, GenericInputBinding::L2}, // AXIS_TRIGGERLEFT
|
|
|
|
{GenericInputBinding::Unknown, GenericInputBinding::R2}, // AXIS_TRIGGERRIGHT
|
|
|
|
};
|
|
|
|
|
2023-11-26 11:34:00 +00:00
|
|
|
static const char* s_button_names[XInputSource::NUM_BUTTONS] = {
|
2022-07-11 13:03:29 +00:00
|
|
|
"DPadUp", // XINPUT_GAMEPAD_DPAD_UP
|
|
|
|
"DPadDown", // XINPUT_GAMEPAD_DPAD_DOWN
|
|
|
|
"DPadLeft", // XINPUT_GAMEPAD_DPAD_LEFT
|
|
|
|
"DPadRight", // XINPUT_GAMEPAD_DPAD_RIGHT
|
|
|
|
"Start", // XINPUT_GAMEPAD_START
|
|
|
|
"Back", // XINPUT_GAMEPAD_BACK
|
|
|
|
"LeftStick", // XINPUT_GAMEPAD_LEFT_THUMB
|
|
|
|
"RightStick", // XINPUT_GAMEPAD_RIGHT_THUMB
|
|
|
|
"LeftShoulder", // XINPUT_GAMEPAD_LEFT_SHOULDER
|
|
|
|
"RightShoulder", // XINPUT_GAMEPAD_RIGHT_SHOULDER
|
|
|
|
"A", // XINPUT_GAMEPAD_A
|
|
|
|
"B", // XINPUT_GAMEPAD_B
|
|
|
|
"X", // XINPUT_GAMEPAD_X
|
|
|
|
"Y", // XINPUT_GAMEPAD_Y
|
|
|
|
"Guide", // XINPUT_GAMEPAD_GUIDE
|
|
|
|
};
|
2023-11-26 11:34:00 +00:00
|
|
|
static const u16 s_button_masks[XInputSource::NUM_BUTTONS] = {
|
2022-07-11 13:03:29 +00:00
|
|
|
XINPUT_GAMEPAD_DPAD_UP,
|
|
|
|
XINPUT_GAMEPAD_DPAD_DOWN,
|
|
|
|
XINPUT_GAMEPAD_DPAD_LEFT,
|
|
|
|
XINPUT_GAMEPAD_DPAD_RIGHT,
|
|
|
|
XINPUT_GAMEPAD_START,
|
|
|
|
XINPUT_GAMEPAD_BACK,
|
|
|
|
XINPUT_GAMEPAD_LEFT_THUMB,
|
|
|
|
XINPUT_GAMEPAD_RIGHT_THUMB,
|
|
|
|
XINPUT_GAMEPAD_LEFT_SHOULDER,
|
|
|
|
XINPUT_GAMEPAD_RIGHT_SHOULDER,
|
|
|
|
XINPUT_GAMEPAD_A,
|
|
|
|
XINPUT_GAMEPAD_B,
|
|
|
|
XINPUT_GAMEPAD_X,
|
|
|
|
XINPUT_GAMEPAD_Y,
|
|
|
|
0x400, // XINPUT_GAMEPAD_GUIDE
|
|
|
|
};
|
2023-11-26 11:34:00 +00:00
|
|
|
static constexpr const char* s_button_icons[] = {
|
|
|
|
ICON_PF_XBOX_DPAD_UP, // XINPUT_GAMEPAD_DPAD_UP
|
|
|
|
ICON_PF_XBOX_DPAD_DOWN, // XINPUT_GAMEPAD_DPAD_DOWN
|
|
|
|
ICON_PF_XBOX_DPAD_LEFT, // XINPUT_GAMEPAD_DPAD_LEFT
|
|
|
|
ICON_PF_XBOX_DPAD_RIGHT, // XINPUT_GAMEPAD_DPAD_RIGHT
|
|
|
|
ICON_PF_BURGER_MENU, // XINPUT_GAMEPAD_START
|
|
|
|
ICON_PF_SHARE_CAPTURE, // XINPUT_GAMEPAD_BACK
|
|
|
|
ICON_PF_LEFT_ANALOG_CLICK, // XINPUT_GAMEPAD_LEFT_THUMB
|
|
|
|
ICON_PF_RIGHT_ANALOG_CLICK, // XINPUT_GAMEPAD_RIGHT_THUMB
|
|
|
|
ICON_PF_LEFT_SHOULDER_LB, // XINPUT_GAMEPAD_LEFT_SHOULDER
|
|
|
|
ICON_PF_RIGHT_SHOULDER_RB, // XINPUT_GAMEPAD_RIGHT_SHOULDER
|
|
|
|
ICON_PF_BUTTON_A, // XINPUT_GAMEPAD_A
|
|
|
|
ICON_PF_BUTTON_B, // XINPUT_GAMEPAD_B
|
|
|
|
ICON_PF_BUTTON_X, // XINPUT_GAMEPAD_X
|
|
|
|
ICON_PF_BUTTON_Y, // XINPUT_GAMEPAD_Y
|
|
|
|
ICON_PF_XBOX, // XINPUT_GAMEPAD_GUIDE
|
|
|
|
};
|
2022-07-11 13:03:29 +00:00
|
|
|
static const GenericInputBinding s_xinput_generic_binding_button_mapping[] = {
|
|
|
|
GenericInputBinding::DPadUp, // XINPUT_GAMEPAD_DPAD_UP
|
|
|
|
GenericInputBinding::DPadDown, // XINPUT_GAMEPAD_DPAD_DOWN
|
|
|
|
GenericInputBinding::DPadLeft, // XINPUT_GAMEPAD_DPAD_LEFT
|
|
|
|
GenericInputBinding::DPadRight, // XINPUT_GAMEPAD_DPAD_RIGHT
|
|
|
|
GenericInputBinding::Start, // XINPUT_GAMEPAD_START
|
|
|
|
GenericInputBinding::Select, // XINPUT_GAMEPAD_BACK
|
|
|
|
GenericInputBinding::L3, // XINPUT_GAMEPAD_LEFT_THUMB
|
|
|
|
GenericInputBinding::R3, // XINPUT_GAMEPAD_RIGHT_THUMB
|
|
|
|
GenericInputBinding::L1, // XINPUT_GAMEPAD_LEFT_SHOULDER
|
|
|
|
GenericInputBinding::R1, // XINPUT_GAMEPAD_RIGHT_SHOULDER
|
|
|
|
GenericInputBinding::Cross, // XINPUT_GAMEPAD_A
|
|
|
|
GenericInputBinding::Circle, // XINPUT_GAMEPAD_B
|
|
|
|
GenericInputBinding::Square, // XINPUT_GAMEPAD_X
|
|
|
|
GenericInputBinding::Triangle, // XINPUT_GAMEPAD_Y
|
|
|
|
GenericInputBinding::System, // XINPUT_GAMEPAD_GUIDE
|
|
|
|
};
|
|
|
|
|
|
|
|
XInputSource::XInputSource() = default;
|
|
|
|
|
|
|
|
XInputSource::~XInputSource() = default;
|
|
|
|
|
|
|
|
bool XInputSource::Initialize(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock)
|
|
|
|
{
|
|
|
|
// xinput1_3.dll is flawed and obsolete, but it's also commonly used by wrappers.
|
|
|
|
// For this reason, try to load it *only* from the application directory, and not system32.
|
|
|
|
m_xinput_module = LoadLibraryExW(L"xinput1_3", nullptr, LOAD_LIBRARY_SEARCH_APPLICATION_DIR);
|
|
|
|
if (!m_xinput_module)
|
|
|
|
{
|
|
|
|
m_xinput_module = LoadLibraryW(L"xinput1_4");
|
|
|
|
}
|
|
|
|
if (!m_xinput_module)
|
|
|
|
{
|
|
|
|
m_xinput_module = LoadLibraryW(L"xinput9_1_0");
|
|
|
|
}
|
|
|
|
if (!m_xinput_module)
|
|
|
|
{
|
|
|
|
Log_ErrorPrintf("Failed to load XInput module.");
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Try the hidden version of XInputGetState(), which lets us query the guide button.
|
|
|
|
m_xinput_get_state =
|
|
|
|
reinterpret_cast<decltype(m_xinput_get_state)>(GetProcAddress(m_xinput_module, reinterpret_cast<LPCSTR>(100)));
|
|
|
|
if (!m_xinput_get_state)
|
2023-09-03 04:30:26 +00:00
|
|
|
{
|
|
|
|
m_xinput_get_state =
|
|
|
|
reinterpret_cast<decltype(m_xinput_get_state)>(GetProcAddress(m_xinput_module, "XInputGetState"));
|
|
|
|
}
|
2022-07-11 13:03:29 +00:00
|
|
|
m_xinput_set_state =
|
|
|
|
reinterpret_cast<decltype(m_xinput_set_state)>(GetProcAddress(m_xinput_module, "XInputSetState"));
|
|
|
|
m_xinput_get_capabilities =
|
|
|
|
reinterpret_cast<decltype(m_xinput_get_capabilities)>(GetProcAddress(m_xinput_module, "XInputGetCapabilities"));
|
2022-11-03 03:12:46 +00:00
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
if (!m_xinput_get_state || !m_xinput_set_state || !m_xinput_get_capabilities)
|
|
|
|
{
|
|
|
|
Log_ErrorPrintf("Failed to get XInput function pointers.");
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2022-11-03 03:12:55 +00:00
|
|
|
ReloadDevices();
|
2022-07-11 13:03:29 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2023-09-03 04:30:26 +00:00
|
|
|
void XInputSource::UpdateSettings(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock)
|
|
|
|
{
|
|
|
|
}
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2022-10-23 04:09:54 +00:00
|
|
|
bool XInputSource::ReloadDevices()
|
|
|
|
{
|
|
|
|
bool changed = false;
|
|
|
|
for (u32 i = 0; i < NUM_CONTROLLERS; i++)
|
|
|
|
{
|
|
|
|
XINPUT_STATE new_state;
|
|
|
|
DWORD result = m_xinput_get_state(i, &new_state);
|
|
|
|
|
|
|
|
if (result == ERROR_SUCCESS)
|
|
|
|
{
|
|
|
|
if (m_controllers[i].connected)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
HandleControllerConnection(i);
|
|
|
|
changed = true;
|
|
|
|
}
|
|
|
|
else if (result == ERROR_DEVICE_NOT_CONNECTED)
|
|
|
|
{
|
|
|
|
if (!m_controllers[i].connected)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
HandleControllerDisconnection(i);
|
|
|
|
changed = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return changed;
|
|
|
|
}
|
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
void XInputSource::Shutdown()
|
|
|
|
{
|
|
|
|
for (u32 i = 0; i < NUM_CONTROLLERS; i++)
|
|
|
|
{
|
|
|
|
if (m_controllers[i].connected)
|
|
|
|
HandleControllerDisconnection(i);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (m_xinput_module)
|
|
|
|
{
|
|
|
|
FreeLibrary(m_xinput_module);
|
|
|
|
m_xinput_module = nullptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
m_xinput_get_state = nullptr;
|
|
|
|
m_xinput_set_state = nullptr;
|
|
|
|
m_xinput_get_capabilities = nullptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
void XInputSource::PollEvents()
|
|
|
|
{
|
|
|
|
for (u32 i = 0; i < NUM_CONTROLLERS; i++)
|
|
|
|
{
|
|
|
|
const bool was_connected = m_controllers[i].connected;
|
2022-10-23 04:09:54 +00:00
|
|
|
if (!was_connected)
|
|
|
|
continue;
|
2022-07-11 13:03:29 +00:00
|
|
|
|
|
|
|
XINPUT_STATE new_state;
|
|
|
|
DWORD result = m_xinput_get_state(i, &new_state);
|
|
|
|
|
|
|
|
if (result == ERROR_SUCCESS)
|
|
|
|
{
|
|
|
|
if (!was_connected)
|
|
|
|
HandleControllerConnection(i);
|
|
|
|
|
|
|
|
CheckForStateChanges(i, new_state);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
if (result != ERROR_DEVICE_NOT_CONNECTED)
|
|
|
|
Log_WarningPrintf("XInputGetState(%u) failed: 0x%08X / 0x%08X", i, result, GetLastError());
|
|
|
|
|
|
|
|
if (was_connected)
|
|
|
|
HandleControllerDisconnection(i);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
std::vector<std::pair<std::string, std::string>> XInputSource::EnumerateDevices()
|
|
|
|
{
|
|
|
|
std::vector<std::pair<std::string, std::string>> ret;
|
|
|
|
|
|
|
|
for (u32 i = 0; i < NUM_CONTROLLERS; i++)
|
|
|
|
{
|
|
|
|
if (!m_controllers[i].connected)
|
|
|
|
continue;
|
|
|
|
|
2023-09-20 14:32:39 +00:00
|
|
|
ret.emplace_back(fmt::format("XInput-{}", i), fmt::format("XInput Controller {}", i));
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
std::optional<InputBindingKey> XInputSource::ParseKeyString(const std::string_view& device,
|
|
|
|
const std::string_view& binding)
|
|
|
|
{
|
|
|
|
if (!StringUtil::StartsWith(device, "XInput-") || binding.empty())
|
|
|
|
return std::nullopt;
|
|
|
|
|
|
|
|
const std::optional<s32> player_id = StringUtil::FromChars<s32>(device.substr(7));
|
|
|
|
if (!player_id.has_value() || player_id.value() < 0)
|
|
|
|
return std::nullopt;
|
|
|
|
|
|
|
|
InputBindingKey key = {};
|
|
|
|
key.source_type = InputSourceType::XInput;
|
|
|
|
key.source_index = static_cast<u32>(player_id.value());
|
|
|
|
|
|
|
|
if (StringUtil::EndsWith(binding, "Motor"))
|
|
|
|
{
|
|
|
|
key.source_subtype = InputSubclass::ControllerMotor;
|
|
|
|
if (binding == "LargeMotor")
|
|
|
|
{
|
|
|
|
key.data = 0;
|
|
|
|
return key;
|
|
|
|
}
|
|
|
|
else if (binding == "SmallMotor")
|
|
|
|
{
|
|
|
|
key.data = 1;
|
|
|
|
return key;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
return std::nullopt;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else if (binding[0] == '+' || binding[0] == '-')
|
|
|
|
{
|
|
|
|
// likely an axis
|
|
|
|
const std::string_view axis_name(binding.substr(1));
|
|
|
|
for (u32 i = 0; i < std::size(s_axis_names); i++)
|
|
|
|
{
|
|
|
|
if (axis_name == s_axis_names[i])
|
|
|
|
{
|
|
|
|
// found an axis!
|
|
|
|
key.source_subtype = InputSubclass::ControllerAxis;
|
|
|
|
key.data = i;
|
2023-01-15 04:00:51 +00:00
|
|
|
key.modifier = binding[0] == '-' ? InputModifier::Negate : InputModifier::None;
|
2022-07-11 13:03:29 +00:00
|
|
|
return key;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// must be a button
|
|
|
|
for (u32 i = 0; i < std::size(s_button_names); i++)
|
|
|
|
{
|
|
|
|
if (binding == s_button_names[i])
|
|
|
|
{
|
|
|
|
key.source_subtype = InputSubclass::ControllerButton;
|
|
|
|
key.data = i;
|
|
|
|
return key;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// unknown axis/button
|
|
|
|
return std::nullopt;
|
|
|
|
}
|
|
|
|
|
2023-11-26 11:34:00 +00:00
|
|
|
TinyString XInputSource::ConvertKeyToString(InputBindingKey key)
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-11-26 11:34:00 +00:00
|
|
|
TinyString ret;
|
2022-07-11 13:03:29 +00:00
|
|
|
|
|
|
|
if (key.source_type == InputSourceType::XInput)
|
|
|
|
{
|
|
|
|
if (key.source_subtype == InputSubclass::ControllerAxis && key.data < std::size(s_axis_names))
|
|
|
|
{
|
2023-01-15 04:00:51 +00:00
|
|
|
const char modifier = key.modifier == InputModifier::Negate ? '-' : '+';
|
2023-12-13 11:06:15 +00:00
|
|
|
ret.format("XInput-{}/{}{}", static_cast<u32>(key.source_index), modifier, s_axis_names[key.data]);
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
else if (key.source_subtype == InputSubclass::ControllerButton && key.data < std::size(s_button_names))
|
|
|
|
{
|
2023-12-13 11:06:15 +00:00
|
|
|
ret.format("XInput-{}/{}", static_cast<u32>(key.source_index), s_button_names[key.data]);
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
else if (key.source_subtype == InputSubclass::ControllerMotor)
|
|
|
|
{
|
2023-12-13 11:06:15 +00:00
|
|
|
ret.format("XInput-{}/{}Motor", static_cast<u32>(key.source_index), key.data ? "Large" : "Small");
|
2023-11-26 11:34:00 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
TinyString XInputSource::ConvertKeyToIcon(InputBindingKey key)
|
|
|
|
{
|
|
|
|
TinyString ret;
|
|
|
|
|
|
|
|
if (key.source_type == InputSourceType::SDL)
|
|
|
|
{
|
|
|
|
if (key.source_subtype == InputSubclass::ControllerAxis)
|
|
|
|
{
|
|
|
|
if (key.data < std::size(s_axis_icons) && key.modifier != InputModifier::FullAxis)
|
|
|
|
{
|
2023-12-13 11:06:15 +00:00
|
|
|
ret.format("XInput-{} {}", static_cast<u32>(key.source_index),
|
2023-11-26 11:34:00 +00:00
|
|
|
s_axis_icons[key.data][key.modifier == InputModifier::None]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else if (key.source_subtype == InputSubclass::ControllerButton)
|
|
|
|
{
|
|
|
|
if (key.data < std::size(s_button_icons))
|
2023-12-13 11:06:15 +00:00
|
|
|
ret.format("XInput-{} {}", static_cast<u32>(key.source_index), s_button_icons[key.data]);
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
std::vector<InputBindingKey> XInputSource::EnumerateMotors()
|
|
|
|
{
|
|
|
|
std::vector<InputBindingKey> ret;
|
|
|
|
|
|
|
|
for (u32 i = 0; i < NUM_CONTROLLERS; i++)
|
|
|
|
{
|
|
|
|
const ControllerData& cd = m_controllers[i];
|
|
|
|
if (!cd.connected)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
if (cd.has_large_motor)
|
|
|
|
ret.push_back(MakeGenericControllerMotorKey(InputSourceType::XInput, i, 0));
|
|
|
|
|
|
|
|
if (cd.has_small_motor)
|
|
|
|
ret.push_back(MakeGenericControllerMotorKey(InputSourceType::XInput, i, 1));
|
|
|
|
}
|
|
|
|
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool XInputSource::GetGenericBindingMapping(const std::string_view& device, GenericInputBindingMapping* mapping)
|
|
|
|
{
|
|
|
|
if (!StringUtil::StartsWith(device, "XInput-"))
|
|
|
|
return false;
|
|
|
|
|
|
|
|
const std::optional<s32> player_id = StringUtil::FromChars<s32>(device.substr(7));
|
|
|
|
if (!player_id.has_value() || player_id.value() < 0)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
if (player_id.value() < 0 || player_id.value() >= static_cast<s32>(XUSER_MAX_COUNT))
|
|
|
|
return false;
|
|
|
|
|
|
|
|
// assume all buttons are present.
|
|
|
|
const s32 pid = player_id.value();
|
|
|
|
for (u32 i = 0; i < std::size(s_xinput_generic_binding_axis_mapping); i++)
|
|
|
|
{
|
|
|
|
const GenericInputBinding negative = s_xinput_generic_binding_axis_mapping[i][0];
|
|
|
|
const GenericInputBinding positive = s_xinput_generic_binding_axis_mapping[i][1];
|
|
|
|
if (negative != GenericInputBinding::Unknown)
|
2023-09-20 14:32:39 +00:00
|
|
|
mapping->emplace_back(negative, fmt::format("XInput-{}/-{}", pid, s_axis_names[i]));
|
2022-07-11 13:03:29 +00:00
|
|
|
|
|
|
|
if (positive != GenericInputBinding::Unknown)
|
2023-09-20 14:32:39 +00:00
|
|
|
mapping->emplace_back(positive, fmt::format("XInput-{}/+{}", pid, s_axis_names[i]));
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
for (u32 i = 0; i < std::size(s_xinput_generic_binding_button_mapping); i++)
|
|
|
|
{
|
|
|
|
const GenericInputBinding binding = s_xinput_generic_binding_button_mapping[i];
|
|
|
|
if (binding != GenericInputBinding::Unknown)
|
2023-09-20 14:32:39 +00:00
|
|
|
mapping->emplace_back(binding, fmt::format("XInput-{}/{}", pid, s_button_names[i]));
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (m_controllers[pid].has_small_motor)
|
2023-09-20 14:32:39 +00:00
|
|
|
mapping->emplace_back(GenericInputBinding::SmallMotor, fmt::format("XInput-{}/SmallMotor", pid));
|
2022-07-11 13:03:29 +00:00
|
|
|
if (m_controllers[pid].has_large_motor)
|
2023-09-20 14:32:39 +00:00
|
|
|
mapping->emplace_back(GenericInputBinding::LargeMotor, fmt::format("XInput-{}/LargeMotor", pid));
|
2022-07-11 13:03:29 +00:00
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
void XInputSource::HandleControllerConnection(u32 index)
|
|
|
|
{
|
|
|
|
Log_InfoPrintf("XInput controller %u connected.", index);
|
|
|
|
|
|
|
|
XINPUT_CAPABILITIES caps = {};
|
|
|
|
if (m_xinput_get_capabilities(index, 0, &caps) != ERROR_SUCCESS)
|
|
|
|
Log_WarningPrintf("Failed to get XInput capabilities for controller %u", index);
|
|
|
|
|
|
|
|
ControllerData& cd = m_controllers[index];
|
|
|
|
cd.connected = true;
|
|
|
|
cd.has_large_motor = caps.Vibration.wLeftMotorSpeed != 0;
|
|
|
|
cd.has_small_motor = caps.Vibration.wRightMotorSpeed != 0;
|
|
|
|
cd.last_state = {};
|
|
|
|
|
2023-11-26 11:34:00 +00:00
|
|
|
InputManager::OnInputDeviceConnected(fmt::format("XInput-{}", index), fmt::format("XInput Controller {}", index));
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void XInputSource::HandleControllerDisconnection(u32 index)
|
|
|
|
{
|
|
|
|
Log_InfoPrintf("XInput controller %u disconnected.", index);
|
2023-09-20 14:32:39 +00:00
|
|
|
InputManager::OnInputDeviceDisconnected(fmt::format("XInput-{}", index));
|
2022-07-11 13:03:29 +00:00
|
|
|
m_controllers[index] = {};
|
|
|
|
}
|
|
|
|
|
|
|
|
void XInputSource::CheckForStateChanges(u32 index, const XINPUT_STATE& new_state)
|
|
|
|
{
|
|
|
|
ControllerData& cd = m_controllers[index];
|
|
|
|
if (new_state.dwPacketNumber == cd.last_state.dwPacketNumber)
|
|
|
|
return;
|
|
|
|
|
|
|
|
XINPUT_GAMEPAD& ogp = cd.last_state.Gamepad;
|
|
|
|
const XINPUT_GAMEPAD& ngp = new_state.Gamepad;
|
|
|
|
|
|
|
|
#define CHECK_AXIS(field, axis, min_value, max_value) \
|
|
|
|
if (ogp.field != ngp.field) \
|
|
|
|
{ \
|
|
|
|
InputManager::InvokeEvents(MakeGenericControllerAxisKey(InputSourceType::XInput, index, axis), \
|
|
|
|
static_cast<float>(ngp.field) / ((ngp.field < 0) ? min_value : max_value), \
|
|
|
|
GenericInputBinding::Unknown); \
|
|
|
|
}
|
|
|
|
|
|
|
|
// Y axes is inverted in XInput when compared to SDL.
|
|
|
|
CHECK_AXIS(sThumbLX, AXIS_LEFTX, 32768, 32767);
|
|
|
|
CHECK_AXIS(sThumbLY, AXIS_LEFTY, -32768, -32767);
|
|
|
|
CHECK_AXIS(sThumbRX, AXIS_RIGHTX, 32768, 32767);
|
|
|
|
CHECK_AXIS(sThumbRY, AXIS_RIGHTY, -32768, -32767);
|
|
|
|
CHECK_AXIS(bLeftTrigger, AXIS_LEFTTRIGGER, 0, 255);
|
|
|
|
CHECK_AXIS(bRightTrigger, AXIS_RIGHTTRIGGER, 0, 255);
|
|
|
|
|
|
|
|
#undef CHECK_AXIS
|
|
|
|
|
|
|
|
const u16 old_button_bits = ogp.wButtons;
|
|
|
|
const u16 new_button_bits = ngp.wButtons;
|
|
|
|
if (old_button_bits != new_button_bits)
|
|
|
|
{
|
|
|
|
for (u32 button = 0; button < NUM_BUTTONS; button++)
|
|
|
|
{
|
|
|
|
const u16 button_mask = s_button_masks[button];
|
|
|
|
if ((old_button_bits & button_mask) != (new_button_bits & button_mask))
|
|
|
|
{
|
|
|
|
const GenericInputBinding generic_key = (button < std::size(s_xinput_generic_binding_button_mapping)) ?
|
|
|
|
s_xinput_generic_binding_button_mapping[button] :
|
|
|
|
GenericInputBinding::Unknown;
|
|
|
|
const float value = ((new_button_bits & button_mask) != 0) ? 1.0f : 0.0f;
|
|
|
|
InputManager::InvokeEvents(MakeGenericControllerButtonKey(InputSourceType::XInput, index, button), value,
|
|
|
|
generic_key);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
cd.last_state = new_state;
|
|
|
|
}
|
|
|
|
|
|
|
|
void XInputSource::UpdateMotorState(InputBindingKey key, float intensity)
|
|
|
|
{
|
|
|
|
if (key.source_subtype != InputSubclass::ControllerMotor || key.source_index >= NUM_CONTROLLERS)
|
|
|
|
return;
|
|
|
|
|
|
|
|
ControllerData& cd = m_controllers[key.source_index];
|
|
|
|
if (!cd.connected)
|
|
|
|
return;
|
|
|
|
|
|
|
|
const u16 i_intensity = static_cast<u16>(intensity * 65535.0f);
|
|
|
|
if (key.data != 0)
|
|
|
|
cd.last_vibration.wRightMotorSpeed = i_intensity;
|
|
|
|
else
|
|
|
|
cd.last_vibration.wLeftMotorSpeed = i_intensity;
|
|
|
|
|
|
|
|
m_xinput_set_state(key.source_index, &cd.last_vibration);
|
|
|
|
}
|
|
|
|
|
|
|
|
void XInputSource::UpdateMotorState(InputBindingKey large_key, InputBindingKey small_key, float large_intensity,
|
|
|
|
float small_intensity)
|
|
|
|
{
|
|
|
|
if (large_key.source_index != small_key.source_index || large_key.source_subtype != InputSubclass::ControllerMotor ||
|
|
|
|
small_key.source_subtype != InputSubclass::ControllerMotor)
|
|
|
|
{
|
|
|
|
// bonkers config where they're mapped to different controllers... who would do such a thing?
|
|
|
|
UpdateMotorState(large_key, large_intensity);
|
|
|
|
UpdateMotorState(small_key, small_intensity);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
ControllerData& cd = m_controllers[large_key.source_index];
|
|
|
|
if (!cd.connected)
|
|
|
|
return;
|
|
|
|
|
|
|
|
cd.last_vibration.wLeftMotorSpeed = static_cast<u16>(large_intensity * 65535.0f);
|
|
|
|
cd.last_vibration.wRightMotorSpeed = static_cast<u16>(small_intensity * 65535.0f);
|
|
|
|
m_xinput_set_state(large_key.source_index, &cd.last_vibration);
|
|
|
|
}
|
|
|
|
|
|
|
|
std::unique_ptr<InputSource> InputSource::CreateXInputSource()
|
|
|
|
{
|
|
|
|
return std::make_unique<XInputSource>();
|
|
|
|
}
|