Skip to content

Commit

Permalink
MacOS: Add controller support (#71)
Browse files Browse the repository at this point in the history
  • Loading branch information
angelo-wf authored Sep 18, 2024
1 parent aa1c20a commit 3386937
Show file tree
Hide file tree
Showing 5 changed files with 303 additions and 13 deletions.
38 changes: 38 additions & 0 deletions MacOS/MacOSGameController.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#pragma once

#import <Foundation/Foundation.h>
#import <Cocoa/Cocoa.h>
#import <GameController/GameController.h>
#import <CoreHaptics/CoreHaptics.h>

#include <optional>

class Emulator;

class MacOSGameController
{
private:
Emulator* _emu;

GCController* _controller;
GCExtendedGamepad* _input;
CHHapticEngine* _haptics;
id<CHHapticPatternPlayer> _player;

bool _buttonState[24] = {};
int16_t _axisState[4] = {};

void HandleDpad(GCControllerDirectionPad* dpad);
void HandleThumbstick(GCControllerDirectionPad* stick, int stickNumber);

public:
MacOSGameController(Emulator* emu, GCController* controller);
~MacOSGameController();

bool IsGameController(GCController* controller);

bool IsButtonPressed(int buttonNumber);
std::optional<int16_t> GetAxisPosition(int axis);

void SetForceFeedback(uint16_t magnitude);
};
169 changes: 169 additions & 0 deletions MacOS/MacOSGameController.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
#import <Foundation/Foundation.h>
#import <Cocoa/Cocoa.h>
#import <GameController/GameController.h>
#import <CoreHaptics/CoreHaptics.h>

#include <iostream>
#include <stdint.h>
#include <optional>

#include "MacOSGameController.h"
//The MacOS SDK defines a global function 'Debugger', colliding with Mesen's Debugger class
//Redefine it temporarily so the headers don't cause compilation errors due to this
#define Debugger MesenDebugger
#include "Shared/MessageManager.h"
#include "Shared/Emulator.h"
#include "Shared/EmuSettings.h"
#undef Debugger

MacOSGameController::MacOSGameController(Emulator* emu, GCController* controller)
{
_emu = emu;
_controller = [controller retain];
_input = [[_controller extendedGamepad] retain];

[_input setValueChangedHandler:^ void (GCExtendedGamepad* input, GCControllerElement* element) {
if([input buttonA] == element) _buttonState[0] = [((GCControllerButtonInput*) element) isPressed];
if([input buttonB] == element) _buttonState[1] = [((GCControllerButtonInput*) element) isPressed];
if([input buttonX] == element) _buttonState[2] = [((GCControllerButtonInput*) element) isPressed];
if([input buttonY] == element) _buttonState[3] = [((GCControllerButtonInput*) element) isPressed];
if([input leftShoulder] == element) _buttonState[4] = [((GCControllerButtonInput*) element) isPressed];
if([input rightShoulder] == element) _buttonState[5] = [((GCControllerButtonInput*) element) isPressed];
if([input buttonMenu] == element) _buttonState[6] = [((GCControllerButtonInput*) element) isPressed];
if([input buttonOptions] == element) _buttonState[7] = [((GCControllerButtonInput*) element) isPressed];
if([input dpad] == element) HandleDpad((GCControllerDirectionPad*) element);
if([input leftTrigger] == element) _buttonState[12] = [((GCControllerButtonInput*) element) isPressed];
if([input rightTrigger] == element) _buttonState[13] = [((GCControllerButtonInput*) element) isPressed];
if([input leftThumbstickButton] == element) _buttonState[14] = [((GCControllerButtonInput*) element) isPressed];
if([input rightThumbstickButton] == element) _buttonState[15] = [((GCControllerButtonInput*) element) isPressed];
if([input leftThumbstick] == element) HandleThumbstick((GCControllerDirectionPad*) element, 0);
if([input rightThumbstick] == element) HandleThumbstick((GCControllerDirectionPad*) element, 1);
}];

_haptics = nil;
_player = nil;
if([_controller haptics] != nil) {
_haptics = [[_controller haptics] createEngineWithLocality:GCHapticsLocalityDefault];
NSError* error = nil;
[_haptics startAndReturnError:&error];
if(error) {
MessageManager::Log("[Input Device] Failed to initialize force feedback");
_haptics = nil;
} else {
[_haptics retain];
}
}
}

MacOSGameController::~MacOSGameController()
{
if(_haptics) {
if(_player) {
NSError* error = nil;
[_player stopAtTime:0.0 error:&error];
[_player release];
}
[_haptics stopWithCompletionHandler:^ void (NSError* error) {}];
[_haptics release];
}
[_input setValueChangedHandler:nil];
[_input release];
[_controller release];
}

void MacOSGameController::HandleDpad(GCControllerDirectionPad* dpad)
{
_buttonState[8] = [[dpad up] isPressed];
_buttonState[9] = [[dpad down] isPressed];
_buttonState[10] = [[dpad left] isPressed];
_buttonState[11] = [[dpad right] isPressed];
}

void MacOSGameController::HandleThumbstick(GCControllerDirectionPad* stick, int stickNumber)
{
double deadZoneRatio = _emu->GetSettings()->GetControllerDeadzoneRatio() * 0.4;

float xAxis = [[stick xAxis] value];
float yAxis = [[stick yAxis] value];
_buttonState[(stickNumber * 4) + 16] = xAxis > deadZoneRatio;
_buttonState[(stickNumber * 4) + 17] = xAxis < -deadZoneRatio;
_buttonState[(stickNumber * 4) + 18] = yAxis > deadZoneRatio;
_buttonState[(stickNumber * 4) + 19] = yAxis < -deadZoneRatio;
_axisState[(stickNumber * 2) + 0] = INT16_MAX * xAxis;
_axisState[(stickNumber * 2) + 1] = INT16_MAX * yAxis;
}

bool MacOSGameController::IsGameController(GCController* controller)
{
return _controller == controller;
}

bool MacOSGameController::IsButtonPressed(int buttonNumber)
{
if(buttonNumber < 0 || buttonNumber >= 24) {
return false;
}
return _buttonState[buttonNumber];
}

std::optional<int16_t> MacOSGameController::GetAxisPosition(int axis)
{
axis -= 24;
if(axis < 0 || axis >= 4) {
return std::nullopt;
}
return _axisState[axis];
}

void MacOSGameController::SetForceFeedback(uint16_t magnitude)
{
NSError* error = nil;

if(_haptics == nil) {
return;
}

if(_player != nil) {
[_player stopAtTime:0.0 error:&error];
if(error) {
MessageManager::Log("[Input Device] Failed to stop force feedback effect");
return;
}
[_player release];
_player = nil;
}

//If magnitude is zero, only stop current effect
if(magnitude == 0) {
return;
}

double strength = magnitude / (double) UINT16_MAX;
CHHapticEventParameter* intensityPar = [[CHHapticEventParameter alloc] initWithParameterID:CHHapticEventParameterIDHapticIntensity value:strength];
CHHapticEventParameter* sharpnessPar = [[CHHapticEventParameter alloc] initWithParameterID:CHHapticEventParameterIDHapticSharpness value:0.6];
CHHapticEvent* event = [[CHHapticEvent alloc] initWithEventType:CHHapticEventTypeHapticContinuous parameters:@[intensityPar, sharpnessPar] relativeTime:0.0 duration:GCHapticDurationInfinite];

CHHapticPattern* pattern = [[CHHapticPattern alloc] initWithEvents:@[event] parameters:@[] error:&error];
[intensityPar release];
[sharpnessPar release];
[event release];
if(error) {
MessageManager::Log("[Input Device] Failed to create force feedback pattern");
[pattern release];
return;
}

_player = [_haptics createPlayerWithPattern:pattern error:&error];
[pattern release];
if(error) {
MessageManager::Log("[Input Device] Failed to create force feedback effect");
_player = nil;
return;
}
[_player retain];

[_player startAtTime:0.0 error:&error];
if(error) {
MessageManager::Log("[Input Device] Failed to start force feedback effect");
}
}
27 changes: 17 additions & 10 deletions MacOS/MacOSKeyManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@
#include "Shared/Interfaces/IKeyManager.h"
#include "Shared/KeyDefinitions.h"

class MacOSGameController;
class Emulator;

class MacOSKeyManager : public IKeyManager
{
private:
Emulator* _emu;
std::vector<shared_ptr<MacOSGameController>> _controllers;

vector<KeyDefinition> _keyDefinitions;
bool _keyState[0x205];
Expand All @@ -19,6 +21,8 @@ class MacOSKeyManager : public IKeyManager
bool _disableAllKeys;

void* _eventMonitor;
void* _connectObserver;
void* _disconnectObserver;

//Mapping of MacOS keycodes to Avalonia keycodes
uint16_t _keyCodeMap[128] = {
Expand All @@ -43,16 +47,19 @@ class MacOSKeyManager : public IKeyManager
MacOSKeyManager(Emulator* emu);
virtual ~MacOSKeyManager();

void RefreshState();
bool IsKeyPressed(uint16_t key);
bool IsMouseButtonPressed(MouseButton button);
std::vector<uint16_t> GetPressedKeys();
string GetKeyName(uint16_t key);
uint16_t GetKeyCode(string keyName);
void RefreshState() override;
bool IsKeyPressed(uint16_t key) override;
optional<int16_t> GetAxisPosition(uint16_t key) override;
bool IsMouseButtonPressed(MouseButton button) override;
std::vector<uint16_t> GetPressedKeys() override;
string GetKeyName(uint16_t key) override;
uint16_t GetKeyCode(string keyName) override;

void UpdateDevices();
bool SetKeyState(uint16_t scanCode, bool state);
void ResetKeyState();
void UpdateDevices() override;
bool SetKeyState(uint16_t scanCode, bool state) override;
void ResetKeyState() override;

void SetDisabled(bool disabled);
void SetDisabled(bool disabled) override;

void SetForceFeedback(uint16_t magnitude) override;
};
78 changes: 77 additions & 1 deletion MacOS/MacOSKeyManager.mm
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
#import <Foundation/Foundation.h>
#import <Cocoa/Cocoa.h>
#import <GameController/GameController.h>

#include <algorithm>
#include "MacOSKeyManager.h"
#include "MacOSGameController.h"
//The MacOS SDK defines a global function 'Debugger', colliding with Mesen's Debugger class
//Redefine it temporarily so the headers don't cause compilation errors due to this
#define Debugger MesenDebugger
#include "Shared/MessageManager.h"
#include "Shared/Emulator.h"
#include "Shared/EmuSettings.h"
#include "Shared/KeyDefinitions.h"
Expand All @@ -20,6 +23,19 @@

_keyDefinitions = KeyDefinition::GetSharedKeyDefinitions();

vector<string> buttonNames = {
"A", "B", "X", "Y", "L1", "R1", "Start", "Select",
"Up", "Down", "Left", "Right", "L2", "R2", "L3", "R3",
"X+", "X-", "Y+", "Y-", "X2+", "X2-", "Y2+", "Y2-",
"X", "Y", "X2", "Y2"
};

for(int i = 0; i < 20; i++) {
for(int j = 0; j < (int)buttonNames.size(); j++) {
_keyDefinitions.push_back({ "Pad" + std::to_string(i + 1) + " " + buttonNames[j], (uint32_t)(MacOSKeyManager::BaseGamepadIndex + i * 0x100 + j) });
}
}

for(KeyDefinition &keyDef : _keyDefinitions) {
_keyNames[keyDef.keyCode] = keyDef.name;
_keyCodes[keyDef.name] = keyDef.keyCode;
Expand Down Expand Up @@ -49,11 +65,41 @@

return nil;
}];

_connectObserver = [[NSNotificationCenter defaultCenter] addObserverForName:GCControllerDidConnectNotification object:nil queue:nil usingBlock:^ void (NSNotification* notification) {
GCController* controller = (GCController*) [notification object];

if([controller extendedGamepad] == nil) {
MessageManager::Log(std::string("[Input] Device ignored (Does not support extended gamepad) - Name: ") + [[controller vendorName] UTF8String]);
} else {
_controllers.push_back(std::shared_ptr<MacOSGameController>(new MacOSGameController(_emu, controller)));
MessageManager::Log(std::string("[Input Connected] Name: ") + [[controller vendorName] UTF8String]);
}
}];

_disconnectObserver = [[NSNotificationCenter defaultCenter] addObserverForName:GCControllerDidDisconnectNotification object:nil queue:nil usingBlock:^ void (NSNotification* notification) {
GCController* controller = (GCController*) [notification object];

int indexToRemove = -1;
for(int i = 0; i < _controllers.size(); i++) {
if(_controllers[i]->IsGameController(controller)) {
indexToRemove = i;
break;
}
}

if(indexToRemove >= 0) {
_controllers.erase(_controllers.begin() + indexToRemove);
MessageManager::Log("[Input Device] Disconnected");
}
}];
}

