SDL: Added mapping for the Wii Nunchuk extension

From 396411c09093660fead7999827e9f0db5ea2bc0e Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Thu, 1 Sep 2022 20:27:34 -0700
Subject: [PATCH] Added mapping for the Wii Nunchuk extension

---
 src/joystick/SDL_gamecontroller.c         |  35 +++--
 src/joystick/hidapi/SDL_hidapi_nintendo.h |  47 ++++++
 src/joystick/hidapi/SDL_hidapi_switch.c   |  14 +-
 src/joystick/hidapi/SDL_hidapi_wii.c      | 166 ++++++++++++----------
 4 files changed, 164 insertions(+), 98 deletions(-)
 create mode 100644 src/joystick/hidapi/SDL_hidapi_nintendo.h

diff --git a/src/joystick/SDL_gamecontroller.c b/src/joystick/SDL_gamecontroller.c
index 70f3260ca1a..bca6fc46e82 100644
--- a/src/joystick/SDL_gamecontroller.c
+++ b/src/joystick/SDL_gamecontroller.c
@@ -30,6 +30,7 @@
 #include "SDL_gamecontrollerdb.h"
 #include "controller_type.h"
 #include "usb_ids.h"
+#include "hidapi/SDL_hidapi_nintendo.h"
 
 #if !SDL_EVENTS_DISABLED
 #include "../events/SDL_events_c.h"
