gameSirG8MFi rumbler ready
This commit is contained in:
@@ -20,7 +20,7 @@
|
|||||||
1850C82A2E667E4000845308 /* MicHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1850C8292E667E4000845308 /* MicHandler.swift */; };
|
1850C82A2E667E4000845308 /* MicHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1850C8292E667E4000845308 /* MicHandler.swift */; };
|
||||||
18729EDE2E8BD68800101194 /* UUIDHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18729EDD2E8BD68800101194 /* UUIDHelper.swift */; };
|
18729EDE2E8BD68800101194 /* UUIDHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18729EDD2E8BD68800101194 /* UUIDHelper.swift */; };
|
||||||
18780CC72F10D66000B90483 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 18780CC62F10D66000B90483 /* StoreKit.framework */; };
|
18780CC72F10D66000B90483 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 18780CC62F10D66000B90483 /* StoreKit.framework */; };
|
||||||
A76B00032FFCC00100000003 /* GameSirG8MFiRumble.m in Sources */ = {isa = PBXBuildFile; fileRef = A76B00022FFCC00100000002 /* GameSirG8MFiRumble.m */; };
|
A76B00032FFCC00100000003 /* GameSirG8MFiRumbler.swift in Sources */ = {isa = PBXBuildFile; fileRef = A76B00022FFCC00100000002 /* GameSirG8MFiRumbler.swift */; };
|
||||||
A76B00052FFCC00100000005 /* ExternalAccessory.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A76B00042FFCC00100000004 /* ExternalAccessory.framework */; };
|
A76B00052FFCC00100000005 /* ExternalAccessory.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A76B00042FFCC00100000004 /* ExternalAccessory.framework */; };
|
||||||
18780CCA2F10D6CC00B90483 /* IAPManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18780CC92F10D6CC00B90483 /* IAPManager.swift */; };
|
18780CCA2F10D6CC00B90483 /* IAPManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18780CC92F10D6CC00B90483 /* IAPManager.swift */; };
|
||||||
187F4B282E04658A00EECC59 /* WidgetPanelStackView.m in Sources */ = {isa = PBXBuildFile; fileRef = 187F4B272E04658A00EECC59 /* WidgetPanelStackView.m */; };
|
187F4B282E04658A00EECC59 /* WidgetPanelStackView.m in Sources */ = {isa = PBXBuildFile; fileRef = 187F4B272E04658A00EECC59 /* WidgetPanelStackView.m */; };
|
||||||
@@ -202,8 +202,7 @@
|
|||||||
1850C8292E667E4000845308 /* MicHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MicHandler.swift; sourceTree = "<group>"; };
|
1850C8292E667E4000845308 /* MicHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MicHandler.swift; sourceTree = "<group>"; };
|
||||||
18729EDD2E8BD68800101194 /* UUIDHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UUIDHelper.swift; sourceTree = "<group>"; };
|
18729EDD2E8BD68800101194 /* UUIDHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UUIDHelper.swift; sourceTree = "<group>"; };
|
||||||
18780CC62F10D66000B90483 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; };
|
18780CC62F10D66000B90483 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; };
|
||||||
A76B00012FFCC00100000001 /* GameSirG8MFiRumble.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GameSirG8MFiRumble.h; sourceTree = "<group>"; };
|
A76B00022FFCC00100000002 /* GameSirG8MFiRumbler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GameSirG8MFiRumbler.swift; sourceTree = "<group>"; };
|
||||||
A76B00022FFCC00100000002 /* GameSirG8MFiRumble.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GameSirG8MFiRumble.m; sourceTree = "<group>"; };
|
|
||||||
A76B00042FFCC00100000004 /* ExternalAccessory.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ExternalAccessory.framework; path = System/Library/Frameworks/ExternalAccessory.framework; sourceTree = SDKROOT; };
|
A76B00042FFCC00100000004 /* ExternalAccessory.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ExternalAccessory.framework; path = System/Library/Frameworks/ExternalAccessory.framework; sourceTree = SDKROOT; };
|
||||||
18780CC92F10D6CC00B90483 /* IAPManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IAPManager.swift; sourceTree = "<group>"; };
|
18780CC92F10D6CC00B90483 /* IAPManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IAPManager.swift; sourceTree = "<group>"; };
|
||||||
187F4B232E04633100EECC59 /* WidgetPanelStackView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WidgetPanelStackView.h; sourceTree = "<group>"; };
|
187F4B232E04633100EECC59 /* WidgetPanelStackView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WidgetPanelStackView.h; sourceTree = "<group>"; };
|
||||||
@@ -757,8 +756,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
84351B052C29ACAF00A77989 /* CustomOSC */,
|
84351B052C29ACAF00A77989 /* CustomOSC */,
|
||||||
A76B00012FFCC00100000001 /* GameSirG8MFiRumble.h */,
|
A76B00022FFCC00100000002 /* GameSirG8MFiRumbler.swift */,
|
||||||
A76B00022FFCC00100000002 /* GameSirG8MFiRumble.m */,
|
|
||||||
FB89460A19F646E200339C8A /* ControllerSupport.h */,
|
FB89460A19F646E200339C8A /* ControllerSupport.h */,
|
||||||
FB89460B19F646E200339C8A /* ControllerSupport.m */,
|
FB89460B19F646E200339C8A /* ControllerSupport.m */,
|
||||||
FB89460C19F646E200339C8A /* StreamView.h */,
|
FB89460C19F646E200339C8A /* StreamView.h */,
|
||||||
@@ -1252,7 +1250,7 @@
|
|||||||
FBD3494319FC9C04002D2A60 /* AppAssetManager.m in Sources */,
|
FBD3494319FC9C04002D2A60 /* AppAssetManager.m in Sources */,
|
||||||
FB6549561A57907E001C8F39 /* DiscoveryWorker.m in Sources */,
|
FB6549561A57907E001C8F39 /* DiscoveryWorker.m in Sources */,
|
||||||
84351B0E2C29ACCB00A77989 /* LayoutOnScreenControls.m in Sources */,
|
84351B0E2C29ACCB00A77989 /* LayoutOnScreenControls.m in Sources */,
|
||||||
A76B00032FFCC00100000003 /* GameSirG8MFiRumble.m in Sources */,
|
A76B00032FFCC00100000003 /* GameSirG8MFiRumbler.swift in Sources */,
|
||||||
FB89462A19F646E200339C8A /* ControllerSupport.m in Sources */,
|
FB89462A19F646E200339C8A /* ControllerSupport.m in Sources */,
|
||||||
FB9AFD3D1A7E111600872C98 /* AppAssetResponse.m in Sources */,
|
FB9AFD3D1A7E111600872C98 /* AppAssetResponse.m in Sources */,
|
||||||
1888C1922E24C33100108730 /* AboutViewController.swift in Sources */,
|
1888C1922E24C33100108730 /* AboutViewController.swift in Sources */,
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
#import "ControllerSupport.h"
|
#import "ControllerSupport.h"
|
||||||
#import "GameSirG8MFiRumble.h"
|
|
||||||
#import "OnScreenControls.h"
|
#import "OnScreenControls.h"
|
||||||
#import "VoidController.h"
|
#import "VoidController.h"
|
||||||
#import "VoidLink-Swift.h"
|
#import "VoidLink-Swift.h"
|
||||||
@@ -120,7 +119,7 @@ static void ApplyAdaptiveTriggerEffect(GCDualSenseAdaptiveTrigger* trigger,
|
|||||||
OSCProfile* oscProfile;
|
OSCProfile* oscProfile;
|
||||||
OSCProfilesManager* oscProfileMan;
|
OSCProfilesManager* oscProfileMan;
|
||||||
#if !TARGET_OS_TV
|
#if !TARGET_OS_TV
|
||||||
GameSirG8MFiRumble *_gameSirG8MFiRumble;
|
GameSirG8MFiRumbler *_gameSirG8MFiRumbler;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#define EMULATING_SELECT 0x1
|
#define EMULATING_SELECT 0x1
|
||||||
@@ -159,6 +158,20 @@ static void ApplyAdaptiveTriggerEffect(GCDualSenseAdaptiveTrigger* trigger,
|
|||||||
|
|
||||||
#define MAX_MAGNITUDE(x, y) (abs(x) > abs(y) ? (x) : (y))
|
#define MAX_MAGNITUDE(x, y) (abs(x) > abs(y) ? (x) : (y))
|
||||||
|
|
||||||
|
-(void) applyPhysicalControllerRumble:(VoidController*)controller lowFreqMotor:(unsigned short)lowFreqMotor highFreqMotor:(unsigned short)highFreqMotor
|
||||||
|
{
|
||||||
|
#if !TARGET_OS_TV
|
||||||
|
if (controller.hardware == ControllerHardwareG8PlusMFi) {
|
||||||
|
NSLog(@"[G8Rumble] route native rumble low=%hu high=%hu", lowFreqMotor, highFreqMotor);
|
||||||
|
[_gameSirG8MFiRumbler setLowFrequencyMotor:lowFreqMotor highFrequencyMotor:highFreqMotor];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
[controller.lowFreqMotor setMotorAmplitude:lowFreqMotor];
|
||||||
|
[controller.highFreqMotor setMotorAmplitude:highFreqMotor];
|
||||||
|
}
|
||||||
|
|
||||||
-(void) rumble:(unsigned short)controllerNumber lowFreqMotor:(unsigned short)lowFreqMotor highFreqMotor:(unsigned short)highFreqMotor
|
-(void) rumble:(unsigned short)controllerNumber lowFreqMotor:(unsigned short)lowFreqMotor highFreqMotor:(unsigned short)highFreqMotor
|
||||||
{
|
{
|
||||||
VoidController* voidController = [_voidControllers objectForKey:[NSNumber numberWithInteger:controllerNumber]];
|
VoidController* voidController = [_voidControllers objectForKey:[NSNumber numberWithInteger:controllerNumber]];
|
||||||
@@ -178,48 +191,29 @@ static void ApplyAdaptiveTriggerEffect(GCDualSenseAdaptiveTrigger* trigger,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
#if !TARGET_OS_TV
|
NSLog(@"[G8Rumble] rumble request controller=%hu low=%hu high=%hu preference=%ld vendor=%@",
|
||||||
BOOL useGameSirG8MFiRumble = [_gameSirG8MFiRumble canHandleController:voidController.gamepad];
|
controllerNumber,
|
||||||
#endif
|
lowFreqMotor,
|
||||||
|
highFreqMotor,
|
||||||
|
(long)preference,
|
||||||
|
voidController.gamepad.vendorName);
|
||||||
|
|
||||||
// physical controller connected:
|
// physical controller connected:
|
||||||
switch (preference) {
|
switch (preference) {
|
||||||
case HapticEngineAuto:
|
case HapticEngineAuto:
|
||||||
#if !TARGET_OS_TV
|
|
||||||
if (useGameSirG8MFiRumble) {
|
|
||||||
[_gameSirG8MFiRumble setLowFrequencyMotor:lowFreqMotor highFrequencyMotor:highFreqMotor];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
// if controller has no haptic profile, it already falled bakc to device engine
|
// if controller has no haptic profile, it already falled bakc to device engine
|
||||||
[voidController.lowFreqMotor setMotorAmplitude:lowFreqMotor];
|
[self applyPhysicalControllerRumble:voidController lowFreqMotor:lowFreqMotor highFreqMotor:highFreqMotor];
|
||||||
[voidController.highFreqMotor setMotorAmplitude:highFreqMotor];
|
|
||||||
break;
|
break;
|
||||||
case RumbleDevice:
|
case RumbleDevice:
|
||||||
#if !TARGET_OS_TV
|
[self applyPhysicalControllerRumble:voidController lowFreqMotor:0 highFreqMotor:0];
|
||||||
if (useGameSirG8MFiRumble) {
|
|
||||||
[_gameSirG8MFiRumble setLowFrequencyMotor:0 highFrequencyMotor:0];
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
[_oscController.lowFreqMotor setMotorAmplitude:lowFreqMotor];
|
[_oscController.lowFreqMotor setMotorAmplitude:lowFreqMotor];
|
||||||
[_oscController.highFreqMotor setMotorAmplitude:highFreqMotor];
|
[_oscController.highFreqMotor setMotorAmplitude:highFreqMotor];
|
||||||
break;
|
break;
|
||||||
case LeftRightSwapped:
|
case LeftRightSwapped:
|
||||||
#if !TARGET_OS_TV
|
[self applyPhysicalControllerRumble:voidController lowFreqMotor:highFreqMotor highFreqMotor:lowFreqMotor];
|
||||||
if (useGameSirG8MFiRumble) {
|
|
||||||
[_gameSirG8MFiRumble setLowFrequencyMotor:highFreqMotor highFrequencyMotor:lowFreqMotor];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
[voidController.lowFreqMotor setMotorAmplitude:highFreqMotor];
|
|
||||||
[voidController.highFreqMotor setMotorAmplitude:lowFreqMotor];
|
|
||||||
break;
|
break;
|
||||||
case RumbleOff:
|
case RumbleOff:
|
||||||
#if !TARGET_OS_TV
|
[self applyPhysicalControllerRumble:voidController lowFreqMotor:0 highFreqMotor:0];
|
||||||
if (useGameSirG8MFiRumble) {
|
|
||||||
[_gameSirG8MFiRumble setLowFrequencyMotor:0 highFrequencyMotor:0];
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
@@ -1777,6 +1771,11 @@ double rc_expo(double x, double expo) {
|
|||||||
voidController.motionTypes = [[NSMutableSet alloc] init];
|
voidController.motionTypes = [[NSMutableSet alloc] init];
|
||||||
voidController.supportedEmulationFlags = EMULATING_SPECIAL | EMULATING_SELECT;
|
voidController.supportedEmulationFlags = EMULATING_SPECIAL | EMULATING_SELECT;
|
||||||
voidController.gamepad = controller;
|
voidController.gamepad = controller;
|
||||||
|
#if !TARGET_OS_TV
|
||||||
|
voidController.hardware = [_gameSirG8MFiRumbler isTargetController:controller] ? ControllerHardwareG8PlusMFi : ControllerHardwareGeneric;
|
||||||
|
#else
|
||||||
|
voidController.hardware = ControllerHardwareGeneric;
|
||||||
|
#endif
|
||||||
voidController.hasAccelerometer = NO;
|
voidController.hasAccelerometer = NO;
|
||||||
voidController.hasGyroscope = NO;
|
voidController.hasGyroscope = NO;
|
||||||
|
|
||||||
@@ -2036,7 +2035,7 @@ double rc_expo(double x, double expo) {
|
|||||||
[ControllerUtil.activeStreamingGCControllers removeAllObjects];
|
[ControllerUtil.activeStreamingGCControllers removeAllObjects];
|
||||||
_controllerNumbers = 0;
|
_controllerNumbers = 0;
|
||||||
#if !TARGET_OS_TV
|
#if !TARGET_OS_TV
|
||||||
_gameSirG8MFiRumble = [[GameSirG8MFiRumble alloc] init];
|
_gameSirG8MFiRumbler = [[GameSirG8MFiRumbler alloc] init];
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
_captureMouse = (streamConfig.localMousePointerMode == 0);
|
_captureMouse = (streamConfig.localMousePointerMode == 0);
|
||||||
@@ -2098,8 +2097,8 @@ double rc_expo(double x, double expo) {
|
|||||||
VoidController* voidController = [self->_voidControllers objectForKey:[NSNumber numberWithInteger:controller.playerIndex]];
|
VoidController* voidController = [self->_voidControllers objectForKey:[NSNumber numberWithInteger:controller.playerIndex]];
|
||||||
if (voidController) {
|
if (voidController) {
|
||||||
#if !TARGET_OS_TV
|
#if !TARGET_OS_TV
|
||||||
if ([self->_gameSirG8MFiRumble isTargetController:controller]) {
|
if ([self->_gameSirG8MFiRumbler isTargetController:controller]) {
|
||||||
[self->_gameSirG8MFiRumble stopAndClose];
|
[self->_gameSirG8MFiRumbler stopAndClose];
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
[self stopTimerForController:voidController];
|
[self stopTimerForController:voidController];
|
||||||
@@ -2306,7 +2305,7 @@ double rc_expo(double x, double expo) {
|
|||||||
{
|
{
|
||||||
[ControllerUtil stopAllDualSenseHaptics];
|
[ControllerUtil stopAllDualSenseHaptics];
|
||||||
#if !TARGET_OS_TV
|
#if !TARGET_OS_TV
|
||||||
[_gameSirG8MFiRumble invalidate];
|
[_gameSirG8MFiRumbler invalidate];
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if (VLSharedControllerSupport == self) {
|
if (VLSharedControllerSupport == self) {
|
||||||
|
|||||||
@@ -267,6 +267,11 @@ import UIKit
|
|||||||
case xboxAndPs = 0x00 // LI_CTYPE_UNKNOWN
|
case xboxAndPs = 0x00 // LI_CTYPE_UNKNOWN
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc enum ControllerHardware: UInt8 {
|
||||||
|
case generic
|
||||||
|
case g8PlusMFi
|
||||||
|
}
|
||||||
|
|
||||||
@objc protocol ControllerUtilDelegate: AnyObject {
|
@objc protocol ControllerUtilDelegate: AnyObject {
|
||||||
func isStreaming() -> Bool
|
func isStreaming() -> Bool
|
||||||
func isInAppView() -> Bool
|
func isInAppView() -> Bool
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
//
|
|
||||||
// GameSirG8MFiRumble.h
|
|
||||||
// VoidLink
|
|
||||||
//
|
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
#include <stdint.h>
|
|
||||||
|
|
||||||
@class GCController;
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
|
||||||
|
|
||||||
@interface GameSirG8MFiRumble : NSObject
|
|
||||||
|
|
||||||
- (BOOL)canHandleController:(GCController *)controller;
|
|
||||||
- (BOOL)isTargetController:(GCController *)controller;
|
|
||||||
- (void)setLowFrequencyMotor:(uint16_t)lowFrequencyMotor highFrequencyMotor:(uint16_t)highFrequencyMotor;
|
|
||||||
- (void)stopAndClose;
|
|
||||||
- (void)invalidate;
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
|
||||||
@@ -1,299 +0,0 @@
|
|||||||
//
|
|
||||||
// GameSirG8MFiRumble.m
|
|
||||||
// VoidLink
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "GameSirG8MFiRumble.h"
|
|
||||||
|
|
||||||
@import ExternalAccessory;
|
|
||||||
@import GameController;
|
|
||||||
@import UIKit;
|
|
||||||
|
|
||||||
static NSString *const GameSirG8MFiProtocol = @"com.xiaoji.M2boot";
|
|
||||||
static NSString *const GameSirManufacturer = @"GameSir";
|
|
||||||
static NSString *const GameSirG8MFiModel = @"G8+ MFi";
|
|
||||||
|
|
||||||
@interface GameSirG8MFiRumble () <NSStreamDelegate>
|
|
||||||
@end
|
|
||||||
|
|
||||||
@implementation GameSirG8MFiRumble {
|
|
||||||
EASession *_session;
|
|
||||||
NSData *_inFlightPacket;
|
|
||||||
NSUInteger _inFlightOffset;
|
|
||||||
NSData *_queuedPacket;
|
|
||||||
NSData *_lastSentPacket;
|
|
||||||
BOOL _appActive;
|
|
||||||
BOOL _invalidated;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (instancetype)init {
|
|
||||||
self = [super init];
|
|
||||||
if (self) {
|
|
||||||
_appActive = UIApplication.sharedApplication.applicationState == UIApplicationStateActive;
|
|
||||||
[NSNotificationCenter.defaultCenter addObserver:self
|
|
||||||
selector:@selector(applicationWillResignActive:)
|
|
||||||
name:UIApplicationWillResignActiveNotification
|
|
||||||
object:nil];
|
|
||||||
[NSNotificationCenter.defaultCenter addObserver:self
|
|
||||||
selector:@selector(applicationDidBecomeActive:)
|
|
||||||
name:UIApplicationDidBecomeActiveNotification
|
|
||||||
object:nil];
|
|
||||||
}
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)dealloc {
|
|
||||||
[NSNotificationCenter.defaultCenter removeObserver:self];
|
|
||||||
NSAssert(NSThread.isMainThread || _session == nil, @"Active GameSir sessions must be closed on the main thread before deallocation");
|
|
||||||
if (NSThread.isMainThread) {
|
|
||||||
[self stopAndCloseOnMainThread];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)applicationWillResignActive:(NSNotification *)notification {
|
|
||||||
_appActive = NO;
|
|
||||||
[self stopAndCloseOnMainThread];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)applicationDidBecomeActive:(NSNotification *)notification {
|
|
||||||
_appActive = YES;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (BOOL)isTargetController:(GCController *)controller {
|
|
||||||
NSString *vendorName = controller.vendorName;
|
|
||||||
if (vendorName.length == 0) {
|
|
||||||
return NO;
|
|
||||||
}
|
|
||||||
|
|
||||||
NSStringCompareOptions options = NSCaseInsensitiveSearch | NSDiacriticInsensitiveSearch;
|
|
||||||
return [vendorName rangeOfString:GameSirManufacturer options:options].location != NSNotFound &&
|
|
||||||
[vendorName rangeOfString:@"G8+" options:options].location != NSNotFound && [vendorName rangeOfString:@"MFi" options:options].location != NSNotFound;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (EAAccessory *)connectedAccessory {
|
|
||||||
for (EAAccessory *accessory in EAAccessoryManager.sharedAccessoryManager.connectedAccessories) {
|
|
||||||
if (!accessory.connected || ![accessory.protocolStrings containsObject:GameSirG8MFiProtocol]) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (accessory.manufacturer.length == 0 || accessory.modelNumber.length == 0 ||
|
|
||||||
[accessory.manufacturer caseInsensitiveCompare:GameSirManufacturer] != NSOrderedSame ||
|
|
||||||
[accessory.modelNumber caseInsensitiveCompare:GameSirG8MFiModel] != NSOrderedSame) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
return accessory;
|
|
||||||
}
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (BOOL)canHandleController:(GCController *)controller {
|
|
||||||
return [self isTargetController:controller] && [self connectedAccessory] != nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
+ (uint8_t)convertMotorAmplitude:(uint16_t)amplitude {
|
|
||||||
return (uint8_t)(((uint32_t)amplitude * 255u + 32767u) / 65535u);
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSData *)packetForLowFrequencyMotor:(uint16_t)lowFrequencyMotor highFrequencyMotor:(uint16_t)highFrequencyMotor {
|
|
||||||
const uint8_t packet[] = {
|
|
||||||
0x04,
|
|
||||||
[GameSirG8MFiRumble convertMotorAmplitude:lowFrequencyMotor],
|
|
||||||
0x01,
|
|
||||||
[GameSirG8MFiRumble convertMotorAmplitude:highFrequencyMotor],
|
|
||||||
0x01,
|
|
||||||
0x00,
|
|
||||||
0x00,
|
|
||||||
0x00,
|
|
||||||
0x00,
|
|
||||||
};
|
|
||||||
return [NSData dataWithBytes:packet length:sizeof(packet)];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (BOOL)openSessionIfNeeded {
|
|
||||||
if (_session != nil) {
|
|
||||||
return YES;
|
|
||||||
}
|
|
||||||
|
|
||||||
EAAccessory *accessory = [self connectedAccessory];
|
|
||||||
if (accessory == nil) {
|
|
||||||
return NO;
|
|
||||||
}
|
|
||||||
|
|
||||||
EASession *session = [[EASession alloc] initWithAccessory:accessory forProtocol:GameSirG8MFiProtocol];
|
|
||||||
if (session == nil || session.inputStream == nil || session.outputStream == nil) {
|
|
||||||
return NO;
|
|
||||||
}
|
|
||||||
|
|
||||||
_session = session;
|
|
||||||
session.inputStream.delegate = self;
|
|
||||||
session.outputStream.delegate = self;
|
|
||||||
[session.inputStream scheduleInRunLoop:NSRunLoop.mainRunLoop forMode:NSDefaultRunLoopMode];
|
|
||||||
[session.outputStream scheduleInRunLoop:NSRunLoop.mainRunLoop forMode:NSDefaultRunLoopMode];
|
|
||||||
[session.inputStream open];
|
|
||||||
[session.outputStream open];
|
|
||||||
return YES;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)setLowFrequencyMotor:(uint16_t)lowFrequencyMotor highFrequencyMotor:(uint16_t)highFrequencyMotor {
|
|
||||||
@synchronized(self) {
|
|
||||||
if (_invalidated) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NSData *packet = [self packetForLowFrequencyMotor:lowFrequencyMotor highFrequencyMotor:highFrequencyMotor];
|
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
|
||||||
@synchronized(self) {
|
|
||||||
if (self->_invalidated) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!self->_appActive) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
BOOL duplicateQueuedPacket = [self->_queuedPacket isEqualToData:packet];
|
|
||||||
BOOL duplicateInFlightPacket = self->_queuedPacket == nil && [self->_inFlightPacket isEqualToData:packet];
|
|
||||||
BOOL duplicateSentPacket = self->_queuedPacket == nil && self->_inFlightPacket == nil && [self->_lastSentPacket isEqualToData:packet];
|
|
||||||
if (!duplicateQueuedPacket && !duplicateInFlightPacket && !duplicateSentPacket) {
|
|
||||||
self->_queuedPacket = packet;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ([self openSessionIfNeeded]) {
|
|
||||||
[self flushOutput];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)flushOutput {
|
|
||||||
NSOutputStream *outputStream = _session.outputStream;
|
|
||||||
if (outputStream.streamStatus != NSStreamStatusOpen || !outputStream.hasSpaceAvailable) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
while (outputStream.hasSpaceAvailable) {
|
|
||||||
if (_inFlightPacket == nil) {
|
|
||||||
if (_queuedPacket == nil) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_inFlightPacket = _queuedPacket;
|
|
||||||
_queuedPacket = nil;
|
|
||||||
_inFlightOffset = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const uint8_t *bytes = _inFlightPacket.bytes;
|
|
||||||
NSUInteger remainingLength = _inFlightPacket.length - _inFlightOffset;
|
|
||||||
NSInteger written = [outputStream write:&bytes[_inFlightOffset] maxLength:remainingLength];
|
|
||||||
if (written <= 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_inFlightOffset += (NSUInteger)written;
|
|
||||||
if (_inFlightOffset == _inFlightPacket.length) {
|
|
||||||
_lastSentPacket = _inFlightPacket;
|
|
||||||
_inFlightPacket = nil;
|
|
||||||
_inFlightOffset = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)writeStopPacketBestEffort {
|
|
||||||
NSOutputStream *outputStream = _session.outputStream;
|
|
||||||
if (outputStream.streamStatus != NSStreamStatusOpen || !outputStream.hasSpaceAvailable) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_queuedPacket = nil;
|
|
||||||
[self flushOutput];
|
|
||||||
if (_inFlightPacket != nil) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
NSData *stopPacket = [self packetForLowFrequencyMotor:0 highFrequencyMotor:0];
|
|
||||||
NSUInteger offset = 0;
|
|
||||||
while (offset < stopPacket.length && outputStream.hasSpaceAvailable) {
|
|
||||||
const uint8_t *bytes = stopPacket.bytes;
|
|
||||||
NSInteger written = [outputStream write:&bytes[offset] maxLength:stopPacket.length - offset];
|
|
||||||
if (written <= 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
offset += (NSUInteger)written;
|
|
||||||
}
|
|
||||||
if (offset == stopPacket.length) {
|
|
||||||
_lastSentPacket = stopPacket;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)closeSession {
|
|
||||||
NSAssert(NSThread.isMainThread, @"GameSir sessions must be closed on the main thread");
|
|
||||||
if (_session != nil) {
|
|
||||||
[_session.inputStream close];
|
|
||||||
[_session.outputStream close];
|
|
||||||
[_session.inputStream removeFromRunLoop:NSRunLoop.mainRunLoop forMode:NSDefaultRunLoopMode];
|
|
||||||
[_session.outputStream removeFromRunLoop:NSRunLoop.mainRunLoop forMode:NSDefaultRunLoopMode];
|
|
||||||
_session.inputStream.delegate = nil;
|
|
||||||
_session.outputStream.delegate = nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
_session = nil;
|
|
||||||
_inFlightPacket = nil;
|
|
||||||
_inFlightOffset = 0;
|
|
||||||
_queuedPacket = nil;
|
|
||||||
_lastSentPacket = nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)stopAndCloseOnMainThread {
|
|
||||||
NSAssert(NSThread.isMainThread, @"GameSir sessions must be stopped on the main thread");
|
|
||||||
[self writeStopPacketBestEffort];
|
|
||||||
[self closeSession];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)stopAndClose {
|
|
||||||
if (NSThread.isMainThread) {
|
|
||||||
[self stopAndCloseOnMainThread];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
|
||||||
[self stopAndCloseOnMainThread];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)invalidate {
|
|
||||||
@synchronized(self) {
|
|
||||||
_invalidated = YES;
|
|
||||||
}
|
|
||||||
[self stopAndClose];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode {
|
|
||||||
if (stream != _session.inputStream && stream != _session.outputStream) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (eventCode) {
|
|
||||||
case NSStreamEventHasBytesAvailable: {
|
|
||||||
uint8_t buffer[64];
|
|
||||||
while (_session.inputStream.hasBytesAvailable) {
|
|
||||||
if ([_session.inputStream read:buffer maxLength:sizeof(buffer)] <= 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case NSStreamEventOpenCompleted:
|
|
||||||
case NSStreamEventHasSpaceAvailable:
|
|
||||||
if (stream == _session.outputStream) {
|
|
||||||
[self flushOutput];
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case NSStreamEventErrorOccurred:
|
|
||||||
case NSStreamEventEndEncountered:
|
|
||||||
[self writeStopPacketBestEffort];
|
|
||||||
[self closeSession];
|
|
||||||
break;
|
|
||||||
case NSStreamEventNone:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
|
||||||
@@ -0,0 +1,456 @@
|
|||||||
|
//
|
||||||
|
// GameSirG8MFiRumble.m -> GameSirG8MFiRumbler.swift
|
||||||
|
// VoidLink
|
||||||
|
//
|
||||||
|
// Created by chrisnch on 2026/08/22
|
||||||
|
// Copyright © 2026 chrisnch@github. All rights reserved.
|
||||||
|
//
|
||||||
|
// Modified by True砖家 on 2026/08/25
|
||||||
|
// Copyright © 2026 True砖家 on Bilibili. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import ExternalAccessory
|
||||||
|
import Foundation
|
||||||
|
import GameController
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
@objcMembers
|
||||||
|
final class GameSirG8MFiRumbler: NSObject, StreamDelegate {
|
||||||
|
private static let gameSirG8MFiProtocol = "com.xiaoji.M2boot"
|
||||||
|
private static let gameSirManufacturer = "GameSir"
|
||||||
|
private static let gameSirG8MFiModel = "G8+ MFi"
|
||||||
|
static var repeatInterval: TimeInterval = 0.167253
|
||||||
|
|
||||||
|
private var session: EASession?
|
||||||
|
private var inFlightPacket: Data?
|
||||||
|
private var inFlightOffset = 0
|
||||||
|
private var queuedPacket: Data?
|
||||||
|
private var lastSentPacket: Data?
|
||||||
|
private var repeatTimer: Timer?
|
||||||
|
private var repeatPacket: Data?
|
||||||
|
private var appActive: Bool
|
||||||
|
private var invalidated = false
|
||||||
|
|
||||||
|
override init() {
|
||||||
|
appActive = UIApplication.shared.applicationState == .active
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(applicationWillResignActive(_:)),
|
||||||
|
name: UIApplication.willResignActiveNotification,
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(applicationDidBecomeActive(_:)),
|
||||||
|
name: UIApplication.didBecomeActiveNotification,
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
NotificationCenter.default.removeObserver(self)
|
||||||
|
assert(Thread.isMainThread || session == nil, "Active GameSir sessions must be closed on the main thread before deallocation")
|
||||||
|
if Thread.isMainThread {
|
||||||
|
stopAndCloseOnMainThread()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func canHandleController(_ controller: GCController) -> Bool {
|
||||||
|
return isTargetController(controller) && connectedAccessory() != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isTargetController(_ controller: GCController) -> Bool {
|
||||||
|
guard let vendorName = controller.vendorName, !vendorName.isEmpty else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return vendorName.range(of: Self.gameSirManufacturer, options: [.caseInsensitive, .diacriticInsensitive]) != nil &&
|
||||||
|
vendorName.range(of: "G8+", options: [.caseInsensitive, .diacriticInsensitive]) != nil &&
|
||||||
|
vendorName.range(of: "MFi", options: [.caseInsensitive, .diacriticInsensitive]) != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(setLowFrequencyMotor:highFrequencyMotor:)
|
||||||
|
func setLowFrequencyMotor(_ lowFrequencyMotor: UInt16, highFrequencyMotor: UInt16) {
|
||||||
|
objc_sync_enter(self)
|
||||||
|
let isInvalidated = invalidated
|
||||||
|
objc_sync_exit(self)
|
||||||
|
if isInvalidated {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let packet = packet(lowFrequencyMotor: lowFrequencyMotor, highFrequencyMotor: highFrequencyMotor)
|
||||||
|
// NSLog("[G8Rumble] queue request low=%hu high=%hu packet=%@", lowFrequencyMotor, highFrequencyMotor, packet as NSData)
|
||||||
|
let shouldRepeat = lowFrequencyMotor != 0 || highFrequencyMotor != 0
|
||||||
|
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
objc_sync_enter(self)
|
||||||
|
let isInvalidated = self.invalidated
|
||||||
|
objc_sync_exit(self)
|
||||||
|
if isInvalidated {
|
||||||
|
// NSLog("[G8Rumble] drop packet because rumble handler is invalidated")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !self.appActive {
|
||||||
|
// NSLog("[G8Rumble] drop packet because app is inactive packet=%@", packet as NSData)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if shouldRepeat {
|
||||||
|
self.stopRepeatTimer(clearPacket: false)
|
||||||
|
self.repeatPacket = packet
|
||||||
|
self.startRepeatTimer()
|
||||||
|
} else {
|
||||||
|
self.stopRepeatTimer()
|
||||||
|
}
|
||||||
|
|
||||||
|
self.queuePacket(packet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopAndClose() {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
stopAndCloseOnMainThread()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
self?.stopAndCloseOnMainThread()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func invalidate() {
|
||||||
|
objc_sync_enter(self)
|
||||||
|
invalidated = true
|
||||||
|
objc_sync_exit(self)
|
||||||
|
stopAndClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func applicationWillResignActive(_ notification: Notification) {
|
||||||
|
appActive = false
|
||||||
|
stopAndCloseOnMainThread()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func applicationDidBecomeActive(_ notification: Notification) {
|
||||||
|
appActive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private func connectedAccessory() -> EAAccessory? {
|
||||||
|
var sawAccessory = false
|
||||||
|
for accessory in EAAccessoryManager.shared().connectedAccessories {
|
||||||
|
sawAccessory = true
|
||||||
|
/*
|
||||||
|
NSLog(
|
||||||
|
"[G8Rumble] EA accessory name=%@ manufacturer=%@ model=%@ protocols=%@ connected=%d",
|
||||||
|
accessory.name,
|
||||||
|
accessory.manufacturer,
|
||||||
|
accessory.modelNumber,
|
||||||
|
accessory.protocolStrings,
|
||||||
|
accessory.isConnected
|
||||||
|
) */
|
||||||
|
|
||||||
|
guard accessory.isConnected,
|
||||||
|
accessory.protocolStrings.contains(Self.gameSirG8MFiProtocol),
|
||||||
|
!accessory.manufacturer.isEmpty,
|
||||||
|
!accessory.modelNumber.isEmpty,
|
||||||
|
accessory.manufacturer.caseInsensitiveCompare(Self.gameSirManufacturer) == .orderedSame,
|
||||||
|
accessory.modelNumber.caseInsensitiveCompare(Self.gameSirG8MFiModel) == .orderedSame else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
NSLog("[G8Rumble] matched GameSir G8+ MFi accessory name=%@", accessory.name)
|
||||||
|
return accessory
|
||||||
|
}
|
||||||
|
|
||||||
|
if sawAccessory {
|
||||||
|
NSLog("[G8Rumble] no matching GameSir G8+ MFi accessory")
|
||||||
|
} else {
|
||||||
|
NSLog("[G8Rumble] no connected EA accessories")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func convertMotorAmplitude(_ amplitude: UInt16) -> UInt8 {
|
||||||
|
return UInt8((UInt32(amplitude) * 255 + 32767) / 65535)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func packet(lowFrequencyMotor: UInt16, highFrequencyMotor: UInt16) -> Data {
|
||||||
|
return Data([
|
||||||
|
0x04,
|
||||||
|
Self.convertMotorAmplitude(lowFrequencyMotor),
|
||||||
|
0x01,
|
||||||
|
Self.convertMotorAmplitude(highFrequencyMotor),
|
||||||
|
0x01,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
private func openSessionIfNeeded() -> Bool {
|
||||||
|
if session != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let accessory = connectedAccessory() else {
|
||||||
|
NSLog("[G8Rumble] cannot open session: accessory not found")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
NSLog("[G8Rumble] opening EASession protocol=%@ accessory=%@", Self.gameSirG8MFiProtocol, accessory.name)
|
||||||
|
guard let newSession = EASession(accessory: accessory, forProtocol: Self.gameSirG8MFiProtocol),
|
||||||
|
let inputStream = newSession.inputStream,
|
||||||
|
let outputStream = newSession.outputStream else {
|
||||||
|
NSLog("[G8Rumble] failed to create EASession")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
session = newSession
|
||||||
|
inputStream.delegate = self
|
||||||
|
outputStream.delegate = self
|
||||||
|
inputStream.schedule(in: .main, forMode: .default)
|
||||||
|
outputStream.schedule(in: .main, forMode: .default)
|
||||||
|
inputStream.open()
|
||||||
|
outputStream.open()
|
||||||
|
/*
|
||||||
|
NSLog(
|
||||||
|
"[G8Rumble] EASession opened inputStatus=%ld outputStatus=%ld",
|
||||||
|
inputStream.streamStatus.rawValue,
|
||||||
|
outputStream.streamStatus.rawValue
|
||||||
|
) */
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startRepeatTimer() {
|
||||||
|
let timer = Timer(timeInterval: Self.repeatInterval, repeats: true) { [weak self] _ in
|
||||||
|
self?.repeatCurrentPacket()
|
||||||
|
}
|
||||||
|
repeatTimer = timer
|
||||||
|
RunLoop.main.add(timer, forMode: .common)
|
||||||
|
// NSLog("[G8Rumble] repeat timer started interval=%.3f", Self.repeatInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func stopRepeatTimer(clearPacket: Bool = true) {
|
||||||
|
guard let timer = repeatTimer else {
|
||||||
|
if clearPacket {
|
||||||
|
repeatPacket = nil
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
timer.invalidate()
|
||||||
|
repeatTimer = nil
|
||||||
|
if clearPacket {
|
||||||
|
repeatPacket = nil
|
||||||
|
}
|
||||||
|
// NSLog("[G8Rumble] repeat timer stopped")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func repeatCurrentPacket() {
|
||||||
|
objc_sync_enter(self)
|
||||||
|
let isInvalidated = invalidated
|
||||||
|
objc_sync_exit(self)
|
||||||
|
if isInvalidated || !appActive {
|
||||||
|
stopRepeatTimer()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let packet = repeatPacket else {
|
||||||
|
stopRepeatTimer()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
queuePacket(packet)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func queuePacket(_ packet: Data) {
|
||||||
|
queuedPacket = packet
|
||||||
|
// NSLog("[G8Rumble] queued packet=%@", packet as NSData)
|
||||||
|
|
||||||
|
if openSessionIfNeeded() {
|
||||||
|
flushOutput()
|
||||||
|
} else {
|
||||||
|
// NSLog("[G8Rumble] packet queued but session is not open packet=%@", packet as NSData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func flushOutput() {
|
||||||
|
guard let outputStream = session?.outputStream else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard outputStream.streamStatus == .open, outputStream.hasSpaceAvailable else {
|
||||||
|
/*
|
||||||
|
NSLog(
|
||||||
|
"[G8Rumble] flush skipped status=%ld hasSpace=%d queued=%@ inFlight=%@",
|
||||||
|
outputStream.streamStatus.rawValue,
|
||||||
|
outputStream.hasSpaceAvailable,
|
||||||
|
(queuedPacket as NSData?) ?? NSNull(),
|
||||||
|
(inFlightPacket as NSData?) ?? NSNull()
|
||||||
|
) */
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
while outputStream.hasSpaceAvailable {
|
||||||
|
if inFlightPacket == nil {
|
||||||
|
guard let packet = queuedPacket else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
inFlightPacket = packet
|
||||||
|
queuedPacket = nil
|
||||||
|
inFlightOffset = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let packet = inFlightPacket else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let remainingLength = packet.count - inFlightOffset
|
||||||
|
let written = packet.withUnsafeBytes { rawBuffer -> Int in
|
||||||
|
guard let baseAddress = rawBuffer.bindMemory(to: UInt8.self).baseAddress else {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return outputStream.write(baseAddress.advanced(by: inFlightOffset), maxLength: remainingLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
if written <= 0 {
|
||||||
|
NSLog(
|
||||||
|
"[G8Rumble] write failed packet=%@ offset=%lu remaining=%lu written=%ld status=%ld error=%@",
|
||||||
|
packet as NSData,
|
||||||
|
UInt(inFlightOffset),
|
||||||
|
UInt(remainingLength),
|
||||||
|
written,
|
||||||
|
outputStream.streamStatus.rawValue,
|
||||||
|
outputStream.streamError as NSError? ?? NSNull()
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
NSLog(
|
||||||
|
"[G8Rumble] write packet=%@ offset=%lu remaining=%lu written=%ld status=%ld",
|
||||||
|
packet as NSData,
|
||||||
|
UInt(inFlightOffset),
|
||||||
|
UInt(remainingLength),
|
||||||
|
written,
|
||||||
|
outputStream.streamStatus.rawValue
|
||||||
|
) */
|
||||||
|
|
||||||
|
inFlightOffset += written
|
||||||
|
if inFlightOffset == packet.count {
|
||||||
|
// NSLog("[G8Rumble] packet sent %@", packet as NSData)
|
||||||
|
lastSentPacket = packet
|
||||||
|
inFlightPacket = nil
|
||||||
|
inFlightOffset = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func writeStopPacketBestEffort() {
|
||||||
|
guard let outputStream = session?.outputStream else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard outputStream.streamStatus == .open, outputStream.hasSpaceAvailable else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
queuedPacket = nil
|
||||||
|
flushOutput()
|
||||||
|
if inFlightPacket != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let stopPacket = packet(lowFrequencyMotor: 0, highFrequencyMotor: 0)
|
||||||
|
var offset = 0
|
||||||
|
while offset < stopPacket.count && outputStream.hasSpaceAvailable {
|
||||||
|
let written = stopPacket.withUnsafeBytes { rawBuffer -> Int in
|
||||||
|
guard let baseAddress = rawBuffer.bindMemory(to: UInt8.self).baseAddress else {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return outputStream.write(baseAddress.advanced(by: offset), maxLength: stopPacket.count - offset)
|
||||||
|
}
|
||||||
|
if written <= 0 {
|
||||||
|
NSLog(
|
||||||
|
"[G8Rumble] stop packet write failed offset=%lu written=%ld status=%ld error=%@",
|
||||||
|
UInt(offset),
|
||||||
|
written,
|
||||||
|
outputStream.streamStatus.rawValue,
|
||||||
|
outputStream.streamError as NSError? ?? NSNull()
|
||||||
|
)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
offset += written
|
||||||
|
}
|
||||||
|
if offset == stopPacket.count {
|
||||||
|
lastSentPacket = stopPacket
|
||||||
|
}
|
||||||
|
NSLog("[G8Rumble] stop packet written=%lu/%lu packet=%@", UInt(offset), UInt(stopPacket.count), stopPacket as NSData)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func closeSession() {
|
||||||
|
assert(Thread.isMainThread, "GameSir sessions must be closed on the main thread")
|
||||||
|
stopRepeatTimer()
|
||||||
|
if let session,
|
||||||
|
let inputStream = session.inputStream,
|
||||||
|
let outputStream = session.outputStream {
|
||||||
|
NSLog(
|
||||||
|
"[G8Rumble] closing EASession inputStatus=%ld outputStatus=%ld",
|
||||||
|
inputStream.streamStatus.rawValue,
|
||||||
|
outputStream.streamStatus.rawValue
|
||||||
|
)
|
||||||
|
inputStream.close()
|
||||||
|
outputStream.close()
|
||||||
|
inputStream.remove(from: .main, forMode: .default)
|
||||||
|
outputStream.remove(from: .main, forMode: .default)
|
||||||
|
inputStream.delegate = nil
|
||||||
|
outputStream.delegate = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
session = nil
|
||||||
|
inFlightPacket = nil
|
||||||
|
inFlightOffset = 0
|
||||||
|
queuedPacket = nil
|
||||||
|
lastSentPacket = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func stopAndCloseOnMainThread() {
|
||||||
|
assert(Thread.isMainThread, "GameSir sessions must be stopped on the main thread")
|
||||||
|
writeStopPacketBestEffort()
|
||||||
|
closeSession()
|
||||||
|
}
|
||||||
|
|
||||||
|
func stream(_ stream: Stream, handle eventCode: Stream.Event) {
|
||||||
|
guard let session,
|
||||||
|
let inputStream = session.inputStream,
|
||||||
|
let outputStream = session.outputStream else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard stream === inputStream || stream === outputStream else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch eventCode {
|
||||||
|
case .hasBytesAvailable:
|
||||||
|
var buffer = [UInt8](repeating: 0, count: 64)
|
||||||
|
while inputStream.hasBytesAvailable {
|
||||||
|
if inputStream.read(&buffer, maxLength: buffer.count) <= 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case .openCompleted, .hasSpaceAvailable:
|
||||||
|
if stream === outputStream {
|
||||||
|
flushOutput()
|
||||||
|
}
|
||||||
|
case .errorOccurred, .endEncountered:
|
||||||
|
writeStopPacketBestEffort()
|
||||||
|
closeSession()
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
#import "HapticContext.h"
|
#import "HapticContext.h"
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
@import GameController;
|
@import GameController;
|
||||||
@import CoreHaptics;
|
@import CoreHaptics;
|
||||||
@@ -14,6 +15,8 @@
|
|||||||
@import CoreMotion;
|
@import CoreMotion;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
typedef NS_ENUM(uint8_t, ControllerHardware);
|
||||||
|
|
||||||
@interface VoidController : NSObject
|
@interface VoidController : NSObject
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
@@ -69,6 +72,7 @@ typedef NS_ENUM(NSInteger, ControllerDeviceBatteryState) {
|
|||||||
@property(nonatomic, strong) NSMutableSet* _Nullable motionTypes;
|
@property(nonatomic, strong) NSMutableSet* _Nullable motionTypes;
|
||||||
@property(nonatomic, assign) uint16_t reportRateHz;
|
@property(nonatomic, assign) uint16_t reportRateHz;
|
||||||
@property(nonatomic, assign) uint16_t controllerNumber;
|
@property(nonatomic, assign) uint16_t controllerNumber;
|
||||||
|
@property(nonatomic, assign) ControllerHardware hardware;
|
||||||
@property(nonatomic, assign) BOOL hasAccelerometer;
|
@property(nonatomic, assign) BOOL hasAccelerometer;
|
||||||
@property(nonatomic, assign) BOOL hasGyroscope;
|
@property(nonatomic, assign) BOOL hasGyroscope;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user