ObjFW  OFGameController.m at [59b45e87d1]

File src/platform/Linux/OFGameController.m artifact cec84b63cd part of check-in 59b45e87d1


/*
 * 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 "OFGameController.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"

static const uint16_t vendorIDNintendo = 0x057E;
static const uint16_t productIDLeftJoycon = 0x2006;
static const uint16_t productIDRightJoycon = 0x2007;
static const uint16_t productIDN64Controller = 0x2019;

@interface OFGameController ()
- (instancetype)of_initWithPath: (OFString *)path OF_METHOD_FAMILY(init);
@end

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

static OFGameControllerButton
buttonToName(uint16_t button, uint16_t vendorID, uint16_t productID)
{
	if (vendorID == vendorIDNintendo &&
	    productID == productIDLeftJoycon) {
		switch (button) {
		case BTN_SELECT:
			return OFGameControllerButtonMinus;
		case BTN_Z:
			return OFGameControllerButtonCapture;
		case BTN_TR:
			return OFGameControllerButtonSL;
		case BTN_TR2:
			return OFGameControllerButtonSR;
		}
	} else if (vendorID == vendorIDNintendo &&
	    productID == productIDRightJoycon) {
		switch (button) {
		case BTN_B:
			return OFGameControllerButtonA;
		case BTN_A:
			return OFGameControllerButtonB;
		case BTN_START:
			return OFGameControllerButtonPlus;
		case BTN_TL:
			return OFGameControllerButtonSL;
		case BTN_TL2:
			return OFGameControllerButtonSR;
		}
	} else if (vendorID == vendorIDNintendo &&
	    productID == productIDN64Controller) {
		switch (button) {
		case BTN_TL2:
			return OFGameControllerButtonZ;
		case BTN_Y:
			return OFGameControllerButtonCPadLeft;
		case BTN_C:
			return OFGameControllerButtonCPadRight;
		case BTN_SELECT:
			return OFGameControllerButtonCPadUp;
		case BTN_X:
			return OFGameControllerButtonCPadDown;
		case BTN_MODE:
			return OFGameControllerButtonHome;
		case BTN_Z:
			return OFGameControllerButtonCapture;
		}
	}

	switch (button) {
	case BTN_A:
		return OFGameControllerButtonA;
	case BTN_B:
		return OFGameControllerButtonB;
	case BTN_C:
		return OFGameControllerButtonC;
	case BTN_X:
		return OFGameControllerButtonX;
	case BTN_Y:
		return OFGameControllerButtonY;
	case BTN_Z:
		return OFGameControllerButtonZ;
	case BTN_TL:
		return OFGameControllerButtonL;
	case BTN_TR:
		return OFGameControllerButtonR;
	case BTN_TL2:
		return OFGameControllerButtonZL;
	case BTN_TR2:
		return OFGameControllerButtonZR;
	case BTN_SELECT:
		return OFGameControllerButtonSelect;
	case BTN_START:
		return OFGameControllerButtonStart;
	case BTN_MODE:
		return OFGameControllerButtonHome;
	case BTN_THUMBL:
		return OFGameControllerButtonLeftStick;
	case BTN_THUMBR:
		return OFGameControllerButtonRightStick;
	case BTN_DPAD_UP:
		return OFGameControllerButtonDPadUp;
	case BTN_DPAD_DOWN:
		return OFGameControllerButtonDPadDown;
	case BTN_DPAD_LEFT:
		return OFGameControllerButtonDPadLeft;
	case BTN_DPAD_RIGHT:
		return OFGameControllerButtonDPadRight;
	}

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

@implementation OFGameController
@synthesize name = _name, buttons = _buttons;
@synthesize hasLeftAnalogStick = _hasLeftAnalogStick;
@synthesize hasRightAnalogStick = _hasRightAnalogStick;
@synthesize leftAnalogStickPosition = _leftAnalogStickPosition;
@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 = [[[OFGameController 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)init
{
	OF_INVALID_INIT_METHOD
}

- (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 keyBits[OFRoundUpToPowerOf2(OF_ULONG_BIT,
		    KEY_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];

		if (ioctl(_fd, EVIOCGBIT(EV_KEY, sizeof(keyBits)), 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]))
				[_buttons addObject: buttonToName(
				    buttons[i], _vendorID, _productID)];

		_pressedButtons = [[OFMutableSet alloc] init];

		if (OFBitSetIsSet(evBits, EV_ABS)) {
			if (ioctl(_fd, EVIOCGBIT(EV_ABS, sizeof(absBits)),
			    absBits) == -1)
				@throw [OFInitializationFailedException
				    exception];

			if (OFBitSetIsSet(absBits, ABS_X) &&
			    OFBitSetIsSet(absBits, ABS_Y)) {
				struct input_absinfo infoX, infoY;

				_hasLeftAnalogStick = true;

				if (ioctl(_fd, EVIOCGABS(ABS_X), &infoX) == -1)
					@throw [OFInitializationFailedException
					    exception];

				if (ioctl(_fd, EVIOCGABS(ABS_Y), &infoY) == -1)
					@throw [OFInitializationFailedException
					    exception];

				_leftAnalogStickMinX = infoX.minimum;
				_leftAnalogStickMaxX = infoX.maximum;
				_leftAnalogStickMinY = infoY.minimum;
				_leftAnalogStickMaxY = infoY.maximum;
			}

			if (OFBitSetIsSet(absBits, ABS_RX) &&
			    OFBitSetIsSet(absBits, ABS_RY)) {
				struct input_absinfo infoX, infoY;

				_hasRightAnalogStick = true;

				if (ioctl(_fd, EVIOCGABS(ABS_RX), &infoX) == -1)
					@throw [OFInitializationFailedException
					    exception];

				if (ioctl(_fd, EVIOCGABS(ABS_RY), &infoY) == -1)
					@throw [OFInitializationFailedException
					    exception];

				_rightAnalogStickMinX = infoX.minimum;
				_rightAnalogStickMaxX = infoX.maximum;
				_rightAnalogStickMinY = infoY.minimum;
				_rightAnalogStickMaxY = infoY.maximum;
			}

			if (OFBitSetIsSet(absBits, ABS_HAT0X) &&
			    OFBitSetIsSet(absBits, ABS_HAT0Y)) {
				[_buttons addObject:
				    OFGameControllerButtonDPadLeft];
				[_buttons addObject:
				    OFGameControllerButtonDPadRight];
				[_buttons addObject:
				    OFGameControllerButtonDPadUp];
				[_buttons addObject:
				    OFGameControllerButtonDPadDown];
			}

			if (OFBitSetIsSet(absBits, ABS_Z)) {
				struct input_absinfo info;

				_hasZLPressure = true;

				if (ioctl(_fd, EVIOCGABS(ABS_Z), &info) == -1)
					@throw [OFInitializationFailedException
					    exception];

				_ZLMinPressure = info.minimum;
				_ZLMaxPressure = info.maximum;

				[_buttons addObject: OFGameControllerButtonZL];
			}

			if (OFBitSetIsSet(absBits, ABS_RZ)) {
				struct input_absinfo info;

				_hasZRPressure = true;

				if (ioctl(_fd, EVIOCGABS(ABS_RZ), &info) == -1)
					@throw [OFInitializationFailedException
					    exception];

				_ZRMinPressure = info.minimum;
				_ZRMaxPressure = info.maximum;

				[_buttons addObject: OFGameControllerButtonZR];
			}
		}

		[_buttons makeImmutable];

		[self retrieveState];
	} @catch (id e) {
		[self release];
		@throw e;
	}

	return self;
}

- (void)dealloc
{
	[_path release];

	if (_fd != -1)
		close(_fd);

	[_name release];
	[_buttons release];
	[_pressedButtons release];

	[super dealloc];
}

- (OFNumber *)vendorID
{
	return [OFNumber numberWithUnsignedShort: _vendorID];
}

- (OFNumber *)productID
{
	return [OFNumber numberWithUnsignedShort: _productID];
}

- (void)retrieveState
{
	struct input_event event;

	for (;;) {
		errno = 0;

		if (read(_fd, &event, sizeof(event)) < (int)sizeof(event)) {
			if (errno == EWOULDBLOCK)
				return;

			@throw [OFReadFailedException
			    exceptionWithObject: self
				requestedLength: sizeof(event)
					  errNo: errno];
		}

		switch (event.type) {
		case EV_KEY:
			if (event.value)
				[_pressedButtons addObject: buttonToName(
				    event.code, _vendorID, _productID)];
			else
				[_pressedButtons removeObject: buttonToName(
				    event.code, _vendorID, _productID)];
			break;
		case EV_ABS:
			switch (event.code) {
			case ABS_X:
				_leftAnalogStickPosition.x = scale(event.value,
				    _leftAnalogStickMinX, _leftAnalogStickMaxX);
				break;
			case ABS_Y:
				_leftAnalogStickPosition.y = scale(event.value,
				    _leftAnalogStickMinY, _leftAnalogStickMaxY);
				break;
			case ABS_RX:
				_rightAnalogStickPosition.x = scale(event.value,
				    _rightAnalogStickMinX,
				    _rightAnalogStickMaxX);
				break;
			case ABS_RY:
				_rightAnalogStickPosition.y = scale(event.value,
				    _rightAnalogStickMinY,
				    _rightAnalogStickMaxY);
				break;
			case ABS_HAT0X:
				if (event.value < 0) {
					[_pressedButtons addObject:
					    OFGameControllerButtonDPadLeft];
					[_pressedButtons removeObject:
					    OFGameControllerButtonDPadRight];
				} else if (event.value > 0) {
					[_pressedButtons addObject:
					    OFGameControllerButtonDPadRight];
					[_pressedButtons removeObject:
					    OFGameControllerButtonDPadLeft];
				} else {
					[_pressedButtons removeObject:
					    OFGameControllerButtonDPadLeft];
					[_pressedButtons removeObject:
					    OFGameControllerButtonDPadRight];
				}
				break;
			case ABS_HAT0Y:
				if (event.value < 0) {
					[_pressedButtons addObject:
					    OFGameControllerButtonDPadUp];
					[_pressedButtons removeObject:
					    OFGameControllerButtonDPadDown];
				} else if (event.value > 0) {
					[_pressedButtons addObject:
					    OFGameControllerButtonDPadDown];
					[_pressedButtons removeObject:
					    OFGameControllerButtonDPadUp];
				} else {
					[_pressedButtons removeObject:
					    OFGameControllerButtonDPadUp];
					[_pressedButtons removeObject:
					    OFGameControllerButtonDPadDown];
				}
				break;
			case ABS_Z:
				_ZLPressure = scale(event.value,
				    _ZLMinPressure, _ZLMaxPressure);

				if (_ZLPressure > 0)
					[_pressedButtons addObject:
					    OFGameControllerButtonZL];
				else
					[_pressedButtons removeObject:
					    OFGameControllerButtonZL];
				break;
			case ABS_RZ:
				_ZRPressure = scale(event.value,
				    _ZRMinPressure, _ZRMaxPressure);

				if (_ZRPressure > 0)
					[_pressedButtons addObject:
					    OFGameControllerButtonZR];
				else
					[_pressedButtons removeObject:
					    OFGameControllerButtonZR];
				break;
			}

			break;
		}
	}
}

- (OFComparisonResult)compare: (OFGameController *)otherController
{
	unsigned long long selfIndex, otherIndex;

	if (![otherController isKindOfClass: [OFGameController 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
{
	return [[_pressedButtons copy] autorelease];
}

- (float)pressureForButton: (OFGameControllerButton)button
{
	if (button == OFGameControllerButtonZL && _hasZLPressure)
		return _ZLPressure;
	if (button == OFGameControllerButtonZR && _hasZRPressure)
		return _ZRPressure;

	return ([self.pressedButtons containsObject: button] ? 1 : 0);
}

- (OFGameControllerButton)northButton
{
	if (_vendorID == vendorIDNintendo && _productID == productIDLeftJoycon)
		return nil;
	if (_vendorID == vendorIDNintendo && _productID == productIDRightJoycon)
		return OFGameControllerButtonX;
	if (_vendorID == vendorIDNintendo &&
	    _productID == productIDN64Controller)
		return nil;

	return OFGameControllerButtonY;
}

- (OFGameControllerButton)southButton
{
	if (_vendorID == vendorIDNintendo && _productID == productIDLeftJoycon)
		return nil;
	if (_vendorID == vendorIDNintendo && _productID == productIDRightJoycon)
		return OFGameControllerButtonB;
	if (_vendorID == vendorIDNintendo &&
	    _productID == productIDN64Controller)
		return nil;

	return OFGameControllerButtonA;
}

- (OFGameControllerButton)westButton
{
	if (_vendorID == vendorIDNintendo && _productID == productIDLeftJoycon)
		return nil;
	if (_vendorID == vendorIDNintendo && _productID == productIDRightJoycon)
		return OFGameControllerButtonY;
	if (_vendorID == vendorIDNintendo &&
	    _productID == productIDN64Controller)
		return nil;

	return OFGameControllerButtonX;
}

- (OFGameControllerButton)eastButton
{
	if (_vendorID == vendorIDNintendo && _productID == productIDLeftJoycon)
		return nil;
	if (_vendorID == vendorIDNintendo && _productID == productIDRightJoycon)
		return OFGameControllerButtonA;
	if (_vendorID == vendorIDNintendo &&
	    _productID == productIDN64Controller)
		return nil;

	return OFGameControllerButtonB;
}

- (OFString *)description
{
	return [OFString stringWithFormat: @"<%@: %@>", self.class, self.name];
}
@end