@@ -570,25 +571,39 @@ static ControllerMapping_t *SDL_CreateMappingForHIDAPIController(SDL_JoystickGUI
         (vendor == USB_VENDOR_SHENZHEN && product == USB_PRODUCT_EVORETRO_GAMECUBE_ADAPTER)) {
         /* GameCube driver has 12 buttons and 6 axes */
         SDL_strlcat(mapping_string, "a:b0,b:b1,dpdown:b6,dpleft:b4,dpright:b5,dpup:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b9,righttrigger:a5,rightx:a2,righty:a3,start:b8,x:b2,y:b3,", sizeof(mapping_string));
-    } else if (vendor == USB_VENDOR_NINTENDO && guid.data[15] != 0 && guid.data[15] != 3) {
+    } else if (vendor == USB_VENDOR_NINTENDO &&
+               guid.data[15] != k_eSwitchDeviceInfoControllerType_Unknown &&
+               guid.data[15] != k_eSwitchDeviceInfoControllerType_ProController &&
+               guid.data[15] != k_eWiiExtensionControllerType_WiiUPro) {
         switch (guid.data[15]) {
-        case 9:
-        case 10:
-            /* NES Controller */
+        case k_eSwitchDeviceInfoControllerType_NESLeft:
+        case k_eSwitchDeviceInfoControllerType_NESRight:
             SDL_strlcat(mapping_string, "a:b0,b:b1,back:b4,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b9,rightshoulder:b10,start:b6,", sizeof(mapping_string));
             break;
-        case 11:
-            /* SNES Controller */
+        case k_eSwitchDeviceInfoControllerType_SNES:
             SDL_strlcat(mapping_string, "a:b0,b:b1,back:b4,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b9,lefttrigger:a4,rightshoulder:b10,righttrigger:a5,start:b6,x:b2,y:b3,", sizeof(mapping_string));
             break;
-        case 12:
-            /* N64 Controller */
+        case k_eSwitchDeviceInfoControllerType_N64:
             SDL_strlcat(mapping_string, "a:b0,b:b1,back:b4,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,righttrigger:a5,start:b6,x:b2,y:b3,misc1:b15,", sizeof(mapping_string));
             break;
-        case 13:
-            /* SEGA Genesis Controller */
+        case k_eSwitchDeviceInfoControllerType_SEGA_Genesis:
             SDL_strlcat(mapping_string, "a:b0,b:b1,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b5,rightshoulder:b10,righttrigger:a5,start:b6,misc1:b15,", sizeof(mapping_string));
             break;
+        case k_eWiiExtensionControllerType_None:
+            SDL_strlcat(mapping_string, "a:b0,b:b1,back:b4,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b5,start:b6,x:b2,y:b3,", sizeof(mapping_string));
+            break;
+        case k_eWiiExtensionControllerType_Nunchuck:
+            {
+                /* FIXME: Should we map this to the left or right side? */
+                const SDL_bool map_nunchuck_left_side = SDL_TRUE;
+
+                if (map_nunchuck_left_side) {
+                    SDL_strlcat(mapping_string, "a:b0,b:b1,back:b4,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b5,leftshoulder:b9,lefttrigger:a4,leftx:a0,lefty:a1,start:b6,x:b2,y:b3,", sizeof(mapping_string));
+                } else {
+                    SDL_strlcat(mapping_string, "a:b0,b:b1,back:b4,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b5,rightshoulder:b9,righttrigger:a4,rightx:a0,righty:a1,start:b6,x:b2,y:b3,", sizeof(mapping_string));
+                }
+            }
+            break;
         default:
             /* Mini gamepad mode */
             SDL_strlcat(mapping_string, "a:b0,b:b1,guide:b5,leftshoulder:b9,leftstick:b7,leftx:a0,lefty:a1,rightshoulder:b10,start:b6,x:b2,y:b3,", sizeof(mapping_string));
diff --git a/src/joystick/hidapi/SDL_hidapi_nintendo.h b/src/joystick/hidapi/SDL_hidapi_nintendo.h
new file mode 100644
index 00000000000..600f3227db1
--- /dev/null
+++ b/src/joystick/hidapi/SDL_hidapi_nintendo.h
@@ -0,0 +1,47 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2022 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+
+/* These are values used in the controller type byte of the controller GUID */
+
+/* These values come directly out of the hardware, so don't change them */
+typedef enum {
+    k_eSwitchDeviceInfoControllerType_Unknown           = 0,
+    k_eSwitchDeviceInfoControllerType_JoyConLeft        = 1,
+    k_eSwitchDeviceInfoControllerType_JoyConRight       = 2,
+    k_eSwitchDeviceInfoControllerType_ProController     = 3,
+    k_eSwitchDeviceInfoControllerType_NESLeft           = 9,
+    k_eSwitchDeviceInfoControllerType_NESRight          = 10,
+    k_eSwitchDeviceInfoControllerType_SNES              = 11,
+    k_eSwitchDeviceInfoControllerType_N64               = 12,
+    k_eSwitchDeviceInfoControllerType_SEGA_Genesis      = 13,
+} ESwitchDeviceInfoControllerType;
+
+/* These values are used internally but can be updated as needed */
+typedef enum {
+    k_eWiiExtensionControllerType_Unknown               = 0,
+    k_eWiiExtensionControllerType_None                  = 128,
+    k_eWiiExtensionControllerType_Nunchuck              = 129,
+    k_eWiiExtensionControllerType_ClassicController     = 130,
+    k_eWiiExtensionControllerType_ClassicControllerPro  = 131,
+    k_eWiiExtensionControllerType_WiiUPro               = 132,
+} EWiiExtensionControllerType;
+
+/* vi: set ts=4 sw=4 expandtab: */
diff --git a/src/joystick/hidapi/SDL_hidapi_switch.c b/src/joystick/hidapi/SDL_hidapi_switch.c
index af7ef533547..799dbb1cf50 100644
--- a/src/joystick/hidapi/SDL_hidapi_switch.c
+++ b/src/joystick/hidapi/SDL_hidapi_switch.c
@@ -33,6 +33,7 @@
 #include "../SDL_sysjoystick.h"
 #include "SDL_hidapijoystick_c.h"
 #include "SDL_hidapi_rumble.h"
+#include "SDL_hidapi_nintendo.h"
 
 
 #ifdef SDL_JOYSTICK_HIDAPI_SWITCH
@@ -102,18 +103,6 @@ typedef enum {
     k_eSwitchProprietaryCommandIDs_ResetMCU  = 0x06,
 } ESwitchProprietaryCommandIDs;
 
-typedef enum {
-    k_eSwitchDeviceInfoControllerType_Unknown        = 0,
-    k_eSwitchDeviceInfoControllerType_JoyConLeft     = 1,
-    k_eSwitchDeviceInfoControllerType_JoyConRight    = 2,
-    k_eSwitchDeviceInfoControllerType_ProController  = 3,
-    k_eSwitchDeviceInfoControllerType_NESLeft        = 9,
-    k_eSwitchDeviceInfoControllerType_NESRight       = 10,
-    k_eSwitchDeviceInfoControllerType_SNES           = 11,
-    k_eSwitchDeviceInfoControllerType_N64            = 12,
-    k_eSwitchDeviceInfoControllerType_SEGA_Genesis   = 13,
-} ESwitchDeviceInfoControllerType;
-
 #define k_unSwitchOutputPacketDataLength 49
 #define k_unSwitchMaxOutputPacketLength  64
 #define k_unSwitchBluetoothPacketLength  k_unSwitchOutputPacketDataLength
@@ -1244,6 +1233,7 @@ UpdateDeviceIdentity(SDL_HIDAPI_Device *device)
     if (name && (!name || SDL_strcmp(name, device->name) != 0)) {
         SDL_free(device->name);
         device->name = SDL_strdup(name);
+        SDL_SetJoystickGUIDCRC(&device->guid, SDL_crc16(0, name, SDL_strlen(name)));
     }
 }
 
diff --git a/src/joystick/hidapi/SDL_hidapi_wii.c b/src/joystick/hidapi/SDL_hidapi_wii.c
index 88521f5b0ed..69c199b8ee1 100644
--- a/src/joystick/hidapi/SDL_hidapi_wii.c
+++ b/src/joystick/hidapi/SDL_hidapi_wii.c
@@ -31,13 +31,13 @@
 #include "../SDL_sysjoystick.h"
 #include "SDL_hidapijoystick_c.h"
 #include "SDL_hidapi_rumble.h"
+#include "SDL_hidapi_nintendo.h"
 
 
 #ifdef SDL_JOYSTICK_HIDAPI_WII
 
 /* 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)))
@@ -80,15 +80,6 @@ typedef enum {
     k_eWiiPlayerLEDs_P4 = 0x80,
 } EWiiPlayerLEDs;
 
-typedef enum {
-    k_eWiiExtensionControllerType_None,
-    k_eWiiExtensionControllerType_Unknown,
-    k_eWiiExtensionControllerType_Nunchuck,
-    k_eWiiExtensionControllerType_ClassicController,
-    k_eWiiExtensionControllerType_ClassicControllerPro,
-    k_eWiiExtensionControllerType_WiiUPro,
-} EWiiExtensionControllerType;
-
 #define k_unWiiPacketDataLength 22
 
 typedef struct {
@@ -312,13 +303,13 @@ static SDL_bool ParseExtensionResponse(SDL_DriverWii_Context *ctx)
 static void UpdatePowerLevelWii(SDL_Joystick *joystick, Uint8 batteryLevelByte)
 {
     if (batteryLevelByte > 178) {
-        joystick->epowerlevel = SDL_JOYSTICK_POWER_FULL;
+        SDL_PrivateJoystickBatteryLevel(joystick, SDL_JOYSTICK_POWER_FULL);
     } else if (batteryLevelByte > 51) {
-        joystick->epowerlevel = SDL_JOYSTICK_POWER_MEDIUM;
+        SDL_PrivateJoystickBatteryLevel(joystick, SDL_JOYSTICK_POWER_MEDIUM);
     } else if (batteryLevelByte > 13) {
-        joystick->epowerlevel = SDL_JOYSTICK_POWER_LOW;
+        SDL_PrivateJoystickBatteryLevel(joystick, SDL_JOYSTICK_POWER_LOW);
     } else {
-        joystick->epowerlevel = SDL_JOYSTICK_POWER_EMPTY;
+        SDL_PrivateJoystickBatteryLevel(joystick, SDL_JOYSTICK_POWER_EMPTY);
     }
 }
 
@@ -335,43 +326,16 @@ static void UpdatePowerLevelWiiU(SDL_Joystick *joystick, Uint8 extensionBatteryB
      * No value above 4 has been observed.
      */
     if (pluggedIn && !charging) {
-        joystick->epowerlevel = SDL_JOYSTICK_POWER_WIRED;
+        SDL_PrivateJoystickBatteryLevel(joystick, SDL_JOYSTICK_POWER_WIRED);
     } else if (batteryLevel >= 4) {
-        joystick->epowerlevel = SDL_JOYSTICK_POWER_FULL;
+        SDL_PrivateJoystickBatteryLevel(joystick, SDL_JOYSTICK_POWER_FULL);
     } else if (batteryLevel > 1) {
-        joystick->epowerlevel = SDL_JOYSTICK_POWER_MEDIUM;
+        SDL_PrivateJoystickBatteryLevel(joystick, SDL_JOYSTICK_POWER_MEDIUM);
     } else if (batteryLevel == 1) {
-        joystick->epowerlevel = SDL_JOYSTICK_POWER_LOW;
-    } else {
-        joystick->epowerlevel = SDL_JOYSTICK_POWER_EMPTY;
-    }
-}
-
-static SDL_bool IdentifyController(SDL_DriverWii_Context *ctx, SDL_Joystick *joystick)
-{
-    const Uint8 statusRequest[2] = { k_eWiiOutputReportIDs_StatusRequest, 0 };
-    SDL_bool hasExtension;
-    WriteOutput(ctx, statusRequest, sizeof(statusRequest), SDL_TRUE);
-    if (!ReadInputSync(ctx, k_eWiiInputReportIDs_Status, NULL)) {
-        return SDL_FALSE;
-    }
-    UpdatePowerLevelWii(joystick, ctx->m_rgucReadBuffer[6]);
-    hasExtension = ctx->m_rgucReadBuffer[3] & 2 ? SDL_TRUE : SDL_FALSE;
-    if (hasExtension) {
-        /* http://wiibrew.org/wiki/Wiimote/Extension_Controllers#The_New_Way */
-        Uint8 data_0x55 = 0x55;
-        Uint8 data_0x00 = 0x00;
-        SDL_bool ok = WriteRegister(ctx, 0xA400F0, &data_0x55, sizeof(data_0x55), SDL_TRUE)
-                   && WriteRegister(ctx, 0xA400FB, &data_0x00, sizeof(data_0x00), SDL_TRUE)
-                   && ReadRegister(ctx, 0xA400FA, 6, SDL_TRUE)
-                   && ParseExtensionResponse(ctx);
-        if (!ok) {
-            return SDL_FALSE;
-        }
+        SDL_PrivateJoystickBatteryLevel(joystick, SDL_JOYSTICK_POWER_LOW);
     } else {
-        ctx->m_eExtensionControllerType = k_eWiiExtensionControllerType_None;
+        SDL_PrivateJoystickBatteryLevel(joystick, SDL_JOYSTICK_POWER_EMPTY);
     }
-    return SDL_TRUE;
 }
 
 static EWiiInputReportIDs GetButtonPacketType(SDL_DriverWii_Context *ctx)
