SPDX-FileCopyrightText: 2024 Arthur Kasimov <kodemeister@outlook.com>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "device.h"
#include <KLocalizedString>
#include <SDL2/SDL_hidapi.h>
#include "logging.h"
#include "udevmatcher.h"
static QStringList kJoystickTypeNames = {i18n("Unknown"),
i18n("Game Controller"),
i18n("Wheel"),
i18n("Arcade Stick"),
i18n("Flight Stick"),
i18n("Dance Pad"),
i18n("Guitar"),
i18n("Drum Kit"),
i18n("Arcade Pad"),
i18n("Throttle")};
static QStringList kControllerTypeNames = {
i18n("Unknown"),
i18n("XBox 360"),
i18n("XBox One"),
i18n("Sony Dualshock 3"),
i18n("Sony Dualshock 4"),
i18n("Switch Pro"),
i18n("Virtual"),
i18n("Sony Dualsense"),
i18n("Amazon Luna"),
i18n("Google Stadia"),
i18n("NVidia Shield"),
i18n("Switch Left Joy-Con"),
i18n("Switch Right Joy-Con"),
i18n("Switch Paired Joy-Con"),
};
static QStringList kButtonNames = {
i18nc("The game controller button with a letter A on it", "A button"),
i18nc("The game controller button with a letter B on it", "B button"),
i18nc("The game controller button with a letter X on it", "X button"),
i18nc("The game controller button with a letter Y on it", "Y button"),
i18nc("The back game controller button", "Back button"),
i18nc("The guide game controller button", "Guide button"),
i18nc("The start game controller button", "Start button"),
i18nc("The button under the game controller's left analog stick", "Left analog stick"),
i18nc("The button under the game controller's right analog stick", "Right analog stick"),
i18nc("The game controller's left shoulder button", "Left shoulder button"),
i18nc("The game controller's right shoulder button", "Right shoulder button"),
i18nc("The game controller's d-pad Up button", "D-pad up button"),
i18nc("The game controller's d-pad down button", "D-pad down button"),
i18nc("The game controller's d-pad left button", "D-pad left button"),
i18nc("The game controller's d-pad right button", "D-pad right button"),
i18nc("The game controller's miscellaneous button (Share on Xbox, mic on Playstation/Luna, capture on Switch)",
"Miscellaneous button"),
i18nc("The game controller's 1st paddle button", "Paddle button 1"),
i18nc("The game controller's 2nd paddle button", "Paddle button 2"),
i18nc("The game controller's 3rd paddle button", "Paddle button 3"),
i18nc("The game controller's 4th paddle button", "Paddle button 4"),
i18nc("The button under the game controller's touchpad", "Touchpad button"),
};
static QStringList kConnectionTypeString = {
i18nc("Unknown controller connection type", "Unknown"),
i18nc("USB connected controller", "USB"),
i18nc("Bluetooth connected controller", "Bluetooth"),
};
QString ButtonToButtonName(SDL_GameControllerButton button)
{
if (button > SDL_CONTROLLER_BUTTON_INVALID && button < SDL_CONTROLLER_BUTTON_MAX) {
return kButtonNames.at(button);
} else {
return i18n("Invalid button value");
}
}
SDL_hid_device_info getJoystickHidInfo(short vendor, short product, std::string path)
{
SDL_hid_device_info *info = SDL_hid_enumerate(vendor, product);
SDL_hid_device_info *current = info;
qCDebug(KCM_GAMECONTROLLER) << "Checking for hidapi with vendor: " << vendor << " and product: " << product;
SDL_hid_device_info result;
if (info) {
if (!info->next) {
result.interface_number = info->interface_number;
qCDebug(KCM_GAMECONTROLLER) << "Found hidapi data for path " << info->path;
} else {
result.interface_number = -2;
qCDebug(KCM_GAMECONTROLLER) << "Found multiple hidapi data with that vendor and product id";
auto to_match = UDevMatcher::deviceToUdevId(path);
qCDebug(KCM_GAMECONTROLLER) << "Looking for device with this udev id: " << to_match;
while (current) {
auto test_id = UDevMatcher::deviceToUdevId(current->path);
qCDebug(KCM_GAMECONTROLLER) << "Found device: " << current->path << " udev id: " << test_id;
if (to_match == test_id) {
result.interface_number = current->interface_number;
qCDebug(KCM_GAMECONTROLLER) << "Found hidapi data for path " << current->path;
current = nullptr;
} else {
current = current->next;
}
}
}
SDL_hid_free_enumeration(info);
} else {
result.interface_number = -2;
}
return result;
}
Device::Device(int deviceIndex, QObject *parent)
: QObject(parent)
, m_deviceIndex(deviceIndex)
, m_connectionType(UnknownType)
{
}
Device::~Device()
{
close();
}
bool Device::open()
{
if (m_joystick != nullptr) {
return true;
}
m_joystick = SDL_JoystickOpen(m_deviceIndex);
if (!m_joystick) {
qCWarning(KCM_GAMECONTROLLER) << "Failed to load joystick at: " << m_deviceIndex;
return false;
}
short vendor = SDL_JoystickGetVendor(m_joystick);
short product = SDL_JoystickGetProduct(m_joystick);
auto path = SDL_JoystickPath(m_joystick);
qCDebug(KCM_GAMECONTROLLER) << "Device path: " << path;
SDL_hid_device_info info = getJoystickHidInfo(vendor, product, path);
if (info.interface_number == -2) {
m_connectionType = UnknownType;
} else if (info.interface_number == -1) {
m_connectionType = BluetoothType;
} else {
m_connectionType = USBType;
}
m_controller = SDL_GameControllerOpen(m_deviceIndex);
if (!m_controller) {
m_leftTrigger = SDL_JoystickGetAxis(m_joystick, SDL_CONTROLLER_AXIS_TRIGGERLEFT);
m_rightTrigger = SDL_JoystickGetAxis(m_joystick, SDL_CONTROLLER_AXIS_TRIGGERRIGHT);
qCDebug(KCM_GAMECONTROLLER) << "Device" << m_deviceIndex << "is not a gamepad. using as joystick";
m_buttonCount = SDL_JoystickNumButtons(m_joystick);
} else {
m_leftTrigger = SDL_GameControllerGetAxis(m_controller, SDL_CONTROLLER_AXIS_TRIGGERLEFT);
m_rightTrigger = SDL_GameControllerGetAxis(m_controller, SDL_CONTROLLER_AXIS_TRIGGERRIGHT);
for (int i = 0; i < SDL_CONTROLLER_BUTTON_MAX; ++i) {
if (SDL_GameControllerHasButton(m_controller, static_cast<SDL_GameControllerButton>(i))) {
m_buttonType[m_buttonCount] = i;
m_buttonCount++;
}
}
}
return m_joystick != nullptr;
}
void Device::close()
{
if (m_joystick == nullptr) {
return;
}
if (m_controller) {
SDL_GameControllerClose(m_controller);
m_controller = nullptr;
}
SDL_JoystickClose(m_joystick);
m_joystick = nullptr;
}
bool Device::isController() const
{
return m_controller != nullptr;
}
SDL_JoystickID Device::id() const
{
return SDL_JoystickInstanceID(m_joystick);
}
QString Device::name() const
{
return QString::fromLocal8Bit(SDL_JoystickName(m_joystick));
}
QString Device::path() const
{
return QString::fromLocal8Bit(SDL_JoystickPath(m_joystick));
}
QString Device::type() const
{
return kJoystickTypeNames.at(SDL_JoystickGetType(m_joystick));
}
SDL_GameControllerType Device::controllerType() const
{
if (m_controller) {
return SDL_GameControllerGetType(m_controller);
}
return SDL_CONTROLLER_TYPE_UNKNOWN;
}
QString Device::controllerTypeName() const
{
if (m_controller) {
return kControllerTypeNames.at(SDL_GameControllerGetType(m_controller));
} else {
return i18n("Not a controller");
}
}
QString Device::connectionType() const
{
return kConnectionTypeString.at(m_connectionType);
}
bool Device::isVirtual() const
{
return SDL_JoystickIsVirtual(m_deviceIndex);
}
int Device::buttonCount() const
{
return m_buttonCount;
}
bool Device::buttonState(int index) const
{
if (index < 0 || index > m_buttonCount)
return false;
if (m_controller) {
return SDL_GameControllerGetButton(m_controller, static_cast<SDL_GameControllerButton>(m_buttonType.value(index))) != 0;
}
return SDL_JoystickGetButton(m_joystick, index) != 0;
}
QString Device::buttonName(int index) const
{
if (m_controller) {
return ButtonToButtonName(static_cast<SDL_GameControllerButton>(m_buttonType.value(index)));
}
return QString::number(index + 1);
}
int Device::axisCount() const
{
return SDL_JoystickNumAxes(m_joystick);
}
QVector2D Device::leftAxisValue() const
{
return m_leftAxis;
}
QVector2D Device::rightAxisValue() const
{
return m_rightAxis;
}
float Device::leftTriggerValue() const
{
return m_leftTrigger;
}
float Device::rightTriggerValue() const
{
return m_rightTrigger;
}
int Device::hatCount() const
{
return SDL_JoystickNumHats(m_joystick);
}
QVector2D Device::hatPosition(int index) const
{
const Uint8 hat = SDL_JoystickGetHat(m_joystick, index);
QVector2D position;
if ((hat & SDL_HAT_LEFT) != 0) {
position.setX(std::numeric_limits<Sint16>::min());
} else if ((hat & SDL_HAT_RIGHT) != 0) {
position.setX(std::numeric_limits<Sint16>::max());
}
if ((hat & SDL_HAT_UP) != 0) {
position.setY(std::numeric_limits<Sint16>::min());
} else if ((hat & SDL_HAT_DOWN) != 0) {
position.setY(std::numeric_limits<Sint16>::max());
}
return position;
}
void Device::onButtonEvent(const SDL_JoyButtonEvent &event)
{
Q_EMIT buttonStateChanged(event.button);
}
void Device::onControllerButtonEvent(const SDL_ControllerButtonEvent &event)
{
qCDebug(KCM_GAMECONTROLLER) << "Got controller button event for button: " << ButtonToButtonName(SDL_GameControllerButton(event.button));
Q_EMIT buttonStateChanged(event.button);
}
void Device::onAxisEvent(const SDL_JoyAxisEvent &event)
{
const float value = static_cast<float>(event.value) / std::numeric_limits<Sint16>::max();
if (event.axis == SDL_CONTROLLER_AXIS_LEFTX) {
m_leftAxis.setX(value);
Q_EMIT leftAxisChanged();
} else if (event.axis == SDL_CONTROLLER_AXIS_LEFTY) {
m_leftAxis.setY(value);
Q_EMIT leftAxisChanged();
} else if (event.axis == SDL_CONTROLLER_AXIS_RIGHTX) {
m_rightAxis.setX(value);
Q_EMIT rightAxisChanged();
} else if (event.axis == SDL_CONTROLLER_AXIS_RIGHTY) {
m_rightAxis.setY(value);
Q_EMIT rightAxisChanged();
} else if (event.axis == SDL_CONTROLLER_AXIS_TRIGGERLEFT) {
m_leftTrigger = value;
Q_EMIT leftTriggerChanged();
} else if (event.axis == SDL_CONTROLLER_AXIS_TRIGGERRIGHT) {
m_rightTrigger = value;
Q_EMIT rightTriggerChanged();
}
}
void Device::onControllerAxisEvent(const SDL_ControllerAxisEvent &event)
{
const float value = static_cast<float>(event.value) / std::numeric_limits<Sint16>::max();
if (event.axis == SDL_CONTROLLER_AXIS_LEFTX) {
m_leftAxis.setX(value);
Q_EMIT leftAxisChanged();
} else if (event.axis == SDL_CONTROLLER_AXIS_LEFTY) {
m_leftAxis.setY(value);
Q_EMIT leftAxisChanged();
} else if (event.axis == SDL_CONTROLLER_AXIS_RIGHTX) {
m_rightAxis.setX(value);
Q_EMIT rightAxisChanged();
} else if (event.axis == SDL_CONTROLLER_AXIS_RIGHTY) {
m_rightAxis.setY(value);
Q_EMIT rightAxisChanged();
} else if (event.axis == SDL_CONTROLLER_AXIS_TRIGGERLEFT) {
m_leftTrigger = value;
Q_EMIT leftTriggerChanged();
} else if (event.axis == SDL_CONTROLLER_AXIS_TRIGGERRIGHT) {
m_rightTrigger = value;
Q_EMIT rightTriggerChanged();
}
}
void Device::onHatEvent(const SDL_JoyHatEvent &event)
{
Q_EMIT hatPositionChanged(event.hat);
}
#include "moc_device.cpp"