diff options
Diffstat (limited to 'source/3rd-party/SDL2/src/joystick/iphoneos/SDL_sysjoystick.m')
-rw-r--r-- | source/3rd-party/SDL2/src/joystick/iphoneos/SDL_sysjoystick.m | 734 |
1 files changed, 734 insertions, 0 deletions
diff --git a/source/3rd-party/SDL2/src/joystick/iphoneos/SDL_sysjoystick.m b/source/3rd-party/SDL2/src/joystick/iphoneos/SDL_sysjoystick.m new file mode 100644 index 0000000..d85efad --- /dev/null +++ b/source/3rd-party/SDL2/src/joystick/iphoneos/SDL_sysjoystick.m @@ -0,0 +1,734 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2018 Sam Lantinga <slouken@libsdl.org> + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "../../SDL_internal.h" + +/* This is the iOS implementation of the SDL joystick API */ +#include "SDL_sysjoystick_c.h" + +/* needed for SDL_IPHONE_MAX_GFORCE macro */ +#include "SDL_config_iphoneos.h" + +#include "SDL_assert.h" +#include "SDL_events.h" +#include "SDL_joystick.h" +#include "SDL_hints.h" +#include "SDL_stdinc.h" +#include "../SDL_sysjoystick.h" +#include "../SDL_joystick_c.h" + + +#if !SDL_EVENTS_DISABLED +#include "../../events/SDL_events_c.h" +#endif + +#if !TARGET_OS_TV +#import <CoreMotion/CoreMotion.h> +#endif + +#ifdef SDL_JOYSTICK_MFI +#import <GameController/GameController.h> + +static id connectObserver = nil; +static id disconnectObserver = nil; +#endif /* SDL_JOYSTICK_MFI */ + +#if !TARGET_OS_TV +static const char *accelerometerName = "iOS Accelerometer"; +static CMMotionManager *motionManager = nil; +#endif /* !TARGET_OS_TV */ + +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; +} + +static void +IOS_AddMFIJoystickDevice(SDL_JoystickDeviceItem *device, GCController *controller) +{ +#ifdef SDL_JOYSTICK_MFI + const Uint16 VENDOR_APPLE = 0x05AC; + Uint16 *guid16 = (Uint16 *)device->guid.data; + Uint16 vendor = 0; + Uint16 product = 0; + Uint16 version = 0; + Uint8 subtype = 0; + + const char *name = NULL; + /* Explicitly retain the controller because SDL_JoystickDeviceItem is a + * struct, and ARC doesn't work with structs. */ + device->controller = (__bridge GCController *) CFBridgingRetain(controller); + + if (controller.vendorName) { + name = controller.vendorName.UTF8String; + } + + if (!name) { + name = "MFi Gamepad"; + } + + device->name = SDL_strdup(name); + + if (controller.extendedGamepad) { + vendor = VENDOR_APPLE; + product = 1; + subtype = 1; + device->naxes = 6; /* 2 thumbsticks and 2 triggers */ + device->nhats = 1; /* d-pad */ + device->nbuttons = 7; /* ABXY, shoulder buttons, pause button */ + } else if (controller.gamepad) { + vendor = VENDOR_APPLE; + product = 2; + subtype = 2; + device->naxes = 0; /* no traditional analog inputs */ + device->nhats = 1; /* d-pad */ + device->nbuttons = 7; /* ABXY, shoulder buttons, pause button */ + } +#if TARGET_OS_TV + else if (controller.microGamepad) { + vendor = VENDOR_APPLE; + product = 3; + subtype = 3; + device->naxes = 2; /* treat the touch surface as two axes */ + device->nhats = 0; /* apparently the touch surface-as-dpad is buggy */ + device->nbuttons = 3; /* AX, pause button */ + + controller.microGamepad.allowsRotation = SDL_GetHintBoolean(SDL_HINT_APPLE_TV_REMOTE_ALLOW_ROTATION, SDL_FALSE); + } +#endif /* TARGET_OS_TV */ + + /* We only need 16 bits for each of these; space them out to fill 128. */ + /* Byteswap so devices get same GUID on little/big endian platforms. */ + *guid16++ = SDL_SwapLE16(SDL_HARDWARE_BUS_BLUETOOTH); + *guid16++ = 0; + *guid16++ = SDL_SwapLE16(vendor); + *guid16++ = 0; + *guid16++ = SDL_SwapLE16(product); + *guid16++ = 0; + *guid16++ = SDL_SwapLE16(version); + *guid16++ = 0; + + /* Note that this is an MFI controller and what subtype it is */ + device->guid.data[14] = 'm'; + device->guid.data[15] = subtype; + + /* This will be set when the first button press of the controller is + * detected. */ + controller.playerIndex = -1; + +#endif /* SDL_JOYSTICK_MFI */ +} + +static void +IOS_AddJoystickDevice(GCController *controller, SDL_bool accelerometer) +{ + SDL_JoystickDeviceItem *device = deviceList; + +#if TARGET_OS_TV + if (!SDL_GetHintBoolean(SDL_HINT_TV_REMOTE_AS_JOYSTICK, SDL_TRUE)) { + /* Ignore devices that aren't actually controllers (e.g. remotes), they'll be handled as keyboard input */ + if (controller && !controller.extendedGamepad && !controller.gamepad && controller.microGamepad) { + return; + } + } +#endif + + 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->accelerometer = accelerometer; + device->instance_id = SDL_GetNextJoystickInstanceID(); + + if (accelerometer) { +#if TARGET_OS_TV + SDL_free(device); + return; +#else + device->name = SDL_strdup(accelerometerName); + device->naxes = 3; /* Device acceleration in the x, y, and z axes. */ + device->nhats = 0; + device->nbuttons = 0; + + /* Use the accelerometer name as a GUID. */ + SDL_memcpy(&device->guid.data, device->name, SDL_min(sizeof(SDL_JoystickGUID), SDL_strlen(device->name))); +#endif /* TARGET_OS_TV */ + } else if (controller) { + IOS_AddMFIJoystickDevice(device, controller); + } + + 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); +} + +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; + } + + /* Unlink the device item from the device list. */ + 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) { + /* The controller was explicitly retained in the struct, so it + * should be explicitly released before freeing the struct. */ + GCController *controller = CFBridgingRelease((__bridge CFTypeRef)(device->controller)); + controller.controllerPausedHandler = nil; + device->controller = nil; + } + } +#endif /* SDL_JOYSTICK_MFI */ + + --numjoysticks; + + SDL_PrivateJoystickRemoved(device->instance_id); + + SDL_free(device->name); + SDL_free(device); + + return next; +} + +#if TARGET_OS_TV +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 /* TARGET_OS_TV */ + +static int +IOS_JoystickInit(void) +{ + @autoreleasepool { + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; + +#if !TARGET_OS_TV + if (SDL_GetHintBoolean(SDL_HINT_ACCELEROMETER_AS_JOYSTICK, SDL_TRUE)) { + /* Default behavior, accelerometer as joystick */ + IOS_AddJoystickDevice(nil, SDL_TRUE); + } +#endif /* !TARGET_OS_TV */ + +#ifdef SDL_JOYSTICK_MFI + /* GameController.framework was added in iOS 7. */ + if (![GCController class]) { + return 0; + } + + for (GCController *controller in [GCController controllers]) { + IOS_AddJoystickDevice(controller, SDL_FALSE); + } + +#if TARGET_OS_TV + SDL_AddHintCallback(SDL_HINT_APPLE_TV_REMOTE_ALLOW_ROTATION, + SDL_AppleTVRemoteRotationHintChanged, NULL); +#endif /* TARGET_OS_TV */ + + connectObserver = [center addObserverForName:GCControllerDidConnectNotification + object:nil + queue:nil + usingBlock:^(NSNotification *note) { + GCController *controller = note.object; + IOS_AddJoystickDevice(controller, SDL_FALSE); + }]; + + disconnectObserver = [center addObserverForName:GCControllerDidDisconnectNotification + object:nil + queue:nil + usingBlock:^(NSNotification *note) { + GCController *controller = note.object; + SDL_JoystickDeviceItem *device = deviceList; + while (device != NULL) { + if (device->controller == controller) { + IOS_RemoveJoystickDevice(device); + break; + } + device = device->next; + } + }]; +#endif /* SDL_JOYSTICK_MFI */ + } + + return 0; +} + +static int +IOS_JoystickGetCount(void) +{ + return numjoysticks; +} + +static void +IOS_JoystickDetect(void) +{ +} + +static const char * +IOS_JoystickGetDeviceName(int device_index) +{ + SDL_JoystickDeviceItem *device = GetDeviceForIndex(device_index); + return device ? device->name : "Unknown"; +} + +static int +IOS_JoystickGetDevicePlayerIndex(int device_index) +{ + return -1; +} + +static SDL_JoystickGUID +IOS_JoystickGetDeviceGUID( int device_index ) +{ + SDL_JoystickDeviceItem *device = GetDeviceForIndex(device_index); + SDL_JoystickGUID 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 : -1; +} + +static int +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->instance_id = device->instance_id; + + joystick->naxes = device->naxes; + joystick->nhats = device->nhats; + joystick->nbuttons = device->nbuttons; + joystick->nballs = 0; + + device->joystick = joystick; + + @autoreleasepool { + if (device->accelerometer) { +#if !TARGET_OS_TV + if (motionManager == nil) { + motionManager = [[CMMotionManager alloc] init]; + } + + /* Shorter times between updates can significantly increase CPU usage. */ + motionManager.accelerometerUpdateInterval = 0.1; + [motionManager startAccelerometerUpdates]; +#endif /* !TARGET_OS_TV */ + } else { +#ifdef SDL_JOYSTICK_MFI + GCController *controller = device->controller; + controller.controllerPausedHandler = ^(GCController *c) { + if (joystick->hwdata) { + ++joystick->hwdata->num_pause_presses; + } + }; +#endif /* SDL_JOYSTICK_MFI */ + } + } + if (device->remote) { + ++SDL_AppleTVRemoteOpenedAsJoystick; + } + + return 0; +} + +static void +IOS_AccelerometerUpdate(SDL_Joystick * joystick) +{ +#if !TARGET_OS_TV + const float maxgforce = SDL_IPHONE_MAX_GFORCE; + const SInt16 maxsint16 = 0x7FFF; + CMAcceleration accel; + + @autoreleasepool { + if (!motionManager.isAccelerometerActive) { + return; + } + + accel = motionManager.accelerometerData.acceleration; + } + + /* + Convert accelerometer data from floating point to Sint16, which is what + the joystick system expects. + + To do the conversion, the data is first clamped onto the interval + [-SDL_IPHONE_MAX_G_FORCE, SDL_IPHONE_MAX_G_FORCE], then the data is multiplied + by MAX_SINT16 so that it is mapped to the full range of an Sint16. + + You can customize the clamped range of this function by modifying the + SDL_IPHONE_MAX_GFORCE macro in SDL_config_iphoneos.h. + + Once converted to Sint16, the accelerometer data no longer has coherent + units. You can convert the data back to units of g-force by multiplying + it in your application's code by SDL_IPHONE_MAX_GFORCE / 0x7FFF. + */ + + /* clamp the data */ + accel.x = SDL_min(SDL_max(accel.x, -maxgforce), maxgforce); + accel.y = SDL_min(SDL_max(accel.y, -maxgforce), maxgforce); + accel.z = SDL_min(SDL_max(accel.z, -maxgforce), maxgforce); + + /* pass in data mapped to range of SInt16 */ + SDL_PrivateJoystickAxis(joystick, 0, (accel.x / maxgforce) * maxsint16); + SDL_PrivateJoystickAxis(joystick, 1, -(accel.y / maxgforce) * maxsint16); + SDL_PrivateJoystickAxis(joystick, 2, (accel.z / maxgforce) * maxsint16); +#endif /* !TARGET_OS_TV */ +} + +#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) +{ +#if SDL_JOYSTICK_MFI + @autoreleasepool { + GCController *controller = joystick->hwdata->controller; + Uint8 hatstate = SDL_HAT_CENTERED; + int i; + int updateplayerindex = 0; + + if (controller.extendedGamepad) { + GCExtendedGamepad *gamepad = controller.extendedGamepad; + + /* Axis order matches the XInput Windows mappings. */ + 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), + }; + + /* Button order matches the XInput Windows mappings. */ + Uint8 buttons[] = { + gamepad.buttonA.isPressed, gamepad.buttonB.isPressed, + gamepad.buttonX.isPressed, gamepad.buttonY.isPressed, + gamepad.leftShoulder.isPressed, + gamepad.rightShoulder.isPressed, + }; + + hatstate = IOS_MFIJoystickHatStateForDPad(gamepad.dpad); + + for (i = 0; i < SDL_arraysize(axes); i++) { + /* The triggers (axes 2 and 5) are resting at -32768 but SDL + * initializes its values to 0. We only want to make sure the + * player index is up to date if the user actually moves an axis. */ + if ((i != 2 && i != 5) || axes[i] != -32768) { + updateplayerindex |= (joystick->axes[i].value != axes[i]); + } + SDL_PrivateJoystickAxis(joystick, i, axes[i]); + } + + for (i = 0; i < SDL_arraysize(buttons); i++) { + updateplayerindex |= (joystick->buttons[i] != buttons[i]); + SDL_PrivateJoystickButton(joystick, i, buttons[i]); + } + } else if (controller.gamepad) { + GCGamepad *gamepad = controller.gamepad; + + /* Button order matches the XInput Windows mappings. */ + Uint8 buttons[] = { + gamepad.buttonA.isPressed, gamepad.buttonB.isPressed, + gamepad.buttonX.isPressed, gamepad.buttonY.isPressed, + gamepad.leftShoulder.isPressed, + gamepad.rightShoulder.isPressed, + }; + + hatstate = IOS_MFIJoystickHatStateForDPad(gamepad.dpad); + + for (i = 0; i < SDL_arraysize(buttons); i++) { + updateplayerindex |= (joystick->buttons[i] != buttons[i]); + SDL_PrivateJoystickButton(joystick, i, buttons[i]); + } + } +#if TARGET_OS_TV + 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++) { + updateplayerindex |= (joystick->axes[i].value != axes[i]); + SDL_PrivateJoystickAxis(joystick, i, axes[i]); + } + + Uint8 buttons[] = { + gamepad.buttonA.isPressed, + gamepad.buttonX.isPressed, + }; + + for (i = 0; i < SDL_arraysize(buttons); i++) { + updateplayerindex |= (joystick->buttons[i] != buttons[i]); + SDL_PrivateJoystickButton(joystick, i, buttons[i]); + } + } +#endif /* TARGET_OS_TV */ + + if (joystick->nhats > 0) { + updateplayerindex |= (joystick->hats[0] != hatstate); + SDL_PrivateJoystickHat(joystick, 0, hatstate); + } + + for (i = 0; i < joystick->hwdata->num_pause_presses; i++) { + const Uint8 pausebutton = joystick->nbuttons - 1; /* The pause button is always last. */ + SDL_PrivateJoystickButton(joystick, pausebutton, SDL_PRESSED); + SDL_PrivateJoystickButton(joystick, pausebutton, SDL_RELEASED); + updateplayerindex = YES; + } + joystick->hwdata->num_pause_presses = 0; + + if (updateplayerindex && controller.playerIndex == -1) { + BOOL usedPlayerIndexSlots[4] = {NO, NO, NO, NO}; + + /* Find the player index of all other connected controllers. */ + for (GCController *c in [GCController controllers]) { + if (c != controller && c.playerIndex >= 0) { + usedPlayerIndexSlots[c.playerIndex] = YES; + } + } + + /* Set this controller's player index to the first unused index. + * FIXME: This logic isn't great... but SDL doesn't expose this + * concept in its external API, so we don't have much to go on. */ + for (i = 0; i < SDL_arraysize(usedPlayerIndexSlots); i++) { + if (!usedPlayerIndexSlots[i]) { + controller.playerIndex = i; + break; + } + } + } + } +#endif /* SDL_JOYSTICK_MFI */ +} + +static int +IOS_JoystickRumble(SDL_Joystick * joystick, Uint16 low_frequency_rumble, Uint16 high_frequency_rumble, Uint32 duration_ms) +{ + return SDL_Unsupported(); +} + +static void +IOS_JoystickUpdate(SDL_Joystick * joystick) +{ + SDL_JoystickDeviceItem *device = joystick->hwdata; + + if (device == NULL) { + return; + } + + if (device->accelerometer) { + IOS_AccelerometerUpdate(joystick); + } else 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; + + @autoreleasepool { + if (device->accelerometer) { +#if !TARGET_OS_TV + [motionManager stopAccelerometerUpdates]; +#endif /* !TARGET_OS_TV */ + } else if (device->controller) { +#ifdef SDL_JOYSTICK_MFI + GCController *controller = device->controller; + controller.controllerPausedHandler = nil; + controller.playerIndex = -1; +#endif + } + } + if (device->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; + } + +#if TARGET_OS_TV + SDL_DelHintCallback(SDL_HINT_APPLE_TV_REMOTE_ALLOW_ROTATION, + SDL_AppleTVRemoteRotationHintChanged, NULL); +#endif /* TARGET_OS_TV */ +#endif /* SDL_JOYSTICK_MFI */ + + while (deviceList != NULL) { + IOS_RemoveJoystickDevice(deviceList); + } + +#if !TARGET_OS_TV + motionManager = nil; +#endif /* !TARGET_OS_TV */ + } + + numjoysticks = 0; +} + +SDL_JoystickDriver SDL_IOS_JoystickDriver = +{ + IOS_JoystickInit, + IOS_JoystickGetCount, + IOS_JoystickDetect, + IOS_JoystickGetDeviceName, + IOS_JoystickGetDevicePlayerIndex, + IOS_JoystickGetDeviceGUID, + IOS_JoystickGetDeviceInstanceID, + IOS_JoystickOpen, + IOS_JoystickRumble, + IOS_JoystickUpdate, + IOS_JoystickClose, + IOS_JoystickQuit, +}; + +/* vi: set ts=4 sw=4 expandtab: */ |