diff --git a/src/joystick/SDL_gamepad.c b/src/joystick/SDL_gamepad.c index a365254b3..689816abd 100644 --- a/src/joystick/SDL_gamepad.c +++ b/src/joystick/SDL_gamepad.c @@ -935,7 +935,7 @@ static GamepadMapping_t *SDL_PrivateGetGamepadMappingForGUID(SDL_JoystickGUID gu /* Try harder to get the best match, or create a mapping */ - if (vendor && product) { + if (SDL_JoystickGUIDUsesVersion(guid)) { /* Try again, ignoring the version */ if (crc) { mapping = SDL_PrivateMatchGamepadMappingForGUID(guid, SDL_TRUE, SDL_FALSE); @@ -1720,7 +1720,11 @@ static void SDL_PrivateAppendToMappingString(char *mapping_string, (void)SDL_snprintf(buffer, sizeof(buffer), "b%i", mapping->target); break; case EMappingKind_Axis: - (void)SDL_snprintf(buffer, sizeof(buffer), "a%i", mapping->target); + (void)SDL_snprintf(buffer, sizeof(buffer), "%sa%i%s", + mapping->half_axis_positive ? "+" : + mapping->half_axis_negative ? "-" : "", + mapping->target, + mapping->axis_reversed ? "~" : ""); break; case EMappingKind_Hat: (void)SDL_snprintf(buffer, sizeof(buffer), "h%i.%i", mapping->target >> 4, mapping->target & 0x0F); @@ -1780,6 +1784,7 @@ static GamepadMapping_t *SDL_PrivateGenerateAutomaticGamepadMapping(const char * SDL_PrivateAppendToMappingString(mapping, sizeof(mapping), "righty", &raw_map->righty); SDL_PrivateAppendToMappingString(mapping, sizeof(mapping), "lefttrigger", &raw_map->lefttrigger); SDL_PrivateAppendToMappingString(mapping, sizeof(mapping), "righttrigger", &raw_map->righttrigger); + SDL_PrivateAppendToMappingString(mapping, sizeof(mapping), "touchpad", &raw_map->touchpad); return SDL_PrivateAddMappingForGUID(guid, mapping, &existing, SDL_GAMEPAD_MAPPING_PRIORITY_DEFAULT); } diff --git a/src/joystick/SDL_joystick.c b/src/joystick/SDL_joystick.c index 664f05e7e..f26c3db20 100644 --- a/src/joystick/SDL_joystick.c +++ b/src/joystick/SDL_joystick.c @@ -2436,6 +2436,22 @@ SDL_GamepadType SDL_GetGamepadTypeFromGUID(SDL_JoystickGUID guid, const char *na return type; } +SDL_bool SDL_JoystickGUIDUsesVersion(SDL_JoystickGUID guid) +{ + Uint16 vendor, product; + + if (SDL_IsJoystickMFI(guid)) { + /* The version bits are used as button capability mask */ + return SDL_FALSE; + } + + SDL_GetJoystickGUIDInfo(guid, &vendor, &product, NULL, NULL); + if (vendor && product) { + return SDL_TRUE; + } + return SDL_FALSE; +} + SDL_bool SDL_IsJoystickXboxOne(Uint16 vendor_id, Uint16 product_id) { EControllerType eType = GuessControllerType(vendor_id, product_id); @@ -2656,6 +2672,11 @@ SDL_bool SDL_IsJoystickHIDAPI(SDL_JoystickGUID guid) return (guid.data[14] == 'h') ? SDL_TRUE : SDL_FALSE; } +SDL_bool SDL_IsJoystickMFI(SDL_JoystickGUID guid) +{ + return (guid.data[14] == 'm') ? SDL_TRUE : SDL_FALSE; +} + SDL_bool SDL_IsJoystickRAWINPUT(SDL_JoystickGUID guid) { return (guid.data[14] == 'r') ? SDL_TRUE : SDL_FALSE; diff --git a/src/joystick/SDL_joystick_c.h b/src/joystick/SDL_joystick_c.h index 8b37204d5..68424775b 100644 --- a/src/joystick/SDL_joystick_c.h +++ b/src/joystick/SDL_joystick_c.h @@ -80,6 +80,9 @@ extern void SDL_SetJoystickGUIDCRC(SDL_JoystickGUID *guid, Uint16 crc); extern SDL_GamepadType SDL_GetGamepadTypeFromVIDPID(Uint16 vendor, Uint16 product, const char *name, SDL_bool forUI); extern SDL_GamepadType SDL_GetGamepadTypeFromGUID(SDL_JoystickGUID guid, const char *name); +/* Function to return whether a joystick GUID uses the version field */ +extern SDL_bool SDL_JoystickGUIDUsesVersion(SDL_JoystickGUID guid); + /* Function to return whether a joystick is an Xbox One controller */ extern SDL_bool SDL_IsJoystickXboxOne(Uint16 vendor_id, Uint16 product_id); @@ -132,6 +135,9 @@ extern SDL_bool SDL_IsJoystickWGI(SDL_JoystickGUID guid); /* Function to return whether a joystick guid comes from the HIDAPI driver */ extern SDL_bool SDL_IsJoystickHIDAPI(SDL_JoystickGUID guid); +/* Function to return whether a joystick guid comes from the MFI driver */ +extern SDL_bool SDL_IsJoystickMFI(SDL_JoystickGUID guid); + /* Function to return whether a joystick guid comes from the RAWINPUT driver */ extern SDL_bool SDL_IsJoystickRAWINPUT(SDL_JoystickGUID guid); @@ -166,16 +172,19 @@ extern SDL_bool SDL_IsJoystickValid(SDL_Joystick *joystick); typedef enum { - EMappingKind_None = 0, - EMappingKind_Button = 1, - EMappingKind_Axis = 2, - EMappingKind_Hat = 3 + EMappingKind_None, + EMappingKind_Button, + EMappingKind_Axis, + EMappingKind_Hat, } EMappingKind; typedef struct SDL_InputMapping { EMappingKind kind; Uint8 target; + SDL_bool axis_reversed; + SDL_bool half_axis_positive; + SDL_bool half_axis_negative; } SDL_InputMapping; typedef struct SDL_GamepadMapping @@ -206,6 +215,7 @@ typedef struct SDL_GamepadMapping SDL_InputMapping righty; SDL_InputMapping lefttrigger; SDL_InputMapping righttrigger; + SDL_InputMapping touchpad; } SDL_GamepadMapping; /* Function to get autodetected gamepad controller mapping from the driver */ diff --git a/src/joystick/apple/SDL_mfijoystick.m b/src/joystick/apple/SDL_mfijoystick.m index 624a8a0fe..b3929f192 100644 --- a/src/joystick/apple/SDL_mfijoystick.m +++ b/src/joystick/apple/SDL_mfijoystick.m @@ -231,6 +231,78 @@ static BOOL IsControllerBackboneOne(GCController *controller) } return FALSE; } +static BOOL IsControllerSiriRemote(GCController *controller) +{ + if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) { + if ([controller.productCategory hasPrefix:@"Siri Remote"]) { + return TRUE; + } + } + return FALSE; +} + +static BOOL ElementAlreadyHandled(NSString *element, NSDictionary *elements) +{ + if ([element isEqualToString:@"Left Thumbstick Left"] || + [element isEqualToString:@"Left Thumbstick Right"]) { + if (elements[@"Left Thumbstick X Axis"]) { + return TRUE; + } + } + if ([element isEqualToString:@"Left Thumbstick Up"] || + [element isEqualToString:@"Left Thumbstick Down"]) { + if (elements[@"Left Thumbstick Y Axis"]) { + return TRUE; + } + } + if ([element isEqualToString:@"Right Thumbstick Left"] || + [element isEqualToString:@"Right Thumbstick Right"]) { + if (elements[@"Right Thumbstick X Axis"]) { + return TRUE; + } + } + if ([element isEqualToString:@"Right Thumbstick Up"] || + [element isEqualToString:@"Right Thumbstick Down"]) { + if (elements[@"Right Thumbstick Y Axis"]) { + return TRUE; + } + } + if ([element isEqualToString:@"Direction Pad X Axis"]) { + if (elements[@"Direction Pad Left"] && + elements[@"Direction Pad Right"]) { + return TRUE; + } + } + if ([element isEqualToString:@"Direction Pad Y Axis"]) { + if (elements[@"Direction Pad Up"] && + elements[@"Direction Pad Down"]) { + return TRUE; + } + } + if ([element isEqualToString:@"Touchpad 1 X Axis"] || + [element isEqualToString:@"Touchpad 1 Y Axis"] || + [element isEqualToString:@"Touchpad 1 Left"] || + [element isEqualToString:@"Touchpad 1 Right"] || + [element isEqualToString:@"Touchpad 1 Up"] || + [element isEqualToString:@"Touchpad 1 Down"] || + [element isEqualToString:@"Touchpad 2 X Axis"] || + [element isEqualToString:@"Touchpad 2 Y Axis"] || + [element isEqualToString:@"Touchpad 2 Left"] || + [element isEqualToString:@"Touchpad 2 Right"] || + [element isEqualToString:@"Touchpad 2 Up"] || + [element isEqualToString:@"Touchpad 2 Down"]) { + /* The touchpad is handled separately */ + return TRUE; + } +#if TARGET_OS_TV + if ([element isEqualToString:GCInputButtonHome]) { + /* The OS uses the home button, it's not available to apps */ + return TRUE; + } +#endif + return FALSE; +} + static BOOL IOS_AddMFIJoystickDevice(SDL_JoystickDeviceItem *device, GCController *controller) { Uint16 vendor = 0; @@ -259,13 +331,20 @@ static BOOL IOS_AddMFIJoystickDevice(SDL_JoystickDeviceItem *device, GCControlle device->name = SDL_CreateJoystickName(0, 0, NULL, name); #ifdef DEBUG_CONTROLLER_PROFILE + NSLog(@"Product name: %@\n", controller.vendorName); + NSLog(@"Product category: %@\n", controller.productCategory); + NSLog(@"Elements available:\n"); if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) { if (controller.physicalInputProfile) { + NSDictionary *elements = controller.physicalInputProfile.elements; for (id key in controller.physicalInputProfile.buttons) { - NSLog(@"Button %@ available\n", key); + NSLog(@"\tButton: %@ (%s)\n", key, elements[key].analog ? "analog" : "digital"); } for (id key in controller.physicalInputProfile.axes) { - NSLog(@"Axis %@ available\n", key); + NSLog(@"\tAxis: %@\n", key); + } + for (id key in controller.physicalInputProfile.dpads) { + NSLog(@"\tHat: %@\n", key); } } } @@ -297,6 +376,21 @@ static BOOL IOS_AddMFIJoystickDevice(SDL_JoystickDeviceItem *device, GCControlle (void)is_switch_joyconL; (void)is_switch_joyconR; #endif + device->is_siri_remote = IsControllerSiriRemote(controller); + +#ifdef ENABLE_PHYSICAL_INPUT_PROFILE + if ([controller respondsToSelector:@selector(physicalInputProfile)]) { + if (controller.physicalInputProfile.buttons[GCInputDualShockTouchpadButton] != nil) { + device->has_dualshock_touchpad = SDL_TRUE; + } + if (controller.physicalInputProfile.buttons[GCInputXboxPaddleOne] != nil) { + device->has_xbox_paddles = SDL_TRUE; + } + if (controller.physicalInputProfile.buttons[GCInputXboxShareButton] != nil) { + device->has_xbox_share_button = SDL_TRUE; + } + } +#endif // ENABLE_PHYSICAL_INPUT_PROFILE if (is_backbone_one) { vendor = USB_VENDOR_BACKBONE; @@ -305,21 +399,17 @@ static BOOL IOS_AddMFIJoystickDevice(SDL_JoystickDeviceItem *device, GCControlle } else { product = USB_PRODUCT_BACKBONE_ONE_IOS; } - subtype = 0; } else if (is_xbox) { vendor = USB_VENDOR_MICROSOFT; if (device->has_xbox_paddles) { /* Assume Xbox One Elite Series 2 Controller unless/until GCController flows VID/PID */ product = USB_PRODUCT_XBOX_ONE_ELITE_SERIES_2_BLUETOOTH; - subtype = 1; } else if (device->has_xbox_share_button) { /* Assume Xbox Series X Controller unless/until GCController flows VID/PID */ product = USB_PRODUCT_XBOX_SERIES_X_BLE; - subtype = 1; } else { /* Assume Xbox One S Bluetooth Controller unless/until GCController flows VID/PID */ product = USB_PRODUCT_XBOX_ONE_S_REV1_BLUETOOTH; - subtype = 0; } } else if (is_ps4) { /* Assume DS4 Slim unless/until GCController flows VID/PID */ @@ -327,29 +417,32 @@ static BOOL IOS_AddMFIJoystickDevice(SDL_JoystickDeviceItem *device, GCControlle product = USB_PRODUCT_SONY_DS4_SLIM; if (device->has_dualshock_touchpad) { subtype = 1; - } else { - subtype = 0; } } else if (is_ps5) { vendor = USB_VENDOR_SONY; product = USB_PRODUCT_SONY_DS5; - subtype = 0; } else if (is_switch_pro) { vendor = USB_VENDOR_NINTENDO; product = USB_PRODUCT_NINTENDO_SWITCH_PRO; - subtype = 0; + device->has_nintendo_buttons = SDL_TRUE; } else if (is_switch_joycon_pair) { vendor = USB_VENDOR_NINTENDO; product = USB_PRODUCT_NINTENDO_SWITCH_JOYCON_PAIR; - subtype = 0; + device->has_nintendo_buttons = SDL_TRUE; } else if (is_switch_joyconL) { vendor = USB_VENDOR_NINTENDO; product = USB_PRODUCT_NINTENDO_SWITCH_JOYCON_LEFT; - subtype = 0; + device->is_single_joycon = SDL_TRUE; } else if (is_switch_joyconR) { vendor = USB_VENDOR_NINTENDO; product = USB_PRODUCT_NINTENDO_SWITCH_JOYCON_RIGHT; - subtype = 0; + device->is_single_joycon = SDL_TRUE; +#ifdef ENABLE_PHYSICAL_INPUT_PROFILE + } else if ([controller respondsToSelector:@selector(physicalInputProfile)]) { + vendor = USB_VENDOR_APPLE; + product = 4; + subtype = 4; +#endif } else if (controller.extendedGamepad) { vendor = USB_VENDOR_APPLE; product = 1; @@ -363,12 +456,6 @@ static BOOL IOS_AddMFIJoystickDevice(SDL_JoystickDeviceItem *device, GCControlle vendor = USB_VENDOR_APPLE; product = 3; subtype = 3; -#endif -#ifdef ENABLE_PHYSICAL_INPUT_PROFILE - } else if ([controller respondsToSelector:@selector(physicalInputProfile)]) { - vendor = USB_VENDOR_APPLE; - product = 4; - subtype = 4; #endif } else { vendor = USB_VENDOR_APPLE; @@ -376,6 +463,61 @@ static BOOL IOS_AddMFIJoystickDevice(SDL_JoystickDeviceItem *device, GCControlle subtype = 5; } +#ifdef ENABLE_PHYSICAL_INPUT_PROFILE + if ([controller respondsToSelector:@selector(physicalInputProfile)]) { + NSDictionary *elements = controller.physicalInputProfile.elements; + + /* Provide both axes and analog buttons as SDL axes */ + device->use_physical_profile = SDL_TRUE; + device->axes = [[[elements allKeys] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)] + filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id object, NSDictionary *bindings) { + if (ElementAlreadyHandled((NSString *)object, elements)) { + return NO; + } + + GCControllerElement *element = elements[object]; + if (element.analog) { + if ([element isKindOfClass:[GCControllerAxisInput class]] || + [element isKindOfClass:[GCControllerButtonInput class]]) { + return YES; + } + } + return NO; + }]]; + device->naxes = device->axes.count; + device->buttons = [[[elements allKeys] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)] + filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id object, NSDictionary *bindings) { + if (ElementAlreadyHandled((NSString *)object, elements)) { + return NO; + } + + GCControllerElement *element = elements[object]; + if ([element isKindOfClass:[GCControllerButtonInput class]]) { + return YES; + } + return NO; + }]]; + device->nbuttons = device->buttons.count; + subtype = 4; + +#ifdef DEBUG_CONTROLLER_PROFILE + NSLog(@"Elements used:\n", controller.vendorName); + for (id key in device->buttons) { + NSLog(@"\tButton: %@ (%s)\n", key, elements[key].analog ? "analog" : "digital"); + } + for (id key in device->axes) { + NSLog(@"\tAxis: %@\n", key); + } +#endif /* DEBUG_CONTROLLER_PROFILE */ + +#if TARGET_OS_TV + /* tvOS turns the menu button into a system gesture, so we grab it here instead */ + if (elements[GCInputButtonMenu] && !elements[GCInputButtonHome]) { + device->pause_button_index = [device->buttons indexOfObject:GCInputButtonMenu]; + } +#endif + } else +#endif if (controller.extendedGamepad) { GCExtendedGamepad *gamepad = controller.extendedGamepad; int nbuttons = 0; @@ -415,12 +557,12 @@ static BOOL IOS_AddMFIJoystickDevice(SDL_JoystickDeviceItem *device, GCControlle has_direct_menu = [gamepad respondsToSelector:@selector(buttonMenu)] && gamepad.buttonMenu; if (!has_direct_menu) { - device->uses_pause_handler = SDL_TRUE; + device->pause_button_index = (nbuttons - 1); } #if TARGET_OS_TV /* The single menu button isn't very reliable, at least as of tvOS 16.1 */ if ((device->button_mask & (1 << SDL_GAMEPAD_BUTTON_BACK)) == 0) { - device->uses_pause_handler = SDL_TRUE; + device->pause_button_index = (nbuttons - 1); } #endif @@ -437,15 +579,13 @@ static BOOL IOS_AddMFIJoystickDevice(SDL_JoystickDeviceItem *device, GCControlle ++nbuttons; } if (controller.physicalInputProfile.buttons[GCInputXboxPaddleTwo] != nil) { - /* TODO: Is this right? SDL_gamepad.h says P2 is the lower right */ device->has_xbox_paddles = SDL_TRUE; - device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_LEFT_PADDLE1); + device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2); ++nbuttons; } if (controller.physicalInputProfile.buttons[GCInputXboxPaddleThree] != nil) { - /* TODO: Is this right? SDL_gamepad.h says P3 is the upper left */ device->has_xbox_paddles = SDL_TRUE; - device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2); + device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_LEFT_PADDLE1); ++nbuttons; } if (controller.physicalInputProfile.buttons[GCInputXboxPaddleFour] != nil) { @@ -485,14 +625,9 @@ static BOOL IOS_AddMFIJoystickDevice(SDL_JoystickDeviceItem *device, GCControlle device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_NORTH); device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_LEFT_SHOULDER); device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER); -#if TARGET_OS_TV - /* The menu button is used by the OS and not available to applications */ - nbuttons += 6; -#else device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_START); nbuttons += 7; - device->uses_pause_handler = SDL_TRUE; -#endif + device->pause_button_index = (nbuttons - 1); device->naxes = 0; /* no traditional analog inputs */ device->nhats = 1; /* d-pad */ @@ -504,11 +639,9 @@ static BOOL IOS_AddMFIJoystickDevice(SDL_JoystickDeviceItem *device, GCControlle device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_SOUTH); device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_EAST); /* Button X on microGamepad */ - nbuttons += 2; - device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_START); - ++nbuttons; - device->uses_pause_handler = SDL_TRUE; + nbuttons += 3; + device->pause_button_index = (nbuttons - 1); device->naxes = 2; /* treat the touch surface as two axes */ device->nhats = 0; /* apparently the touch surface-as-dpad is buggy */ @@ -516,35 +649,32 @@ static BOOL IOS_AddMFIJoystickDevice(SDL_JoystickDeviceItem *device, GCControlle controller.microGamepad.allowsRotation = SDL_GetHintBoolean(SDL_HINT_APPLE_TV_REMOTE_ALLOW_ROTATION, SDL_FALSE); } -#endif -#ifdef ENABLE_PHYSICAL_INPUT_PROFILE - else if ([controller respondsToSelector:@selector(physicalInputProfile)]) { - device->use_physical_profile = SDL_TRUE; - device->axes = [controller.physicalInputProfile.axes allKeys]; - device->naxes = controller.physicalInputProfile.axes.count; - device->dpads = [controller.physicalInputProfile.dpads allKeys]; - device->nhats = controller.physicalInputProfile.dpads.count; - device->buttons = [controller.physicalInputProfile.buttons allKeys]; - device->nbuttons = controller.physicalInputProfile.buttons.count; - subtype = 4; - } #endif else { /* We can't detect any inputs on this */ return SDL_FALSE; } + Uint16 signature; + if (device->use_physical_profile) { + signature = 0; + signature = SDL_crc16(signature, device->name, SDL_strlen(device->name)); + for (id key in device->axes) { + const char *string = ((NSString *)key).UTF8String; + signature = SDL_crc16(signature, string, SDL_strlen(string)); + } + for (id key in device->buttons) { + const char *string = ((NSString *)key).UTF8String; + signature = SDL_crc16(signature, string, SDL_strlen(string)); + } + } else { + signature = device->button_mask; + } if (vendor == USB_VENDOR_APPLE) { /* Note that this is an MFI controller and what subtype it is */ - device->guid = SDL_CreateJoystickGUID(SDL_HARDWARE_BUS_BLUETOOTH, vendor, product, 0, name, 'm', subtype); + device->guid = SDL_CreateJoystickGUID(SDL_HARDWARE_BUS_BLUETOOTH, vendor, product, signature, name, 'm', subtype); } else { - device->guid = SDL_CreateJoystickGUID(SDL_HARDWARE_BUS_BLUETOOTH, vendor, product, 0, name, 0, subtype); - } - - /* Update the GUID with capability bits */ - { - Uint16 *guid16 = (Uint16 *)device->guid.data; - guid16[6] = SDL_SwapLE16(device->button_mask); + device->guid = SDL_CreateJoystickGUID(SDL_HARDWARE_BUS_BLUETOOTH, vendor, product, signature, name, 0, subtype); } /* This will be set when the first button press of the controller is @@ -582,6 +712,7 @@ static void IOS_AddJoystickDevice(GCController *controller, SDL_bool acceleromet device->accelerometer = accelerometer; device->instance_id = SDL_GetNextObjectID(); + device->pause_button_index = -1; if (accelerometer) { #ifdef SDL_JOYSTICK_iOS_ACCELEROMETER @@ -864,11 +995,11 @@ static int IOS_JoystickOpen(SDL_Joystick *joystick, int device_index) #endif } else { #ifdef SDL_JOYSTICK_MFI - if (device->uses_pause_handler) { + if (device->pause_button_index >= 0) { GCController *controller = device->controller; controller.controllerPausedHandler = ^(GCController *c) { if (joystick->hwdata) { - ++joystick->hwdata->num_pause_presses; + joystick->hwdata->pause_button_pressed = SDL_GetTicks(); } }; } @@ -901,7 +1032,7 @@ static int IOS_JoystickOpen(SDL_Joystick *joystick, int device_index) #endif /* SDL_JOYSTICK_MFI */ } } - if (device->remote) { + if (device->is_siri_remote) { ++SDL_AppleTVRemoteOpenedAsJoystick; } @@ -985,9 +1116,9 @@ static void IOS_MFIJoystickUpdate(SDL_Joystick *joystick) GCController *controller = device->controller; Uint8 hatstate = SDL_HAT_CENTERED; int i; - int pause_button_index = 0; Uint64 timestamp = SDL_GetTicksNS(); +#define DEBUG_CONTROLLER_STATE #ifdef DEBUG_CONTROLLER_STATE if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) { if (controller.physicalInputProfile) { @@ -1016,23 +1147,31 @@ static void IOS_MFIJoystickUpdate(SDL_Joystick *joystick) #endif if (device->use_physical_profile) { + NSDictionary *elements = controller.physicalInputProfile.elements; + NSDictionary *buttons = controller.physicalInputProfile.buttons; + int axis = 0; for (id key in device->axes) { - Sint16 value = (Sint16)(controller.physicalInputProfile.axes[key].value * 32767); + Sint16 value; + GCControllerElement *element = elements[key]; + if ([element isKindOfClass:[GCControllerAxisInput class]]) { + value = (Sint16)([(GCControllerAxisInput *)element value] * 32767); + } else { + value = (Sint16)([(GCControllerButtonInput *)element value] * 32767); + } SDL_SendJoystickAxis(timestamp, joystick, axis++, value); } int button = 0; for (id key in device->buttons) { - Uint8 value = controller.physicalInputProfile.buttons[key].isPressed; + Uint8 value; + if (button == device->pause_button_index) { + value = (device->pause_button_pressed > 0); + } else { + value = buttons[key].isPressed; + } SDL_SendJoystickButton(timestamp, joystick, button++, value); } - - int hat = 0; - for (id key in device->dpads) { - hatstate = IOS_MFIJoystickHatStateForDPad(controller.physicalInputProfile.dpads[key]); - SDL_SendJoystickHat(timestamp, joystick, hat++, hatstate); - } } else if (controller.extendedGamepad) { SDL_bool isstack; GCExtendedGamepad *gamepad = controller.extendedGamepad; @@ -1079,11 +1218,9 @@ static void IOS_MFIJoystickUpdate(SDL_Joystick *joystick) if (device->button_mask & (1 << SDL_GAMEPAD_BUTTON_GUIDE)) { buttons[button_count++] = gamepad.buttonHome.isPressed; } - /* This must be the last button, so we can optionally handle it with pause_button_index below */ if (device->button_mask & (1 << SDL_GAMEPAD_BUTTON_START)) { - if (device->uses_pause_handler) { - pause_button_index = button_count; - buttons[button_count++] = joystick->delayed_guide_button; + if (device->pause_button_index >= 0) { + buttons[button_count++] = (device->pause_button_pressed > 0); } else { buttons[button_count++] = gamepad.buttonMenu.isPressed; } @@ -1091,45 +1228,22 @@ static void IOS_MFIJoystickUpdate(SDL_Joystick *joystick) #ifdef ENABLE_PHYSICAL_INPUT_PROFILE if (device->has_dualshock_touchpad) { - GCControllerDirectionPad *dpad; buttons[button_count++] = controller.physicalInputProfile.buttons[GCInputDualShockTouchpadButton].isPressed; - - dpad = controller.physicalInputProfile.dpads[GCInputDualShockTouchpadOne]; - if (dpad.xAxis.value || dpad.yAxis.value) { - SDL_SendJoystickTouchpad(timestamp, joystick, 0, 0, SDL_PRESSED, (1.0f + dpad.xAxis.value) * 0.5f, 1.0f - (1.0f + dpad.yAxis.value) * 0.5f, 1.0f); - } else { - SDL_SendJoystickTouchpad(timestamp, joystick, 0, 0, SDL_RELEASED, 0.0f, 0.0f, 1.0f); - } - - dpad = controller.physicalInputProfile.dpads[GCInputDualShockTouchpadTwo]; - if (dpad.xAxis.value || dpad.yAxis.value) { - SDL_SendJoystickTouchpad(timestamp, joystick, 0, 1, SDL_PRESSED, (1.0f + dpad.xAxis.value) * 0.5f, 1.0f - (1.0f + dpad.yAxis.value) * 0.5f, 1.0f); - } else { - SDL_SendJoystickTouchpad(timestamp, joystick, 0, 1, SDL_RELEASED, 0.0f, 0.0f, 1.0f); - } } if (device->has_xbox_paddles) { if (device->button_mask & (1 << SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1)) { buttons[button_count++] = controller.physicalInputProfile.buttons[GCInputXboxPaddleOne].isPressed; } - if (device->button_mask & (1 << SDL_GAMEPAD_BUTTON_LEFT_PADDLE1)) { + if (device->button_mask & (1 << SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2)) { buttons[button_count++] = controller.physicalInputProfile.buttons[GCInputXboxPaddleTwo].isPressed; } - if (device->button_mask & (1 << SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2)) { + if (device->button_mask & (1 << SDL_GAMEPAD_BUTTON_LEFT_PADDLE1)) { buttons[button_count++] = controller.physicalInputProfile.buttons[GCInputXboxPaddleThree].isPressed; } if (device->button_mask & (1 << SDL_GAMEPAD_BUTTON_LEFT_PADDLE2)) { buttons[button_count++] = controller.physicalInputProfile.buttons[GCInputXboxPaddleFour].isPressed; } - - /* - SDL_Log("Paddles: [%d,%d,%d,%d]", - controller.physicalInputProfile.buttons[GCInputXboxPaddleOne].isPressed, - controller.physicalInputProfile.buttons[GCInputXboxPaddleTwo].isPressed, - controller.physicalInputProfile.buttons[GCInputXboxPaddleThree].isPressed, - controller.physicalInputProfile.buttons[GCInputXboxPaddleFour].isPressed); - */ } if (device->has_xbox_share_button) { @@ -1148,30 +1262,6 @@ static void IOS_MFIJoystickUpdate(SDL_Joystick *joystick) SDL_SendJoystickButton(timestamp, joystick, i, buttons[i]); } -#ifdef ENABLE_MFI_SENSORS - if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) { - GCMotion *motion = controller.motion; - if (motion && motion.sensorsActive) { - float data[3]; - - if (motion.hasRotationRate) { - GCRotationRate rate = motion.rotationRate; - data[0] = rate.x; - data[1] = rate.z; - data[2] = -rate.y; - SDL_SendJoystickSensor(timestamp, joystick, SDL_SENSOR_GYRO, timestamp, data, 3); - } - if (motion.hasGravityAndUserAcceleration) { - GCAcceleration accel = motion.acceleration; - data[0] = -accel.x * SDL_STANDARD_GRAVITY; - data[1] = -accel.y * SDL_STANDARD_GRAVITY; - data[2] = -accel.z * SDL_STANDARD_GRAVITY; - SDL_SendJoystickSensor(timestamp, joystick, SDL_SENSOR_ACCEL, timestamp, data, 3); - } - } - } -#endif /* ENABLE_MFI_SENSORS */ - SDL_small_free(buttons, isstack); } else if (controller.gamepad) { SDL_bool isstack; @@ -1192,8 +1282,7 @@ static void IOS_MFIJoystickUpdate(SDL_Joystick *joystick) buttons[button_count++] = gamepad.buttonY.isPressed; buttons[button_count++] = gamepad.leftShoulder.isPressed; buttons[button_count++] = gamepad.rightShoulder.isPressed; - pause_button_index = button_count; - buttons[button_count++] = joystick->delayed_guide_button; + buttons[button_count++] = (device->pause_button_pressed > 0); hatstate = IOS_MFIJoystickHatStateForDPad(gamepad.dpad); @@ -1220,18 +1309,7 @@ static void IOS_MFIJoystickUpdate(SDL_Joystick *joystick) int button_count = 0; buttons[button_count++] = gamepad.buttonA.isPressed; buttons[button_count++] = gamepad.buttonX.isPressed; -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wunguarded-availability-new" - /* This must be the last button, so we can optionally handle it with pause_button_index below */ - if (device->button_mask & (1 << SDL_GAMEPAD_BUTTON_START)) { - if (device->uses_pause_handler) { - pause_button_index = button_count; - buttons[button_count++] = joystick->delayed_guide_button; - } else { - buttons[button_count++] = gamepad.buttonMenu.isPressed; - } - } -#pragma clang diagnostic pop + buttons[button_count++] = (device->pause_button_pressed > 0); for (i = 0; i < button_count; i++) { SDL_SendJoystickButton(timestamp, joystick, i, buttons[i]); @@ -1239,18 +1317,62 @@ static void IOS_MFIJoystickUpdate(SDL_Joystick *joystick) } #endif /* TARGET_OS_TV */ - if (joystick->nhats > 0 && !device->use_physical_profile) { + if (joystick->nhats > 0) { SDL_SendJoystickHat(timestamp, joystick, 0, hatstate); } - if (device->uses_pause_handler) { - for (i = 0; i < device->num_pause_presses; i++) { - SDL_SendJoystickButton(timestamp, joystick, pause_button_index, SDL_PRESSED); - SDL_SendJoystickButton(timestamp, joystick, pause_button_index, SDL_RELEASED); + if (device->pause_button_pressed) { + /* The pause callback is instantaneous, so we extend the duration to allow "holding down" by pressing it repeatedly */ + const int PAUSE_BUTTON_PRESS_DURATION_MS = 250; + if (SDL_GetTicks() >= device->pause_button_pressed + PAUSE_BUTTON_PRESS_DURATION_MS) { + device->pause_button_pressed = 0; } - device->num_pause_presses = 0; } +#ifdef ENABLE_PHYSICAL_INPUT_PROFILE + if (device->has_dualshock_touchpad) { + GCControllerDirectionPad *dpad; + + dpad = controller.physicalInputProfile.dpads[GCInputDualShockTouchpadOne]; + if (dpad.xAxis.value || dpad.yAxis.value) { + SDL_SendJoystickTouchpad(timestamp, joystick, 0, 0, SDL_PRESSED, (1.0f + dpad.xAxis.value) * 0.5f, 1.0f - (1.0f + dpad.yAxis.value) * 0.5f, 1.0f); + } else { + SDL_SendJoystickTouchpad(timestamp, joystick, 0, 0, SDL_RELEASED, 0.0f, 0.0f, 1.0f); + } + + dpad = controller.physicalInputProfile.dpads[GCInputDualShockTouchpadTwo]; + if (dpad.xAxis.value || dpad.yAxis.value) { + SDL_SendJoystickTouchpad(timestamp, joystick, 0, 1, SDL_PRESSED, (1.0f + dpad.xAxis.value) * 0.5f, 1.0f - (1.0f + dpad.yAxis.value) * 0.5f, 1.0f); + } else { + SDL_SendJoystickTouchpad(timestamp, joystick, 0, 1, SDL_RELEASED, 0.0f, 0.0f, 1.0f); + } + } +#endif /* ENABLE_PHYSICAL_INPUT_PROFILE */ + +#ifdef ENABLE_MFI_SENSORS + if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) { + GCMotion *motion = controller.motion; + if (motion && motion.sensorsActive) { + float data[3]; + + if (motion.hasRotationRate) { + GCRotationRate rate = motion.rotationRate; + data[0] = rate.x; + data[1] = rate.z; + data[2] = -rate.y; + SDL_SendJoystickSensor(timestamp, joystick, SDL_SENSOR_GYRO, timestamp, data, 3); + } + if (motion.hasGravityAndUserAcceleration) { + GCAcceleration accel = motion.acceleration; + data[0] = -accel.x * SDL_STANDARD_GRAVITY; + data[1] = -accel.y * SDL_STANDARD_GRAVITY; + data[2] = -accel.z * SDL_STANDARD_GRAVITY; + SDL_SendJoystickSensor(timestamp, joystick, SDL_SENSOR_ACCEL, timestamp, data, 3); + } + } + } +#endif /* ENABLE_MFI_SENSORS */ + #ifdef ENABLE_MFI_BATTERY if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) { GCDeviceBattery *battery = controller.battery; @@ -1698,7 +1820,7 @@ static void IOS_JoystickClose(SDL_Joystick *joystick) #endif /* SDL_JOYSTICK_MFI */ } } - if (device->remote) { + if (device->is_siri_remote) { --SDL_AppleTVRemoteOpenedAsJoystick; } } @@ -1739,7 +1861,138 @@ static void IOS_JoystickQuit(void) static SDL_bool IOS_JoystickGetGamepadMapping(int device_index, SDL_GamepadMapping *out) { - return SDL_FALSE; + SDL_JoystickDeviceItem *device = GetDeviceForIndex(device_index); + if (device == NULL) { + return SDL_FALSE; + } + + if (!device->use_physical_profile) { + return SDL_FALSE; + } + + int axis = 0; + for (id key in device->axes) { + if ([(NSString *)key isEqualToString:@"Left Thumbstick X Axis"]) { + out->leftx.kind = EMappingKind_Axis; + out->leftx.target = axis; + } else if ([(NSString *)key isEqualToString:@"Left Thumbstick Y Axis"]) { + out->lefty.kind = EMappingKind_Axis; + out->lefty.target = axis; + out->lefty.axis_reversed = SDL_TRUE; + } else if ([(NSString *)key isEqualToString:@"Right Thumbstick X Axis"]) { + out->rightx.kind = EMappingKind_Axis; + out->rightx.target = axis; + } else if ([(NSString *)key isEqualToString:@"Right Thumbstick Y Axis"]) { + out->righty.kind = EMappingKind_Axis; + out->righty.target = axis; + out->righty.axis_reversed = SDL_TRUE; + } else if ([(NSString *)key isEqualToString:GCInputLeftTrigger]) { + out->lefttrigger.kind = EMappingKind_Axis; + out->lefttrigger.target = axis; + out->lefttrigger.half_axis_positive = SDL_TRUE; + } else if ([(NSString *)key isEqualToString:GCInputRightTrigger]) { + out->righttrigger.kind = EMappingKind_Axis; + out->righttrigger.target = axis; + out->righttrigger.half_axis_positive = SDL_TRUE; + } else if ([(NSString *)key isEqualToString:@"Direction Pad Left"] && device->is_siri_remote) { + out->dpleft.kind = EMappingKind_Axis; + out->dpleft.target = axis; + out->dpleft.half_axis_positive = SDL_TRUE; + } else if ([(NSString *)key isEqualToString:@"Direction Pad Right"] && device->is_siri_remote) { + out->dpright.kind = EMappingKind_Axis; + out->dpright.target = axis; + out->dpright.half_axis_positive = SDL_TRUE; + } else if ([(NSString *)key isEqualToString:@"Direction Pad Up"] && device->is_siri_remote) { + out->dpup.kind = EMappingKind_Axis; + out->dpup.target = axis; + out->dpup.half_axis_positive = SDL_TRUE; + } else if ([(NSString *)key isEqualToString:@"Direction Pad Down"] && device->is_siri_remote) { + out->dpdown.kind = EMappingKind_Axis; + out->dpdown.target = axis; + out->dpdown.half_axis_positive = SDL_TRUE; + } + ++axis; + } + + int button = 0; + for (id key in device->buttons) { + SDL_InputMapping *mapping = NULL; + + if ([(NSString *)key isEqualToString:GCInputButtonA]) { + if (device->has_nintendo_buttons) { + mapping = &out->b; + } else { + mapping = &out->a; + } + } else if ([(NSString *)key isEqualToString:GCInputButtonB]) { + if (device->has_nintendo_buttons) { + mapping = &out->a; + } else if (device->is_single_joycon) { + mapping = &out->x; + } else { + mapping = &out->b; + } + } else if ([(NSString *)key isEqualToString:GCInputButtonX]) { + if (device->has_nintendo_buttons) { + mapping = &out->y; + } else if (device->is_single_joycon) { + mapping = &out->b; + } else { + mapping = &out->x; + } + } else if ([(NSString *)key isEqualToString:GCInputButtonY]) { + if (device->has_nintendo_buttons) { + mapping = &out->x; + } else { + mapping = &out->y; + } + } else if ([(NSString *)key isEqualToString:@"Direction Pad Left"] && !device->is_siri_remote) { + mapping = &out->dpleft; + } else if ([(NSString *)key isEqualToString:@"Direction Pad Right"] && !device->is_siri_remote) { + mapping = &out->dpright; + } else if ([(NSString *)key isEqualToString:@"Direction Pad Up"] && !device->is_siri_remote) { + mapping = &out->dpup; + } else if ([(NSString *)key isEqualToString:@"Direction Pad Down"] && !device->is_siri_remote) { + mapping = &out->dpdown; + } else if ([(NSString *)key isEqualToString:GCInputLeftShoulder]) { + mapping = &out->leftshoulder; + } else if ([(NSString *)key isEqualToString:GCInputRightShoulder]) { + mapping = &out->rightshoulder; + } else if ([(NSString *)key isEqualToString:GCInputLeftThumbstickButton]) { + mapping = &out->leftstick; + } else if ([(NSString *)key isEqualToString:GCInputRightThumbstickButton]) { + mapping = &out->rightstick; + } else if ([(NSString *)key isEqualToString:GCInputButtonHome]) { + mapping = &out->guide; + } else if ([(NSString *)key isEqualToString:GCInputButtonMenu]) { + mapping = &out->start; + } else if ([(NSString *)key isEqualToString:GCInputButtonOptions]) { + mapping = &out->back; + } else if ([(NSString *)key isEqualToString:GCInputButtonShare]) { + mapping = &out->misc1; + } else if ([(NSString *)key isEqualToString:GCInputXboxPaddleOne]) { + mapping = &out->right_paddle1; + } else if ([(NSString *)key isEqualToString:GCInputXboxPaddleTwo]) { + mapping = &out->right_paddle2; + } else if ([(NSString *)key isEqualToString:GCInputXboxPaddleThree]) { + mapping = &out->left_paddle1; + } else if ([(NSString *)key isEqualToString:GCInputXboxPaddleFour]) { + mapping = &out->left_paddle2; + } else if ([(NSString *)key isEqualToString:GCInputLeftTrigger]) { + mapping = &out->lefttrigger; + } else if ([(NSString *)key isEqualToString:GCInputRightTrigger]) { + mapping = &out->righttrigger; + } else if ([(NSString *)key isEqualToString:GCInputDualShockTouchpadButton]) { + mapping = &out->touchpad; + } + if (mapping && mapping->kind == EMappingKind_None) { + mapping->kind = EMappingKind_Button; + mapping->target = button; + } + ++button; + } + + return SDL_TRUE; } #if defined(SDL_JOYSTICK_MFI) && defined(__MACOS__) @@ -1777,6 +2030,10 @@ static void GetAppleSFSymbolsNameForElement(GCControllerElement *element, char * static GCControllerDirectionPad *GetDirectionalPadForController(GCController *controller) { + if ([controller respondsToSelector:@selector(physicalInputProfile)]) { + return controller.physicalInputProfile.dpads[GCInputDirectionPad]; + } + if (controller.extendedGamepad) { return controller.extendedGamepad.dpad; } @@ -1789,10 +2046,6 @@ static GCControllerDirectionPad *GetDirectionalPadForController(GCController *co return controller.microGamepad.dpad; } - if ([controller respondsToSelector:@selector(physicalInputProfile)]) { - return controller.physicalInputProfile.dpads[GCInputDirectionPad]; - } - return nil; } #endif /* SDL_JOYSTICK_MFI && ENABLE_PHYSICAL_INPUT_PROFILE */ diff --git a/src/joystick/apple/SDL_mfijoystick_c.h b/src/joystick/apple/SDL_mfijoystick_c.h index 06a9cd8f2..fa289fc84 100644 --- a/src/joystick/apple/SDL_mfijoystick_c.h +++ b/src/joystick/apple/SDL_mfijoystick_c.h @@ -32,13 +32,11 @@ typedef struct joystick_hwdata { SDL_bool accelerometer; - SDL_bool remote; GCController __unsafe_unretained *controller; void *rumble; - SDL_bool uses_pause_handler; - int num_pause_presses; - Uint32 pause_button_down_time; + int pause_button_index; + Uint64 pause_button_pressed; char *name; SDL_Joystick *joystick; @@ -52,10 +50,12 @@ typedef struct joystick_hwdata SDL_bool has_dualshock_touchpad; SDL_bool has_xbox_paddles; SDL_bool has_xbox_share_button; + SDL_bool has_nintendo_buttons; + SDL_bool is_single_joycon; + SDL_bool is_siri_remote; SDL_bool use_physical_profile; NSArray *axes; - NSArray *dpads; NSArray *buttons; struct joystick_hwdata *next;