SDL: joystick: Add initial support for GIP flight sticks

From 72dd79752e268e197eecf7aa6555ddfac9bdfd98 Mon Sep 17 00:00:00 2001
From: Vicki Pfau <[EMAIL REDACTED]>
Date: Tue, 6 May 2025 20:11:50 -0700
Subject: [PATCH] joystick: Add initial support for GIP flight sticks

At the moment, only the ThrustMaster T.Flight Hotas One has full support. The
documentation says you can query the extra buttons via a specific command, but
the stick appears to reject the command. Further investigation is needed for
automatically querying this state.
---
 src/joystick/hidapi/SDL_hidapi_gip.c | 92 ++++++++++++++++++++++++++++
 src/joystick/usb_ids.h               |  1 +
 2 files changed, 93 insertions(+)

diff --git a/src/joystick/hidapi/SDL_hidapi_gip.c b/src/joystick/hidapi/SDL_hidapi_gip.c
index a1f72ce4bff91..0fe53f789726a 100644
--- a/src/joystick/hidapi/SDL_hidapi_gip.c
+++ b/src/joystick/hidapi/SDL_hidapi_gip.c
@@ -314,6 +314,8 @@ typedef struct GIP_Quirks
     Uint32 extra_in_system[8];
     Uint32 extra_out_system[8];
     GIP_AttachmentType device_type;
+    Uint8 extra_buttons;
+    Uint8 extra_axes;
 } GIP_Quirks;
 
 static const GIP_Quirks quirks[] = {
@@ -348,6 +350,12 @@ static const GIP_Quirks quirks[] = {
       .filtered_features = GIP_FEATURE_MOTOR_CONTROL,
       .device_type = GIP_TYPE_ARCADE_STICK },
 
+    { USB_VENDOR_THRUSTMASTER, USB_PRODUCT_THRUSTMASTER_T_FLIGHT_HOTAS_ONE, 0,
+      .filtered_features = GIP_FEATURE_MOTOR_CONTROL,
+      .device_type = GIP_TYPE_FLIGHT_STICK,
+      .extra_buttons = 5,
+      .extra_axes = 3 },
+
     {0},
 };
 
@@ -451,6 +459,10 @@ typedef struct GIP_Attachment
     Uint8 share_button_idx;
     Uint8 paddle_idx;
     int paddle_offset;
+
+    Uint8 extra_button_idx;
+    int extra_buttons;
+    int extra_axes;
 } GIP_Attachment;
 
 typedef struct GIP_Device
@@ -658,6 +670,9 @@ static void GIP_HandleQuirks(GIP_Attachment *attachment)
             attachment->metadata.device.in_system_messages[j] |= quirks[i].extra_in_system[j];
             attachment->metadata.device.out_system_messages[j] |= quirks[i].extra_out_system[j];
         }
+
+        attachment->extra_buttons = quirks[i].extra_buttons;
+        attachment->extra_axes = quirks[i].extra_axes;
         break;
     }
 }
@@ -1168,6 +1183,10 @@ static bool GIP_SendInitSequence(GIP_Attachment *attachment)
         GIP_SendVendorMessage(attachment, GIP_CMD_INITIAL_REPORTS_REQUEST, 0, (const Uint8 *)&request, sizeof(request));
     }
 
+    if (GIP_SupportsVendorMessage(attachment, GIP_CMD_DEVICE_CAPABILITIES, false)) {
+        GIP_SendVendorMessage(attachment, GIP_CMD_DEVICE_CAPABILITIES, 0, NULL, 0);
+    }
+
     if (!attachment->joystick) {
         return HIDAPI_JoystickConnected(attachment->device->device, &attachment->joystick);
     }
@@ -1833,6 +1852,67 @@ static void GIP_HandleArcadeStickReport(
     }
 }
 
