SDL: Added support for the Xbox Elite controller paddles with firmware version 5.13+ (b5076)

From b5076ef5e31baff85f7a6abe2326625bcc5c7d7f Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Mon, 28 Nov 2022 23:10:02 -0800
Subject: [PATCH] Added support for the Xbox Elite controller paddles with
 firmware version 5.13+

---
 src/joystick/SDL_joystick.c              |  3 +-
 src/joystick/hidapi/SDL_hidapi_xboxone.c | 92 ++++++++++++++++++++++--
 2 files changed, 89 insertions(+), 6 deletions(-)

diff --git a/src/joystick/SDL_joystick.c b/src/joystick/SDL_joystick.c
index e2b723b296c9..4ee6a2603678 100644
--- a/src/joystick/SDL_joystick.c
+++ b/src/joystick/SDL_joystick.c
@@ -2222,7 +2222,8 @@ SDL_IsJoystickXboxOneElite(Uint16 vendor_id, Uint16 product_id)
     if (vendor_id == USB_VENDOR_MICROSOFT) {
         if (product_id == USB_PRODUCT_XBOX_ONE_ELITE_SERIES_1 ||
             product_id == USB_PRODUCT_XBOX_ONE_ELITE_SERIES_2 ||
-            product_id == USB_PRODUCT_XBOX_ONE_ELITE_SERIES_2_BLUETOOTH) {
+            product_id == USB_PRODUCT_XBOX_ONE_ELITE_SERIES_2_BLUETOOTH ||
+            product_id == USB_PRODUCT_XBOX_ONE_ELITE_SERIES_2_BLE) {
             return SDL_TRUE;
         }
     }