@@ -434,24 +398,6 @@ static void InitStickCalibrationData(SDL_DriverWii_Context *ctx)
     }
 }
 
-static const char* GetNameFromExtensionInfo(SDL_DriverWii_Context *ctx)
-{
-    switch (ctx->m_eExtensionControllerType) {
-        case k_eWiiExtensionControllerType_None:
-            return "Nintendo Wii Remote";
-        case k_eWiiExtensionControllerType_Nunchuck:
-            return "Nintendo Wii Remote with Nunchuck";
-        case k_eWiiExtensionControllerType_ClassicController:
-            return "Nintendo Wii Remote with Classic Controller";
-        case k_eWiiExtensionControllerType_ClassicControllerPro:
-            return "Nintendo Wii Remote with Classic Controller Pro";
-        case k_eWiiExtensionControllerType_WiiUPro:
-            return "Nintendo Wii U Pro Controller";
-        default:
-            return "Nintendo Wii Remote with Unknown Extension";
-    }
-}
-
 static void SDLCALL SDL_GameControllerButtonReportingHintChanged(void *userdata, const char *name, const char *oldValue, const char *hint)
 {
     SDL_DriverWii_Context *ctx = (SDL_DriverWii_Context *)userdata;
@@ -523,9 +469,84 @@ static void SDLCALL SDL_PlayerLEDHintChanged(void *userdata, const char *name, c
     }
 }
 