+static void GIP_HandleFlightStickReport(
+    GIP_Attachment *attachment,
+    SDL_Joystick *joystick,
+    Uint64 timestamp,
+    const Uint8 *bytes,
+    int num_bytes)
+{
+    Sint16 axis;
+    int i;
+
+    if (num_bytes < 19) {
+        return;
+    }
+
+    if (attachment->last_input[2] != bytes[2]) {
+        /* Fire 1 and 2 */
+        SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_LEFT_STICK, ((bytes[2] & 0x01) != 0));
+        SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_RIGHT_STICK, ((bytes[2] & 0x02) != 0));
+    }
+    for (i = 0; i < attachment->extra_buttons;) {
+        if (attachment->last_input[i / 8 + 3] != bytes[i / 8 + 3]) {
+            for (; i < attachment->extra_buttons; i++) {
+                SDL_SendJoystickButton(timestamp,
+                    joystick,
+                    (Uint8) (attachment->extra_button_idx + i),
+                    ((bytes[i / 8 + 3] & (1u << i)) != 0));
+            }
+        } else {
+            i += 8;
+        }
+    }
+
+    /* Roll, pitch and yaw are signed. Throttle and any extra axes are unsigned. All values are full-range. */
+    axis = bytes[11];
+    axis |= bytes[12] << 8;
+    SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_LEFTX, axis);
+
+    axis = bytes[13];
+    axis |= bytes[14] << 8;
+    SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_LEFTY, axis);
+
+    axis = bytes[15];
+    axis |= bytes[16] << 8;
+    SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_RIGHTX, axis);
+
+    /* There are no more signed values, so skip RIGHTY */
+
+    axis = (bytes[18] << 8) - 0x8000;
+    axis |= bytes[17];
+    SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_LEFT_TRIGGER, axis);
+
+    for (i = 0; i < attachment->extra_axes; i++) {
+        if (20 + i * 2 >= num_bytes) {
+            return;
+        }
+        axis = (bytes[20 + i * 2] << 8) - 0x8000;
+        axis |= bytes[19 + i * 2];
+        SDL_SendJoystickAxis(timestamp, joystick, (Uint8) (SDL_GAMEPAD_AXIS_RIGHT_TRIGGER + i), axis);
+    }
+}
+
 static bool GIP_HandleLLInputReport(
     GIP_Attachment *attachment,
     const GIP_Header *header,
@@ -1875,6 +1955,9 @@ static bool GIP_HandleLLInputReport(
     case GIP_TYPE_ARCADE_STICK:
         GIP_HandleArcadeStickReport(attachment, joystick, timestamp, bytes, num_bytes);
         break;
+    case GIP_TYPE_FLIGHT_STICK:
+        GIP_HandleFlightStickReport(attachment, joystick, timestamp, bytes, num_bytes);
+        break;
     }
 
     if ((attachment->features & GIP_FEATURE_ELITE_BUTTONS) &&
@@ -2395,8 +2478,17 @@ static bool HIDAPI_DriverGIP_OpenJoystick(SDL_HIDAPI_Device *device, SDL_Joystic
         attachment->share_button_idx = (Uint8) joystick->nbuttons;
         joystick->nbuttons++;
     }
+    if (attachment->extra_buttons > 0) {
+        attachment->extra_button_idx = (Uint8) joystick->nbuttons;
+        joystick->nbuttons += attachment->extra_buttons;
+    }
 
     joystick->naxes = SDL_GAMEPAD_AXIS_COUNT;
+    if (attachment->attachment_type == GIP_TYPE_FLIGHT_STICK) {
+        /* Flight sticks have at least 4 axes, but only 3 are signed values, so we leave RIGHTY unused */
+        joystick->naxes += attachment->extra_axes - 1;
+    }
+
     joystick->nhats = 1;
 
     return true;
diff --git a/src/joystick/usb_ids.h b/src/joystick/usb_ids.h
index 32a8c9418841f..c2418e0a3701b 100644
--- a/src/joystick/usb_ids.h
+++ b/src/joystick/usb_ids.h
@@ -131,6 +131,7 @@
 #define USB_PRODUCT_STEALTH_ULTRA_WIRED                   0x7073
 #define USB_PRODUCT_SWITCH_RETROBIT_CONTROLLER            0x0575
 #define USB_PRODUCT_THRUSTMASTER_ESWAPX_PRO_PS4           0xd00e
+#define USB_PRODUCT_THRUSTMASTER_T_FLIGHT_HOTAS_ONE       0xb68c
 #define USB_PRODUCT_VALVE_STEAM_CONTROLLER_DONGLE         0x1142
 #define USB_PRODUCT_VICTRIX_FS_PRO                        0x0203
 #define USB_PRODUCT_VICTRIX_FS_PRO_V2                     0x0207