#include "SDL_internal.h"
#include "../SDL_sysjoystick.h"
#include "../SDL_joystick_c.h"
#include "../hidapi/SDL_hidapijoystick_c.h"
#include "../usb_ids.h"
#include "../../events/SDL_events_c.h"
#ifdef SDL_VIDEO_DRIVER_UIKIT
#include "../../video/uikit/SDL_uikitvideo.h"
#endif
#include "SDL_mfijoystick_c.h"
#if defined(SDL_PLATFORM_IOS) && !defined(SDL_PLATFORM_TVOS)
#import <CoreMotion/CoreMotion.h>
#endif
#ifdef SDL_PLATFORM_MACOS
#include <IOKit/hid/IOHIDManager.h>
#include <AppKit/NSApplication.h>
#ifndef NSAppKitVersionNumber10_15
#define NSAppKitVersionNumber10_15 1894
#endif
#endif
#import <GameController/GameController.h>
#ifdef SDL_JOYSTICK_MFI
static id connectObserver = nil;
static id disconnectObserver = nil;
#include <objc/message.h>
@interface GCController (SDL)
#if !((__IPHONE_OS_VERSION_MAX_ALLOWED >= 140500) || (__APPLETV_OS_VERSION_MAX_ALLOWED >= 140500) || (__MAC_OS_X_VERSION_MAX_ALLOWED >= 110300))
@property(class, nonatomic, readwrite) BOOL shouldMonitorBackgroundEvents;
#endif
@end
#import <CoreHaptics/CoreHaptics.h>
#endif
static SDL_JoystickDeviceItem *deviceList = NULL;
static int numjoysticks = 0;
int SDL_AppleTVRemoteOpenedAsJoystick = 0;
static SDL_JoystickDeviceItem *GetDeviceForIndex(int device_index)
{
SDL_JoystickDeviceItem *device = deviceList;
int i = 0;
while (i < device_index) {
if (device == NULL) {
return NULL;
}
device = device->next;
i++;
}
return device;
}
#ifdef SDL_JOYSTICK_MFI
static bool IsControllerPS4(GCController *controller)
{
if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) {
if ([controller.productCategory isEqualToString:@"DualShock 4"]) {
return true;
}
} else {
if ([controller.vendorName containsString:@"DUALSHOCK"]) {
return true;
}
}
return false;
}
static bool IsControllerPS5(GCController *controller)
{
if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) {
if ([controller.productCategory isEqualToString:@"DualSense"]) {
return true;
}
} else {
if ([controller.vendorName containsString:@"DualSense"]) {
return true;
}
}
return false;
}
static bool IsControllerXbox(GCController *controller)
{
if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) {
if ([controller.productCategory isEqualToString:@"Xbox One"]) {
return true;
}
} else {
if ([controller.vendorName containsString:@"Xbox"]) {
return true;
}
}
return false;
}
static bool IsControllerSwitchPro(GCController *controller)
{
if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) {
if ([controller.productCategory isEqualToString:@"Switch Pro Controller"]) {
return true;
}
}
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 IsControllerNVIDIASHIELD(GCController *controller)
{
if ([controller.vendorName hasPrefix:@"NVIDIA Controller"]) {
return true;
}
return false;
}
static bool IsControllerStadia(GCController *controller)
{
if ([controller.vendorName hasPrefix:@"Stadia"]) {
return true;
}
return false;
}
static bool IsControllerBackboneOne(GCController *controller)
{
if ([controller.vendorName hasPrefix:@"Backbone One"]) {
return true;
}
return false;
}
static void CheckControllerSiriRemote(GCController *controller, int *is_siri_remote)
{
if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) {
if ([controller.productCategory hasPrefix:@"Siri Remote"]) {
*is_siri_remote = 1;
SDL_sscanf(controller.productCategory.UTF8String, "Siri Remote (%i%*s Generation)", is_siri_remote);
return;
}
}
*is_siri_remote = 0;
}
static bool ElementAlreadyHandled(SDL_JoystickDeviceItem *device, NSString *element, NSDictionary<NSString *, GCControllerElement *> *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 (device->is_siri_remote) {
if ([element isEqualToString:@"Direction Pad Left"] ||
[element isEqualToString:@"Direction Pad Right"]) {
if (elements[@"Direction Pad X Axis"]) {
return true;
}
}
if ([element isEqualToString:@"Direction Pad Up"] ||
[element isEqualToString:@"Direction Pad Down"]) {
if (elements[@"Direction Pad Y Axis"]) {
return true;
}
}
} else {
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:@"Cardinal Direction Pad X Axis"]) {
if (elements[@"Cardinal Direction Pad Left"] &&
elements[@"Cardinal Direction Pad Right"]) {
return true;
}
}
if ([element isEqualToString:@"Cardinal Direction Pad Y Axis"]) {
if (elements[@"Cardinal Direction Pad Up"] &&
elements[@"Cardinal 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"]) {
return true;
}
if ([element isEqualToString:@"Button Home"]) {
if (device->is_switch_joycon_pair) {
return true;
}
#ifdef SDL_PLATFORM_TVOS
return true;
#endif
}
if ([element isEqualToString:@"Button Share"]) {
if (device->is_backbone_one) {
return true;
}
}
return false;
}
static bool IOS_AddMFIJoystickDevice(SDL_JoystickDeviceItem *device, GCController *controller)
{
Uint16 vendor = 0;
Uint16 product = 0;
Uint8 subtype = 0;
const char *name = NULL;
if (@available(macOS 11.0, iOS 14.0, tvOS 14.0, *)) {
for (id key in controller.physicalInputProfile.buttons) {
GCControllerButtonInput *button = controller.physicalInputProfile.buttons[key];
if ([button isBoundToSystemGesture]) {
button.preferredSystemGestureState = GCSystemGestureStateDisabled;
}
}
}
if (@available(macOS 11.3, iOS 14.5, tvOS 14.5, *)) {
if (!GCController.shouldMonitorBackgroundEvents) {
GCController.shouldMonitorBackgroundEvents = YES;
}
}
device->controller = (__bridge GCController *)CFBridgingRetain(controller);
if (controller.vendorName) {
name = controller.vendorName.UTF8String;
} else {
if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) {
if (controller.productCategory) {
name = controller.productCategory.UTF8String;
}
}
}
if (!name) {
name = "MFi Gamepad";
}
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 11.0, iOS 14.0, tvOS 14.0, *)) {
NSDictionary<NSString *, GCControllerElement *> *elements = controller.physicalInputProfile.elements;
for (id key in controller.physicalInputProfile.buttons) {
NSLog(@"\tButton: %@ (%s)\n", key, elements[key].analog ? "analog" : "digital");
}
for (id key in controller.physicalInputProfile.axes) {
NSLog(@"\tAxis: %@\n", key);
}
for (id key in controller.physicalInputProfile.dpads) {
NSLog(@"\tHat: %@\n", key);
}
}
#endif
device->is_xbox = IsControllerXbox(controller);
device->is_ps4 = IsControllerPS4(controller);
device->is_ps5 = IsControllerPS5(controller);
device->is_switch_pro = IsControllerSwitchPro(controller);
device->is_switch_joycon_pair = IsControllerSwitchJoyConPair(controller);
device->is_shield = IsControllerNVIDIASHIELD(controller);
device->is_stadia = IsControllerStadia(controller);
device->is_backbone_one = IsControllerBackboneOne(controller);
device->is_switch_joyconL = IsControllerSwitchJoyConL(controller);
device->is_switch_joyconR = IsControllerSwitchJoyConR(controller);
#ifdef SDL_JOYSTICK_HIDAPI
if ((device->is_xbox && (HIDAPI_IsDeviceTypePresent(SDL_GAMEPAD_TYPE_XBOXONE) ||
HIDAPI_IsDeviceTypePresent(SDL_GAMEPAD_TYPE_XBOX360))) ||
(device->is_ps4 && HIDAPI_IsDeviceTypePresent(SDL_GAMEPAD_TYPE_PS4)) ||
(device->is_ps5 && HIDAPI_IsDeviceTypePresent(SDL_GAMEPAD_TYPE_PS5)) ||
(device->is_switch_pro && HIDAPI_IsDeviceTypePresent(SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_PRO)) ||
(device->is_switch_joycon_pair && HIDAPI_IsDevicePresent(USB_VENDOR_NINTENDO, USB_PRODUCT_NINTENDO_SWITCH_JOYCON_PAIR, 0, "")) ||
(device->is_shield && HIDAPI_IsDevicePresent(USB_VENDOR_NVIDIA, USB_PRODUCT_NVIDIA_SHIELD_CONTROLLER_V104, 0, "")) ||
(device->is_stadia && HIDAPI_IsDevicePresent(USB_VENDOR_GOOGLE, USB_PRODUCT_GOOGLE_STADIA_CONTROLLER, 0, "")) ||
(device->is_switch_joyconL && HIDAPI_IsDevicePresent(USB_VENDOR_NINTENDO, USB_PRODUCT_NINTENDO_SWITCH_JOYCON_LEFT, 0, "")) ||
(device->is_switch_joyconR && HIDAPI_IsDevicePresent(USB_VENDOR_NINTENDO, USB_PRODUCT_NINTENDO_SWITCH_JOYCON_RIGHT, 0, "")) ||
(SDL_strstr(name, "GameCube Controller Adapter") &&
(HIDAPI_IsDevicePresent(USB_VENDOR_NINTENDO, USB_PRODUCT_NINTENDO_GAMECUBE_ADAPTER, 0, "") ||
HIDAPI_IsDevicePresent(USB_VENDOR_DRAGONRISE, USB_PRODUCT_EVORETRO_GAMECUBE_ADAPTER1, 0, "") ||
HIDAPI_IsDevicePresent(USB_VENDOR_DRAGONRISE, USB_PRODUCT_EVORETRO_GAMECUBE_ADAPTER2, 0, "") ||
HIDAPI_IsDevicePresent(USB_VENDOR_DRAGONRISE, USB_PRODUCT_EVORETRO_GAMECUBE_ADAPTER3, 0, ""))) ||
(SDL_strcmp(name, "8Bitdo SN30 Pro") == 0 && (HIDAPI_IsDevicePresent(USB_VENDOR_8BITDO, USB_PRODUCT_8BITDO_SN30_PRO, 0, "") || HIDAPI_IsDevicePresent(USB_VENDOR_8BITDO, USB_PRODUCT_8BITDO_SN30_PRO_BT, 0, ""))) ||
(SDL_strcmp(name, "8BitDo Pro 2") == 0 && (HIDAPI_IsDevicePresent(USB_VENDOR_8BITDO, USB_PRODUCT_8BITDO_PRO_2, 0, "") || HIDAPI_IsDevicePresent(USB_VENDOR_8BITDO, USB_PRODUCT_8BITDO_PRO_2_BT, 0, ""))) ||
(SDL_startswith(name, "8BitDo Ultimate 2 Wireless") && HIDAPI_IsDevicePresent(USB_VENDOR_8BITDO, USB_PRODUCT_8BITDO_ULTIMATE2_WIRELESS, 0, ""))) {
return false;
}
#endif
if (device->is_xbox && SDL_strncmp(name, "GamePad-", 8) == 0) {
return false;
}
CheckControllerSiriRemote(controller, &device->is_siri_remote);
if (device->is_siri_remote && !SDL_GetHintBoolean(SDL_HINT_TV_REMOTE_AS_JOYSTICK, true)) {
return false;
}
if (@available(macOS 11.0, iOS 14.0, tvOS 14.0, *)) {
if (controller.physicalInputProfile.buttons[GCInputDualShockTouchpadButton] != nil) {
device->has_dualshock_touchpad = TRUE;
}
if (controller.physicalInputProfile.buttons[GCInputXboxPaddleOne] != nil) {
device->has_xbox_paddles = TRUE;
}
if (controller.physicalInputProfile.buttons[@"Button Share"] != nil) {
device->has_xbox_share_button = TRUE;
}
}
if (device->is_backbone_one) {
vendor = USB_VENDOR_BACKBONE;
if (device->is_ps5) {
product = USB_PRODUCT_BACKBONE_ONE_IOS_PS5;
} else {
product = USB_PRODUCT_BACKBONE_ONE_IOS;
}
} else if (device->is_xbox) {
vendor = USB_VENDOR_MICROSOFT;
if (device->has_xbox_paddles) {
product = USB_PRODUCT_XBOX_ONE_ELITE_SERIES_2_BLUETOOTH;
} else if (device->has_xbox_share_button) {
product = USB_PRODUCT_XBOX_SERIES_X_BLE;
} else {
product = USB_PRODUCT_XBOX_ONE_S_REV1_BLUETOOTH;
}
} else if (device->is_ps4) {
vendor = USB_VENDOR_SONY;
product = USB_PRODUCT_SONY_DS4_SLIM;
if (device->has_dualshock_touchpad) {
subtype = 1;
}
} else if (device->is_ps5) {
vendor = USB_VENDOR_SONY;
product = USB_PRODUCT_SONY_DS5;
} else if (device->is_switch_pro) {
vendor = USB_VENDOR_NINTENDO;
product = USB_PRODUCT_NINTENDO_SWITCH_PRO;
device->has_nintendo_buttons = TRUE;
} else if (device->is_switch_joycon_pair) {
vendor = USB_VENDOR_NINTENDO;
product = USB_PRODUCT_NINTENDO_SWITCH_JOYCON_PAIR;
device->has_nintendo_buttons = TRUE;
} else if (device->is_switch_joyconL) {
vendor = USB_VENDOR_NINTENDO;
product = USB_PRODUCT_NINTENDO_SWITCH_JOYCON_LEFT;
} else if (device->is_switch_joyconR) {
vendor = USB_VENDOR_NINTENDO;
product = USB_PRODUCT_NINTENDO_SWITCH_JOYCON_RIGHT;
} else if (@available(macOS 11.0, iOS 14.0, tvOS 14.0, *)) {
vendor = USB_VENDOR_APPLE;
product = 4;
subtype = 4;
} else if (controller.extendedGamepad) {
vendor = USB_VENDOR_APPLE;
product = 1;
subtype = 1;
#ifdef SDL_PLATFORM_TVOS
} else if (controller.microGamepad) {
vendor = USB_VENDOR_APPLE;
product = 3;
subtype = 3;
#endif
} else {
return false;
}
if (SDL_ShouldIgnoreJoystick(vendor, product, 0, name)) {
return false;
}
if (@available(macOS 11.0, iOS 14.0, tvOS 14.0, *)) {
NSDictionary<NSString *, GCControllerElement *> *elements = controller.physicalInputProfile.elements;
NSArray *axes = [[[elements allKeys] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)]
filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id object, NSDictionary *bindings) {
if (ElementAlreadyHandled(device, (NSString *)object, elements)) {
return false;
}
GCControllerElement *element = elements[object];
if (element.analog) {
if ([element isKindOfClass:[GCControllerAxisInput class]] ||
[element isKindOfClass:[GCControllerButtonInput class]]) {
return true;
}
}
return false;
}]];
NSArray *buttons = [[[elements allKeys] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)]
filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id object, NSDictionary *bindings) {
if (ElementAlreadyHandled(device, (NSString *)object, elements)) {
return false;
}
GCControllerElement *element = elements[object];
if ([element isKindOfClass:[GCControllerButtonInput class]]) {
return true;
}
return false;
}]];
device->naxes = (int)axes.count;
device->axes = (__bridge NSArray *)CFBridgingRetain(axes);
device->nbuttons = (int)buttons.count;
device->buttons = (__bridge NSArray *)CFBridgingRetain(buttons);
subtype = 4;
#ifdef DEBUG_CONTROLLER_PROFILE
NSLog(@"Elements used:\n");
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
#ifdef SDL_PLATFORM_TVOS
if (elements[GCInputButtonMenu] && !elements[@"Button Home"]) {
device->pause_button_index = (int)[device->buttons indexOfObject:GCInputButtonMenu];
}
#endif
} else if (controller.extendedGamepad) {
GCExtendedGamepad *gamepad = controller.extendedGamepad;
int nbuttons = 0;
BOOL has_direct_menu = FALSE;
device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_SOUTH);
device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_EAST);
device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_WEST);
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);
nbuttons += 6;
if (@available(macOS 10.14.1, iOS 12.1, tvOS 12.1, *)) {
if (gamepad.leftThumbstickButton) {
device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_LEFT_STICK);
++nbuttons;
}
if (gamepad.rightThumbstickButton) {
device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_RIGHT_STICK);
++nbuttons;
}
}
if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) {
if (gamepad.buttonOptions) {
device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_BACK);
++nbuttons;
}
}
device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_START);
++nbuttons;
if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) {
if (gamepad.buttonMenu) {
has_direct_menu = TRUE;
}
}
#ifdef SDL_PLATFORM_TVOS
if ((device->button_mask & (1 << SDL_GAMEPAD_BUTTON_BACK)) == 0) {
has_direct_menu = FALSE;
}
#endif
if (!has_direct_menu) {
device->pause_button_index = (nbuttons - 1);
}
device->naxes = 6; device->nhats = 1; device->nbuttons = nbuttons;
}
#ifdef SDL_PLATFORM_TVOS
else if (controller.microGamepad) {
int nbuttons = 0;
device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_SOUTH);
device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_WEST); device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_EAST);
nbuttons += 3;
device->pause_button_index = (nbuttons - 1);
device->naxes = 2; device->nhats = 0; device->nbuttons = nbuttons;
controller.microGamepad.allowsRotation = SDL_GetHintBoolean(SDL_HINT_APPLE_TV_REMOTE_ALLOW_ROTATION, false);
}
#endif
else {
return false;
}
Uint16 signature;
if (@available(macOS 11.0, iOS 14.0, tvOS 14.0, *)) {
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;
}
device->guid = SDL_CreateJoystickGUID(SDL_HARDWARE_BUS_BLUETOOTH, vendor, product, signature, NULL, name, 'm', subtype);
controller.playerIndex = -1;
return true;
}
#endif
#ifdef SDL_JOYSTICK_MFI
static void IOS_AddJoystickDevice(GCController *controller)
{
SDL_JoystickDeviceItem *device = deviceList;
while (device != NULL) {
if (device->controller == controller) {
return;
}
device = device->next;
}
device = (SDL_JoystickDeviceItem *)SDL_calloc(1, sizeof(SDL_JoystickDeviceItem));
if (device == NULL) {
return;
}
device->instance_id = SDL_GetNextObjectID();
device->pause_button_index = -1;
if (controller) {
#ifdef SDL_JOYSTICK_MFI
if (!IOS_AddMFIJoystickDevice(device, controller)) {
SDL_free(device->name);
SDL_free(device);
return;
}
#else
SDL_free(device);
return;
#endif }
if (deviceList == NULL) {
deviceList = device;
} else {
SDL_JoystickDeviceItem *lastdevice = deviceList;
while (lastdevice->next != NULL) {
lastdevice = lastdevice->next;
}
lastdevice->next = device;
}
++numjoysticks;
SDL_PrivateJoystickAdded(device->instance_id);
}
#endif
static SDL_JoystickDeviceItem *IOS_RemoveJoystickDevice(SDL_JoystickDeviceItem *device)
{
SDL_JoystickDeviceItem *prev = NULL;
SDL_JoystickDeviceItem *next = NULL;
SDL_JoystickDeviceItem *item = deviceList;
if (device == NULL) {
return NULL;
}
next = device->next;
while (item != NULL) {
if (item == device) {
break;
}
prev = item;
item = item->next;
}
if (prev) {
prev->next = device->next;
} else if (device == deviceList) {
deviceList = device->next;
}
if (device->joystick) {
device->joystick->hwdata = NULL;
}
#ifdef SDL_JOYSTICK_MFI
@autoreleasepool {
if (device->controller) {
GCController *controller = CFBridgingRelease((__bridge CFTypeRef)(device->controller));
controller.controllerPausedHandler = nil;
device->controller = nil;
}
if (device->axes) {
CFRelease((__bridge CFTypeRef)device->axes);
device->axes = nil;
}
if (device->buttons) {
CFRelease((__bridge CFTypeRef)device->buttons);
device->buttons = nil;
}
}
#endif
--numjoysticks;
SDL_PrivateJoystickRemoved(device->instance_id);
SDL_free(device->name);
SDL_free(device);
return next;
}
#ifdef SDL_PLATFORM_TVOS
static void SDLCALL SDL_AppleTVRemoteRotationHintChanged(void *udata, const char *name, const char *oldValue, const char *newValue)
{
BOOL allowRotation = newValue != NULL && *newValue != '0';
@autoreleasepool {
for (GCController *controller in [GCController controllers]) {
if (controller.microGamepad) {
controller.microGamepad.allowsRotation = allowRotation;
}
}
}
}
#endif
static bool IOS_JoystickInit(void)
{
if (!SDL_GetHintBoolean(SDL_HINT_JOYSTICK_MFI, true)) {
return true;
}
#ifdef SDL_PLATFORM_MACOS
if (@available(macOS 11.0, *)) {
} else {
return true;
}
#endif
@autoreleasepool {
#ifdef SDL_JOYSTICK_MFI
NSNotificationCenter *center;
#endif
#ifdef SDL_JOYSTICK_MFI
if (![GCController class]) {
return true;
}
for (GCController *controller in [GCController controllers]) {
IOS_AddJoystickDevice(controller);
}
#ifdef SDL_PLATFORM_TVOS
SDL_AddHintCallback(SDL_HINT_APPLE_TV_REMOTE_ALLOW_ROTATION,
SDL_AppleTVRemoteRotationHintChanged, NULL);
#endif
center = [NSNotificationCenter defaultCenter];
connectObserver = [center addObserverForName:GCControllerDidConnectNotification
object:nil
queue:nil
usingBlock:^(NSNotification *note) {
GCController *controller = note.object;
SDL_LockJoysticks();
IOS_AddJoystickDevice(controller);
SDL_UnlockJoysticks();
}];
disconnectObserver = [center addObserverForName:GCControllerDidDisconnectNotification
object:nil
queue:nil
usingBlock:^(NSNotification *note) {
GCController *controller = note.object;
SDL_JoystickDeviceItem *device;
SDL_LockJoysticks();
for (device = deviceList; device != NULL; device = device->next) {
if (device->controller == controller) {
IOS_RemoveJoystickDevice(device);
break;
}
}
SDL_UnlockJoysticks();
}];
#endif
#ifdef SDL_VIDEO_DRIVER_UIKIT
UIKit_SetGameControllerInteraction(true);
#endif
}
return true;
}
static int IOS_JoystickGetCount(void)
{
return numjoysticks;
}
static void IOS_JoystickDetect(void)
{
}
static bool IOS_JoystickIsDevicePresent(Uint16 vendor_id, Uint16 product_id, Uint16 version, const char *name)
{
return false;
}
static const char *IOS_JoystickGetDeviceName(int device_index)
{
SDL_JoystickDeviceItem *device = GetDeviceForIndex(device_index);
return device ? device->name : "Unknown";
}
static const char *IOS_JoystickGetDevicePath(int device_index)
{
return NULL;
}
static int IOS_JoystickGetDeviceSteamVirtualGamepadSlot(int device_index)
{
return -1;
}
static int IOS_JoystickGetDevicePlayerIndex(int device_index)
{
#ifdef SDL_JOYSTICK_MFI
SDL_JoystickDeviceItem *device = GetDeviceForIndex(device_index);
if (device && device->controller) {
return (int)device->controller.playerIndex;
}
#endif
return -1;
}
static void IOS_JoystickSetDevicePlayerIndex(int device_index, int player_index)
{
#ifdef SDL_JOYSTICK_MFI
SDL_JoystickDeviceItem *device = GetDeviceForIndex(device_index);
if (device && device->controller) {
device->controller.playerIndex = player_index;
}
#endif
}
static SDL_GUID IOS_JoystickGetDeviceGUID(int device_index)
{
SDL_JoystickDeviceItem *device = GetDeviceForIndex(device_index);
SDL_GUID guid;
if (device) {
guid = device->guid;
} else {
SDL_zero(guid);
}
return guid;
}
static SDL_JoystickID IOS_JoystickGetDeviceInstanceID(int device_index)
{
SDL_JoystickDeviceItem *device = GetDeviceForIndex(device_index);
return device ? device->instance_id : 0;
}
static bool IOS_JoystickOpen(SDL_Joystick *joystick, int device_index)
{
SDL_JoystickDeviceItem *device = GetDeviceForIndex(device_index);
if (device == NULL) {
return SDL_SetError("Could not open Joystick: no hardware device for the specified index");
}
joystick->hwdata = device;
joystick->naxes = device->naxes;
joystick->nhats = device->nhats;
joystick->nbuttons = device->nbuttons;
if (device->has_dualshock_touchpad) {
SDL_PrivateJoystickAddTouchpad(joystick, 2);
}
device->joystick = joystick;
@autoreleasepool {
#ifdef SDL_JOYSTICK_MFI
if (device->pause_button_index >= 0) {
GCController *controller = device->controller;
controller.controllerPausedHandler = ^(GCController *c) {
if (joystick->hwdata) {
joystick->hwdata->pause_button_pressed = SDL_GetTicks();
}
};
}
if (@available(macOS 11.0, iOS 14.0, tvOS 14.0, *)) {
GCController *controller = joystick->hwdata->controller;
GCMotion *motion = controller.motion;
if (motion && motion.hasRotationRate) {
SDL_PrivateJoystickAddSensor(joystick, SDL_SENSOR_GYRO, 0.0f);
}
if (motion && motion.hasGravityAndUserAcceleration) {
SDL_PrivateJoystickAddSensor(joystick, SDL_SENSOR_ACCEL, 0.0f);
}
}
if (@available(macOS 11.0, iOS 14.0, tvOS 14.0, *)) {
GCController *controller = joystick->hwdata->controller;
for (id key in controller.physicalInputProfile.buttons) {
GCControllerButtonInput *button = controller.physicalInputProfile.buttons[key];
if ([button isBoundToSystemGesture]) {
button.preferredSystemGestureState = GCSystemGestureStateDisabled;
}
}
}
if (@available(macOS 11.0, iOS 14.0, tvOS 14.0, *)) {
GCController *controller = device->controller;
if (controller.light) {
SDL_SetBooleanProperty(SDL_GetJoystickProperties(joystick), SDL_PROP_JOYSTICK_CAP_RGB_LED_BOOLEAN, true);
}
if (controller.haptics) {
for (GCHapticsLocality locality in controller.haptics.supportedLocalities) {
if ([locality isEqualToString:GCHapticsLocalityHandles]) {
SDL_SetBooleanProperty(SDL_GetJoystickProperties(joystick), SDL_PROP_JOYSTICK_CAP_RUMBLE_BOOLEAN, true);
} else if ([locality isEqualToString:GCHapticsLocalityTriggers]) {
SDL_SetBooleanProperty(SDL_GetJoystickProperties(joystick), SDL_PROP_JOYSTICK_CAP_TRIGGER_RUMBLE_BOOLEAN, true);
}
}
}
}
#endif }
if (device->is_siri_remote) {
++SDL_AppleTVRemoteOpenedAsJoystick;
}
return true;
}
#ifdef SDL_JOYSTICK_MFI
static Uint8 IOS_MFIJoystickHatStateForDPad(GCControllerDirectionPad *dpad)
{
Uint8 hat = 0;
if (dpad.up.isPressed) {
hat |= SDL_HAT_UP;
} else if (dpad.down.isPressed) {
hat |= SDL_HAT_DOWN;
}
if (dpad.left.isPressed) {
hat |= SDL_HAT_LEFT;
} else if (dpad.right.isPressed) {
hat |= SDL_HAT_RIGHT;
}
if (hat == 0) {
return SDL_HAT_CENTERED;
}
return hat;
}
#endif
static void IOS_MFIJoystickUpdate(SDL_Joystick *joystick)
{
#ifdef SDL_JOYSTICK_MFI
@autoreleasepool {
SDL_JoystickDeviceItem *device = joystick->hwdata;
GCController *controller = device->controller;
Uint8 hatstate = SDL_HAT_CENTERED;
int i;
Uint64 timestamp = SDL_GetTicksNS();
#ifdef DEBUG_CONTROLLER_STATE
if (@available(macOS 11.0, 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 %@ = %g\n", key, axis.value);
}
for (id key in controller.physicalInputProfile.dpads) {
GCControllerDirectionPad *dpad = controller.physicalInputProfile.dpads[key];
if (dpad.up.isPressed || dpad.down.isPressed || dpad.left.isPressed || dpad.right.isPressed) {
NSLog(@"Hat %@ =%s%s%s%s\n", key,
dpad.up.isPressed ? " UP" : "",
dpad.down.isPressed ? " DOWN" : "",
dpad.left.isPressed ? " LEFT" : "",
dpad.right.isPressed ? " RIGHT" : "");
}
}
}
}
#endif
if (@available(macOS 11.0, iOS 14.0, tvOS 14.0, *)) {
NSDictionary<NSString *, GCControllerElement *> *elements = controller.physicalInputProfile.elements;
NSDictionary<NSString *, GCControllerButtonInput *> *buttons = controller.physicalInputProfile.buttons;
int axis = 0;
for (id key in device->axes) {
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) {
bool down;
if (button == device->pause_button_index) {
down = (device->pause_button_pressed > 0);
} else {
down = buttons[key].isPressed;
}
SDL_SendJoystickButton(timestamp, joystick, button++, down);
}
} else if (controller.extendedGamepad) {
bool isstack;
GCExtendedGamepad *gamepad = controller.extendedGamepad;
Sint16 axes[] = {
(Sint16)(gamepad.leftThumbstick.xAxis.value * 32767),
(Sint16)(gamepad.leftThumbstick.yAxis.value * -32767),
(Sint16)((gamepad.leftTrigger.value * 65535) - 32768),
(Sint16)(gamepad.rightThumbstick.xAxis.value * 32767),
(Sint16)(gamepad.rightThumbstick.yAxis.value * -32767),
(Sint16)((gamepad.rightTrigger.value * 65535) - 32768),
};
bool *buttons = SDL_small_alloc(bool, joystick->nbuttons, &isstack);
int button_count = 0;
if (buttons == NULL) {
return;
}
buttons[button_count++] = gamepad.buttonA.isPressed;
buttons[button_count++] = gamepad.buttonB.isPressed;
buttons[button_count++] = gamepad.buttonX.isPressed;
buttons[button_count++] = gamepad.buttonY.isPressed;
buttons[button_count++] = gamepad.leftShoulder.isPressed;
buttons[button_count++] = gamepad.rightShoulder.isPressed;
if (@available(macOS 10.14.1, iOS 12.1, tvOS 12.1, *)) {
if (device->button_mask & (1 << SDL_GAMEPAD_BUTTON_LEFT_STICK)) {
buttons[button_count++] = gamepad.leftThumbstickButton.isPressed;
}
if (device->button_mask & (1 << SDL_GAMEPAD_BUTTON_RIGHT_STICK)) {
buttons[button_count++] = gamepad.rightThumbstickButton.isPressed;
}
}
if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) {
if (device->button_mask & (1 << SDL_GAMEPAD_BUTTON_BACK)) {
buttons[button_count++] = gamepad.buttonOptions.isPressed;
}
}
if (device->button_mask & (1 << SDL_GAMEPAD_BUTTON_START)) {
if (device->pause_button_index >= 0) {
buttons[button_count++] = (device->pause_button_pressed > 0);
} else {
if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) {
buttons[button_count++] = gamepad.buttonMenu.isPressed;
}
}
}
hatstate = IOS_MFIJoystickHatStateForDPad(gamepad.dpad);
for (i = 0; i < SDL_arraysize(axes); i++) {
SDL_SendJoystickAxis(timestamp, joystick, i, axes[i]);
}
for (i = 0; i < button_count; i++) {
SDL_SendJoystickButton(timestamp, joystick, i, buttons[i]);
}
SDL_small_free(buttons, isstack);
}
#ifdef SDL_PLATFORM_TVOS
else if (controller.microGamepad) {
GCMicroGamepad *gamepad = controller.microGamepad;
Sint16 axes[] = {
(Sint16)(gamepad.dpad.xAxis.value * 32767),
(Sint16)(gamepad.dpad.yAxis.value * -32767),
};
for (i = 0; i < SDL_arraysize(axes); i++) {
SDL_SendJoystickAxis(timestamp, joystick, i, axes[i]);
}
bool buttons[joystick->nbuttons];
int button_count = 0;
buttons[button_count++] = gamepad.buttonA.isPressed;
buttons[button_count++] = gamepad.buttonX.isPressed;
buttons[button_count++] = (device->pause_button_pressed > 0);
for (i = 0; i < button_count; i++) {
SDL_SendJoystickButton(timestamp, joystick, i, buttons[i]);
}
}
#endif
if (joystick->nhats > 0) {
SDL_SendJoystickHat(timestamp, joystick, 0, hatstate);
}
if (device->pause_button_pressed) {
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;
}
}
if (@available(macOS 11.0, iOS 14.0, tvOS 14.0, *)) {
if (device->has_dualshock_touchpad) {
GCControllerDirectionPad *dpad;
dpad = controller.physicalInputProfile.dpads[GCInputDualShockTouchpadOne];
if (dpad.xAxis.value != 0.f || dpad.yAxis.value != 0.f) {
SDL_SendJoystickTouchpad(timestamp, joystick, 0, 0, true, (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, false, 0.0f, 0.0f, 1.0f);
}
dpad = controller.physicalInputProfile.dpads[GCInputDualShockTouchpadTwo];
if (dpad.xAxis.value != 0.f || dpad.yAxis.value != 0.f) {
SDL_SendJoystickTouchpad(timestamp, joystick, 0, 1, true, (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, false, 0.0f, 0.0f, 1.0f);
}
}
}
if (@available(macOS 11.0, 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);
}
}
}
if (@available(macOS 11.0, iOS 14.0, tvOS 14.0, *)) {
GCDeviceBattery *battery = controller.battery;
if (battery) {
SDL_PowerState state = SDL_POWERSTATE_UNKNOWN;
int percent = (int)SDL_roundf(battery.batteryLevel * 100.0f);
switch (battery.batteryState) {
case GCDeviceBatteryStateDischarging:
state = SDL_POWERSTATE_ON_BATTERY;
break;
case GCDeviceBatteryStateCharging:
state = SDL_POWERSTATE_CHARGING;
break;
case GCDeviceBatteryStateFull:
state = SDL_POWERSTATE_CHARGED;
break;
default:
break;
}
SDL_SendJoystickPowerInfo(joystick, state, percent);
}
}
}
#endif }
#ifdef SDL_JOYSTICK_MFI
@interface SDL3_RumbleMotor : NSObject
@property(nonatomic, strong) CHHapticEngine *engine API_AVAILABLE(macos(11.0), ios(13.0), tvos(14.0));
@property(nonatomic, strong) id<CHHapticPatternPlayer> player API_AVAILABLE(macos(11.0), ios(13.0), tvos(14.0));
@property bool active;
@end
@implementation SDL3_RumbleMotor
{
}
- (void)cleanup
{
@autoreleasepool {
if (@available(macOS 11.0, iOS 14.0, tvOS 14.0, *)) {
if (self.player != nil) {
[self.player cancelAndReturnError:nil];
self.player = nil;
}
if (self.engine != nil) {
[self.engine stopWithCompletionHandler:nil];
self.engine = nil;
}
}
}
}
- (bool)setIntensity:(float)intensity
{
@autoreleasepool {
if (@available(macOS 11.0, iOS 14.0, tvOS 14.0, *)) {
NSError *error = nil;
CHHapticDynamicParameter *param;
if (self.engine == nil) {
return SDL_SetError("Haptics engine was stopped");
}
if (intensity == 0.0f) {
if (self.player && self.active) {
[self.player stopAtTime:0 error:&error];
}
self.active = false;
return true;
}
if (self.player == nil) {
CHHapticEventParameter *event_param = [[CHHapticEventParameter alloc] initWithParameterID:CHHapticEventParameterIDHapticIntensity value:1.0f];
CHHapticEvent *event = [[CHHapticEvent alloc] initWithEventType:CHHapticEventTypeHapticContinuous parameters:[NSArray arrayWithObjects:event_param, nil] relativeTime:0 duration:GCHapticDurationInfinite];
CHHapticPattern *pattern = [[CHHapticPattern alloc] initWithEvents:[NSArray arrayWithObject:event] parameters:[[NSArray alloc] init] error:&error];
if (error != nil) {
return SDL_SetError("Couldn't create haptic pattern: %s", [error.localizedDescription UTF8String]);
}
self.player = [self.engine createPlayerWithPattern:pattern error:&error];
if (error != nil) {
return SDL_SetError("Couldn't create haptic player: %s", [error.localizedDescription UTF8String]);
}
self.active = false;
}
param = [[CHHapticDynamicParameter alloc] initWithParameterID:CHHapticDynamicParameterIDHapticIntensityControl value:intensity relativeTime:0];
[self.player sendParameters:[NSArray arrayWithObject:param] atTime:0 error:&error];
if (error != nil) {
return SDL_SetError("Couldn't update haptic player: %s", [error.localizedDescription UTF8String]);
}
if (!self.active) {
[self.player startAtTime:0 error:&error];
self.active = true;
}
}
return true;
}
}
- (id)initWithController:(GCController *)controller locality:(GCHapticsLocality)locality API_AVAILABLE(macos(11.0), ios(14.0), tvos(14.0))
{
@autoreleasepool {
NSError *error;
__weak __typeof(self) weakSelf;
self = [super init];
weakSelf = self;
self.engine = [controller.haptics createEngineWithLocality:locality];
if (self.engine == nil) {
SDL_SetError("Couldn't create haptics engine");
return nil;
}
[self.engine startAndReturnError:&error];
if (error != nil) {
SDL_SetError("Couldn't start haptics engine");
return nil;
}
self.engine.stoppedHandler = ^(CHHapticEngineStoppedReason stoppedReason) {
SDL3_RumbleMotor *_this = weakSelf;
if (_this == nil) {
return;
}
_this.player = nil;
_this.engine = nil;
};
self.engine.resetHandler = ^{
SDL3_RumbleMotor *_this = weakSelf;
if (_this == nil) {
return;
}
_this.player = nil;
[_this.engine startAndReturnError:nil];
};
return self;
}
}
@end
@interface SDL3_RumbleContext : NSObject
@property(nonatomic, strong) SDL3_RumbleMotor *lowFrequencyMotor;
@property(nonatomic, strong) SDL3_RumbleMotor *highFrequencyMotor;
@property(nonatomic, strong) SDL3_RumbleMotor *leftTriggerMotor;
@property(nonatomic, strong) SDL3_RumbleMotor *rightTriggerMotor;
@end
@implementation SDL3_RumbleContext
{
}
- (id)initWithLowFrequencyMotor:(SDL3_RumbleMotor *)low_frequency_motor
HighFrequencyMotor:(SDL3_RumbleMotor *)high_frequency_motor
LeftTriggerMotor:(SDL3_RumbleMotor *)left_trigger_motor
RightTriggerMotor:(SDL3_RumbleMotor *)right_trigger_motor
{
self = [super init];
self.lowFrequencyMotor = low_frequency_motor;
self.highFrequencyMotor = high_frequency_motor;
self.leftTriggerMotor = left_trigger_motor;
self.rightTriggerMotor = right_trigger_motor;
return self;
}
- (bool)rumbleWithLowFrequency:(Uint16)low_frequency_rumble andHighFrequency:(Uint16)high_frequency_rumble
{
bool result = true;
result &= [self.lowFrequencyMotor setIntensity:((float)low_frequency_rumble / 65535.0f)];
result &= [self.highFrequencyMotor setIntensity:((float)high_frequency_rumble / 65535.0f)];
return result;
}
- (bool)rumbleLeftTrigger:(Uint16)left_rumble andRightTrigger:(Uint16)right_rumble
{
bool result = false;
if (self.leftTriggerMotor && self.rightTriggerMotor) {
result &= [self.leftTriggerMotor setIntensity:((float)left_rumble / 65535.0f)];
result &= [self.rightTriggerMotor setIntensity:((float)right_rumble / 65535.0f)];
} else {
result = SDL_Unsupported();
}
return result;
}
- (void)cleanup
{
[self.lowFrequencyMotor cleanup];
[self.highFrequencyMotor cleanup];
}
@end
static SDL3_RumbleContext *IOS_JoystickInitRumble(GCController *controller)
{
@autoreleasepool {
if (@available(macOS 11.0, iOS 14.0, tvOS 14.0, *)) {
SDL3_RumbleMotor *low_frequency_motor = [[SDL3_RumbleMotor alloc] initWithController:controller locality:GCHapticsLocalityLeftHandle];
SDL3_RumbleMotor *high_frequency_motor = [[SDL3_RumbleMotor alloc] initWithController:controller locality:GCHapticsLocalityRightHandle];
SDL3_RumbleMotor *left_trigger_motor = [[SDL3_RumbleMotor alloc] initWithController:controller locality:GCHapticsLocalityLeftTrigger];
SDL3_RumbleMotor *right_trigger_motor = [[SDL3_RumbleMotor alloc] initWithController:controller locality:GCHapticsLocalityRightTrigger];
if (low_frequency_motor && high_frequency_motor) {
return [[SDL3_RumbleContext alloc] initWithLowFrequencyMotor:low_frequency_motor
HighFrequencyMotor:high_frequency_motor
LeftTriggerMotor:left_trigger_motor
RightTriggerMotor:right_trigger_motor];
}
}
}
return nil;
}
#endif
static bool IOS_JoystickRumble(SDL_Joystick *joystick, Uint16 low_frequency_rumble, Uint16 high_frequency_rumble)
{
#ifdef SDL_JOYSTICK_MFI
SDL_JoystickDeviceItem *device = joystick->hwdata;
if (device == NULL) {
return SDL_SetError("Controller is no longer connected");
}
if (@available(macOS 11.0, iOS 14.0, tvOS 14.0, *)) {
if (!device->rumble && device->controller && device->controller.haptics) {
SDL3_RumbleContext *rumble = IOS_JoystickInitRumble(device->controller);
if (rumble) {
device->rumble = (void *)CFBridgingRetain(rumble);
}
}
}
if (device->rumble) {
SDL3_RumbleContext *rumble = (__bridge SDL3_RumbleContext *)device->rumble;
return [rumble rumbleWithLowFrequency:low_frequency_rumble andHighFrequency:high_frequency_rumble];
}
#endif
return SDL_Unsupported();
}
static bool IOS_JoystickRumbleTriggers(SDL_Joystick *joystick, Uint16 left_rumble, Uint16 right_rumble)
{
#ifdef SDL_JOYSTICK_MFI
SDL_JoystickDeviceItem *device = joystick->hwdata;
if (device == NULL) {
return SDL_SetError("Controller is no longer connected");
}
if (@available(macOS 11.0, iOS 14.0, tvOS 14.0, *)) {
if (!device->rumble && device->controller && device->controller.haptics) {
SDL3_RumbleContext *rumble = IOS_JoystickInitRumble(device->controller);
if (rumble) {
device->rumble = (void *)CFBridgingRetain(rumble);
}
}
}
if (device->rumble) {
SDL3_RumbleContext *rumble = (__bridge SDL3_RumbleContext *)device->rumble;
return [rumble rumbleLeftTrigger:left_rumble andRightTrigger:right_rumble];
}
#endif
return SDL_Unsupported();
}
static bool IOS_JoystickSetLED(SDL_Joystick *joystick, Uint8 red, Uint8 green, Uint8 blue)
{
@autoreleasepool {
SDL_JoystickDeviceItem *device = joystick->hwdata;
if (device == NULL) {
return SDL_SetError("Controller is no longer connected");
}
if (@available(macOS 11.0, iOS 14.0, tvOS 14.0, *)) {
GCController *controller = device->controller;
GCDeviceLight *light = controller.light;
if (light) {
light.color = [[GCColor alloc] initWithRed:(float)red / 255.0f
green:(float)green / 255.0f
blue:(float)blue / 255.0f];
return true;
}
}
}
return SDL_Unsupported();
}
static bool IOS_JoystickSendEffect(SDL_Joystick *joystick, const void *data, int size)
{
return SDL_Unsupported();
}
static bool IOS_JoystickSetSensorsEnabled(SDL_Joystick *joystick, bool enabled)
{
@autoreleasepool {
SDL_JoystickDeviceItem *device = joystick->hwdata;
if (device == NULL) {
return SDL_SetError("Controller is no longer connected");
}
if (@available(macOS 11.0, iOS 14.0, tvOS 14.0, *)) {
GCController *controller = device->controller;
GCMotion *motion = controller.motion;
if (motion) {
motion.sensorsActive = enabled ? YES : NO;
return true;
}
}
}
return SDL_Unsupported();
}
static void IOS_JoystickUpdate(SDL_Joystick *joystick)
{
SDL_JoystickDeviceItem *device = joystick->hwdata;
if (device == NULL) {
return;
}
if (device->controller) {
IOS_MFIJoystickUpdate(joystick);
}
}
static void IOS_JoystickClose(SDL_Joystick *joystick)
{
SDL_JoystickDeviceItem *device = joystick->hwdata;
if (device == NULL) {
return;
}
device->joystick = NULL;
#ifdef SDL_JOYSTICK_MFI
@autoreleasepool {
if (device->rumble) {
SDL3_RumbleContext *rumble = (__bridge SDL3_RumbleContext *)device->rumble;
[rumble cleanup];
CFRelease(device->rumble);
device->rumble = NULL;
}
if (device->controller) {
GCController *controller = device->controller;
controller.controllerPausedHandler = nil;
controller.playerIndex = -1;
if (@available(macOS 11.0, iOS 14.0, tvOS 14.0, *)) {
for (id key in controller.physicalInputProfile.buttons) {
GCControllerButtonInput *button = controller.physicalInputProfile.buttons[key];
if ([button isBoundToSystemGesture]) {
button.preferredSystemGestureState = GCSystemGestureStateEnabled;
}
}
}
}
}
#endif
if (device->is_siri_remote) {
--SDL_AppleTVRemoteOpenedAsJoystick;
}
}
static void IOS_JoystickQuit(void)
{
@autoreleasepool {
#ifdef SDL_JOYSTICK_MFI
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
if (connectObserver) {
[center removeObserver:connectObserver name:GCControllerDidConnectNotification object:nil];
connectObserver = nil;
}
if (disconnectObserver) {
[center removeObserver:disconnectObserver name:GCControllerDidDisconnectNotification object:nil];
disconnectObserver = nil;
}
#ifdef SDL_PLATFORM_TVOS
SDL_RemoveHintCallback(SDL_HINT_APPLE_TV_REMOTE_ALLOW_ROTATION,
SDL_AppleTVRemoteRotationHintChanged, NULL);
#endif #endif
while (deviceList != NULL) {
IOS_RemoveJoystickDevice(deviceList);
}
#ifdef SDL_VIDEO_DRIVER_UIKIT
UIKit_SetGameControllerInteraction(false);
#endif
}
numjoysticks = 0;
}
static bool IOS_JoystickGetGamepadMapping(int device_index, SDL_GamepadMapping *out)
{
SDL_JoystickDeviceItem *device = GetDeviceForIndex(device_index);
if (device == NULL) {
return false;
}
if (@available(macOS 11.0, iOS 14.0, tvOS 14.0, *)) {
int axis = 0;
for (id key in device->axes) {
if ([(NSString *)key isEqualToString:@"Left Thumbstick X Axis"] ||
[(NSString *)key isEqualToString:@"Direction Pad X Axis"]) {
out->leftx.kind = EMappingKind_Axis;
out->leftx.target = axis;
} else if ([(NSString *)key isEqualToString:@"Left Thumbstick Y Axis"] ||
[(NSString *)key isEqualToString:@"Direction Pad Y Axis"]) {
out->lefty.kind = EMappingKind_Axis;
out->lefty.target = axis;
out->lefty.axis_reversed = 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 = true;
} else if ([(NSString *)key isEqualToString:GCInputLeftTrigger]) {
out->lefttrigger.kind = EMappingKind_Axis;
out->lefttrigger.target = axis;
out->lefttrigger.half_axis_positive = true;
} else if ([(NSString *)key isEqualToString:GCInputRightTrigger]) {
out->righttrigger.kind = EMappingKind_Axis;
out->righttrigger.target = axis;
out->righttrigger.half_axis_positive = true;
}
++axis;
}
int button = 0;
for (id key in device->buttons) {
SDL_InputMapping *mapping = NULL;
if ([(NSString *)key isEqualToString:GCInputButtonA]) {
if (device->is_siri_remote > 1) {
} else 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_switch_joyconL || device->is_switch_joyconR) {
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_switch_joyconL || device->is_switch_joyconR) {
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"]) {
mapping = &out->dpleft;
} else if ([(NSString *)key isEqualToString:@"Direction Pad Right"]) {
mapping = &out->dpright;
} else if ([(NSString *)key isEqualToString:@"Direction Pad Up"]) {
mapping = &out->dpup;
} else if ([(NSString *)key isEqualToString:@"Direction Pad Down"]) {
mapping = &out->dpdown;
} else if ([(NSString *)key isEqualToString:@"Cardinal Direction Pad Left"]) {
mapping = &out->dpleft;
} else if ([(NSString *)key isEqualToString:@"Cardinal Direction Pad Right"]) {
mapping = &out->dpright;
} else if ([(NSString *)key isEqualToString:@"Cardinal Direction Pad Up"]) {
mapping = &out->dpup;
} else if ([(NSString *)key isEqualToString:@"Cardinal Direction Pad Down"]) {
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:@"Button Home"]) {
mapping = &out->guide;
} else if ([(NSString *)key isEqualToString:GCInputButtonMenu]) {
if (device->is_siri_remote) {
mapping = &out->b;
} else {
mapping = &out->start;
}
} else if ([(NSString *)key isEqualToString:GCInputButtonOptions]) {
mapping = &out->back;
} else if ([(NSString *)key isEqualToString:@"Button Share"]) {
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;
} else if ([(NSString *)key isEqualToString:@"Button Center"]) {
mapping = &out->a;
}
if (mapping && mapping->kind == EMappingKind_None) {
mapping->kind = EMappingKind_Button;
mapping->target = button;
}
++button;
}
return true;
}
return false;
}
#if defined(SDL_JOYSTICK_MFI) && defined(SDL_PLATFORM_MACOS)
bool IOS_SupportedHIDDevice(IOHIDDeviceRef device)
{
if (!SDL_GetHintBoolean(SDL_HINT_JOYSTICK_MFI, true)) {
return false;
}
if (@available(macOS 11.0, *)) {
const int MAX_ATTEMPTS = 3;
for (int attempt = 0; attempt < MAX_ATTEMPTS; ++attempt) {
if ([GCController supportsHIDDevice:device]) {
return true;
}
SDL_Delay(10);
}
}
return false;
}
#endif
#ifdef SDL_JOYSTICK_MFI
static void GetAppleSFSymbolsNameForElement(GCControllerElement *element, char *name)
{
if (@available(macOS 11.0, iOS 14.0, tvOS 14.0, *)) {
if (element) {
[element.sfSymbolsName getCString:name maxLength:255 encoding:NSASCIIStringEncoding];
}
}
}
static GCControllerDirectionPad *GetDirectionalPadForController(GCController *controller)
{
if (@available(macOS 11.0, iOS 14.0, tvOS 14.0, *)) {
return controller.physicalInputProfile.dpads[GCInputDirectionPad];
}
if (controller.extendedGamepad) {
return controller.extendedGamepad.dpad;
}
if (controller.microGamepad) {
return controller.microGamepad.dpad;
}
return nil;
}
#endif
const char *IOS_GetAppleSFSymbolsNameForButton(SDL_Gamepad *gamepad, SDL_GamepadButton button)
{
char elementName[256];
elementName[0] = '\0';
#ifdef SDL_JOYSTICK_MFI
if (gamepad && SDL_GetGamepadJoystick(gamepad)->driver == &SDL_IOS_JoystickDriver) {
if (@available(macOS 11.0, iOS 14.0, tvOS 14.0, *)) {
GCController *controller = SDL_GetGamepadJoystick(gamepad)->hwdata->controller;
NSDictionary<NSString *, GCControllerElement *> *elements = controller.physicalInputProfile.elements;
switch (button) {
case SDL_GAMEPAD_BUTTON_SOUTH:
GetAppleSFSymbolsNameForElement(elements[GCInputButtonA], elementName);
break;
case SDL_GAMEPAD_BUTTON_EAST:
GetAppleSFSymbolsNameForElement(elements[GCInputButtonB], elementName);
break;
case SDL_GAMEPAD_BUTTON_WEST:
GetAppleSFSymbolsNameForElement(elements[GCInputButtonX], elementName);
break;
case SDL_GAMEPAD_BUTTON_NORTH:
GetAppleSFSymbolsNameForElement(elements[GCInputButtonY], elementName);
break;
case SDL_GAMEPAD_BUTTON_BACK:
GetAppleSFSymbolsNameForElement(elements[GCInputButtonOptions], elementName);
break;
case SDL_GAMEPAD_BUTTON_GUIDE:
GetAppleSFSymbolsNameForElement(elements[@"Button Home"], elementName);
break;
case SDL_GAMEPAD_BUTTON_START:
GetAppleSFSymbolsNameForElement(elements[GCInputButtonMenu], elementName);
break;
case SDL_GAMEPAD_BUTTON_LEFT_STICK:
GetAppleSFSymbolsNameForElement(elements[GCInputLeftThumbstickButton], elementName);
break;
case SDL_GAMEPAD_BUTTON_RIGHT_STICK:
GetAppleSFSymbolsNameForElement(elements[GCInputRightThumbstickButton], elementName);
break;
case SDL_GAMEPAD_BUTTON_LEFT_SHOULDER:
GetAppleSFSymbolsNameForElement(elements[GCInputLeftShoulder], elementName);
break;
case SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER:
GetAppleSFSymbolsNameForElement(elements[GCInputRightShoulder], elementName);
break;
case SDL_GAMEPAD_BUTTON_DPAD_UP:
{
GCControllerDirectionPad *dpad = GetDirectionalPadForController(controller);
if (dpad) {
GetAppleSFSymbolsNameForElement(dpad.up, elementName);
if (SDL_strlen(elementName) == 0) {
SDL_strlcpy(elementName, "dpad.up.fill", sizeof(elementName));
}
}
break;
}
case SDL_GAMEPAD_BUTTON_DPAD_DOWN:
{
GCControllerDirectionPad *dpad = GetDirectionalPadForController(controller);
if (dpad) {
GetAppleSFSymbolsNameForElement(dpad.down, elementName);
if (SDL_strlen(elementName) == 0) {
SDL_strlcpy(elementName, "dpad.down.fill", sizeof(elementName));
}
}
break;
}
case SDL_GAMEPAD_BUTTON_DPAD_LEFT:
{
GCControllerDirectionPad *dpad = GetDirectionalPadForController(controller);
if (dpad) {
GetAppleSFSymbolsNameForElement(dpad.left, elementName);
if (SDL_strlen(elementName) == 0) {
SDL_strlcpy(elementName, "dpad.left.fill", sizeof(elementName));
}
}
break;
}
case SDL_GAMEPAD_BUTTON_DPAD_RIGHT:
{
GCControllerDirectionPad *dpad = GetDirectionalPadForController(controller);
if (dpad) {
GetAppleSFSymbolsNameForElement(dpad.right, elementName);
if (SDL_strlen(elementName) == 0) {
SDL_strlcpy(elementName, "dpad.right.fill", sizeof(elementName));
}
}
break;
}
case SDL_GAMEPAD_BUTTON_MISC1:
GetAppleSFSymbolsNameForElement(elements[GCInputDualShockTouchpadButton], elementName);
break;
case SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1:
GetAppleSFSymbolsNameForElement(elements[GCInputXboxPaddleOne], elementName);
break;
case SDL_GAMEPAD_BUTTON_LEFT_PADDLE1:
GetAppleSFSymbolsNameForElement(elements[GCInputXboxPaddleThree], elementName);
break;
case SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2:
GetAppleSFSymbolsNameForElement(elements[GCInputXboxPaddleTwo], elementName);
break;
case SDL_GAMEPAD_BUTTON_LEFT_PADDLE2:
GetAppleSFSymbolsNameForElement(elements[GCInputXboxPaddleFour], elementName);
break;
case SDL_GAMEPAD_BUTTON_TOUCHPAD:
GetAppleSFSymbolsNameForElement(elements[GCInputDualShockTouchpadButton], elementName);
break;
default:
break;
}
}
}
#endif
return *elementName ? SDL_GetPersistentString(elementName) : NULL;
}
const char *IOS_GetAppleSFSymbolsNameForAxis(SDL_Gamepad *gamepad, SDL_GamepadAxis axis)
{
char elementName[256];
elementName[0] = '\0';
#ifdef SDL_JOYSTICK_MFI
if (gamepad && SDL_GetGamepadJoystick(gamepad)->driver == &SDL_IOS_JoystickDriver) {
if (@available(macOS 11.0, iOS 14.0, tvOS 14.0, *)) {
GCController *controller = SDL_GetGamepadJoystick(gamepad)->hwdata->controller;
NSDictionary<NSString *, GCControllerElement *> *elements = controller.physicalInputProfile.elements;
switch (axis) {
case SDL_GAMEPAD_AXIS_LEFTX:
GetAppleSFSymbolsNameForElement(elements[GCInputLeftThumbstick], elementName);
break;
case SDL_GAMEPAD_AXIS_LEFTY:
GetAppleSFSymbolsNameForElement(elements[GCInputLeftThumbstick], elementName);
break;
case SDL_GAMEPAD_AXIS_RIGHTX:
GetAppleSFSymbolsNameForElement(elements[GCInputRightThumbstick], elementName);
break;
case SDL_GAMEPAD_AXIS_RIGHTY:
GetAppleSFSymbolsNameForElement(elements[GCInputRightThumbstick], elementName);
break;
case SDL_GAMEPAD_AXIS_LEFT_TRIGGER:
GetAppleSFSymbolsNameForElement(elements[GCInputLeftTrigger], elementName);
break;
case SDL_GAMEPAD_AXIS_RIGHT_TRIGGER:
GetAppleSFSymbolsNameForElement(elements[GCInputRightTrigger], elementName);
break;
default:
break;
}
}
}
#endif
return *elementName ? SDL_GetPersistentString(elementName) : NULL;
}
SDL_JoystickDriver SDL_IOS_JoystickDriver = {
IOS_JoystickInit,
IOS_JoystickGetCount,
IOS_JoystickDetect,
IOS_JoystickIsDevicePresent,
IOS_JoystickGetDeviceName,
IOS_JoystickGetDevicePath,
IOS_JoystickGetDeviceSteamVirtualGamepadSlot,
IOS_JoystickGetDevicePlayerIndex,
IOS_JoystickSetDevicePlayerIndex,
IOS_JoystickGetDeviceGUID,
IOS_JoystickGetDeviceInstanceID,
IOS_JoystickOpen,
IOS_JoystickRumble,
IOS_JoystickRumbleTriggers,
IOS_JoystickSetLED,
IOS_JoystickSendEffect,
IOS_JoystickSetSensorsEnabled,
IOS_JoystickUpdate,
IOS_JoystickClose,
IOS_JoystickQuit,
IOS_JoystickGetGamepadMapping
};