+static EWiiExtensionControllerType
+ReadControllerType(SDL_HIDAPI_Device *device)
+{
+    EWiiExtensionControllerType eControllerType = k_eWiiExtensionControllerType_Unknown;
+
+    /* Create enough of a context to read the controller type from the device */
+    SDL_DriverWii_Context *ctx = (SDL_DriverWii_Context *)SDL_calloc(1, sizeof(*ctx));
+    if (ctx) {
+        ctx->device = device;
+
+        device->dev = SDL_hid_open_path(device->path, 0);
+        if (device->dev) {
+            const Uint8 statusRequest[2] = { k_eWiiOutputReportIDs_StatusRequest, 0 };
+            WriteOutput(ctx, statusRequest, sizeof(statusRequest), SDL_TRUE);
+            if (ReadInputSync(ctx, k_eWiiInputReportIDs_Status, NULL)) {
+                SDL_bool hasExtension = (ctx->m_rgucReadBuffer[3] & 2) ? SDL_TRUE : SDL_FALSE;
+                if (hasExtension) {
+                    /* http://wiibrew.org/wiki/Wiimote/Extension_Controllers#The_New_Way */
+                    Uint8 data_0x55 = 0x55;
+                    Uint8 data_0x00 = 0x00;
+                    if (WriteRegister(ctx, 0xA400F0, &data_0x55, sizeof(data_0x55), SDL_TRUE) &&
+                        WriteRegister(ctx, 0xA400FB, &data_0x00, sizeof(data_0x00), SDL_TRUE) &&
+                        ReadRegister(ctx, 0xA400FA, 6, SDL_TRUE) && ParseExtensionResponse(ctx)) {
+                        eControllerType = ctx->m_eExtensionControllerType;
+                    }
+                } else {
+                    eControllerType = k_eWiiExtensionControllerType_None;
+                }
+            }
+            SDL_hid_close(device->dev);
+            device->dev = NULL;
+        }
+        SDL_free(ctx);
+    }
+    return eControllerType;
+}
+
+static void
+UpdateDeviceIdentity(SDL_HIDAPI_Device *device)
+{
+    EWiiExtensionControllerType eControllerType = (EWiiExtensionControllerType)device->guid.data[15];
+    const char *name = NULL;
+
+    switch (eControllerType) {
+        case k_eWiiExtensionControllerType_None:
+            name = "Nintendo Wii Remote";
+            break;
+        case k_eWiiExtensionControllerType_Nunchuck:
+            name = "Nintendo Wii Remote with Nunchuck";
+            break;
+        case k_eWiiExtensionControllerType_ClassicController:
+            name = "Nintendo Wii Remote with Classic Controller";
+            break;
+        case k_eWiiExtensionControllerType_ClassicControllerPro:
+            name = "Nintendo Wii Remote with Classic Controller Pro";
+            break;
+        case k_eWiiExtensionControllerType_WiiUPro:
+            name = "Nintendo Wii U Pro Controller";
+            break;
+        default:
+            name = "Nintendo Wii Remote with Unknown Extension";
+            break;
+    }
+    if (name && (!name || SDL_strcmp(name, device->name) != 0)) {
+        SDL_free(device->name);
+        device->name = SDL_strdup(name);
+        SDL_SetJoystickGUIDCRC(&device->guid, SDL_crc16(0, name, SDL_strlen(name)));
+    }
+}
+
 static SDL_bool
 HIDAPI_DriverWii_InitDevice(SDL_HIDAPI_Device *device)
 {
+    if (device->vendor_id == USB_VENDOR_NINTENDO) {
+        EWiiExtensionControllerType eControllerType = ReadControllerType(device);
+        device->guid.data[15] = eControllerType;
+        UpdateDeviceIdentity(device);
+    }
     return HIDAPI_JoystickConnected(device, NULL);
 }
 
