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);