SDL: Added the gamepad hint SDL_GAMECONTROLLER_USE_GAMECUBE_LABELS

From 48dfc03a8727a7a27f6b3ee5aeb925b3e1a59caa Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Thu, 1 May 2025 10:35:49 -0700
Subject: [PATCH] Added the gamepad hint SDL_GAMECONTROLLER_USE_GAMECUBE_LABELS

This is for internal use to signal that a mapping uses positional GameCube buttons. It's set so we know whether a mapping uses the older style labeled buttons or the newer style positional buttons. If a positional mapping is used with SDL2, then it will be ignored, since the hint is marked as defaulting true and the mapping conditional is that the hint is false.
---
 src/joystick/SDL_gamepad.c     | 170 ++++++++++++++++++++++-----------
 src/joystick/SDL_joystick.c    |   4 +-
 test/testautomation_joystick.c |  17 ++++
 test/testcontroller.c          |  11 ---
 4 files changed, 132 insertions(+), 70 deletions(-)

diff --git a/src/joystick/SDL_gamepad.c b/src/joystick/SDL_gamepad.c
index a54b79996dccd..5f08f94752fd2 100644
--- a/src/joystick/SDL_gamepad.c
+++ b/src/joystick/SDL_gamepad.c
@@ -714,7 +714,7 @@ static GamepadMapping_t *SDL_CreateMappingForHIDAPIGamepad(SDL_GUID guid)
           product == USB_PRODUCT_EVORETRO_GAMECUBE_ADAPTER2 ||
           product == USB_PRODUCT_EVORETRO_GAMECUBE_ADAPTER3))) {
         // 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));
+        SDL_strlcat(mapping_string, "a:b0,b:b2,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:b1,y:b3,hint:!SDL_GAMECONTROLLER_USE_GAMECUBE_LABELS:=1,", sizeof(mapping_string));
     } else if (vendor == USB_VENDOR_NINTENDO &&
                (guid.data[15] == k_eSwitchDeviceInfoControllerType_HVCLeft ||
                 guid.data[15] == k_eSwitchDeviceInfoControllerType_HVCRight ||
@@ -781,7 +781,11 @@ static GamepadMapping_t *SDL_CreateMappingForHIDAPIGamepad(SDL_GUID guid)
         }
     } else {
         // All other gamepads have the standard set of 19 buttons and 6 axes
-        SDL_strlcat(mapping_string, "a:b0,b:b1,back:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,", sizeof(mapping_string));
+        if (SDL_IsJoystickGameCube(vendor, product)) {
+            SDL_strlcat(mapping_string, "a:b0,b:b2,back:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b1,y:b3,hint:!SDL_GAMECONTROLLER_USE_GAMECUBE_LABELS:=1,", sizeof(mapping_string));
+        } else {
+            SDL_strlcat(mapping_string, "a:b0,b:b1,back:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,", sizeof(mapping_string));
+        }
 
         if (SDL_IsJoystickSteamController(vendor, product)) {
             // Steam controllers have 2 back paddle buttons
@@ -1110,7 +1114,7 @@ SDL_COMPILE_TIME_ASSERT(map_StringForGamepadButton, SDL_arraysize(map_StringForG
 /*
  * convert a string to its enum equivalent
  */
-static SDL_GamepadButton SDL_PrivateGetGamepadButtonFromString(const char *str, bool baxy)
+static SDL_GamepadButton SDL_PrivateGetGamepadButtonFromString(const char *str, bool axby, bool baxy)
 {
     int i;
 
@@ -1120,7 +1124,17 @@ static SDL_GamepadButton SDL_PrivateGetGamepadButtonFromString(const char *str,
 
     for (i = 0; i < SDL_arraysize(map_StringForGamepadButton); ++i) {
         if (SDL_strcasecmp(str, map_StringForGamepadButton[i]) == 0) {
-            if (baxy) {
+            if (axby) {
+                // Need to swap face buttons
+                switch (i) {
+                case SDL_GAMEPAD_BUTTON_EAST:
+                    return SDL_GAMEPAD_BUTTON_WEST;
+                case SDL_GAMEPAD_BUTTON_WEST:
+                    return SDL_GAMEPAD_BUTTON_EAST;
+                default:
+                    break;
+                }
+            } else if (baxy) {
                 // Need to swap face buttons
                 switch (i) {
                 case SDL_GAMEPAD_BUTTON_SOUTH:
@@ -1142,7 +1156,7 @@ static SDL_GamepadButton SDL_PrivateGetGamepadButtonFromString(const char *str,
 }
 SDL_GamepadButton SDL_GetGamepadButtonFromString(const char *str)
 {
-    return SDL_PrivateGetGamepadButtonFromString(str, false);
+    return SDL_PrivateGetGamepadButtonFromString(str, false, false);
 }
 
 /*
@@ -1169,6 +1183,7 @@ static bool SDL_PrivateParseGamepadElement(SDL_Gamepad *gamepad, const char *szG
     char half_axis_output = 0;
     int i;
     SDL_GamepadBinding *new_bindings;
+    bool axby_mapping = false;
     bool baxy_mapping = false;
 
     SDL_AssertJoysticksLocked();
@@ -1179,12 +1194,17 @@ static bool SDL_PrivateParseGamepadElement(SDL_Gamepad *gamepad, const char *szG
         half_axis_output = *szGameButton++;
     }
 
+    if (SDL_strstr(gamepad->mapping->mapping, ",hint:SDL_GAMECONTROLLER_USE_GAMECUBE_LABELS:=1") != NULL) {
+        axby_mapping = true;
+    }
     if (SDL_strstr(gamepad->mapping->mapping, ",hint:SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1") != NULL) {
         baxy_mapping = true;
     }
+    // FIXME: We fix these up when loading the mapping, does this ever get hit?
+    //SDL_assert(!axby_mapping && !baxy_mapping);
 
     axis = SDL_GetGamepadAxisFromString(szGameButton);
-    button = SDL_PrivateGetGamepadButtonFromString(szGameButton, baxy_mapping);
+    button = SDL_PrivateGetGamepadButtonFromString(szGameButton, axby_mapping, baxy_mapping);
     if (axis != SDL_GAMEPAD_AXIS_INVALID) {
         bind.output_type = SDL_GAMEPAD_BINDTYPE_AXIS;
         bind.output.axis.axis = axis;
@@ -1401,6 +1421,11 @@ static void SDL_UpdateGamepadFaceStyle(SDL_Gamepad *gamepad)
         }
     }
 
+    if (gamepad->face_style == SDL_GAMEPAD_FACE_STYLE_UNKNOWN &&
+        SDL_strstr(gamepad->mapping->mapping, "SDL_GAMECONTROLLER_USE_GAMECUBE_LABELS") != NULL) {
+        // This controller uses GameCube button style
+        gamepad->face_style = SDL_GAMEPAD_FACE_STYLE_AXBY;
+    }
     if (gamepad->face_style == SDL_GAMEPAD_FACE_STYLE_UNKNOWN &&
         SDL_strstr(gamepad->mapping->mapping, "SDL_GAMECONTROLLER_USE_BUTTON_LABELS") != NULL) {
         // This controller uses Nintendo button style
@@ -1466,24 +1491,6 @@ static void SDL_FixupHIDAPIMapping(SDL_Gamepad *gamepad)
     }
 }
 
-static void SDL_FixupGameCubeMapping(SDL_Gamepad *gamepad, const char *pchString)
-{
-    if (SDL_strstr(gamepad->mapping->mapping, SDL_GAMEPAD_FACE_FIELD) != NULL) {
-        return;
-    }
-
-    for (int i = 0; i < gamepad->num_bindings; ++i) {
-        SDL_GamepadBinding *binding = &gamepad->bindings[i];
-        if (binding->output_type == SDL_GAMEPAD_BINDTYPE_BUTTON) {
-            if (binding->output.button == SDL_GAMEPAD_BUTTON_EAST) {
-                binding->output.button = SDL_GAMEPAD_BUTTON_WEST;
-            } else if (binding->output.button == SDL_GAMEPAD_BUTTON_WEST) {
-                binding->output.button = SDL_GAMEPAD_BUTTON_EAST;
-            }
-        }
-    }
-}
-
 /*
  * Make a new button mapping struct
  */
@@ -1509,10 +1516,6 @@ static void SDL_PrivateLoadButtonMapping(SDL_Gamepad *gamepad, GamepadMapping_t
         SDL_FixupHIDAPIMapping(gamepad);
     }
 
-    if (SDL_IsJoystickGameCube(SDL_GetGamepadVendor(gamepad), SDL_GetGamepadProduct(gamepad))) {
-        SDL_FixupGameCubeMapping(gamepad, pGamepadMapping->mapping);
-    }
-
     // Set the zero point for triggers
     for (i = 0; i < gamepad->num_bindings; ++i) {
         SDL_GamepadBinding *binding = &gamepad->bindings[i];
@@ -1983,7 +1986,37 @@ bool SDL_ReloadGamepadMappings(void)
     return true;
 }
 
-static char *SDL_ConvertMappingToPositional(const char *mapping)
+static char *SDL_ConvertMappingToPositionalAXBY(const char *mapping)
+{
+    // Add space for '!' and null terminator
+    size_t length = SDL_strlen(mapping) + 1 + 1;
+    char *remapped = (char *)SDL_malloc(length);
+    if (remapped) {
+        char *button_B;
+        char *button_X;
+        char *hint;
+
+        SDL_strlcpy(remapped, mapping, length);
+        button_B = SDL_strstr(remapped, ",b:");
+        button_X = SDL_strstr(remapped, ",x:");
+        hint = SDL_strstr(remapped, "hint:SDL_GAMECONTROLLER_USE_GAMECUBE_LABELS");
+
+        if (button_B) {
+            button_B[1] = 'x';
+        }
+        if (button_X) {
+            button_X[1] = 'b';
+        }
+        if (hint) {
+            hint += 5;
+            SDL_memmove(hint + 1, hint, SDL_strlen(hint) + 1);
+            *hint = '!';
+        }
+    }
+    return remapped;
+}
+
+static char *SDL_ConvertMappingToPositionalBAXY(const char *mapping)
 {
     // Add space for '!' and null terminator
     size_t length = SDL_strlen(mapping) + 1 + 1;
@@ -1996,23 +2029,23 @@ static char *SDL_ConvertMappingToPositional(const char *mapping)
         char *hint;
 
         SDL_strlcpy(remapped, mapping, length);
-        button_A = SDL_strstr(remapped, "a:");
-        button_B = SDL_strstr(remapped, "b:");
-        button_X = SDL_strstr(remapped, "x:");
-        button_Y = SDL_strstr(remapped, "y:");
+        button_A = SDL_strstr(remapped, ",a:");
+        button_B = SDL_strstr(remapped, ",b:");
+        button_X = SDL_strstr(remapped, ",x:");
+        button_Y = SDL_strstr(remapped, ",y:");
         hint = SDL_strstr(remapped, "hint:SDL_GAMECONTROLLER_USE_BUTTON_LABELS");
 
         if (button_A) {
-            *button_A = 'b';
+            button_A[1] = 'b';
         }
         if (button_B) {
-            *button_B = 'a';
+            button_B[1] = 'a';
         }
         if (button_X) {
-            *button_X = 'y';
+            button_X[1] = 'y';
         }
         if (button_Y) {
-            *button_Y = 'x';
+            button_Y[1] = 'x';
         }
         if (hint) {
             hint += 5;
@@ -2028,9 +2061,11 @@ static char *SDL_ConvertMappingToPositional(const char *mapping)
  */
 static int SDL_PrivateAddGamepadMapping(const char *mappingString, SDL_GamepadMappingPriority priority)
 {
+    char *appended = NULL;
     char *remapped = NULL;
     char *pchGUID;
-    SDL_GUID jGUID;
+    SDL_GUID guid;
+    Uint16 vendor, product;
     bool is_default_mapping = false;
     bool is_xinput_mapping = false;
     bool existing = false;
@@ -2044,6 +2079,28 @@ static int SDL_PrivateAddGamepadMapping(const char *mappingString, SDL_GamepadMa
         return -1;
     }
 
+    pchGUID = SDL_PrivateGetGamepadGUIDFromMappingString(mappingString);
+    if (!pchGUID) {
+        SDL_SetError("Couldn't parse GUID from %s", mappingString);
+        return -1;
+    }
+    if (!SDL_strcasecmp(pchGUID, "default")) {
+        is_default_mapping = true;
+    } else if (!SDL_strcasecmp(pchGUID, "xinput")) {
+        is_xinput_mapping = true;
+    }
+    guid = SDL_StringToGUID(pchGUID);
+    SDL_free(pchGUID);
+
+    SDL_GetJoystickGUIDInfo(guid, &vendor, &product, NULL, NULL);
+    if (SDL_IsJoystickGameCube(vendor, product) &&
+        SDL_strstr(mappingString, "SDL_GAMECONTROLLER_USE_GAMECUBE_LABELS") == NULL) {
+        SDL_asprintf(&appended, "%shint:SDL_GAMECONTROLLER_USE_GAMECUBE_LABELS:=1,", mappingString);
+        if (appended) {
+            mappingString = appended;
+        }
+    }
+
     { // Extract and verify the hint field
         const char *tmp;
 
@@ -2075,18 +2132,31 @@ static int SDL_PrivateAddGamepadMapping(const char *mappingString, SDL_GamepadMa
                 default_value = false;
             }
 
-            if (SDL_strcmp(hint, "SDL_GAMECONTROLLER_USE_BUTTON_LABELS") == 0) {
+            if (SDL_strcmp(hint, "SDL_GAMECONTROLLER_USE_GAMECUBE_LABELS") == 0) {
+                // This hint is used to signal whether the mapping uses positional buttons or not
+                if (negate) {
+                    // This mapping uses positional buttons, we can use it as-is
+                } else {
+                    // This mapping uses labeled buttons, we need to swap them to positional
+                    remapped = SDL_ConvertMappingToPositionalAXBY(mappingString);
+                    if (!remapped) {
+                        goto done;
+                    }
+                    mappingString = remapped;
+                }
+            } else if (SDL_strcmp(hint, "SDL_GAMECONTROLLER_USE_BUTTON_LABELS") == 0) {
                 // This hint is used to signal whether the mapping uses positional buttons or not
                 if (negate) {
                     // This mapping uses positional buttons, we can use it as-is
                 } else {
                     // This mapping uses labeled buttons, we need to swap them to positional
-                    remapped = SDL_ConvertMappingToPositional(mappingString);
+                    remapped = SDL_ConvertMappingToPositionalBAXY(mappingString);
                     if (!remapped) {
                         goto done;
                     }
                     mappingString = remapped;
                 }
+
             } else {
                 value = SDL_GetHintBoolean(hint, default_value);
                 if (negate) {
@@ -2123,20 +2193,7 @@ static int SDL_PrivateAddGamepadMapping(const char *mappingString, SDL_GamepadMa
     }
 #endif
 
-    pchGUID = SDL_PrivateGetGamepadGUIDFromMappingString(mappingString);
-    if (!pchGUID) {
-        SDL_SetError("Couldn't parse GUID from %s", mappingString);
-        goto done;
-    }
-    if (!SDL_strcasecmp(pchGUID, "default")) {
-        is_default_mapping = true;
-    } else if (!SDL_strcasecmp(pchGUID, "xinput")) {
-        is_xinput_mapping = true;
-    }
-    jGUID = SDL_StringToGUID(pchGUID);
-    SDL_free(pchGUID);
-
-    pGamepadMapping = SDL_PrivateAddMappingForGUID(jGUID, mappingString, &existing, priority);
+    pGamepadMapping = SDL_PrivateAddMappingForGUID(guid, mappingString, &existing, priority);
     if (!pGamepadMapping) {
         goto done;
     }
@@ -2152,9 +2209,8 @@ static int SDL_PrivateAddGamepadMapping(const char *mappingString, SDL_GamepadMa
         result = 1;
     }
 done:
-    if (remapped) {
-        SDL_free(remapped);
-    }
+    SDL_free(appended);
+    SDL_free(remapped);
     return result;
 }
 
diff --git a/src/joystick/SDL_joystick.c b/src/joystick/SDL_joystick.c
index 298b4aa65bf73..df6988909a35b 100644
--- a/src/joystick/SDL_joystick.c
+++ b/src/joystick/SDL_joystick.c
@@ -828,8 +828,6 @@ bool SDL_InitJoysticks(void)
 
     SDL_joysticks_initialized = true;
 
-    SDL_InitGamepadMappings();
-
     SDL_LoadVIDPIDList(&old_xboxone_controllers);
     SDL_LoadVIDPIDList(&arcadestick_devices);
     SDL_LoadVIDPIDList(&blacklist_devices);
@@ -840,6 +838,8 @@ bool SDL_InitJoysticks(void)
     SDL_LoadVIDPIDList(&wheel_devices);
     SDL_LoadVIDPIDList(&zero_centered_devices);
 
+    SDL_InitGamepadMappings();
+
     // See if we should allow joystick events while in the background
     SDL_AddHintCallback(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS,
                         SDL_JoystickAllowBackgroundEventsChanged, NULL);
diff --git a/test/testautomation_joystick.c b/test/testautomation_joystick.c
index 274ba79f6bcb7..bce6c7f2d1ba0 100644
--- a/test/testautomation_joystick.c
+++ b/test/testautomation_joystick.c
@@ -132,6 +132,23 @@ static int SDLCALL TestVirtualJoystick(void *arg)
                 SDL_UpdateJoysticks();
                 SDLTest_AssertCheck(SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_SOUTH) == false, "SDL_GetGamepadButton(SDL_GAMEPAD_BUTTON_SOUTH) == false");
 
+                /* Set an explicit mapping with legacy GameCube style buttons */
+                SDL_SetGamepadMapping(SDL_GetJoystickID(joystick), "ff0013db5669727475616c2043007601,Virtual Nintendo GameCube,a:b0,b:b1,x:b2,y:b3,back:b4,guide:b5,start:b6,leftstick:b7,rightstick:b8,leftshoulder:b9,rightshoulder:b10,dpup:b11,dpdown:b12,dpleft:b13,dpright:b14,misc1:b15,paddle1:b16,paddle2:b17,paddle3:b18,paddle4:b19,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:a4,righttrigger:a5,hint:SDL_GAMECONTROLLER_USE_GAMECUBE_LABELS:=1,");
+                {
+                    const char *name = SDL_GetGamepadName(gamepad);
+                    SDLTest_AssertCheck(name && SDL_strcmp(name, "Virtual Nintendo GameCube") == 0, "SDL_GetGamepadName() -> \"%s\" (expected \"%s\")", name, "Virtual Nintendo GameCube");
+                }
+                SDLTest_AssertCheck(SDL_GetGamepadButtonLabel(gamepad, SDL_GAMEPAD_BUTTON_EAST) == SDL_GAMEPAD_BUTTON_LABEL_X, "SDL_GetGamepadButtonLabel(SDL_GAMEPAD_BUTTON_EAST) == SDL_GAMEPAD_BUTTON_LABEL_X");
+
+                /* Set the east button and verify that the gamepad responds, mapping "B" to the west button */
+                SDLTest_AssertCheck(SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_EAST, true), "SDL_SetJoystickVirtualButton(SDL_GAMEPAD_BUTTON_EAST, true)");
+                SDL_UpdateJoysticks();
+                SDLTest_AssertCheck(SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_WEST) == true, "SDL_GetGamepadButton(SDL_GAMEPAD_BUTTON_WEST) == true");
+
+                SDLTest_AssertCheck(SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_EAST, false), "SDL_SetJoystickVirtualButton(SDL_GAMEPAD_BUTTON_EAST, false)");
+                SDL_UpdateJoysticks();
+                SDLTest_AssertCheck(SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_WEST) == false, "SDL_GetGamepadButton(SDL_GAMEPAD_BUTTON_WEST) == false");
+
                 /* Set an explicit mapping with legacy Nintendo style buttons */
                 SDL_SetGamepadMapping(SDL_GetJoystickID(joystick), "ff0013db5669727475616c2043007601,Virtual Nintendo Gamepad,a:b1,b:b0,x:b3,y:b2,back:b4,guide:b5,start:b6,leftstick:b7,rightstick:b8,leftshoulder:b9,rightshoulder:b10,dpup:b11,dpdown:b12,dpleft:b13,dpright:b14,misc1:b15,paddle1:b16,paddle2:b17,paddle3:b18,paddle4:b19,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:a4,righttrigger:a5,hint:SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,");
                 {
diff --git a/test/testcontroller.c b/test/testcontroller.c
index c15aa65ab7c16..b717b30eb9d4f 100644
--- a/test/testcontroller.c
+++ b/test/testcontroller.c
@@ -478,17 +478,6 @@ static void CommitBindingElement(const char *binding, bool force)
         mapping = NULL;
     }
 
-    if (mapping && SDL_GetGamepadType(controller->gamepad) == SDL_GAMEPAD_TYPE_GAMECUBE) {
-        if (SDL_strstr(mapping, "face:") == NULL) {
-            char *new_mapping = NULL;
-            SDL_asprintf(&new_mapping, "%sface:axby,", mapping);
-            if (new_mapping) {
-                SDL_free(mapping);
-                mapping = new_mapping;
-            }
-        }
-    }
-
     /* If the controller generates multiple events for a single element, pick the best one */
     if (!force && binding_advance_time) {
         char *current = GetElementBinding(mapping, binding_element);