@@ -568,19 +589,12 @@ HIDAPI_DriverWii_OpenJoystick(SDL_HIDAPI_Device *device, SDL_Joystick *joystick)
         goto error;
     }
 
-    SDL_AddHintCallback(SDL_HINT_GAMECONTROLLER_USE_BUTTON_LABELS,
-                        SDL_GameControllerButtonReportingHintChanged, ctx);
-
-    if (!IdentifyController(ctx, joystick)) {
-        char msg[512];
-        SDL_GetErrorMsg(msg, sizeof(msg) - 1);
-        SDL_SetError("Couldn't read device info: %s", msg);
-        goto error;
-    }
+    ctx->m_eExtensionControllerType = (EWiiExtensionControllerType)device->guid.data[15];
 
     InitStickCalibrationData(ctx);
-    SDL_free(device->name);
-    device->name = SDL_strdup(GetNameFromExtensionInfo(ctx));
+
+    SDL_AddHintCallback(SDL_HINT_GAMECONTROLLER_USE_BUTTON_LABELS,
+                        SDL_GameControllerButtonReportingHintChanged, ctx);
 
     /* Initialize player index (needed for setting LEDs) */
     ctx->m_nPlayerIndex = SDL_JoystickGetPlayerIndex(joystick);
@@ -953,7 +967,7 @@ HIDAPI_DriverWii_UpdateDevice(SDL_HIDAPI_Device *device)
     }
 
     /* Request a status update periodically to make sure our battery value is up to date */
-    if (SDL_TICKS_PASSED(now, ctx->m_unLastStatus + FIFTEEN_MINUTES_IN_MS)) {
+    if (!ctx->m_unLastStatus || SDL_TICKS_PASSED(now, ctx->m_unLastStatus + FIFTEEN_MINUTES_IN_MS)) {
         Uint8 data[2];
 
         data[0] = k_eWiiOutputReportIDs_StatusRequest;