// // ControllerSupport.m // Moonlight // // Created by Cameron Gutman on 10/20/14. // Copyright (c) 2014 Moonlight Stream. All rights reserved. // // Modified by True砖家 since 2024.6.16 // Copyright © 2024 True砖家 @ Bilibili. All rights reserved. // #import "ControllerSupport.h" #import "VoidController.h" #import "VoidLink-Swift.h" #import "OnScreenControls.h" #import "DataManager.h" #include "Limelight.h" @import GameController; #if !TARGET_OS_TV @import CoreMotion; #endif @import AudioToolbox; static const double MOUSE_SPEED_DIVISOR = 1.25; @interface ControllerSupport() @property (assign,nonatomic) bool shallDisableGyroHotSwitch; @end @implementation ControllerSupport { id _controllerConnectObserver; id _controllerDisconnectObserver; id _mouseConnectObserver; id _mouseDisconnectObserver; id _keyboardConnectObserver; id _keyboardDisconnectObserver; NSLock *_controllerStreamLock; NSMutableDictionary *_voidControllers; StreamConfiguration* _streamConfig; __weak id _delegate; float accumulatedDeltaX; float accumulatedDeltaY; float accumulatedScrollX; float accumulatedScrollY; int _controllerMouseSwitch; bool _mouseSwitchButtonPressed; bool _mouseSwitchButtonBeingClicked; NSTimeInterval mouseSwitchDownTimestamp; int _controllerMouseLeftButton; int _controllerMouseRightButton; ControllerMouseStick _controllerMouseStick; bool _mapControllerToMouse; bool _controllerMouseEnabledFlag; float stickToMouseInputX; float stickToMouseInputY; float stickToWheelInputX; float stickToWheelInputY; float _stickToMouseExpo; float _stickToMouseVelocity; CADisplayLink *_displayLink; float stickMaxOffset; float _leftStickMinOffset; float _rightStickMinOffset; OnScreenControls *_osc; VoidController *_oscController; TemporarySettings* tempSettings; OSCProfile* oscProfile; OSCProfilesManager* oscProfileMan; #define EMULATING_SELECT 0x1 #define EMULATING_SPECIAL 0x2 bool _oscEnabled; char _controllerNumbers; bool _multiController; bool _swapABXYButtons; int _gyroMode; CGFloat _gyroSensitivity; bool _captureMouse; bool _controllerGyroSwitchEnabled; bool _gyroEnabledFlag; int _controllerGyroSwitchToggle; int _controllerGyroSwitchHold; bool _reverseHoldButton; bool _controllerGyroSwitchTogglePressed; bool _controllerGyroSwitchHoldPressed; ControllerGyroSwitchMode _gyroSwitchMode; __weak MotionHandler* motionHandler; } // UPDATE_BUTTON_FLAG(controller, flag, pressed) #define UPDATE_BUTTON_FLAG(controller, x, y) \ ((y) ? [self setButtonFlag:controller flags:x] : [self clearButtonFlag:controller flags:x]) #define MAX_MAGNITUDE(x, y) (abs(x) > abs(y) ? (x) : (y)) -(void) rumble:(unsigned short)controllerNumber lowFreqMotor:(unsigned short)lowFreqMotor highFreqMotor:(unsigned short)highFreqMotor { VoidController* voidController = [_voidControllers objectForKey:[NSNumber numberWithInteger:controllerNumber]]; HapticEnginePreference preference = tempSettings.hapticEngine.intValue; if (voidController == nil && controllerNumber == 0 && _oscEnabled) { // no physical controller connected: if(preference != RumbleOff){ [_oscController.lowFreqMotor setMotorAmplitude:lowFreqMotor]; [_oscController.highFreqMotor setMotorAmplitude:highFreqMotor]; } } if (voidController == nil) { // No connected controller for this player return; } // physical controller connected: switch (preference) { case HapticEngineAuto: // if controller has no haptic profile, it already falled bakc to device engine [voidController.lowFreqMotor setMotorAmplitude:lowFreqMotor]; [voidController.highFreqMotor setMotorAmplitude:highFreqMotor]; break; case RumbleDevice: [_oscController.lowFreqMotor setMotorAmplitude:lowFreqMotor]; [_oscController.highFreqMotor setMotorAmplitude:highFreqMotor]; case LeftRightSwapped: [voidController.lowFreqMotor setMotorAmplitude:highFreqMotor]; [voidController.highFreqMotor setMotorAmplitude:lowFreqMotor]; case RumbleOff: break; default: break; } } -(void) rumbleTriggers:(uint16_t)controllerNumber leftTrigger:(uint16_t)leftTrigger rightTrigger:(uint16_t)rightTrigger { VoidController* controller = [_voidControllers objectForKey:[NSNumber numberWithInteger:controllerNumber]]; if (controller == nil && controllerNumber == 0 && _oscEnabled) { // TODO: Trigger rumble emulation for OSC } if (controller == nil) { // No connected controller for this player return; } [controller.leftTriggerMotor setMotorAmplitude:leftTrigger]; [controller.rightTriggerMotor setMotorAmplitude:rightTrigger]; } - (bool)isLandscapeNow { return CGRectGetWidth([[UIScreen mainScreen]bounds]) > CGRectGetHeight([[UIScreen mainScreen]bounds]); } -(void)updateTimerStateForController:(VoidController* )voidController{ // if (@available(iOS 14.0, tvOS 14.0, *)) { if (true) { if(_gyroMode == GyroModeOff){ [self stopTimerForController:voidController]; return; } if(voidController.gamepad.motion.hasAttitudeAndRotationRate) [voidController.motionTypes addObject:@(LI_MOTION_TYPE_ACCEL)]; if (@available(iOS 14.0, *)) if(voidController.gamepad.motion.hasRotationRate) [voidController.motionTypes addObject:@(LI_MOTION_TYPE_GYRO)]; for(NSNumber* motionTypeObj in voidController.motionTypes){ uint8_t motionType = motionTypeObj.intValue; #if !TARGET_OS_TV //tvOS has no device motion if(voidController == _oscController){ //Player has no controller *or* no motion for controller 1 *or* wants to override controller 1 motion with device motion if(!voidController.motionManager) { voidController.motionManager = [[CMMotionManager alloc] init]; } switch (motionType) { case LI_MOTION_TYPE_ACCEL: [voidController.accelTimer invalidate]; voidController.accelTimer = nil; // Reset the last motion sample CMAcceleration emptyDeviceAccelSample = {}; voidController.lastDeviceAccelSample = emptyDeviceAccelSample; {dispatch_async(dispatch_get_main_queue(), ^{ NSLog(@"setup device built-in gyro accelTimer"); voidController.hasAccelerometer = YES; voidController.accelTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 / voidController.reportRateHz repeats:YES block:^(NSTimer *timer) { // Don't send duplicate samples CMAcceleration lastDeviceAccelSample = voidController.lastDeviceAccelSample; CMAcceleration deviceAccelSample = voidController.motionManager.deviceMotion.userAcceleration; //userAcceleration does not contain gravity, add gravity to x, y and z values: deviceAccelSample.x += voidController.motionManager.deviceMotion.gravity.x * self->_gyroSensitivity; deviceAccelSample.y += voidController.motionManager.deviceMotion.gravity.y * self->_gyroSensitivity; deviceAccelSample.z += voidController.motionManager.deviceMotion.gravity.z * self->_gyroSensitivity; if (memcmp(&deviceAccelSample, &lastDeviceAccelSample, sizeof(deviceAccelSample)) == 0) { return; } voidController.lastDeviceAccelSample = deviceAccelSample; // Convert g to m/s^2 if (@available(iOS 13.0, *)) { if(UIApplication.sharedApplication.windows.firstObject.windowScene.interfaceOrientation == 4){ //check for landscape left or landscape right LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber, LI_MOTION_TYPE_ACCEL, deviceAccelSample.y * -9.80665f * self->_gyroSensitivity, deviceAccelSample.z * -9.80665f * self->_gyroSensitivity, deviceAccelSample.x * -9.80665f * self->_gyroSensitivity); } else{ LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber, LI_MOTION_TYPE_ACCEL, deviceAccelSample.y * +9.80665f * self->_gyroSensitivity, deviceAccelSample.z * -9.80665f * self->_gyroSensitivity, deviceAccelSample.x * +9.80665f * self->_gyroSensitivity); } } else{ LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber, LI_MOTION_TYPE_ACCEL, deviceAccelSample.y * -9.80665f * self->_gyroSensitivity, deviceAccelSample.z * -9.80665f * self->_gyroSensitivity, deviceAccelSample.x * -9.80665f * self->_gyroSensitivity); } }]; });} break; case LI_MOTION_TYPE_GYRO: [voidController.gyroTimer invalidate]; voidController.gyroTimer = nil; // Reset the last motion sample CMRotationRate emptyDeviceGyroSample = {}; voidController.lastDeviceGyroSample = emptyDeviceGyroSample; [voidController.motionManager startDeviceMotionUpdates]; NSLog(@"setup device built-in gyro gyroTimer"); voidController.hasGyroscope = YES; voidController.gyroTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 / voidController.reportRateHz repeats:YES block:^(NSTimer *timer) { // Don't send duplicate samples CMRotationRate lastDeviceGyroSample = voidController.lastDeviceGyroSample; CMRotationRate deviceGyroSample = voidController.motionManager.deviceMotion.rotationRate; if (memcmp(&deviceGyroSample, &lastDeviceGyroSample, sizeof(deviceGyroSample)) == 0) { return; } voidController.lastDeviceGyroSample = deviceGyroSample; // Convert rad/s to deg/s UIInterfaceOrientation interfaceOrientation = UIInterfaceOrientationUnknown; if (@available(iOS 13.0, *)) { interfaceOrientation = UIApplication.sharedApplication.windows.firstObject.windowScene.interfaceOrientation; } else { interfaceOrientation = UIApplication.sharedApplication.statusBarOrientation; } switch (interfaceOrientation) { case UIInterfaceOrientationLandscapeLeft: LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber, LI_MOTION_TYPE_GYRO, self->_gyroEnabledFlag ? deviceGyroSample.y * 57.2957795f * self->_gyroSensitivity : 0, self->_gyroEnabledFlag ? deviceGyroSample.z * 57.2957795f * self->_gyroSensitivity : 0, self->_gyroEnabledFlag ? deviceGyroSample.x * 57.2957795f * self->_gyroSensitivity : 0); break; case UIInterfaceOrientationLandscapeRight: LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber, LI_MOTION_TYPE_GYRO, self->_gyroEnabledFlag ? deviceGyroSample.y * -57.2957795f * self->_gyroSensitivity : 0, self->_gyroEnabledFlag ? deviceGyroSample.z * 57.2957795f * self->_gyroSensitivity : 0, self->_gyroEnabledFlag ? deviceGyroSample.x * -57.2957795f * self->_gyroSensitivity : 0); break; case UIInterfaceOrientationPortrait: LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber, LI_MOTION_TYPE_GYRO, self->_gyroEnabledFlag ? deviceGyroSample.x * 57.2957795f * self->_gyroSensitivity : 0, self->_gyroEnabledFlag ? deviceGyroSample.z * 57.2957795f * self->_gyroSensitivity : 0, self->_gyroEnabledFlag ? deviceGyroSample.y * -57.2957795f * self->_gyroSensitivity : 0); break; case UIInterfaceOrientationPortraitUpsideDown: LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber, LI_MOTION_TYPE_GYRO, self->_gyroEnabledFlag ? deviceGyroSample.x * -57.2957795f * self->_gyroSensitivity : 0, self->_gyroEnabledFlag ? deviceGyroSample.z * 57.2957795f * self->_gyroSensitivity : 0, self->_gyroEnabledFlag ? deviceGyroSample.y * 57.2957795f * self->_gyroSensitivity : 0); break; default: LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber, LI_MOTION_TYPE_GYRO, self->_gyroEnabledFlag ? deviceGyroSample.y * 57.2957795f * self->_gyroSensitivity : 0, self->_gyroEnabledFlag ? deviceGyroSample.z * 57.2957795f * self->_gyroSensitivity : 0, self->_gyroEnabledFlag ? deviceGyroSample.x * 57.2957795f * self->_gyroSensitivity : 0); break; } /* if(UIApplication.sharedApplication.windows.firstObject.windowScene.interfaceOrientation == 4){//check for landscape left or landscape right LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber, LI_MOTION_TYPE_GYRO, self->_gyroEnabledFlag ? deviceGyroSample.y * 57.2957795f * self->_gyroSensitivity : 0, self->_gyroEnabledFlag ? deviceGyroSample.z * 57.2957795f * self->_gyroSensitivity : 0, self->_gyroEnabledFlag ? deviceGyroSample.x * 57.2957795f * self->_gyroSensitivity : 0); } else{ LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber, LI_MOTION_TYPE_GYRO, self->_gyroEnabledFlag ? deviceGyroSample.y * -57.2957795f * self->_gyroSensitivity : 0, self->_gyroEnabledFlag ? deviceGyroSample.z * 57.2957795f * self->_gyroSensitivity : 0, self->_gyroEnabledFlag ? deviceGyroSample.x * -57.2957795f * self->_gyroSensitivity : 0); } */ }]; break; } } #endif else{ NSLog(@"controller obj timer update: controller timer "); if (@available(iOS 14.0, *)) { switch (motionType) { case LI_MOTION_TYPE_ACCEL: [voidController.accelTimer invalidate]; voidController.accelTimer = nil; if (voidController.reportRateHz && voidController.gamepad.motion.hasGravityAndUserAcceleration) { // Reset the last motion sample GCAcceleration emptyAccelSample = {}; voidController.lastAccelSample = emptyAccelSample; NSLog(@"setup controller gyro accelTimer"); dispatch_sync(dispatch_get_main_queue(), ^{ voidController.hasAccelerometer = YES; voidController.accelTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 / voidController.reportRateHz repeats:YES block:^(NSTimer *timer) { // Don't send duplicate samples GCAcceleration lastAccelSample = voidController.lastAccelSample; GCAcceleration accelSample = voidController.gamepad.motion.acceleration; if (memcmp(&accelSample, &lastAccelSample, sizeof(accelSample)) == 0) { return; } voidController.lastAccelSample = accelSample; // Convert g to m/s^2 //NSLog(@"sending controller gyro data, accelSample data 00: %f, playerIndex: %ld, obj: %@",accelSample.x, (long)voidController.gamepad.playerIndex, voidController); LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber, LI_MOTION_TYPE_ACCEL, accelSample.x * -9.80665f * self->_gyroSensitivity, accelSample.y * -9.80665f * self->_gyroSensitivity, accelSample.z * -9.80665f * self->_gyroSensitivity); }]; }); } break; case LI_MOTION_TYPE_GYRO: [voidController.gyroTimer invalidate]; voidController.gyroTimer = nil; if (voidController.reportRateHz && voidController.gamepad.motion.hasRotationRate) { // Reset the last motion sample GCRotationRate emptyGyroSample = {}; voidController.lastGyroSample = emptyGyroSample; //dispatch_sync(dispatch_get_main_queue(), ^{ {dispatch_async(dispatch_get_main_queue(), ^{ voidController.hasGyroscope = YES; voidController.gyroTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 / voidController.reportRateHz repeats:YES block:^(NSTimer *timer) { // Don't send duplicate samples GCRotationRate lastGyroSample = voidController.lastGyroSample; GCRotationRate gyroSample = voidController.gamepad.motion.rotationRate; if (memcmp(&gyroSample, &lastGyroSample, sizeof(gyroSample)) == 0) { return; } voidController.lastGyroSample = gyroSample; // Convert rad/s to deg/s // NSLog(@"sending controller gyro data, gyroSample data 00: %f, playerIndex: %ld, obj: %@",gyroSample.x, (long)voidController.gamepad.playerIndex, voidController); LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber, LI_MOTION_TYPE_GYRO, self->_gyroEnabledFlag ? gyroSample.x * 57.2957795f * self->_gyroSensitivity : 0, self->_gyroEnabledFlag ? gyroSample.z * 57.2957795f * self->_gyroSensitivity : 0, self->_gyroEnabledFlag ? gyroSample.y * -57.2957795f * self->_gyroSensitivity : 0); }]; // }); });} } break; } } } } NSLog(@"controller obj timer, motionTypes: %lu", (unsigned long)voidController.motionTypes.count); // Set the motion sensor state if they require manual activation [self updateSensorSateForController:voidController]; // NSLog(@"sensor active: %d", voidController.gamepad.motion.sensorsActive); } } - (void)updateSensorSateForController:(VoidController* )voidController{ if (@available(iOS 14.0, *)) { if (voidController.gamepad.motion.sensorsRequireManualActivation) { if ((voidController.hasGyroscope || voidController.hasAccelerometer) && _gyroMode != GyroModeOff) { voidController.gamepad.motion.sensorsActive = YES; } else { voidController.gamepad.motion.sensorsActive = NO; } } } } - (void) setMotionEventState:(uint16_t)controllerNumber motionType:(uint8_t)motionType reportRateHz:(uint16_t)reportRateHz { //if (@available(iOS 14.0, tvOS 14.0, *)) { NSLog(@"gyroMode: %ld", (long)_gyroMode); VoidController* voidController = [_voidControllers objectForKey:[NSNumber numberWithInteger:controllerNumber]]; //using device motion if (voidController == nil || _gyroMode == AlwaysDevice) { // No connected controller for this player, use the _oscController instead voidController = _oscController; if(!voidController.motionTypes){ voidController.motionTypes = [[NSMutableSet alloc] init]; } [voidController.motionTypes addObject:@(motionType)]; voidController.hasGyroscope = NO; voidController.hasAccelerometer = NO; voidController.reportRateHz = reportRateHz; } voidController.controllerNumber = controllerNumber; if(voidController == _oscController) [self updateTimerStateForController:voidController]; //} } -(void) setControllerLed:(uint16_t)controllerNumber r:(uint8_t)r g:(uint8_t)g b:(uint8_t)b { if (@available(iOS 14.0, tvOS 14.0, *)) { VoidController* controller = [_voidControllers objectForKey:[NSNumber numberWithInteger:controllerNumber]]; if (controller == nil) { // No connected controller for this player return; } if (controller.gamepad.light == nil) { // No LED control supported for this controller return; } controller.gamepad.light.color = [[GCColor alloc] initWithRed:(r / 255.0f) green:(g / 255.0f) blue:(b / 255.0f)]; } } -(void) updateLeftStick:(VoidController*)controller x:(short)x y:(short)y { @synchronized(controller) { controller.lastLeftStickX = x; controller.lastLeftStickY = y; } } -(void) updateRightStick:(VoidController*)controller x:(short)x y:(short)y { @synchronized(controller) { controller.lastRightStickX = x; controller.lastRightStickY = y; } } -(void) updateLeftTrigger:(VoidController*)controller left:(unsigned char)left { @synchronized(controller) { controller.lastLeftTrigger = left; } } -(void) updateRightTrigger:(VoidController*)controller right:(unsigned char)right { @synchronized(controller) { controller.lastRightTrigger = right; } } -(void) updateTriggers:(VoidController*) controller left:(unsigned char)left right:(unsigned char)right { @synchronized(controller) { controller.lastLeftTrigger = left; controller.lastRightTrigger = right; } } -(void) handleSpecialCombosReleased:(VoidController*)controller releasedButtons:(int)releasedButtons { if ((controller.emulatingButtonFlags & EMULATING_SELECT) && (releasedButtons & (LB_FLAG | PLAY_FLAG))) { controller.lastButtonFlags &= ~BACK_FLAG; controller.emulatingButtonFlags &= ~EMULATING_SELECT; } if (controller.emulatingButtonFlags & EMULATING_SPECIAL) { // If Select is emulated, we use RB+Start to emulate special, otherwise we use Start+Select if (controller.supportedEmulationFlags & EMULATING_SELECT) { if (releasedButtons & (RB_FLAG | PLAY_FLAG)) { controller.lastButtonFlags &= ~SPECIAL_FLAG; controller.emulatingButtonFlags &= ~EMULATING_SPECIAL; } } else { if (releasedButtons & (BACK_FLAG | PLAY_FLAG)) { controller.lastButtonFlags &= ~SPECIAL_FLAG; controller.emulatingButtonFlags &= ~EMULATING_SPECIAL; } } } } -(void) handleSpecialCombosPressed:(VoidController*)controller pressedButtons:(int)pressedButtons { // Special button combos for select and special if (controller.lastButtonFlags & PLAY_FLAG) { // If LB and start are down, trigger select if (controller.lastButtonFlags & LB_FLAG) { if (controller.supportedEmulationFlags & EMULATING_SELECT) { controller.lastButtonFlags |= BACK_FLAG; controller.lastButtonFlags &= ~(pressedButtons & (PLAY_FLAG | LB_FLAG)); controller.emulatingButtonFlags |= EMULATING_SELECT; } } else if (controller.supportedEmulationFlags & EMULATING_SPECIAL) { // If Select is emulated too, use RB+Start to emulate special if (controller.supportedEmulationFlags & EMULATING_SELECT) { if (controller.lastButtonFlags & RB_FLAG) { controller.lastButtonFlags |= SPECIAL_FLAG; controller.lastButtonFlags &= ~(pressedButtons & (PLAY_FLAG | RB_FLAG)); controller.emulatingButtonFlags |= EMULATING_SPECIAL; } } else { // If Select is physical, use Start+Select to emulate special if (controller.lastButtonFlags & BACK_FLAG) { controller.lastButtonFlags |= SPECIAL_FLAG; controller.lastButtonFlags &= ~(pressedButtons & (PLAY_FLAG | BACK_FLAG)); controller.emulatingButtonFlags |= EMULATING_SPECIAL; } } } } } -(void) updateButtonFlags:(VoidController*)controller flags:(int)flags { @synchronized(controller) { controller.lastButtonFlags = flags; // This must be called before handleSpecialCombosPressed // because we clear the original button flags there int releasedButtons = (controller.lastButtonFlags ^ flags) & ~flags; int pressedButtons = (controller.lastButtonFlags ^ flags) & flags; [self handleSpecialCombosReleased:controller releasedButtons:releasedButtons]; [self handleSpecialCombosPressed:controller pressedButtons:pressedButtons]; } } -(void) setButtonFlag:(VoidController*)controller flags:(int)flags { @synchronized(controller) { controller.lastButtonFlags |= flags; [self handleSpecialCombosPressed:controller pressedButtons:flags]; } } -(void) clearButtonFlag:(VoidController*)controller flags:(int)flags { @synchronized(controller) { controller.lastButtonFlags &= ~flags; [self handleSpecialCombosReleased:controller releasedButtons:flags]; } } -(uint16_t) getActiveGamepadMask { return (_multiController ? _controllerNumbers : 1) | (_oscEnabled ? 1 : 0); } -(void) updateFinished:(VoidController*)controller { BOOL exitRequested = NO; [_controllerStreamLock lock]; @synchronized(controller) { // Handle Start+Select+L1+R1 gamepad quit combo if (controller.lastButtonFlags == (PLAY_FLAG | BACK_FLAG | LB_FLAG | RB_FLAG)) { controller.lastButtonFlags = 0; exitRequested = YES; } // Only send controller events if we successfully reported controller arrival if ([self reportControllerArrival:controller]) { uint32_t buttonFlags = controller.lastButtonFlags; uint8_t leftTrigger = controller.lastLeftTrigger; uint8_t rightTrigger = controller.lastRightTrigger; int16_t leftStickX = controller.lastLeftStickX; int16_t leftStickY = controller.lastLeftStickY; int16_t rightStickX = controller.lastRightStickX; int16_t rightStickY = controller.lastRightStickY; // If this is merged with another controller, combine the inputs if (controller.mergedWithController) { buttonFlags |= controller.mergedWithController.lastButtonFlags; leftTrigger = MAX(leftTrigger, controller.mergedWithController.lastLeftTrigger); rightTrigger = MAX(rightTrigger, controller.mergedWithController.lastRightTrigger); leftStickX = MAX_MAGNITUDE(leftStickX, controller.mergedWithController.lastLeftStickX); leftStickY = MAX_MAGNITUDE(leftStickY, controller.mergedWithController.lastLeftStickY); rightStickX = MAX_MAGNITUDE(rightStickX, controller.mergedWithController.lastRightStickX); rightStickY = MAX_MAGNITUDE(rightStickY, controller.mergedWithController.lastRightStickY); } //NSLog(@"gamepadMask: %@", [self binaryRepresentationOfInteger:buttonFlags]); // we got the pressed OSC buttons here. // Player 1 is always present for OSC LiSendMultiControllerEvent(_multiController ? controller.playerIndex : 0, [self getActiveGamepadMask], buttonFlags, leftTrigger, rightTrigger, leftStickX, leftStickY, rightStickX, rightStickY); } } [_controllerStreamLock unlock]; if (exitRequested) { // Invoke the delegate callback on the main thread dispatch_async(dispatch_get_main_queue(), ^{ [self->_delegate streamExitRequested]; }); } } - (NSString *)binaryRepresentationOfInteger:(int)number { NSMutableString *binaryString = [NSMutableString string]; int numBits = sizeof(number) * 8; for (int i = numBits - 1; i >= 0; i--) { [binaryString appendString:((number >> i) & 1) ? @"1" : @"0"]; } return binaryString; } +(BOOL) hasKeyboardOrMouse { if (@available(iOS 14.0, tvOS 14.0, *)) { return GCMouse.mice.count > 0 || GCKeyboard.coalescedKeyboard != nil; } else { return NO; } } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" -(void) unregisterControllerCallbacks:(GCController*) controller { if (controller != NULL) { controller.controllerPausedHandler = NULL; if (controller.extendedGamepad != NULL) { // Re-enable system gestures on the gamepad buttons now if (@available(iOS 14.0, tvOS 14.0, *)) { for (GCControllerElement* element in controller.physicalInputProfile.allElements) { element.preferredSystemGestureState = GCSystemGestureStateEnabled; } } controller.extendedGamepad.valueChangedHandler = NULL; } } } -(void) initializeControllerHaptics:(VoidController*) controller { controller.lowFreqMotor = [HapticContext createContextForLowFreqMotor:controller.gamepad]; controller.highFreqMotor = [HapticContext createContextForHighFreqMotor:controller.gamepad]; controller.leftTriggerMotor = [HapticContext createContextForLeftTrigger:controller.gamepad]; controller.rightTriggerMotor = [HapticContext createContextForRightTrigger:controller.gamepad]; } -(void) cleanupControllerHaptics:(VoidController*) controller { [controller.lowFreqMotor cleanup]; [controller.highFreqMotor cleanup]; [controller.leftTriggerMotor cleanup]; [controller.rightTriggerMotor cleanup]; } -(void) cleanupControllerMotion:(VoidController*) controller { if (@available(iOS 14.0, tvOS 14.0, *)) { // Stop sensor sampling timers [controller.gyroTimer invalidate]; [controller.accelTimer invalidate]; // Disable motion sensors if they require manual activation if (controller.gamepad && controller.gamepad.motion && controller.gamepad.motion.sensorsRequireManualActivation) { controller.gamepad.motion.sensorsActive = NO; } } } -(void) initializeControllerBattery:(VoidController*) controller { if (@available(iOS 14.0, tvOS 14.0, *)) { if (controller.gamepad.battery) { // Poll for updated battery status every 30 seconds controller.batteryTimer = [NSTimer scheduledTimerWithTimeInterval:30 repeats:YES block:^(NSTimer *timer) { if ((GCDeviceBatteryState)controller.lastBatteryState != controller.gamepad.battery.batteryState || controller.lastBatteryLevel != controller.gamepad.battery.batteryLevel) { uint8_t batteryState; switch (controller.gamepad.battery.batteryState) { case GCDeviceBatteryStateFull: batteryState = LI_BATTERY_STATE_FULL; break; case GCDeviceBatteryStateCharging: batteryState = LI_BATTERY_STATE_CHARGING; break; case GCDeviceBatteryStateDischarging: batteryState = LI_BATTERY_STATE_DISCHARGING; break; case GCDeviceBatteryStateUnknown: default: batteryState = LI_BATTERY_STATE_UNKNOWN; break; } LiSendControllerBatteryEvent(controller.playerIndex, batteryState, (uint8_t)(controller.gamepad.battery.batteryLevel * 100)); controller.lastBatteryState = (ControllerDeviceBatteryState)controller.gamepad.battery.batteryState; controller.lastBatteryLevel = controller.gamepad.battery.batteryLevel; } }]; // Fire the timer immediately to send the initial battery state [controller.batteryTimer fire]; } } } -(void) cleanupControllerBattery:(VoidController*) controller { if (@available(iOS 14.0, tvOS 14.0, *)) { [controller.batteryTimer invalidate]; } } -(BOOL) reportControllerArrival:(VoidController*) voidController { // Only report arrival once if (voidController.reportedArrival) { return YES; } uint8_t type = LI_CTYPE_UNKNOWN; uint16_t capabilities = 0; uint32_t supportedButtonFlags = 0; GCController *controller = voidController.gamepad; if (controller) { // This is a physical controller with a corresponding GCController object // Start is always present supportedButtonFlags |= PLAY_FLAG; // Detect buttons present in the GCExtendedGamepad profile if (controller.extendedGamepad.dpad) { supportedButtonFlags |= UP_FLAG | DOWN_FLAG | LEFT_FLAG | RIGHT_FLAG; } if (controller.extendedGamepad.leftShoulder) { supportedButtonFlags |= LB_FLAG; } if (controller.extendedGamepad.rightShoulder) { supportedButtonFlags |= RB_FLAG; } if (@available(iOS 13.0, tvOS 13.0, *)) { if (controller.extendedGamepad.buttonOptions) { supportedButtonFlags |= BACK_FLAG; } } if (@available(iOS 14.0, tvOS 14.0, *)) { if (controller.extendedGamepad.buttonHome) { supportedButtonFlags |= SPECIAL_FLAG; } } if (controller.extendedGamepad.buttonA) { supportedButtonFlags |= A_FLAG; } if (controller.extendedGamepad.buttonB) { supportedButtonFlags |= B_FLAG; } if (controller.extendedGamepad.buttonX) { supportedButtonFlags |= X_FLAG; } if (controller.extendedGamepad.buttonY) { supportedButtonFlags |= Y_FLAG; } if (@available(iOS 12.1, tvOS 12.1, *)) { if (controller.extendedGamepad.leftThumbstickButton) { supportedButtonFlags |= LS_CLK_FLAG; } if (controller.extendedGamepad.rightThumbstickButton) { supportedButtonFlags |= RS_CLK_FLAG; } } if (@available(iOS 14.0, tvOS 14.0, *)) { // Xbox One/Series controller if (controller.physicalInputProfile.buttons[GCInputXboxPaddleOne]) { supportedButtonFlags |= PADDLE1_FLAG; } if (controller.physicalInputProfile.buttons[GCInputXboxPaddleTwo]) { supportedButtonFlags |= PADDLE2_FLAG; } if (controller.physicalInputProfile.buttons[GCInputXboxPaddleThree]) { supportedButtonFlags |= PADDLE3_FLAG; } if (controller.physicalInputProfile.buttons[GCInputXboxPaddleFour]) { supportedButtonFlags |= PADDLE4_FLAG; } if (@available(iOS 15.0, tvOS 15.0, *)) { if (controller.physicalInputProfile.buttons[GCInputButtonShare]) { supportedButtonFlags |= MISC_FLAG; } } // DualShock/DualSense controller if (controller.physicalInputProfile.buttons[GCInputDualShockTouchpadButton]) { supportedButtonFlags |= TOUCHPAD_FLAG; } if (controller.physicalInputProfile.dpads[GCInputDualShockTouchpadOne]) { capabilities |= LI_CCAP_TOUCHPAD; } // LI_CTYPE_UNKNOWN for option "Both" if(voidController.playerIndex == 0){ type = _streamConfig.emulatedControllerType == LI_CTYPE_UNKNOWN ? LI_CTYPE_PS : _streamConfig.emulatedControllerType; } if(voidController.playerIndex == 1 && _streamConfig.emulatedControllerType == LI_CTYPE_UNKNOWN){ type = LI_CTYPE_XBOX; } if(voidController.playerIndex >= 1 && _streamConfig.emulatedControllerType != LI_CTYPE_UNKNOWN){ if ([controller.extendedGamepad isKindOfClass:[GCXboxGamepad class]]) { type = LI_CTYPE_XBOX; } if ([controller.extendedGamepad isKindOfClass:[GCDualShockGamepad class]]) { type = LI_CTYPE_PS; } if (@available(iOS 14.5, tvOS 14.5, *)) { if ([controller.extendedGamepad isKindOfClass:[GCDualSenseGamepad class]]) { type = LI_CTYPE_PS; } } } // Detect supported haptics localities if (controller.haptics) { if ([controller.haptics.supportedLocalities containsObject:GCHapticsLocalityHandles]) { capabilities |= LI_CCAP_RUMBLE; } if ([controller.haptics.supportedLocalities containsObject:GCHapticsLocalityTriggers]) { capabilities |= LI_CCAP_TRIGGER_RUMBLE; } } // Detect supported motion sensors if (controller.motion) { if (controller.motion.hasGravityAndUserAcceleration) { capabilities |= LI_CCAP_ACCEL; } if (controller.motion.hasRotationRate) { capabilities |= LI_CCAP_GYRO; } } // Detect RGB LED support if (controller.light) { capabilities |= LI_CCAP_RGB_LED; } // Detect battery support if (controller.battery) { capabilities |= LI_CCAP_BATTERY_STATE; } bool controllerLacksGyro = (capabilities & LI_CCAP_GYRO) == 0; if(_streamConfig.emulatedControllerType == LI_CTYPE_PS && (_gyroMode == AlwaysDevice || (_gyroMode == GyroModeAuto && controllerLacksGyro))) { type = LI_CTYPE_PS; capabilities |= LI_CCAP_GYRO | LI_CCAP_ACCEL; } } } else { // This is a virtual controller corresponding to our OSC // set osc to PS to utilize built-in gyro when "both" is selected type = _streamConfig.emulatedControllerType == LI_CTYPE_UNKNOWN ? LI_CTYPE_PS : _streamConfig.emulatedControllerType; /* if (_streamConfig.gyroMode != GyroModeOff) { type = LI_CTYPE_PS; capabilities = LI_CCAP_GYRO | LI_CCAP_ACCEL; } else { type = LI_CTYPE_XBOX; capabilities = 0; } */ // Set the standard supported buttons for the virtual controller. supportedButtonFlags = PLAY_FLAG | BACK_FLAG | UP_FLAG | DOWN_FLAG | LEFT_FLAG | RIGHT_FLAG | LB_FLAG | RB_FLAG | LS_CLK_FLAG | RS_CLK_FLAG | A_FLAG | B_FLAG | X_FLAG | Y_FLAG; } // Report the new controller to the host // NB: This will fail if the connection hasn't been fully established yet // and we will try again later. if (LiSendControllerArrivalEvent(controller.playerIndex, [self getActiveGamepadMask], type, supportedButtonFlags, capabilities) != 0) { return NO; } // Begin polling for battery status [self initializeControllerBattery:voidController]; // Remember that we've reported arrival already voidController.reportedArrival = YES; return YES; } -(void) handleControllerTouchpad:(VoidController*)controller touch:(GCControllerDirectionPad*)touch index:(int)index { controller_touch_context_t context = index == 0 ? controller.primaryTouch : controller.secondaryTouch; // This magic is courtesy of SDL float normalizedX = (1.0f + touch.xAxis.value) * 0.5f; float normalizedY = 1.0f - (1.0f + touch.yAxis.value) * 0.5f; // If we went from a touch to no touch, generate a touch up event if ((context.lastX || context.lastY) && (!touch.xAxis.value && !touch.yAxis.value)) { LiSendControllerTouchEvent(controller.playerIndex, LI_TOUCH_EVENT_UP, index, normalizedX, normalizedY, 1.0f); } else if (touch.xAxis.value || touch.yAxis.value) { // If we went from no touch to a touch, generate a touch down event if (!context.lastX && !context.lastY) { LiSendControllerTouchEvent(controller.playerIndex, LI_TOUCH_EVENT_DOWN, index, normalizedX, normalizedY, 1.0f); } else if (context.lastX != touch.xAxis.value || context.lastY != touch.yAxis.value) { // Otherwise it's just a move LiSendControllerTouchEvent(controller.playerIndex, LI_TOUCH_EVENT_MOVE, index, normalizedX, normalizedY, 1.0f); } } // We have to assign the whole struct because this is a property rather than a standard // field that we could modify through a pointer. if (index == 0) { controller.primaryTouch = (controller_touch_context_t) { touch.xAxis.value, touch.yAxis.value }; } else { controller.secondaryTouch = (controller_touch_context_t) { touch.xAxis.value, touch.yAxis.value }; } } double rc_expo(double x, double expo) { if (x == 0.0) return 0.0; double ax = fabs(x); double y = pow(ax, expo); return x > 0 ? y : -y; } - (void)sendStickToMouseMoveEventWithStickX:(float)stickX stickY:(float)stickY expo:(float)expo { CGFloat mouseDeltaX = _stickToMouseVelocity*rc_expo(stickX, expo); CGFloat mouseDeltaY = _stickToMouseVelocity*rc_expo(stickY, expo); LiSendMouseMoveEvent(mouseDeltaX, -mouseDeltaY); } - (void)stopDisplayLink { [_displayLink invalidate]; _displayLink = nil; } - (void)displayLinkCallBack { if(!_controllerMouseEnabledFlag){ return; } NSTimeInterval delay = 0.5/_displayLink.preferredFramesPerSecond*NSEC_PER_SEC; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delay), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [self sendStickToMouseMoveEventWithStickX:self->stickToMouseInputX stickY:self->stickToMouseInputY expo:self->_stickToMouseExpo]; LiSendHighResScrollEvent(15*self->stickToWheelInputY); LiSendHighResHScrollEvent(15*self->stickToWheelInputX); }); } - (void)sendControllerMouseSwitchClick:(VoidController*) voidController{ [self setButtonFlag:voidController flags:self->_controllerMouseSwitch]; _mouseSwitchButtonBeingClicked = true; [self updateFinished:voidController]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.03*NSEC_PER_SEC), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [self clearButtonFlag:voidController flags:self->_controllerMouseSwitch]; self->_mouseSwitchButtonBeingClicked = false; [self updateFinished:voidController]; }); } - (bool)useMotionHandler{ return self->tempSettings.emulatedControllerType.intValue!=LI_CTYPE_PS ||self->tempSettings.gyroMode.intValue==GyroModeOff; } - (void)switchGyroOnOffByControllerButton{ self->_gyroEnabledFlag = self->_gyroEnabledFlag && self->_controllerGyroSwitchEnabled; if([self useMotionHandler]){ if(self->_gyroEnabledFlag) [self->motionHandler startGyroByControllerButton]; else [self->motionHandler stopGyroUpdateWithInterruptNoneGyroInput:false resetLeftStick:true]; } else self->_gyroEnabledFlag = self->_gyroEnabledFlag || !self->_controllerGyroSwitchEnabled; } -(void) registerControllerCallbacks:(GCController*) controller { if (controller != NULL) { // iOS 13 allows the Start button to behave like a normal button, however // older MFi controllers can send an instant down+up event for the start button // which means the button will not be down long enough to register on the PC. // To work around this issue, use the old controllerPausedHandler if the controller // doesn't have a Select button (which indicates it probably doesn't have a proper // Start button either). BOOL useLegacyPausedHandler = YES; if (@available(iOS 13.0, tvOS 13.0, *)) { if (controller.extendedGamepad != nil && controller.extendedGamepad.buttonOptions != nil) { useLegacyPausedHandler = NO; } } if (useLegacyPausedHandler) { controller.controllerPausedHandler = ^(GCController *controller) { VoidController* voidController = [self->_voidControllers objectForKey:[NSNumber numberWithInteger:controller.playerIndex]]; // Get off the main thread dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ [self setButtonFlag:voidController flags:PLAY_FLAG]; [self updateFinished:voidController]; // Pause for 100 ms usleep(100 * 1000); [self clearButtonFlag:voidController flags:PLAY_FLAG]; [self updateFinished:voidController]; }); }; } if (controller.extendedGamepad != NULL) { // Disable system gestures on the gamepad to avoid interfering // with in-game controller actions if (@available(iOS 14.0, tvOS 14.0, *)) { for (GCControllerElement* element in controller.physicalInputProfile.allElements) { element.preferredSystemGestureState = GCSystemGestureStateDisabled; } } if(self->_swapABXYButtons){ switch (self->_controllerMouseLeftButton) { case ControllerButtonA: self->_controllerMouseLeftButton = ControllerButtonB; break; case ControllerButtonB: self->_controllerMouseLeftButton = ControllerButtonA; break; case ControllerButtonX: self->_controllerMouseLeftButton = ControllerButtonY; break; case ControllerButtonY: self->_controllerMouseLeftButton = ControllerButtonX; break; default: break; } switch (self->_controllerMouseRightButton) { case ControllerButtonA: self->_controllerMouseRightButton = ControllerButtonB; break; case ControllerButtonB: self->_controllerMouseRightButton = ControllerButtonA; break; case ControllerButtonX: self->_controllerMouseRightButton = ControllerButtonY; break; case ControllerButtonY: self->_controllerMouseRightButton = ControllerButtonX; break; default: break; } } [ControllerUtil listenWithController:controller swapABXY:self->_swapABXYButtons handler:^(NSDictionary * buttonDict, GCExtendedGamepad * gamepad, GCControllerElement * element) { VoidController* voidController = [self->_voidControllers objectForKey:[NSNumber numberWithInteger:gamepad.controller.playerIndex]]; short leftStickX, leftStickY; short rightStickX, rightStickY; unsigned char leftTrigger, rightTrigger; for(NSNumber* buttonFlagId in buttonDict){ GCControllerButtonInput * button = (GCControllerButtonInput *)buttonDict[buttonFlagId]; if(self->_mapControllerToMouse){ if(button.pressed){ if(buttonFlagId.intValue == self->_controllerMouseSwitch){ self->_mouseSwitchButtonPressed = true; self->mouseSwitchDownTimestamp = CACurrentMediaTime(); } } else{ if(buttonFlagId.intValue == self->_controllerMouseSwitch && self->_mouseSwitchButtonPressed){ if(CACurrentMediaTime()-self->mouseSwitchDownTimestamp>1){ self->_controllerMouseEnabledFlag = !self->_controllerMouseEnabledFlag; [self updateLeftStick:voidController x:0 y:0]; [self updateRightStick:voidController x:0 y:0]; } else [self sendControllerMouseSwitchClick:voidController]; self->_mouseSwitchButtonPressed = false; self->mouseSwitchDownTimestamp = 0; } } } else self->_controllerMouseEnabledFlag = false; // controller switch buttons if(true){ if(button.pressed){ if (buttonFlagId.intValue == self->_controllerGyroSwitchToggle && !self->_controllerGyroSwitchTogglePressed) { self->_controllerGyroSwitchTogglePressed = true; self->_gyroEnabledFlag = !self->_gyroEnabledFlag; [self switchGyroOnOffByControllerButton]; } if (buttonFlagId.intValue == self->_controllerGyroSwitchHold && !self->_controllerGyroSwitchHoldPressed) { self->_controllerGyroSwitchHoldPressed = true; self->_gyroEnabledFlag = !self->_reverseHoldButton; [self switchGyroOnOffByControllerButton]; } } else{ if (buttonFlagId.intValue == self->_controllerGyroSwitchToggle && self->_controllerGyroSwitchTogglePressed) { self->_controllerGyroSwitchTogglePressed = false; } if (buttonFlagId.intValue == self->_controllerGyroSwitchHold && self->_controllerGyroSwitchHoldPressed) { self->_controllerGyroSwitchHoldPressed = false; self->_gyroEnabledFlag = self->_reverseHoldButton; [self switchGyroOnOffByControllerButton]; } } } if(self->_controllerMouseEnabledFlag){ if(buttonFlagId.intValue == self->_controllerMouseLeftButton || buttonFlagId.intValue == self->_controllerMouseRightButton){ if(buttonFlagId.intValue == self->_controllerMouseLeftButton) LiSendMouseButtonEvent(button.pressed ? BUTTON_ACTION_PRESS : BUTTON_ACTION_RELEASE, BUTTON_LEFT); if(buttonFlagId.intValue == self->_controllerMouseRightButton) LiSendMouseButtonEvent(button.pressed ? BUTTON_ACTION_PRESS : BUTTON_ACTION_RELEASE, BUTTON_RIGHT); UPDATE_BUTTON_FLAG(voidController, buttonFlagId.intValue, NO); } else if(buttonFlagId.intValue!=self->_controllerMouseSwitch || !self->_mapControllerToMouse) UPDATE_BUTTON_FLAG(voidController, buttonFlagId.intValue, button.pressed); } else if(buttonFlagId.intValue!=self->_controllerMouseSwitch || !self->_mapControllerToMouse) UPDATE_BUTTON_FLAG(voidController, buttonFlagId.intValue, button.pressed); } CGFloat leftStickXRaw = gamepad.leftThumbstick.xAxis.value * self->stickMaxOffset; CGFloat leftStickYRaw = gamepad.leftThumbstick.yAxis.value * self->stickMaxOffset; CGFloat rightStickXRaw = gamepad.rightThumbstick.xAxis.value * self->stickMaxOffset; CGFloat rightStickYRaw = gamepad.rightThumbstick.yAxis.value * self->stickMaxOffset; CGVector leftStickOffset = [ControllerUtil compensatedWithOffsetVector:CGVectorMake(leftStickXRaw, leftStickYRaw) minOffset:self->_leftStickMinOffset circulate:false]; CGVector rightStickOffset = [ControllerUtil compensatedWithOffsetVector:CGVectorMake(rightStickXRaw, rightStickYRaw) minOffset:self->_rightStickMinOffset circulate:false]; leftStickX = self->_controllerMouseEnabledFlag ? 0 : leftStickOffset.dx; leftStickY = self->_controllerMouseEnabledFlag ? 0 : leftStickOffset.dy; rightStickX = self->_controllerMouseEnabledFlag ? 0 : rightStickOffset.dx; rightStickY = self->_controllerMouseEnabledFlag ? 0 : rightStickOffset.dy; if(self->_controllerMouseEnabledFlag){ self->stickToMouseInputX = self->_controllerMouseStick == LeftStickToMouse ? gamepad.leftThumbstick.xAxis.value : gamepad.rightThumbstick.xAxis.value; self->stickToMouseInputY = self->_controllerMouseStick == LeftStickToMouse ? gamepad.leftThumbstick.yAxis.value : gamepad.rightThumbstick.yAxis.value; self->stickToWheelInputX = self->_controllerMouseStick == LeftStickToMouse ? gamepad.rightThumbstick.xAxis.value: gamepad.leftThumbstick.xAxis.value; self->stickToWheelInputY = self->_controllerMouseStick == LeftStickToMouse ? gamepad.rightThumbstick.yAxis.value: gamepad.leftThumbstick.yAxis.value; } else{ self->stickToMouseInputX = 0; self->stickToMouseInputY = 0; } /* if(self->oscProfile.mapGyroTo!=mapGyroToControllerStick ||!self->oscProfile.rollToLeftStick) [self updateLeftStick:voidController x:leftStickX y:leftStickY]; */ if([self useMotionHandler] && self->oscProfile.mapGyroTo==mapGyroToControllerStick && self->oscProfile.yawPitchToRightStick && self->_gyroEnabledFlag ) [self->motionHandler mixPhysicalRightStickAndGyroInputWithX:rightStickX y:rightStickY]; else [self updateRightStick: voidController.playerIndex==0?self->_oscController:voidController x:rightStickX y:rightStickY]; if([self useMotionHandler] && self->oscProfile.mapGyroTo==mapGyroToControllerStick && self->oscProfile.rollToLeftStick && self->_gyroEnabledFlag ) [self->motionHandler mixPhysicalLeftStickAndGyroInputWithX:leftStickX y:leftStickY]; else [self updateLeftStick: voidController.playerIndex==0?self->_oscController:voidController x:leftStickX y:leftStickY]; leftTrigger = gamepad.leftTrigger.value * 0xFF; rightTrigger = gamepad.rightTrigger.value * 0xFF; [self updateTriggers:voidController left:leftTrigger right:rightTrigger]; [self updateFinished:voidController]; if (@available(iOS 14.0, *)) { if (gamepad.controller.physicalInputProfile.dpads[GCInputDualShockTouchpadOne]) { [self handleControllerTouchpad:voidController touch:gamepad.controller.physicalInputProfile.dpads[GCInputDualShockTouchpadOne] index:0]; } if (gamepad.controller.physicalInputProfile.dpads[GCInputDualShockTouchpadTwo]) { [self handleControllerTouchpad:voidController touch:gamepad.controller.physicalInputProfile.dpads[GCInputDualShockTouchpadTwo] index:1]; } } }]; /* controller.extendedGamepad.valueChangedHandler = ^(GCExtendedGamepad *gamepad, GCControllerElement *element) { VoidController* voidController = [self->_voidControllers objectForKey:[NSNumber numberWithInteger:gamepad.controller.playerIndex]]; short leftStickX, leftStickY; short rightStickX, rightStickY; unsigned char leftTrigger, rightTrigger; if (self->_swapABXYButtons) { UPDATE_BUTTON_FLAG(voidController, B_FLAG, gamepad.buttonA.pressed); UPDATE_BUTTON_FLAG(voidController, A_FLAG, gamepad.buttonB.pressed); UPDATE_BUTTON_FLAG(voidController, Y_FLAG, gamepad.buttonX.pressed); UPDATE_BUTTON_FLAG(voidController, X_FLAG, gamepad.buttonY.pressed); } else { UPDATE_BUTTON_FLAG(voidController, A_FLAG, gamepad.buttonA.pressed); UPDATE_BUTTON_FLAG(voidController, B_FLAG, gamepad.buttonB.pressed); UPDATE_BUTTON_FLAG(voidController, X_FLAG, gamepad.buttonX.pressed); UPDATE_BUTTON_FLAG(voidController, Y_FLAG, gamepad.buttonY.pressed); } UPDATE_BUTTON_FLAG(voidController, UP_FLAG, gamepad.dpad.up.pressed); UPDATE_BUTTON_FLAG(voidController, DOWN_FLAG, gamepad.dpad.down.pressed); UPDATE_BUTTON_FLAG(voidController, LEFT_FLAG, gamepad.dpad.left.pressed); UPDATE_BUTTON_FLAG(voidController, RIGHT_FLAG, gamepad.dpad.right.pressed); UPDATE_BUTTON_FLAG(voidController, LB_FLAG, gamepad.leftShoulder.pressed); UPDATE_BUTTON_FLAG(voidController, RB_FLAG, gamepad.rightShoulder.pressed); // Yay, iOS 12.1 now supports analog stick buttons if (@available(iOS 12.1, tvOS 12.1, *)) { if (gamepad.leftThumbstickButton != nil) { UPDATE_BUTTON_FLAG(voidController, LS_CLK_FLAG, gamepad.leftThumbstickButton.pressed); } if (gamepad.rightThumbstickButton != nil) { UPDATE_BUTTON_FLAG(voidController, RS_CLK_FLAG, gamepad.rightThumbstickButton.pressed); } } if (@available(iOS 13.0, tvOS 13.0, *)) { // Options button is optional (only present on Xbox One S and PS4 gamepads) if (gamepad.buttonOptions != nil) { UPDATE_BUTTON_FLAG(voidController, BACK_FLAG, gamepad.buttonOptions.pressed); // For older MFi gamepads, the menu button will already be handled by // the controllerPausedHandler. UPDATE_BUTTON_FLAG(voidController, PLAY_FLAG, gamepad.buttonMenu.pressed); } } if (@available(iOS 14.0, tvOS 14.0, *)) { // Home/Guide button is optional (only present on Xbox One S and PS4 gamepads) if (gamepad.buttonHome != nil) { UPDATE_BUTTON_FLAG(voidController, SPECIAL_FLAG, gamepad.buttonHome.pressed); } // Xbox One/Series controllers if (gamepad.controller.physicalInputProfile.buttons[GCInputXboxPaddleOne]) { UPDATE_BUTTON_FLAG(voidController, PADDLE1_FLAG, gamepad.controller.physicalInputProfile.buttons[GCInputXboxPaddleOne].pressed); } if (gamepad.controller.physicalInputProfile.buttons[GCInputXboxPaddleTwo]) { UPDATE_BUTTON_FLAG(voidController, PADDLE2_FLAG, gamepad.controller.physicalInputProfile.buttons[GCInputXboxPaddleTwo].pressed); } if (gamepad.controller.physicalInputProfile.buttons[GCInputXboxPaddleThree]) { UPDATE_BUTTON_FLAG(voidController, PADDLE3_FLAG, gamepad.controller.physicalInputProfile.buttons[GCInputXboxPaddleThree].pressed); } if (gamepad.controller.physicalInputProfile.buttons[GCInputXboxPaddleFour]) { UPDATE_BUTTON_FLAG(voidController, PADDLE4_FLAG, gamepad.controller.physicalInputProfile.buttons[GCInputXboxPaddleFour].pressed); } if (@available(iOS 15.0, tvOS 15.0, *)) { if (gamepad.controller.physicalInputProfile.buttons[GCInputButtonShare]) { UPDATE_BUTTON_FLAG(voidController, MISC_FLAG, gamepad.controller.physicalInputProfile.buttons[GCInputButtonShare].pressed); } } // DualShock/DualSense controllers if (gamepad.controller.physicalInputProfile.buttons[GCInputDualShockTouchpadButton]) { UPDATE_BUTTON_FLAG(voidController, TOUCHPAD_FLAG, gamepad.controller.physicalInputProfile.buttons[GCInputDualShockTouchpadButton].pressed); } if (gamepad.controller.physicalInputProfile.dpads[GCInputDualShockTouchpadOne]) { [self handleControllerTouchpad:voidController touch:gamepad.controller.physicalInputProfile.dpads[GCInputDualShockTouchpadOne] index:0]; } if (gamepad.controller.physicalInputProfile.dpads[GCInputDualShockTouchpadTwo]) { [self handleControllerTouchpad:voidController touch:gamepad.controller.physicalInputProfile.dpads[GCInputDualShockTouchpadTwo] index:1]; } } leftStickX = self->controllerToMouseStick != LeftStickToMouse ? gamepad.leftThumbstick.xAxis.value * 0x7FFE : 0; leftStickY = self->controllerToMouseStick != LeftStickToMouse ? gamepad.leftThumbstick.yAxis.value * 0x7FFE : 0;; rightStickX = self->controllerToMouseStick != RightStickToMouse ? gamepad.rightThumbstick.xAxis.value * 0x7FFE : 0; rightStickY = self->controllerToMouseStick != RightStickToMouse ? gamepad.rightThumbstick.yAxis.value * 0x7FFE : 0; self->stickToMouseInputX = self->controllerToMouseStick == LeftStickToMouse ? gamepad.leftThumbstick.xAxis.value : gamepad.rightThumbstick.xAxis.value; self->stickToMouseInputY = self->controllerToMouseStick == LeftStickToMouse ? gamepad.leftThumbstick.yAxis.value : gamepad.rightThumbstick.yAxis.value; leftTrigger = gamepad.leftTrigger.value * 0xFF; rightTrigger = gamepad.rightTrigger.value * 0xFF; [self updateLeftStick:voidController x:leftStickX y:leftStickY]; [self updateRightStick:voidController x:rightStickX y:rightStickY]; [self updateTriggers:voidController left:leftTrigger right:rightTrigger]; [self updateFinished:voidController]; }; */ } } else { Log(LOG_W, @"Tried to register controller callbacks on NULL controller"); } } -(void) unregisterMouseCallbacks:(GCMouse*)mouse API_AVAILABLE(ios(14.0)) { mouse.mouseInput.mouseMovedHandler = nil; mouse.mouseInput.leftButton.pressedChangedHandler = nil; mouse.mouseInput.middleButton.pressedChangedHandler = nil; mouse.mouseInput.rightButton.pressedChangedHandler = nil; for (GCControllerButtonInput* auxButton in mouse.mouseInput.auxiliaryButtons) { auxButton.pressedChangedHandler = nil; } #if TARGET_OS_TV mouse.mouseInput.scroll.xAxis.valueChangedHandler = nil; mouse.mouseInput.scroll.yAxis.valueChangedHandler = nil; #endif } -(void) registerMouseCallbacks:(GCMouse*) mouse API_AVAILABLE(ios(14.0)) { if (_captureMouse){ mouse.mouseInput.mouseMovedHandler = ^(GCMouseInput * _Nonnull mouse, float deltaX, float deltaY) { self->accumulatedDeltaX += deltaX / MOUSE_SPEED_DIVISOR; self->accumulatedDeltaY += -deltaY / MOUSE_SPEED_DIVISOR; short truncatedDeltaX = (short)self->accumulatedDeltaX; short truncatedDeltaY = (short)self->accumulatedDeltaY; if (truncatedDeltaX != 0 || truncatedDeltaY != 0) { LiSendMouseMoveEvent(truncatedDeltaX, truncatedDeltaY); self->accumulatedDeltaX -= truncatedDeltaX; self->accumulatedDeltaY -= truncatedDeltaY; } }; } else { mouse.mouseInput.mouseMovedHandler = nil; } mouse.mouseInput.leftButton.pressedChangedHandler = ^(GCControllerButtonInput * _Nonnull button, float value, BOOL pressed) { LiSendMouseButtonEvent(pressed ? BUTTON_ACTION_PRESS : BUTTON_ACTION_RELEASE, BUTTON_LEFT); }; mouse.mouseInput.middleButton.pressedChangedHandler = ^(GCControllerButtonInput * _Nonnull button, float value, BOOL pressed) { LiSendMouseButtonEvent(pressed ? BUTTON_ACTION_PRESS : BUTTON_ACTION_RELEASE, BUTTON_MIDDLE); }; mouse.mouseInput.rightButton.pressedChangedHandler = ^(GCControllerButtonInput * _Nonnull button, float value, BOOL pressed) { LiSendMouseButtonEvent(pressed ? BUTTON_ACTION_PRESS : BUTTON_ACTION_RELEASE, BUTTON_RIGHT); }; if (mouse.mouseInput.auxiliaryButtons != nil) { if (mouse.mouseInput.auxiliaryButtons.count >= 1) { mouse.mouseInput.auxiliaryButtons[0].pressedChangedHandler = ^(GCControllerButtonInput * _Nonnull button, float value, BOOL pressed) { LiSendMouseButtonEvent(pressed ? BUTTON_ACTION_PRESS : BUTTON_ACTION_RELEASE, BUTTON_X1); }; } if (mouse.mouseInput.auxiliaryButtons.count >= 2) { mouse.mouseInput.auxiliaryButtons[1].pressedChangedHandler = ^(GCControllerButtonInput * _Nonnull button, float value, BOOL pressed) { LiSendMouseButtonEvent(pressed ? BUTTON_ACTION_PRESS : BUTTON_ACTION_RELEASE, BUTTON_X2); }; } } // We use UIPanGestureRecognizer on iPadOS because it allows us to distinguish // between discrete and continuous scroll events and also works around a bug // in iPadOS 15 where discrete scroll events are dropped. tvOS only supports // GCMouse for mice, so we will have to just use it and hope for the best. #if TARGET_OS_TV mouse.mouseInput.scroll.xAxis.valueChangedHandler = ^(GCControllerAxisInput * _Nonnull axis, float value) { self->accumulatedScrollX += value; short truncatedScrollX = (short)self->accumulatedScrollX; if (truncatedScrollX != 0) { // Direction is reversed from vertical scrolling LiSendHighResHScrollEvent(-truncatedScrollX * 20); self->accumulatedScrollX -= truncatedScrollX; } }; mouse.mouseInput.scroll.yAxis.valueChangedHandler = ^(GCControllerAxisInput * _Nonnull axis, float value) { self->accumulatedScrollY += value; short truncatedScrollY = (short)self->accumulatedScrollY; if (truncatedScrollY != 0) { LiSendHighResScrollEvent(truncatedScrollY * 20); self->accumulatedScrollY -= truncatedScrollY; } }; #endif } -(void) updateAutoOnScreenControlMode { // Auto on-screen control support may not be enabled if (_osc == NULL) { return; } OnScreenControlsLevel level = OnScreenControlsLevelFull; // We currently stop after the first controller we find. // Maybe we'll want to change that logic later. for (int i = 0; i < [[GCController controllers] count]; i++) { GCController *controller = [GCController controllers][i]; if (controller != NULL) { if (controller.extendedGamepad != NULL) { level = OnScreenControlsLevelAutoGCExtendedGamepad; if (@available(iOS 12.1, tvOS 12.1, *)) { if (controller.extendedGamepad.leftThumbstickButton != nil && controller.extendedGamepad.rightThumbstickButton != nil) { level = OnScreenControlsLevelAutoGCExtendedGamepadWithStickButtons; if (@available(iOS 13.0, tvOS 13.0, *)) { if (controller.extendedGamepad.buttonOptions != nil) { // Has L3/R3 and Select, so we can show nothing :) level = OnScreenControlsLevelOff; } } } } break; } } } // If we didn't find a gamepad present and we have a keyboard or mouse, turn // the on-screen controls off to get the overlays out of the way. if (level == OnScreenControlsLevelFull && [ControllerSupport hasKeyboardOrMouse]) { level = OnScreenControlsLevelOff; // Ensure the virtual gamepad disappears to avoid confusing some games. // If the mouse and keyboard disconnect later, it will reappear when the // first OSC input is received. LiSendMultiControllerEvent(0, 0, 0, 0, 0, 0, 0, 0, 0); } [_osc setLevel:level]; } -(void) initAutoOnScreenControlMode:(OnScreenControls*)osc { _osc = osc; [self updateAutoOnScreenControlMode]; } -(VoidController* )controllerHasBeenAssignedDeprecated:(GCController*)controller{ if(controller.playerIndex == 0) return nil; if(controller.playerIndex > 0){ for(VoidController* voidController in _voidControllers){ if(voidController.gamepad == controller) return voidController; } } return nil; } - (void)updateVoidController:(VoidController* )voidController withGCController:(GCController* )controller{ //voidController.playerIndex = controller.playerIndex == -1 ? 0 : (uint8_t) controller.playerIndex; voidController.motionTypes = [[NSMutableSet alloc] init]; voidController.supportedEmulationFlags = EMULATING_SPECIAL | EMULATING_SELECT; voidController.gamepad = controller; voidController.hasAccelerometer = NO; voidController.hasGyroscope = NO; if(voidController.gamepad.motion.hasAttitudeAndRotationRate){ [voidController.motionTypes addObject:@(LI_MOTION_TYPE_ACCEL)]; voidController.hasAccelerometer = YES; voidController.reportRateHz = 120; } if (@available(iOS 14.0, *)) { if(voidController.gamepad.motion.hasRotationRate) { [voidController.motionTypes addObject:@(LI_MOTION_TYPE_GYRO)]; voidController.hasGyroscope = YES; voidController.reportRateHz = 120; } } // If this is player 0, it shares state with the OSC voidController.mergedWithController = _oscController; _oscController.mergedWithController = voidController; if (@available(iOS 13.0, tvOS 13.0, *)) { if (controller.extendedGamepad != nil && controller.extendedGamepad.buttonOptions != nil) { // Disable select button emulation since we have a physical select button voidController.supportedEmulationFlags &= ~EMULATING_SELECT; } } if (@available(iOS 14.0, tvOS 14.0, *)) { if (controller.extendedGamepad != nil && controller.extendedGamepad.buttonHome != nil) { // Disable special button emulation since we have a physical special button voidController.supportedEmulationFlags &= ~EMULATING_SPECIAL; } } // Prepare controller haptics for use [self initializeControllerHaptics:voidController]; } -(VoidController* )assignController:(GCController*)controller { NSLog(@"run assignController"); bool newGCControllerArrival = ![ControllerUtil.activeGCControllers containsObject:controller]; if(!newGCControllerArrival){ VoidController* voidController = [_voidControllers objectForKey:@(controller.playerIndex)]; if(!voidController) { voidController = [[VoidController alloc] init]; } [self updateVoidController:voidController withGCController:controller]; [_voidControllers setObject:voidController forKey:[NSNumber numberWithInteger:voidController.playerIndex]]; return voidController; } // extenal controller start from playerIndex 1 for option "Both" int startPlayIndex = _streamConfig.emulatedControllerType == LI_CTYPE_UNKNOWN ? 1 : 0; for (int i = startPlayIndex; i < 4; i++) { if (!(_controllerNumbers & (1 << i))) { _controllerNumbers |= (1 << i); VoidController* voidController = [[VoidController alloc] init]; [ControllerUtil.activeGCControllers addObject:controller]; controller.playerIndex = i; voidController.playerIndex = i; [self updateVoidController:voidController withGCController:controller]; [_voidControllers setObject:voidController forKey:[NSNumber numberWithInteger:controller.playerIndex]]; Log(LOG_I, @"Assigning controller index: %d", i); return voidController; } } return nil; } -(VoidController*) getOscController { return _oscController; } +(bool) isSupportedGamepad:(GCController*) controller { return controller.extendedGamepad != nil; } #pragma clang diagnostic pop +(int) getGamepadCount { int count = 0; for (GCController* controller in [GCController controllers]) { if ([ControllerSupport isSupportedGamepad:controller]) { count++; } } return count; } +(int) getConnectedGamepadMask:(StreamConfiguration*)streamConfig { int mask = 0; if (streamConfig.multiController) { int i = 0; for (GCController* controller in [GCController controllers]) { if ([ControllerSupport isSupportedGamepad:controller]) { mask |= 1 << i++; } } } else { // Some games don't deal with having controller reconnected // properly so always report controller 1 if not in MC mode mask = 0x1; } DataManager* dataMan = [[DataManager alloc] init]; TemporarySettings* settings = [dataMan getSettings]; OnScreenControlsLevel level = (OnScreenControlsLevel)[settings.onscreenControls integerValue]; // Even if no gamepads are present, we will always count one if OSC is enabled, // or it's set to auto and no keyboard or mouse is present. Absolute touch mode // disables the OSC. if (level != OnScreenControlsLevelOff && (![ControllerSupport hasKeyboardOrMouse] || level != OnScreenControlsLevelAuto) && (settings.touchMode.intValue == RelativeTouch)) { mask |= 0x1; } return mask; } -(NSUInteger) getConnectedGamepadCount { return _voidControllers.count; } - (void)assignControllers{ for (GCController* controller in [GCController controllers]) { NSLog(@"controller count: iterating"); if ([ControllerSupport isSupportedGamepad:controller]) { NSLog(@"controller count: is supported,is contained by dict: %d", [ControllerUtil.activeGCControllers containsObject:controller]); NSLog(@"controller obj +1 in dic"); [self assignController:controller]; NSLog(@"controller obj num in dict: %lu", (unsigned long)_voidControllers.allValues.count); [self registerControllerCallbacks:controller]; // Note: We cannot report controller arrival to the host here, // because the connection has not been established yet.play } } NSLog(@"device gyro codes,update gyroMode: %d, controller count: %lu", _gyroMode, (unsigned long)_voidControllers.count); } - (void)updateCommonConfig:(StreamConfiguration* )streamConfig{ _streamConfig = streamConfig; _multiController = streamConfig.multiController; _swapABXYButtons = streamConfig.swapABXYButtons; _oscController.playerIndex = 0; oscProfile = [oscProfileMan getSelectedProfile]; motionHandler = [MotionHandler sharedWithProfile:oscProfile]; DataManager* dataMan = [[DataManager alloc] init]; tempSettings = [dataMan getSettings]; _oscEnabled = _oscEnabled || (OnScreenControlsLevel)[tempSettings.onscreenControls integerValue] != OnScreenControlsLevelOff || streamConfig.gyroMode != GyroModeOff; _gyroSensitivity = tempSettings.gyroSensitivity.floatValue; _mapControllerToMouse = tempSettings.mapControllerToMouse; _controllerMouseSwitch = tempSettings.controllerMouseSwitch.intValue; mouseSwitchDownTimestamp = 0; _mouseSwitchButtonPressed = false; _mouseSwitchButtonBeingClicked = false; _controllerMouseStick = tempSettings.controllerMouseStick.intValue; _controllerMouseLeftButton = tempSettings.controllerMouseLeftButton.intValue; _controllerMouseRightButton = tempSettings.controllerMouseRightButton.intValue; _stickToMouseExpo = tempSettings.controllerMouseExpo.floatValue; _stickToMouseVelocity = tempSettings.controllerMousePointerVelocity.floatValue*60/tempSettings.framerate.intValue; stickToMouseInputX = 0; stickToMouseInputY = 0; stickToWheelInputY = 0; [self stopDisplayLink]; if(_mapControllerToMouse){ _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkCallBack)]; if (@available(iOS 15.0, tvOS 15.0, *)) { [_displayLink setPreferredFrameRateRange:CAFrameRateRangeMake(tempSettings.framerate.intValue,tempSettings.framerate.intValue, tempSettings.framerate.intValue)]; } else { _displayLink.preferredFramesPerSecond = tempSettings.framerate.intValue; } [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; } _controllerGyroSwitchEnabled = oscProfile.controllerGyroSwitchMode != ControllerGyroSwitchDisabled; _gyroSwitchMode = oscProfile.controllerGyroSwitchMode; // _gyroSwitchMode = ControllerGyroSwitchHoldDownToActivate; _controllerGyroSwitchToggle = oscProfile.controllerGyroSwitchToggle; _controllerGyroSwitchHold = oscProfile.controllerGyroSwitchHold; _reverseHoldButton = oscProfile.reverseGyroHoldButton; _controllerGyroSwitchTogglePressed = false; _controllerGyroSwitchHoldPressed = false; stickMaxOffset = 0x7FFE; _leftStickMinOffset = oscProfile.physicalLeftStickMinOffset; _rightStickMinOffset = oscProfile.physicalRightStickMinOffset; if(oscProfile.controllerGyroSwitchMode == ControllerGyroSwitchDisabled && ![self useMotionHandler]) _gyroEnabledFlag = true; if(![self useMotionHandler]) [self->motionHandler stopGyroUpdateWithInterruptNoneGyroInput:false resetLeftStick:true]; } - (void)resetGyroInputForController:(VoidController* )voidController{ if(voidController.hasAccelerometer) LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber,LI_MOTION_TYPE_ACCEL,0,0,0); if(voidController.hasGyroscope) LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber,LI_MOTION_TYPE_GYRO,0,0,0); if (@available(iOS 14.0, *)) { voidController.gamepad.motion.sensorsActive = false; } } - (void)stopTimerForAllControllers{ [self stopTimerForController:_oscController]; for(VoidController* controller in _voidControllers.allValues){ [self stopTimerForController:controller]; } } - (void)updateControllerSupport:(StreamConfiguration*)streamConfig delegate:(id)delegate { NSLog(@"update config call"); _gyroMode = streamConfig.gyroMode; [self updateCommonConfig:streamConfig]; Log(LOG_I, @"Number of supported controllers connected: %d", [ControllerSupport getGamepadCount]); Log(LOG_I, @"Multi-controller: %d", _multiController); [self applyGyroModeSetting]; [self assignControllers]; NSLog(@"controllerNumbers: %d", _controllerNumbers); for(VoidController* controller in _voidControllers.allValues){ NSLog(@"controller obj in dict: %@", controller); } [self updateFinished:_oscController]; } -(id)initWithConfig:(StreamConfiguration*)streamConfig delegate:(id)delegate { self = [super init]; NSLog(@"controller support init"); _delegate = delegate; _controllerStreamLock = [[NSLock alloc] init]; _voidControllers = [[NSMutableDictionary alloc] init]; [ControllerUtil.activeGCControllers removeAllObjects]; _controllerNumbers = 0; _captureMouse = (streamConfig.localMousePointerMode == 0); if (@available(iOS 14.0, tvOS 14.0, *)) { for (GCMouse* mouse in [GCMouse mice]) { [self registerMouseCallbacks:mouse]; } } _controllerConnectObserver = [[NSNotificationCenter defaultCenter] addObserverForName:GCControllerDidConnectNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { Log(LOG_I, @"Controller connected!"); GCController* controller = note.object; if (![ControllerSupport isSupportedGamepad:controller]) { // Ignore micro gamepads and motion controllers return; } //[self->_activeGCControllers addObject:controller]; VoidController* voidController = [self assignController:controller]; if (voidController) { // Register callbacks on the new controller [self registerControllerCallbacks:controller]; // Report the controller arrival to the host if we're connected [self reportControllerArrival:voidController]; // Re-evaluate the on-screen control mode //[self updateAutoOnScreenControlMode]; if((self->_gyroMode == GyroModeAuto && [self externalControllersHaveGyro]) || self->_gyroMode == AlwaysController){ [self stopTimerForController:self->_oscController]; [self updateTimerStateForController:voidController]; } // Notify the delegate [self->_delegate gamepadPresenceChanged]; } }]; _controllerDisconnectObserver = [[NSNotificationCenter defaultCenter] addObserverForName:GCControllerDidDisconnectNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { Log(LOG_I, @"Controller disconnected!"); GCController* controller = note.object; if (![ControllerSupport isSupportedGamepad:controller]) { // Ignore micro gamepads and motion controllers return; } [self unregisterControllerCallbacks:controller]; if([ControllerUtil.activeGCControllers containsObject:controller]){ [ControllerUtil.activeGCControllers removeObject:controller]; self->_controllerNumbers &= ~(1 << controller.playerIndex); } Log(LOG_I, @"Unassigning controller index: %ld", (long)controller.playerIndex); VoidController* voidController = [self->_voidControllers objectForKey:[NSNumber numberWithInteger:controller.playerIndex]]; if (voidController) { [self stopTimerForController:voidController]; // Stop haptics on this controller [self cleanupControllerHaptics:voidController]; // Stop motion reports on this controller [self cleanupControllerMotion:voidController]; // Stop battery reports on this controller [self cleanupControllerBattery:voidController]; // Disassociate this controller from any controllers merged with it if (voidController.mergedWithController) { if(voidController.mergedWithController.mergedWithController == voidController) voidController.mergedWithController.mergedWithController = nil; } // Inform the server of the updated active gamepads before removing this controller [self updateFinished:voidController]; // Re-evaluate the on-screen control mode //[self updateAutoOnScreenControlMode]; [self->_voidControllers removeObjectForKey:@(controller.playerIndex)]; if((self->_voidControllers.allValues.count == 0 || ![self externalControllersHaveGyro]) && self->_gyroMode == GyroModeAuto) [self updateTimerStateForController:self->_oscController]; // Notify the delegate [self->_delegate gamepadPresenceChanged]; } }]; if (@available(iOS 14.0, tvOS 14.0, *)) { _mouseConnectObserver = [[NSNotificationCenter defaultCenter] addObserverForName:GCMouseDidConnectNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { Log(LOG_I, @"Mouse connected!"); GCMouse* mouse = note.object; // Register for mouse events [self registerMouseCallbacks: mouse]; // Re-evaluate the on-screen control mode [self updateAutoOnScreenControlMode]; // Notify the delegate [self->_delegate mousePresenceChanged]; }]; _mouseDisconnectObserver = [[NSNotificationCenter defaultCenter] addObserverForName:GCMouseDidDisconnectNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { Log(LOG_I, @"Mouse disconnected!"); GCMouse* mouse = note.object; // Unregister for mouse events [self unregisterMouseCallbacks: mouse]; // Re-evaluate the on-screen control mode [self updateAutoOnScreenControlMode]; // Notify the delegate [self->_delegate mousePresenceChanged]; }]; _keyboardConnectObserver = [[NSNotificationCenter defaultCenter] addObserverForName:GCKeyboardDidConnectNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { Log(LOG_I, @"Keyboard connected!"); // Re-evaluate the on-screen control mode [self updateAutoOnScreenControlMode]; }]; _keyboardDisconnectObserver = [[NSNotificationCenter defaultCenter] addObserverForName:GCKeyboardDidDisconnectNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { Log(LOG_I, @"Keyboard disconnected!"); // Re-evaluate the on-screen control mode [self updateAutoOnScreenControlMode]; }]; //for(Controller* controller in _controllers) [self updateTimerStateForController:controller]; } _oscController = [[VoidController alloc] init]; [self initializeControllerHaptics:_oscController]; _gyroMode = AlwaysDevice; _controllerMouseEnabledFlag = false; _gyroEnabledFlag = false; oscProfileMan = [OSCProfilesManager sharedManager:CGRectZero]; [self updateCommonConfig:streamConfig]; for(VoidController* voidController in _voidControllers.allValues){ NSLog(@"stop external controller timer %@", voidController); [self stopTimerForController:voidController]; } [self updateTimerStateForController:_oscController]; [self assignControllers]; _shallDisableGyroHotSwitch = streamConfig.gyroMode == GyroModeOff && _voidControllers.allValues.count == 0; NSLog(@"shallDisableGyroHotSwitch %d", _shallDisableGyroHotSwitch); return self; } -(bool)externalControllersHaveGyro{ for(VoidController* voidController in _voidControllers.allValues){ if(voidController.hasGyroscope || voidController.hasAccelerometer) return true; } return false; } -(void)connectionEstablished { for (VoidController* voidController in _voidControllers.allValues) { [self updateFinished:voidController]; } if (_oscEnabled) { [self setButtonFlag:self->_oscController flags:A_FLAG]; [self updateFinished:self->_oscController]; [self clearButtonFlag:self->_oscController flags:A_FLAG]; [self updateFinished:self->_oscController]; } dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ self->_gyroMode = self->_streamConfig.gyroMode; [self applyGyroModeSetting]; }); } -(void)stopTimerForController:(VoidController* )voidController{ [self resetGyroInputForController:voidController]; if (@available(iOS 14.0, *)) { //NSLog(@"stop controller obj: %@, hasAcc %d, hasGyro %d", voidController, voidController.hasAccelerometer, voidController.hasGyroscope); if(voidController.hasAccelerometer){ [voidController.accelTimer invalidate]; voidController.accelTimer = nil; } if(voidController.hasGyroscope){ [voidController.gyroTimer invalidate]; voidController.gyroTimer = nil; } } } - (void)applyGyroModeSetting { // Stop all timers to ensure a clean slate before applying the new setting. [self stopTimerForAllControllers]; switch(_gyroMode) { case AlwaysController: // Activate timers only for physical controllers. for (VoidController* voidController in _voidControllers.allValues) { [self updateTimerStateForController:voidController]; } break; case GyroModeAuto: // Prefer physical controller gyros if they exist. if ([self externalControllersHaveGyro]) { for (VoidController* voidController in _voidControllers.allValues) { [self updateTimerStateForController:voidController]; } } else { // Otherwise, fall back to the device gyro. [self updateTimerStateForController:self->_oscController]; } break; case AlwaysDevice: // Always start the device gyro in this mode. [self updateTimerStateForController:self->_oscController]; break; case GyroModeOff: // Do nothing, all timers are already stopped. break; } } -(void) cleanup { [[NSNotificationCenter defaultCenter] removeObserver:_controllerConnectObserver]; [[NSNotificationCenter defaultCenter] removeObserver:_controllerDisconnectObserver]; [[NSNotificationCenter defaultCenter] removeObserver:_mouseConnectObserver]; [[NSNotificationCenter defaultCenter] removeObserver:_mouseDisconnectObserver]; [[NSNotificationCenter defaultCenter] removeObserver:_keyboardConnectObserver]; [[NSNotificationCenter defaultCenter] removeObserver:_keyboardDisconnectObserver]; _controllerConnectObserver = nil; _controllerDisconnectObserver = nil; _mouseConnectObserver = nil; _mouseDisconnectObserver = nil; _keyboardConnectObserver = nil; _keyboardDisconnectObserver = nil; _controllerNumbers = 0; [self stopTimerForController:_oscController]; for (VoidController* controller in [_voidControllers allValues]) { [self stopTimerForController:controller]; [self cleanupControllerHaptics:controller]; [self cleanupControllerMotion:controller]; [self cleanupControllerBattery:controller]; } [_voidControllers removeAllObjects]; #if !TARGET_OS_TV [self cleanupControllerMotion:_oscController]; [_oscController.motionManager stopDeviceMotionUpdates]; #endif for (GCController* controller in [GCController controllers]) { if ([ControllerSupport isSupportedGamepad:controller]) { [self unregisterControllerCallbacks:controller]; } } if (@available(iOS 14.0, tvOS 14.0, *)) { for (GCMouse* mouse in [GCMouse mice]) { [self unregisterMouseCallbacks:mouse]; } } [self stopDisplayLink]; } @end