diff --git a/src/joystick/hidapi/SDL_hidapi_xboxone.c b/src/joystick/hidapi/SDL_hidapi_xboxone.c
index c23563a285e1..bd5d71695113 100644
--- a/src/joystick/hidapi/SDL_hidapi_xboxone.c
+++ b/src/joystick/hidapi/SDL_hidapi_xboxone.c
@@ -121,8 +121,10 @@ typedef struct {
     SDL_bool has_guide_packet;
     SDL_bool has_color_led;
     SDL_bool has_paddles;
+    SDL_bool has_unmapped_state;
     SDL_bool has_trigger_rumble;
     SDL_bool has_share_button;
+    Uint8 last_paddle_state;
     Uint8 low_frequency_rumble;
     Uint8 high_frequency_rumble;
     Uint8 left_trigger_rumble;
@@ -604,6 +606,70 @@ HIDAPI_DriverXboxOne_SetJoystickSensorsEnabled(SDL_HIDAPI_Device *device, SDL_Jo
     return SDL_Unsupported();
 }
 
+/*
+ * The Xbox One Elite controller with 5.13+ firmware sends the unmapped state in a separate packet.
+ * We can use this to send the paddle state when they aren't mapped
+ */
+static void
+HIDAPI_DriverXboxOne_HandleUnmappedStatePacket(SDL_Joystick *joystick, SDL_DriverXboxOne_Context *ctx, Uint8 *data, int size)
+{
+    Uint8 profile;
+    int paddle_index;
+    int button1_bit;
+    int button2_bit;
+    int button3_bit;
+    int button4_bit;
+    SDL_bool paddles_mapped;
+
+    if (size == 21) {
+        /* XBox One Elite Series 2 */
+        paddle_index = 18;
+        button1_bit = 0x01;
+        button2_bit = 0x02;
+        button3_bit = 0x04;
+        button4_bit = 0x08;
+        profile = data[19];
+
+        if (profile == 0) {
+            paddles_mapped = SDL_FALSE;
+        } else if (SDL_memcmp(&data[4], &ctx->last_state[4], 14) == 0) {
+            /* We're using a profile, but paddles aren't mapped */
+            paddles_mapped = SDL_FALSE;
+        } else {
+            /* Something is mapped, we can't use the paddles */
+            paddles_mapped = SDL_TRUE;
+        }
+
+    } else {
+        /* Unknown format */
+        return;
+    }
+#ifdef DEBUG_XBOX_PROTOCOL
+    SDL_Log(">>> Paddles: %d,%d,%d,%d mapped = %s\n",
+        (data[paddle_index] & button1_bit) ? 1 : 0,
+        (data[paddle_index] & button2_bit) ? 1 : 0,
+        (data[paddle_index] & button3_bit) ? 1 : 0,
+        (data[paddle_index] & button4_bit) ? 1 : 0,
+        paddles_mapped ? "TRUE" : "FALSE"
+    );
+#endif
+
+    if (paddles_mapped) {
+        /* Respect that the paddles are being used for other controls and don't pass them on to the app */
+        data[paddle_index] = 0;
+    }
+
+    if (ctx->last_paddle_state != data[paddle_index]) {
+        int nButton = SDL_CONTROLLER_BUTTON_MISC1 + ctx->has_share_button; /* Next available button */
+        SDL_PrivateJoystickButton(joystick, nButton++, (data[paddle_index] & button1_bit) ? SDL_PRESSED : SDL_RELEASED);
+        SDL_PrivateJoystickButton(joystick, nButton++, (data[paddle_index] & button2_bit) ? SDL_PRESSED : SDL_RELEASED);
+        SDL_PrivateJoystickButton(joystick, nButton++, (data[paddle_index] & button3_bit) ? SDL_PRESSED : SDL_RELEASED);
+        SDL_PrivateJoystickButton(joystick, nButton++, (data[paddle_index] & button4_bit) ? SDL_PRESSED : SDL_RELEASED);
+        ctx->last_paddle_state = data[paddle_index];
+    }
+    ctx->has_unmapped_state = SDL_TRUE;
+}
+
 static void
 HIDAPI_DriverXboxOne_HandleStatePacket(SDL_Joystick *joystick, SDL_DriverXboxOne_Context *ctx, Uint8 *data, int size)
 {
@@ -670,7 +736,7 @@ HIDAPI_DriverXboxOne_HandleStatePacket(SDL_Joystick *joystick, SDL_DriverXboxOne
             P3: 0x04 (A)    P1: 0x01 (B)
             P4: 0x08 (X)    P2: 0x02 (Y)
     */
-    if (ctx->has_paddles && (size == 33 || size == 38 || size == 50)) {
+    if (ctx->has_paddles && !ctx->has_unmapped_state && (size == 33 || size == 38 || size == 50)) {
         int paddle_index;
         int button1_bit;
         int button2_bit;
@@ -722,12 +788,13 @@ HIDAPI_DriverXboxOne_HandleStatePacket(SDL_Joystick *joystick, SDL_DriverXboxOne
             data[paddle_index] = 0;
         }
 
-        if (ctx->last_state[paddle_index] != data[paddle_index]) {
+        if (ctx->last_paddle_state != data[paddle_index]) {
             int nButton = SDL_CONTROLLER_BUTTON_MISC1 + ctx->has_share_button; /* Next available button */
             SDL_PrivateJoystickButton(joystick, nButton++, (data[paddle_index] & button1_bit) ? SDL_PRESSED : SDL_RELEASED);
             SDL_PrivateJoystickButton(joystick, nButton++, (data[paddle_index] & button2_bit) ? SDL_PRESSED : SDL_RELEASED);
             SDL_PrivateJoystickButton(joystick, nButton++, (data[paddle_index] & button3_bit) ? SDL_PRESSED : SDL_RELEASED);
             SDL_PrivateJoystickButton(joystick, nButton++, (data[paddle_index] & button4_bit) ? SDL_PRESSED : SDL_RELEASED);
+            ctx->last_paddle_state = data[paddle_index];
         }
     }
 
@@ -840,7 +907,7 @@ HIDAPI_DriverXboxOneBluetooth_HandleButtons(SDL_Joystick *joystick, SDL_DriverXb
             P3: 0x04 (A)    P1: 0x01 (B)
             P4: 0x08 (X)    P2: 0x02 (Y)
     */
-    if (ctx->has_paddles && (size == 39 || size == 55)) {
+    if (ctx->has_paddles && (size == 20 || size == 39 || size == 55)) {
         int paddle_index;
         int button1_bit;
         int button2_bit;
@@ -856,7 +923,7 @@ HIDAPI_DriverXboxOneBluetooth_HandleButtons(SDL_Joystick *joystick, SDL_DriverXb
             button3_bit = 0x04;
             button4_bit = 0x08;
             paddles_mapped = (data[35] != 0);
-        } else /* if (size == 39) */ {
+        } else if (size == 39) {
             /* Updated firmware for the Xbox Elite Series 2 controller */
             paddle_index = 17;
             button1_bit = 0x01;
@@ -864,6 +931,14 @@ HIDAPI_DriverXboxOneBluetooth_HandleButtons(SDL_Joystick *joystick, SDL_DriverXb
             button3_bit = 0x04;
             button4_bit = 0x08;
             paddles_mapped = (data[19] != 0);
+        } else /* if (size == 20) */ {
+            /* Updated firmware for the Xbox Elite Series 2 controller (5.13+) */
+            paddle_index = 19;
+            button1_bit = 0x01;
+            button2_bit = 0x02;
+            button3_bit = 0x04;
+            button4_bit = 0x08;
+            paddles_mapped = (data[17] != 0);
         }
 
 #ifdef DEBUG_XBOX_PROTOCOL
@@ -881,12 +956,13 @@ HIDAPI_DriverXboxOneBluetooth_HandleButtons(SDL_Joystick *joystick, SDL_DriverXb
             data[paddle_index] = 0;
         }
 
-        if (ctx->last_state[paddle_index] != data[paddle_index]) {
+        if (ctx->last_paddle_state != data[paddle_index]) {
             int nButton = SDL_CONTROLLER_BUTTON_MISC1; /* Next available button */
             SDL_PrivateJoystickButton(joystick, nButton++, (data[paddle_index] & button1_bit) ? SDL_PRESSED : SDL_RELEASED);
             SDL_PrivateJoystickButton(joystick, nButton++, (data[paddle_index] & button2_bit) ? SDL_PRESSED : SDL_RELEASED);
             SDL_PrivateJoystickButton(joystick, nButton++, (data[paddle_index] & button3_bit) ? SDL_PRESSED : SDL_RELEASED);
             SDL_PrivateJoystickButton(joystick, nButton++, (data[paddle_index] & button4_bit) ? SDL_PRESSED : SDL_RELEASED);
+            ctx->last_paddle_state = data[paddle_index];
         }
     }
 }
@@ -1183,6 +1259,12 @@ HIDAPI_DriverXboxOne_UpdateDevice(SDL_HIDAPI_Device *device)
                 }
                 HIDAPI_DriverXboxOne_HandleModePacket(joystick, ctx, data, size);
                 break;
+            case 0x0C:
+                if (joystick == NULL) {
+                    break;
+                }
+                HIDAPI_DriverXboxOne_HandleUnmappedStatePacket(joystick, ctx, data, size);
+                break;
             case 0x1E:
                 /* If the packet starts with this:
                     0x1E 0x30 0x07 0x10 0x04 0x00