SDL: Added support for the Nintendo Switch Joy-Con Controllers on iOS and tvOS 16

From f9beef760a778cc74d48d17971b6ce0451979c2e Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Fri, 22 Jul 2022 18:58:29 -0700
Subject: [PATCH] Added support for the Nintendo Switch Joy-Con Controllers on
 iOS and tvOS 16

---
 src/joystick/SDL_gamecontrollerdb.h     |  5 ++
 src/joystick/iphoneos/SDL_mfijoystick.m | 76 +++++++++++++++++++++++--
 2 files changed, 76 insertions(+), 5 deletions(-)

diff --git a/src/joystick/SDL_gamecontrollerdb.h b/src/joystick/SDL_gamecontrollerdb.h
index 1fa03474784..1e7f1f43f38 100644
--- a/src/joystick/SDL_gamecontrollerdb.h
+++ b/src/joystick/SDL_gamecontrollerdb.h
@@ -913,6 +913,11 @@ static const char *s_ControllerMappings [] =
     "05000000ac05000001000000df076d01,*,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b2,y:b3,",
     "05000000ac05000001000000ff076d01,*,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b9,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b10,x:b2,y:b3,",
     "05000000ac050000020000004f066d02,*,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b6,leftshoulder:b4,rightshoulder:b5,x:b2,y:b3,",
+    "050000007e050000062000004f060000,Nintendo Switch Joy-Con (L),+leftx:h0.1,+lefty:h0.2,-leftx:h0.4,-lefty:h0.8,dpdown:b2,dpleft:b0,dpright:b3,dpup:b1,leftshoulder:b4,misc1:b6,rightshoulder:b5,",
+    "050000007e0500000e200000df070000,Nintendo Switch Joy-Con (L/R),a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b2,y:b3,hint:SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,",
+    "050000007e0500000e200000df070000,Nintendo Switch Joy-Con (L/R),a:b1,b:b0,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b3,y:b2,hint:!SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,",
+    "050000007e050000072000004f060000,Nintendo Switch Joy-Con (R),+rightx:h0.4,+righty:h0.8,-rightx:h0.1,-righty:h0.2,a:b0,b:b1,guide:b6,leftshoulder:b4,rightshoulder:b5,x:b2,y:b3,hint:SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,",
+    "050000007e050000072000004f060000,Nintendo Switch Joy-Con (R),+rightx:h0.4,+righty:h0.8,-rightx:h0.1,-righty:h0.2,a:b1,b:b0,guide:b6,leftshoulder:b4,rightshoulder:b5,x:b3,y:b2,hint:!SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,",
     "050000007e05000009200000ff870000,Nintendo Switch Pro Controller,a:b1,b:b0,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b9,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,misc1:b11,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b10,x:b3,y:b2,hint:SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,",
     "050000007e05000009200000ff870000,Nintendo Switch Pro Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b9,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,misc1:b11,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b10,x:b2,y:b3,hint:!SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,",
     "050000004c050000cc090000df070000,PS4 Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b2,y:b3,",
diff --git a/src/joystick/iphoneos/SDL_mfijoystick.m b/src/joystick/iphoneos/SDL_mfijoystick.m
index 639ee27975e..ad542848cf5 100644
--- a/src/joystick/iphoneos/SDL_mfijoystick.m
+++ b/src/joystick/iphoneos/SDL_mfijoystick.m
@@ -188,6 +188,36 @@ @interface GCMicroGamepad (SDL)
     return FALSE;
 }
 static BOOL
+IsControllerSwitchJoyConL(GCController *controller)
+{
+    if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) {
+        if ([controller.productCategory isEqualToString:@"Nintendo Switch Joy-Con (L)"]) {
+            return TRUE;
+        }
+    }
+    return FALSE;
+}
+static BOOL
+IsControllerSwitchJoyConR(GCController *controller)
+{
+    if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) {
+        if ([controller.productCategory isEqualToString:@"Nintendo Switch Joy-Con (R)"]) {
+            return TRUE;
+        }
+    }
+    return FALSE;
+}
+static BOOL
+IsControllerSwitchJoyConPair(GCController *controller)
+{
+    if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) {
+        if ([controller.productCategory isEqualToString:@"Nintendo Switch Joy-Con (L/R)"]) {
+            return TRUE;
+        }
+    }
+    return FALSE;
+}
+static BOOL
 IOS_AddMFIJoystickDevice(SDL_JoystickDeviceItem *device, GCController *controller)
 {
     Uint16 *guid16 = (Uint16 *)device->guid.data;
@@ -222,8 +252,9 @@ @interface GCMicroGamepad (SDL)
         BOOL is_ps4 = IsControllerPS4(controller);
         BOOL is_ps5 = IsControllerPS5(controller);
         BOOL is_switch_pro = IsControllerSwitchPro(controller);
+        BOOL is_switch_joycon_pair = IsControllerSwitchJoyConPair(controller);
 #if TARGET_OS_TV
-        BOOL is_MFi = (!is_xbox && !is_ps4 && !is_ps5 && !is_switch_pro);
+        BOOL is_MFi = (!is_xbox && !is_ps4 && !is_ps5 && !is_switch_pro && !is_switch_joycon_pair);
 #endif
         int nbuttons = 0;
         BOOL has_direct_menu;
@@ -262,7 +293,8 @@ @interface GCMicroGamepad (SDL)
             device->button_mask |= (1 << SDL_CONTROLLER_BUTTON_BACK);
             ++nbuttons;
         }
