SDL: Initial support for the Wii Remote with Nunchuk extension

From e19b36d871345f0e452ea414a2b92e122765091a Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Thu, 1 Sep 2022 19:29:20 -0700
Subject: [PATCH] Initial support for the Wii Remote with Nunchuk extension

---
 src/joystick/hidapi/SDL_hidapi_wii.c | 87 ++++++++++++++++++++++++++--
 1 file changed, 81 insertions(+), 6 deletions(-)

diff --git a/src/joystick/hidapi/SDL_hidapi_wii.c b/src/joystick/hidapi/SDL_hidapi_wii.c
index 59dfd46e46c..88521f5b0ed 100644
--- a/src/joystick/hidapi/SDL_hidapi_wii.c
+++ b/src/joystick/hidapi/SDL_hidapi_wii.c
@@ -37,6 +37,10 @@
 
 /* Define this if you want to log all packets from the controller */
 /*#define DEBUG_WII_PROTOCOL*/
+#define DEBUG_WII_PROTOCOL
+
+#undef clamp
+#define clamp(val, min, max) (((val) > (max)) ? (max) : (((val) < (min)) ? (min) : (val)))
 
 typedef enum {
     k_eWiiInputReportIDs_Status      = 0x20,
@@ -104,7 +108,8 @@ typedef struct {
     int m_nPlayerIndex;
     SDL_bool m_bRumbleActive;
     Uint8 m_rgucReadBuffer[k_unWiiPacketDataLength];
-    Uint32 m_iLastStatus;
+    Uint32 m_unLastInput;
+    Uint32 m_unLastStatus;
 
     struct StickCalibrationData {
         Uint16 min;
@@ -141,8 +146,7 @@ HIDAPI_DriverWii_IsSupportedDevice(SDL_HIDAPI_Device *device, const char *name,
         return SDL_TRUE;
     }
     if (vendor_id == USB_VENDOR_NINTENDO && product_id == USB_PRODUCT_NINTENDO_WII_REMOTE) {
-        /* Technically can be supported, but we don't interpret inputs yet */
-        return SDL_FALSE;
+        return SDL_TRUE;
     }
     return SDL_FALSE;
 }
@@ -597,6 +601,8 @@ HIDAPI_DriverWii_OpenJoystick(SDL_HIDAPI_Device *device, SDL_Joystick *joystick)
 
     RequestButtonPacketType(ctx, GetButtonPacketType(ctx));
 
+    ctx->m_unLastInput = SDL_GetTicks();
+
     return SDL_TRUE;
 
 error:
@@ -748,10 +754,67 @@ static void HandleWiiUProButtonData(SDL_DriverWii_Context *ctx, SDL_Joystick *jo
     UpdatePowerLevelWiiU(joystick, data.rgucExtension[10]);
 }
 
+static void HandleWiiButtonData(SDL_DriverWii_Context *ctx, SDL_Joystick *joystick, WiiButtonData data)
+{
+    static const Uint8 buttons[2][8] = {
+        {
+            SDL_CONTROLLER_BUTTON_DPAD_LEFT,  SDL_CONTROLLER_BUTTON_DPAD_RIGHT,    SDL_CONTROLLER_BUTTON_DPAD_DOWN, SDL_CONTROLLER_BUTTON_DPAD_UP,
+            SDL_CONTROLLER_BUTTON_START,      0xFF /* Unused */,                   0xFF /* Unused */,               0xFF /* Unused */,
+        }, {
+            SDL_CONTROLLER_BUTTON_X,          SDL_CONTROLLER_BUTTON_Y,             SDL_CONTROLLER_BUTTON_A,         SDL_CONTROLLER_BUTTON_B,
+            SDL_CONTROLLER_BUTTON_BACK,       0xFF /* Unused */,                   0xFF /* Unused */,               SDL_CONTROLLER_BUTTON_GUIDE,
+        }
+    };
+    int i, j;
+
+    /* Buttons */
+    for (i = 0; i < 2; i++) {
+        for (j = 0; j < 8; j++) {
+            Uint8 button = buttons[i][j];
+            if (button != 0xFF) {
+                SDL_bool state = ((data.rgucBaseButtons[i] >> j) & 1) ? SDL_PRESSED : SDL_RELEASED;
+                SDL_PrivateJoystickButton(joystick, button, state);
+            }
+        }
+    }
+}
+
+static void HandleWiiNunchukData(SDL_DriverWii_Context *ctx, SDL_Joystick *joystick, WiiButtonData data)
+{
+    /* FIXME: What is the actual range of these axes? */
+    const int NUNCHUK_THUMBSTICK_MIN = (128 - 96);
+    const int NUNCHUK_THUMBSTICK_MAX = (128 + 96);
+
+    Sint16 axis;
+    Uint8 value;
+
+    if (data.ucNExtensionBytes < 6) {
+        return;
+    }
+
+    value = clamp(data.rgucExtension[0], NUNCHUK_THUMBSTICK_MIN, NUNCHUK_THUMBSTICK_MAX);
+    axis = (Sint16)HIDAPI_RemapVal(value, (float)NUNCHUK_THUMBSTICK_MIN, (float)NUNCHUK_THUMBSTICK_MAX, SDL_JOYSTICK_AXIS_MIN, SDL_JOYSTICK_AXIS_MAX);
+    SDL_PrivateJoystickAxis(joystick, SDL_CONTROLLER_AXIS_LEFTX, axis);
+
+    value = clamp(data.rgucExtension[1], NUNCHUK_THUMBSTICK_MIN, NUNCHUK_THUMBSTICK_MAX);
+    axis = (Sint16)HIDAPI_RemapVal(value, (float)NUNCHUK_THUMBSTICK_MIN, (float)NUNCHUK_THUMBSTICK_MAX, SDL_JOYSTICK_AXIS_MIN, SDL_JOYSTICK_AXIS_MAX);
+    SDL_PrivateJoystickAxis(joystick, SDL_CONTROLLER_AXIS_LEFTY, ~axis);
+
+    value = data.rgucExtension[5];
+    SDL_PrivateJoystickAxis(joystick, SDL_CONTROLLER_AXIS_TRIGGERLEFT,  (value & 0x01) ? SDL_MIN_SINT16 : SDL_MAX_SINT16);
+
+    SDL_PrivateJoystickButton(joystick, SDL_CONTROLLER_BUTTON_LEFTSHOULDER, (value & 0x02) ? SDL_RELEASED : SDL_PRESSED);
+}
+
 static void HandleButtonData(SDL_DriverWii_Context *ctx, SDL_Joystick *joystick, WiiButtonData data)
 {
     if (ctx->m_eExtensionControllerType == k_eWiiExtensionControllerType_WiiUPro) {
         HandleWiiUProButtonData(ctx, joystick, data);
+    } else if (ctx->m_eExtensionControllerType == k_eWiiExtensionControllerType_None) {
+        HandleWiiButtonData(ctx, joystick, data);
+    } else if (ctx->m_eExtensionControllerType == k_eWiiExtensionControllerType_Nunchuck) {
+        HandleWiiButtonData(ctx, joystick, data);
+        HandleWiiNunchukData(ctx, joystick, data);
     }
 }
 
@@ -869,23 +932,35 @@ HIDAPI_DriverWii_UpdateDevice(SDL_HIDAPI_Device *device)
         return SDL_FALSE;
     }
 
+    now = SDL_GetTicks();
+
     while ((size = ReadInput(ctx)) > 0) {
 #ifdef DEBUG_WII_PROTOCOL
         HIDAPI_DumpPacket("Wii packet: size = %d", ctx->m_rgucReadBuffer, size);
 #endif
         HandleInput(ctx, joystick);
+
+        ctx->m_unLastInput = now;
+    }
+
+    if (ctx->m_eExtensionControllerType == k_eWiiExtensionControllerType_WiiUPro) {
+        const Uint32 INPUT_WAIT_TIMEOUT_MS = 3000;
+
+        if (SDL_TICKS_PASSED(now, ctx->m_unLastInput + INPUT_WAIT_TIMEOUT_MS)) {
+            /* Bluetooth may have disconnected, try reopening the controller */
+            size = -1;
+        }
     }
 
     /* Request a status update periodically to make sure our battery value is up to date */
-    now = SDL_GetTicks();
-    if (SDL_TICKS_PASSED(now, ctx->m_iLastStatus + FIFTEEN_MINUTES_IN_MS)) {
+    if (SDL_TICKS_PASSED(now, ctx->m_unLastStatus + FIFTEEN_MINUTES_IN_MS)) {
         Uint8 data[2];
 
         data[0] = k_eWiiOutputReportIDs_StatusRequest;
         data[1] = ctx->m_bRumbleActive;
         WriteOutput(ctx, data, sizeof(data), SDL_FALSE);
 
-        ctx->m_iLastStatus = now;
+        ctx->m_unLastStatus = now;
     }
 
     if (size < 0) {