MacOSKeyManager::~MacOSKeyManager()
{
[NSEvent removeMonitor:(id) _eventMonitor];
[[NSNotificationCenter defaultCenter] removeObserver:(id) _connectObserver];
[[NSNotificationCenter defaultCenter] removeObserver:(id) _disconnectObserver];
}

void MacOSKeyManager::HandleModifiers(uint32_t flags)
Expand All @@ -80,12 +126,28 @@
return false;
}

if(key < 0x205) {
if(key >= MacOSKeyManager::BaseGamepadIndex) {
uint8_t gamepadPort = (key - MacOSKeyManager::BaseGamepadIndex) / 0x100;
uint8_t gamepadButton = (key - MacOSKeyManager::BaseGamepadIndex) % 0x100;
if(_controllers.size() > gamepadPort) {
return _controllers[gamepadPort]->IsButtonPressed(gamepadButton);
}
} else if(key < 0x205) {
return _keyState[key] != 0;
}
return false;
}

optional<int16_t> MacOSKeyManager::GetAxisPosition(uint16_t key)
{
if(key >= MacOSKeyManager::BaseGamepadIndex) {
uint8_t port = (key - MacOSKeyManager::BaseGamepadIndex) / 0x100;
uint8_t button = (key - MacOSKeyManager::BaseGamepadIndex) % 0x100;
return _controllers[port]->GetAxisPosition(button);
}
return std::nullopt;
}

bool MacOSKeyManager::IsMouseButtonPressed(MouseButton button)
{
return _keyState[MacOSKeyManager::BaseMouseButtonIndex + (int)button];
Expand All @@ -94,6 +156,13 @@
vector<uint16_t> MacOSKeyManager::GetPressedKeys()
{
vector<uint16_t> pressedKeys;
for(size_t i = 0; i < _controllers.size(); i++) {
for(int j = 0; j < 24; j++) {
if(_controllers[i]->IsButtonPressed(j)) {
pressedKeys.push_back(MacOSKeyManager::BaseGamepadIndex + i * 0x100 + j);
}
}
}

for(int i = 0; i < 0x205; i++) {
if(_keyState[i]) {
Expand Down Expand Up @@ -145,3 +214,10 @@
{
_disableAllKeys = disabled;
}

void MacOSKeyManager::SetForceFeedback(uint16_t magnitude)
{
for(auto& controller : _controllers) {
controller->SetForceFeedback(magnitude);
}
}
Loading

0 comments on commit 3386937

Please sign in to comment.