/*
* Copyright (c) 2008-2024 Jonathan Schleifer <js@nil.im>
*
* All rights reserved.
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License version 3.0 only,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
* version 3.0 for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* version 3.0 along with this program. If not, see
* <https://www.gnu.org/licenses/>.
*/
#include "config.h"
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#import "OFEvdevGameController.h"
#import "OFArray.h"
#import "OFFileManager.h"
#import "OFLocale.h"
#import "OFNumber.h"
#import "OFSet.h"
#include <sys/ioctl.h>
#include <linux/input.h>
#import "OFInitializationFailedException.h"
#import "OFInvalidArgumentException.h"
#import "OFOpenItemFailedException.h"
#import "OFOutOfRangeException.h"
#import "OFReadFailedException.h"
/*
* Controllers with tested correct mapping:
*
* Microsoft X-Box 360 pad [045E:028E]
* Joy-Con (L) [057E:2006]
* Joy-Con (R) [057E:2007]
* N64 Controller [057E:2019]
* Sony Interactive Entertainment DualSense Wireless Controller [054C:0CE6]
* 8BitDo Pro 2 Wired Controller [2DC8:3106]
* Stadia2SZY-0d6c [18D1:9400]
* Wireless Controller [054C:09CC]
*/
static const uint16_t vendorIDMicrosoft = 0x045E;
static const uint16_t vendorIDNintendo = 0x057E;
static const uint16_t vendorIDSony = 0x054C;
static const uint16_t vendorIDGoogle = 0x18D1;
/* Microsoft controllers */
static const uint16_t productIDXbox360 = 0x028E;
/* Nintendo controllers */
static const uint16_t productIDLeftJoyCon = 0x2006;
static const uint16_t productIDRightJoyCon = 0x2007;
static const uint16_t productIDN64Controller = 0x2019;
/* Sony controllers */
static const uint16_t productIDDualSense = 0x0CE6;
static const uint16_t productIDDualShock4 = 0x09CC;
/* Google controllers */
static const uint16_t productIDStadia = 0x9400;
static const uint16_t buttons[] = {
BTN_A, BTN_B, BTN_C, BTN_X, BTN_Y, BTN_Z, BTN_TL, BTN_TR, BTN_TL2,
BTN_TR2, BTN_SELECT, BTN_START, BTN_MODE, BTN_THUMBL, BTN_THUMBR,
BTN_DPAD_UP, BTN_DPAD_DOWN, BTN_DPAD_LEFT, BTN_DPAD_RIGHT,
BTN_TRIGGER_HAPPY1, BTN_TRIGGER_HAPPY2
};
static OFGameControllerButton
buttonToName(uint16_t button, uint16_t vendorID, uint16_t productID)
{
if (vendorID == vendorIDNintendo &&
productID == productIDLeftJoyCon) {
switch (button) {
case BTN_DPAD_RIGHT:
return OFGameControllerNorthButton;
case BTN_DPAD_LEFT:
return OFGameControllerSouthButton;
case BTN_DPAD_UP:
return OFGameControllerWestButton;
case BTN_DPAD_DOWN:
return OFGameControllerEastButton;
case BTN_Z:
return OFGameControllerCaptureButton;
case BTN_TR:
return @"SL";
case BTN_TR2:
return @"SR";
}
} else if (vendorID == vendorIDNintendo &&
productID == productIDRightJoyCon) {
switch (button) {
case BTN_WEST:
return OFGameControllerNorthButton;
case BTN_EAST:
return OFGameControllerSouthButton;
case BTN_SOUTH:
return OFGameControllerWestButton;
case BTN_NORTH:
return OFGameControllerEastButton;
case BTN_TL:
return @"SL";
case BTN_TL2:
return @"SR";
}
} else if (vendorID == vendorIDNintendo &&
productID == productIDN64Controller) {
switch (button) {
case BTN_B:
return OFGameControllerWestButton;
case BTN_SELECT:
return @"_C-Pad Up";
case BTN_X:
return @"_C-Pad Down";
case BTN_Y:
return @"_C-Pad Left";
case BTN_C:
return @"_C-Pad Right";
case BTN_Z:
return OFGameControllerCaptureButton;
}
} else if (vendorID == vendorIDSony &&
(productID == productIDDualSense ||
productID == productIDDualShock4)) {
switch (button) {
case BTN_NORTH:
return OFGameControllerNorthButton;
case BTN_WEST:
return OFGameControllerWestButton;
}
} else if (vendorID == vendorIDGoogle && productID == productIDStadia) {
switch (button) {
case BTN_TRIGGER_HAPPY1:
return @"Assistant";
case BTN_TRIGGER_HAPPY2:
return OFGameControllerCaptureButton;
}
}
switch (button) {
case BTN_Y:
return OFGameControllerNorthButton;
case BTN_A:
return OFGameControllerSouthButton;
case BTN_X:
return OFGameControllerWestButton;
case BTN_B:
return OFGameControllerEastButton;
case BTN_TL2:
return OFGameControllerLeftTriggerButton;
case BTN_TR2:
return OFGameControllerRightTriggerButton;
case BTN_TL:
return OFGameControllerLeftShoulderButton;
case BTN_TR:
return OFGameControllerRightShoulderButton;
case BTN_THUMBL:
return OFGameControllerLeftStickButton;
case BTN_THUMBR:
return OFGameControllerRightStickButton;
case BTN_DPAD_UP:
return OFGameControllerDPadUpButton;
case BTN_DPAD_DOWN:
return OFGameControllerDPadDownButton;
case BTN_DPAD_LEFT:
return OFGameControllerDPadLeftButton;
case BTN_DPAD_RIGHT:
return OFGameControllerDPadRightButton;
case BTN_START:
return OFGameControllerStartButton;
case BTN_SELECT:
return OFGameControllerSelectButton;
case BTN_MODE:
return OFGameControllerHomeButton;
}
return nil;
}
static float
scale(float value, float min, float max)
{
if (value < min)
value = min;
if (value > max)
value = max;
return ((value - min) / (max - min) * 2) - 1;
}
static bool
emulateRightAnalogStick(uint16_t vendorID, uint16_t productID,
OFMutableSet *pressedButtons, OFPoint *rightAnalogStickPosition)
{
if (vendorID == vendorIDNintendo &&
productID == productIDN64Controller) {
if ([pressedButtons containsObject: @"_C-Pad Left"] &&
[pressedButtons containsObject: @"_C-Pad Right"])
rightAnalogStickPosition->x = -0.f;
else if ([pressedButtons containsObject: @"_C-Pad Left"])
rightAnalogStickPosition->x = -1.f;
else if ([pressedButtons containsObject: @"_C-Pad Right"])
rightAnalogStickPosition->x = 1.f;
else
rightAnalogStickPosition->x = 0.f;
if ([pressedButtons containsObject: @"_C-Pad Up"] &&
[pressedButtons containsObject: @"_C-Pad Down"])
rightAnalogStickPosition->y = -0.f;
else if ([pressedButtons containsObject: @"_C-Pad Up"])
rightAnalogStickPosition->y = -1.f;
else if ([pressedButtons containsObject: @"_C-Pad Down"])
rightAnalogStickPosition->y = 1.f;
else
rightAnalogStickPosition->y = 0.f;
return true;
}
return false;
}
@implementation OFEvdevGameController
@synthesize name = _name, buttons = _buttons;
@synthesize hasLeftAnalogStick = _hasLeftAnalogStick;
@synthesize hasRightAnalogStick = _hasRightAnalogStick;
@synthesize rightAnalogStickPosition = _rightAnalogStickPosition;
+ (OFArray OF_GENERIC(OFGameController *) *)controllers
{
OFMutableArray *controllers = [OFMutableArray array];
void *pool = objc_autoreleasePoolPush();
for (OFString *device in [[OFFileManager defaultManager]
contentsOfDirectoryAtPath: @"/dev/input"]) {
OFString *path;
OFGameController *controller;
if (![device hasPrefix: @"event"])
continue;
path = [@"/dev/input" stringByAppendingPathComponent: device];
@try {
controller = [[[OFEvdevGameController alloc]
of_initWithPath: path] autorelease];
} @catch (OFOpenItemFailedException *e) {
if (e.errNo == EACCES)
continue;
@throw e;
} @catch (OFInvalidArgumentException *e) {
/* Not a game controller. */
continue;
}
[controllers addObject: controller];
}
[controllers sort];
[controllers makeImmutable];
objc_autoreleasePoolPop(pool);
return controllers;
}
- (instancetype)of_initWithPath: (OFString *)path
{
self = [super init];
@try {
OFStringEncoding encoding = [OFLocale encoding];
unsigned long evBits[OFRoundUpToPowerOf2(OF_ULONG_BIT,
EV_MAX) / OF_ULONG_BIT] = { 0 };
unsigned long absBits[OFRoundUpToPowerOf2(OF_ULONG_BIT,
ABS_MAX) / OF_ULONG_BIT] = { 0 };
struct input_id inputID;
char name[128];
_path = [path copy];
if ((_fd = open([_path cStringWithEncoding: encoding],
O_RDONLY | O_NONBLOCK)) == -1)
@throw [OFOpenItemFailedException
exceptionWithPath: _path
mode: @"r"
errNo: errno];
if (ioctl(_fd, EVIOCGBIT(0, sizeof(evBits)), evBits) == -1)
@throw [OFInitializationFailedException exception];
if (!OFBitSetIsSet(evBits, EV_KEY))
@throw [OFInvalidArgumentException exception];
_keyBits = OFAllocZeroedMemory(OFRoundUpToPowerOf2(OF_ULONG_BIT,
KEY_MAX) / OF_ULONG_BIT, sizeof(unsigned long));
if (ioctl(_fd, EVIOCGBIT(EV_KEY, OFRoundUpToPowerOf2(
OF_ULONG_BIT, KEY_MAX) / OF_ULONG_BIT *
sizeof(unsigned long)), _keyBits) == -1)
@throw [OFInitializationFailedException exception];
if (!OFBitSetIsSet(_keyBits, BTN_GAMEPAD) &&
!OFBitSetIsSet(_keyBits, BTN_DPAD_UP))
@throw [OFInvalidArgumentException exception];
if (ioctl(_fd, EVIOCGID, &inputID) == -1)
@throw [OFInvalidArgumentException exception];
_vendorID = inputID.vendor;
_productID = inputID.product;
if (ioctl(_fd, EVIOCGNAME(sizeof(name)), name) == -1)
@throw [OFInitializationFailedException exception];
_name = [[OFString alloc] initWithCString: name
encoding: encoding];
_buttons = [[OFMutableSet alloc] init];
for (size_t i = 0; i < sizeof(buttons) / sizeof(*buttons);
i++) {
if (OFBitSetIsSet(_keyBits, buttons[i])) {
OFGameControllerButton button = buttonToName(
buttons[i], _vendorID, _productID);
if (button != nil && ![button hasPrefix: @"_"])
[_buttons addObject: button];
}
}
_pressedButtons = [[OFMutableSet alloc] init];
if (OFBitSetIsSet(evBits, EV_ABS)) {
if (ioctl(_fd, EVIOCGBIT(EV_ABS, sizeof(absBits)),
absBits) == -1)
@throw [OFInitializationFailedException
exception];
if (_vendorID == vendorIDNintendo &&
_productID == productIDRightJoyCon) {
/*
* Make the right analog stick on the right
* Joy-Con the left analog stick so that it can
* be used as a single controller.
*/
_leftAnalogStickXBit = ABS_RX;
_leftAnalogStickYBit = ABS_RY;
_rightAnalogStickXBit = ABS_X;
_rightAnalogStickYBit = ABS_Y;
_leftTriggerPressureBit = ABS_Z;
_rightTriggerPressureBit = ABS_RZ;
} else if (_vendorID == vendorIDGoogle &&
_productID == productIDStadia) {
/*
* It's unclear how this can be screwed up
* *this* bad.
*/
_leftAnalogStickXBit = ABS_X;
_leftAnalogStickYBit = ABS_Y;
_rightAnalogStickXBit = ABS_Z;
_rightAnalogStickYBit = ABS_RZ;
_leftTriggerPressureBit = ABS_BRAKE;
_rightTriggerPressureBit = ABS_GAS;
} else {
_leftAnalogStickXBit = ABS_X;
_leftAnalogStickYBit = ABS_Y;
_rightAnalogStickXBit = ABS_RX;
_rightAnalogStickYBit = ABS_RY;
_leftTriggerPressureBit = ABS_Z;
_rightTriggerPressureBit = ABS_RZ;
}
if (OFBitSetIsSet(absBits, _leftAnalogStickXBit) &&
OFBitSetIsSet(absBits, _leftAnalogStickYBit))
_hasLeftAnalogStick = true;
if (OFBitSetIsSet(absBits, _rightAnalogStickXBit) &&
OFBitSetIsSet(absBits, _rightAnalogStickYBit))
_hasRightAnalogStick = true;
if (_vendorID == vendorIDNintendo &&
_productID == productIDN64Controller &&
OFBitSetIsSet(_keyBits, BTN_Y) &&
OFBitSetIsSet(_keyBits, BTN_C) &&
OFBitSetIsSet(_keyBits, BTN_SELECT) &&
OFBitSetIsSet(_keyBits, BTN_X))
_hasRightAnalogStick = true;
if (OFBitSetIsSet(absBits, ABS_HAT0X) &&
OFBitSetIsSet(absBits, ABS_HAT0Y)) {
_DPadIsHAT0 = true;
[_buttons addObject:
OFGameControllerDPadLeftButton];
[_buttons addObject:
OFGameControllerDPadRightButton];
[_buttons addObject:
OFGameControllerDPadUpButton];
[_buttons addObject:
OFGameControllerDPadDownButton];
}
if (OFBitSetIsSet(absBits, _leftTriggerPressureBit)) {
_hasLeftTriggerPressure = true;
[_buttons addObject:
OFGameControllerLeftTriggerButton];
}
if (OFBitSetIsSet(absBits, _rightTriggerPressureBit)) {
_hasRightTriggerPressure = true;
[_buttons addObject:
OFGameControllerRightTriggerButton];
}
}
[_buttons makeImmutable];
@try {
[self of_pollState];
} @catch (OFReadFailedException *e) {
@throw [OFInitializationFailedException exception];
}
} @catch (id e) {
[self release];
@throw e;
}
return self;
}
- (void)dealloc
{
[_path release];
if (_fd != -1)
close(_fd);
OFFreeMemory(_keyBits);
[_name release];
[_buttons release];
[_pressedButtons release];
[super dealloc];
}
- (OFNumber *)vendorID
{
return [OFNumber numberWithUnsignedShort: _vendorID];
}
- (OFNumber *)productID
{
return [OFNumber numberWithUnsignedShort: _productID];
}
- (void)of_pollState
{
unsigned long keyState[OFRoundUpToPowerOf2(OF_ULONG_BIT, KEY_MAX) /
OF_ULONG_BIT] = { 0 };
if (ioctl(_fd, EVIOCGKEY(sizeof(keyState)), &keyState) == -1)
@throw [OFReadFailedException
exceptionWithObject: self
requestedLength: sizeof(keyState)
errNo: errno];
[_pressedButtons removeAllObjects];
for (size_t i = 0; i < sizeof(buttons) / sizeof(*buttons);
i++) {
if (OFBitSetIsSet(_keyBits, buttons[i]) &&
OFBitSetIsSet(keyState, buttons[i])) {
OFGameControllerButton button = buttonToName(
buttons[i], _vendorID, _productID);
if (button != nil)
[_pressedButtons addObject: button];
}
}
if (_DPadIsHAT0) {
struct input_absinfo infoX, infoY;
if (ioctl(_fd, EVIOCGABS(ABS_HAT0X), &infoX) == -1)
@throw [OFReadFailedException
exceptionWithObject: self
requestedLength: sizeof(infoX)
errNo: errno];
if (ioctl(_fd, EVIOCGABS(ABS_HAT0Y), &infoY) == -1)
@throw [OFReadFailedException
exceptionWithObject: self
requestedLength: sizeof(infoY)
errNo: errno];
if (infoX.value < 0)
[_pressedButtons addObject:
OFGameControllerDPadLeftButton];
else if (infoX.value > 0)
[_pressedButtons addObject:
OFGameControllerDPadRightButton];
if (infoY.value < 0)
[_pressedButtons addObject:
OFGameControllerDPadUpButton];
else if (infoY.value > 0)
[_pressedButtons addObject:
OFGameControllerDPadDownButton];
}
if (_hasLeftAnalogStick) {
struct input_absinfo infoX, infoY;
if (ioctl(_fd, EVIOCGABS(_leftAnalogStickXBit), &infoX) == -1)
@throw [OFReadFailedException
exceptionWithObject: self
requestedLength: sizeof(infoX)
errNo: errno];
if (ioctl(_fd, EVIOCGABS(_leftAnalogStickYBit), &infoY) == -1)
@throw [OFReadFailedException
exceptionWithObject: self
requestedLength: sizeof(infoY)
errNo: errno];
_leftAnalogStickMinX = infoX.minimum;
_leftAnalogStickMaxX = infoX.maximum;
_leftAnalogStickMinY = infoY.minimum;
_leftAnalogStickMaxY = infoY.maximum;
_leftAnalogStickPosition.x = scale(infoX.value,
_leftAnalogStickMinX, _leftAnalogStickMaxX);
_leftAnalogStickPosition.y = scale(infoY.value,
_leftAnalogStickMinY, _leftAnalogStickMaxY);
}
if (!emulateRightAnalogStick(_vendorID, _productID, _pressedButtons,
&_rightAnalogStickPosition) && _hasRightAnalogStick) {
struct input_absinfo infoX, infoY;
if (ioctl(_fd, EVIOCGABS(_rightAnalogStickXBit), &infoX) == -1)
@throw [OFReadFailedException
exceptionWithObject: self
requestedLength: sizeof(infoX)
errNo: errno];
if (ioctl(_fd, EVIOCGABS(_rightAnalogStickYBit), &infoY) == -1)
@throw [OFReadFailedException
exceptionWithObject: self
requestedLength: sizeof(infoY)
errNo: errno];
_rightAnalogStickMinX = infoX.minimum;
_rightAnalogStickMaxX = infoX.maximum;
_rightAnalogStickMinY = infoY.minimum;
_rightAnalogStickMaxY = infoY.maximum;
_rightAnalogStickPosition.x = scale(infoX.value,
_rightAnalogStickMinX, _rightAnalogStickMaxX);
_rightAnalogStickPosition.y = scale(infoY.value,
_rightAnalogStickMinY, _rightAnalogStickMaxY);
}
if (_hasLeftTriggerPressure) {
struct input_absinfo info;
if (ioctl(_fd, EVIOCGABS( _leftTriggerPressureBit), &info) ==
-1)
@throw [OFReadFailedException
exceptionWithObject: self
requestedLength: sizeof(info)
errNo: errno];
_leftTriggerMinPressure = info.minimum;
_leftTriggerMaxPressure = info.maximum;
_leftTriggerPressure = scale(info.value,
_leftTriggerMinPressure, _leftTriggerMaxPressure);
}
if (_hasRightTriggerPressure) {
struct input_absinfo info;
if (ioctl(_fd, EVIOCGABS(_rightTriggerPressureBit), &info) ==
-1)
@throw [OFReadFailedException
exceptionWithObject: self
requestedLength: sizeof(info)
errNo: errno];
_rightTriggerMinPressure = info.minimum;
_rightTriggerMaxPressure = info.maximum;
_rightTriggerPressure = scale(info.value,
_rightTriggerMinPressure, _rightTriggerMaxPressure);
}
}
- (void)retrieveState
{
struct input_event event;
for (;;) {
OFGameControllerButton button;
errno = 0;
if (read(_fd, &event, sizeof(event)) < (int)sizeof(event)) {
if (errno == EWOULDBLOCK)
return;
@throw [OFReadFailedException
exceptionWithObject: self
requestedLength: sizeof(event)
errNo: errno];
}
if (_discardUntilReport) {
if (event.type == EV_SYN && event.value == SYN_REPORT) {
_discardUntilReport = false;
[self of_pollState];
}
continue;
}
switch (event.type) {
case EV_SYN:
if (event.value == SYN_DROPPED) {
_discardUntilReport = true;
continue;
}
break;
case EV_KEY:
if ((button = buttonToName(event.code, _vendorID,
_productID)) != nil) {
if (event.value)
[_pressedButtons addObject: button];
else
[_pressedButtons removeObject: button];
emulateRightAnalogStick(_vendorID, _productID,
_pressedButtons,
&_rightAnalogStickPosition);
}
break;
case EV_ABS:
if (event.code == _leftAnalogStickXBit)
_leftAnalogStickPosition.x = scale(event.value,
_leftAnalogStickMinX, _leftAnalogStickMaxX);
else if (event.code == _leftAnalogStickYBit)
_leftAnalogStickPosition.y = scale(event.value,
_leftAnalogStickMinY, _leftAnalogStickMaxY);
else if (event.code == _rightAnalogStickXBit)
_rightAnalogStickPosition.x = scale(event.value,
_rightAnalogStickMinX,
_rightAnalogStickMaxX);
else if (event.code == _rightAnalogStickYBit)
_rightAnalogStickPosition.y = scale(event.value,
_rightAnalogStickMinY,
_rightAnalogStickMaxY);
else if (event.code == ABS_HAT0X) {
if (event.value < 0) {
[_pressedButtons addObject:
OFGameControllerDPadLeftButton];
[_pressedButtons removeObject:
OFGameControllerDPadRightButton];
} else if (event.value > 0) {
[_pressedButtons addObject:
OFGameControllerDPadRightButton];
[_pressedButtons removeObject:
OFGameControllerDPadLeftButton];
} else {
[_pressedButtons removeObject:
OFGameControllerDPadLeftButton];
[_pressedButtons removeObject:
OFGameControllerDPadRightButton];
}
} else if (event.code == ABS_HAT0Y) {
if (event.value < 0) {
[_pressedButtons addObject:
OFGameControllerDPadUpButton];
[_pressedButtons removeObject:
OFGameControllerDPadDownButton];
} else if (event.value > 0) {
[_pressedButtons addObject:
OFGameControllerDPadDownButton];
[_pressedButtons removeObject:
OFGameControllerDPadUpButton];
} else {
[_pressedButtons removeObject:
OFGameControllerDPadUpButton];
[_pressedButtons removeObject:
OFGameControllerDPadDownButton];
}
} else if (event.code == _leftTriggerPressureBit) {
_leftTriggerPressure = scale(event.value,
_leftTriggerMinPressure,
_leftTriggerMaxPressure);
if (_leftTriggerPressure > 0)
[_pressedButtons addObject:
OFGameControllerLeftTriggerButton];
else
[_pressedButtons removeObject:
OFGameControllerLeftTriggerButton];
} else if (event.code == _rightTriggerPressureBit) {
_rightTriggerPressure = scale(event.value,
_rightTriggerMinPressure,
_rightTriggerMaxPressure);
if (_rightTriggerPressure > 0)
[_pressedButtons addObject:
OFGameControllerRightTriggerButton];
else
[_pressedButtons removeObject:
OFGameControllerRightTriggerButton];
}
break;
}
}
}
- (OFComparisonResult)compare: (OFEvdevGameController *)otherController
{
unsigned long long selfIndex, otherIndex;
if (![otherController isKindOfClass: [OFEvdevGameController class]])
@throw [OFInvalidArgumentException exception];
selfIndex = [_path substringFromIndex: 16].unsignedLongLongValue;
otherIndex = [otherController->_path substringFromIndex: 16]
.unsignedLongLongValue;
if (selfIndex > otherIndex)
return OFOrderedDescending;
if (selfIndex < otherIndex)
return OFOrderedAscending;
return OFOrderedSame;
}
- (OFSet *)pressedButtons
{
OFMutableSet *pressedButtons =
[OFMutableSet setWithCapacity: _pressedButtons.count];
for (OFGameControllerButton button in _pressedButtons)
if (![button hasPrefix: @"_"])
[pressedButtons addObject: button];
[pressedButtons makeImmutable];
return pressedButtons;
}
- (OFPoint)leftAnalogStickPosition
{
if (_vendorID == vendorIDNintendo && _productID == productIDLeftJoyCon)
return OFMakePoint(
_leftAnalogStickPosition.y, -_leftAnalogStickPosition.x);
if (_vendorID == vendorIDNintendo && _productID == productIDRightJoyCon)
return OFMakePoint(
-_leftAnalogStickPosition.y, _leftAnalogStickPosition.x);
return _leftAnalogStickPosition;
}
- (float)pressureForButton: (OFGameControllerButton)button
{
if (button == OFGameControllerLeftTriggerButton &&
_hasLeftTriggerPressure)
return _leftTriggerPressure;
if (button == OFGameControllerRightTriggerButton &&
_hasRightTriggerPressure)
return _rightTriggerPressure;
return [super pressureForButton: button];
}
@end