-        if ([gamepad respondsToSelector:@selector(buttonHome)] && gamepad.buttonHome) {
+        /* The Nintendo Switch JoyCon home button doesn't ever show as being held down */
+        if ([gamepad respondsToSelector:@selector(buttonHome)] && gamepad.buttonHome && !is_switch_joycon_pair) {
             device->button_mask |= (1 << SDL_CONTROLLER_BUTTON_GUIDE);
             ++nbuttons;
         }
@@ -347,6 +379,10 @@ @interface GCMicroGamepad (SDL)
             vendor = USB_VENDOR_NINTENDO;
             product = USB_PRODUCT_NINTENDO_SWITCH_PRO;
             subtype = 0;
+        } else if (is_switch_joycon_pair) {
+            vendor = USB_VENDOR_NINTENDO;
+            product = USB_PRODUCT_NINTENDO_SWITCH_JOY_CON_GRIP;
+            subtype = 0;
         } else {
             vendor = USB_VENDOR_APPLE;
             product = 1;
@@ -367,8 +403,24 @@ @interface GCMicroGamepad (SDL)
         device->nbuttons = nbuttons;
 
     } else if (controller.gamepad) {
+        BOOL is_switch_joyconL = IsControllerSwitchJoyConL(controller);
+        BOOL is_switch_joyconR = IsControllerSwitchJoyConR(controller);
         int nbuttons = 0;
 
+        if (is_switch_joyconL) {
+            vendor = USB_VENDOR_NINTENDO;
+            product = USB_PRODUCT_NINTENDO_SWITCH_JOY_CON_LEFT;
+            subtype = 0;
+        } else if (is_switch_joyconR) {
+            vendor = USB_VENDOR_NINTENDO;
+            product = USB_PRODUCT_NINTENDO_SWITCH_JOY_CON_RIGHT;
+            subtype = 0;
+        } else {
+            vendor = USB_VENDOR_APPLE;
+            product = 2;
+            subtype = 2;
+        }
+
         /* These buttons are part of the original MFi spec */
         device->button_mask |= (1 << SDL_CONTROLLER_BUTTON_A);
         device->button_mask |= (1 << SDL_CONTROLLER_BUTTON_B);
@@ -380,9 +432,6 @@ @interface GCMicroGamepad (SDL)
         nbuttons += 7;
         device->uses_pause_handler = SDL_TRUE;
 
-        vendor = USB_VENDOR_APPLE;
-        product = 2;
-        subtype = 2;
         device->naxes = 0; /* no traditional analog inputs */
         device->nhats = 1; /* d-pad */
         device->nbuttons = nbuttons;
@@ -877,6 +926,23 @@ static int is_macos11(void)
         int i;
         int pause_button_index = 0;
 
+#ifdef DEBUG_CONTROLLER_STATE
+        if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {
+            if (controller.physicalInputProfile) {
+                for (id key in controller.physicalInputProfile.buttons) {
+                    GCControllerButtonInput *button = controller.physicalInputProfile.buttons[key];
+                    if (button.isPressed)
+                        NSLog(@"Button %@ = %s\n", key, button.isPressed ? "pressed" : "released");
+                }
+                for (id key in controller.physicalInputProfile.axes) {
+                    GCControllerAxisInput *axis = controller.physicalInputProfile.axes[key];
+                    if (axis.value != 0.0f)
+                        NSLog(@"Axis %@ = %.2f\n", key, axis.value);
+                }
+            }
+        }
+#endif
+
         if (controller.extendedGamepad) {
             GCExtendedGamepad *gamepad = controller.extendedGamepad;