From d4d5146f664a35ca58d8b31fb0839122f3a6e8ab Mon Sep 17 00:00:00 2001 From: TrueZhuanjia Date: Wed, 19 Nov 2025 14:35:56 +0800 Subject: [PATCH] controller->mouse demo --- VoidLink.xcodeproj/project.pbxproj | 4 + VoidLink/Database/DataManager.h | 5 + VoidLink/Input/ControllerSupport.m | 94 ++++++++++++++- VoidLink/Input/ControllerUtil.swift | 170 ++++++++++++++++++++++++++++ 4 files changed, 267 insertions(+), 6 deletions(-) create mode 100644 VoidLink/Input/ControllerUtil.swift diff --git a/VoidLink.xcodeproj/project.pbxproj b/VoidLink.xcodeproj/project.pbxproj index 66878c76..28712f87 100644 --- a/VoidLink.xcodeproj/project.pbxproj +++ b/VoidLink.xcodeproj/project.pbxproj @@ -24,6 +24,7 @@ 188973D32E386A670025E48C /* widgetTemplatesIPhone.bin in Resources */ = {isa = PBXBuildFile; fileRef = 188973D22E386A670025E48C /* widgetTemplatesIPhone.bin */; }; 18951D1D2EBB6D2300A86E12 /* TouchPadGestureHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18951D1C2EBB6D2300A86E12 /* TouchPadGestureHandler.swift */; }; 189AD29A2DE7446000E24B88 /* HostCollectionViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 189AD2992DE7446000E24B88 /* HostCollectionViewController.m */; }; + 18A765BD2ECD51FB004784AA /* ControllerUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A765BC2ECD51FB004784AA /* ControllerUtil.swift */; }; 18E4BCEA2E891895000A8657 /* MotionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18E4BCE92E891895000A8657 /* MotionHandler.swift */; }; 840751B32BF49C050096B657 /* CustomTapGestureRecognizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 840751B22BF49C050096B657 /* CustomTapGestureRecognizer.m */; }; 841BAC922DE35D6B00DA639E /* ThemeManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 841BAC912DE35D6B00DA639E /* ThemeManager.m */; }; @@ -180,6 +181,7 @@ 18951D1C2EBB6D2300A86E12 /* TouchPadGestureHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TouchPadGestureHandler.swift; sourceTree = ""; }; 189AD2962DE743B700E24B88 /* HostCollectionViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HostCollectionViewController.h; sourceTree = ""; }; 189AD2992DE7446000E24B88 /* HostCollectionViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = HostCollectionViewController.m; sourceTree = ""; }; + 18A765BC2ECD51FB004784AA /* ControllerUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControllerUtil.swift; sourceTree = ""; }; 18BF141D2E0D7A00009DEE4F /* VoidLink v1.0.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "VoidLink v1.0.xcdatamodel"; sourceTree = ""; }; 18E4BCE92E891895000A8657 /* MotionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MotionHandler.swift; sourceTree = ""; }; 693B3A9A218638CD00982F7B /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = ""; }; @@ -713,6 +715,7 @@ 988FCD40293B091B003050E2 /* KeyboardInputField.m */, 18416EA32EBB3DCE00AE0124 /* UITouchUtil.swift */, 18951D1C2EBB6D2300A86E12 /* TouchPadGestureHandler.swift */, + 18A765BC2ECD51FB004784AA /* ControllerUtil.swift */, 1850C8292E667E4000845308 /* MicHandler.swift */, 18E4BCE92E891895000A8657 /* MotionHandler.swift */, 84E1CDEB2C4F67D300547904 /* CommandManager.swift */, @@ -1038,6 +1041,7 @@ 97E21BC12E21882D000D5DCA /* imgui_widgets.cpp in Sources */, 97E21BC22E21882D000D5DCA /* imgui.cpp in Sources */, 97E21BC32E21882D000D5DCA /* imgui_draw.cpp in Sources */, + 18A765BD2ECD51FB004784AA /* ControllerUtil.swift in Sources */, 84E1CDEC2C4F67D300547904 /* CommandManager.swift in Sources */, 847D2FDB2DDA015F00937A62 /* MenuSectionView.m in Sources */, FB53E1431BE5DC4400CD6ECE /* IdManager.m in Sources */, diff --git a/VoidLink/Database/DataManager.h b/VoidLink/Database/DataManager.h index 8e9357dc..f3d956ee 100644 --- a/VoidLink/Database/DataManager.h +++ b/VoidLink/Database/DataManager.h @@ -55,6 +55,11 @@ typedef NS_ENUM(NSInteger, HapticEnginePreference) { RumbleOff }; +typedef NS_ENUM(NSInteger, ControllerToMouseStick) { + LeftStickToMouse, + RightStickToMouse, +}; + typedef NS_ENUM(NSInteger, FramePacingMode) { FramePacingModeOff, FramePacingModeLegacy, diff --git a/VoidLink/Input/ControllerSupport.m b/VoidLink/Input/ControllerSupport.m index c060332b..c35556e7 100644 --- a/VoidLink/Input/ControllerSupport.m +++ b/VoidLink/Input/ControllerSupport.m @@ -11,7 +11,7 @@ #import "ControllerSupport.h" #import "VoidController.h" - +#import "VoidLink-Swift.h" #import "OnScreenControls.h" #import "DataManager.h" @@ -50,6 +50,15 @@ static const double MOUSE_SPEED_DIVISOR = 1.25; float accumulatedScrollX; float accumulatedScrollY; + ControllerToMouseStick controllerToMouseStick; + bool _stickToMouseEnabled; + float stickToMouseInputX; + float stickToMouseInputY; + float stickToMouseExpo; + float stickToMouseVelocity; + CADisplayLink *_displayLink; + + OnScreenControls *_osc; VoidController *_oscController; NSMutableSet* _activeGCControllers; @@ -930,6 +939,32 @@ static const double MOUSE_SPEED_DIVISOR = 1.25; } } +double rc_expo(double x, double gamma) { + if (x == 0.0) return 0.0; + + double ax = fabs(x); + double y = pow(ax, gamma); + + 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); + // NSLog(@"mouseDeltaX %f, mouseDeltaY %f, %f, %ld", mouseDeltaX, mouseDeltaY, CACurrentMediaTime(), (long)_displayLink.preferredFramesPerSecond); + LiSendMouseMoveEvent(mouseDeltaX, -mouseDeltaY); +} + +- (void)stopDisplayLink { + [_displayLink invalidate]; + _displayLink = nil; +} + +- (void)displayLinkCallBack { + if(!_stickToMouseEnabled) return; + [self sendStickToMouseMoveEventWithStickX:stickToMouseInputX stickY:stickToMouseInputY expo:stickToMouseExpo]; +} + -(void) registerControllerCallbacks:(GCController*) controller { if (controller != NULL) { @@ -974,12 +1009,41 @@ static const double MOUSE_SPEED_DIVISOR = 1.25; } } - controller.extendedGamepad.valueChangedHandler = ^(GCExtendedGamepad *gamepad, GCControllerElement *element) { + [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]; + UPDATE_BUTTON_FLAG(voidController, buttonFlagId.intValue, button.pressed); + } + + 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]; + }]; + + /* + 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); @@ -1062,12 +1126,15 @@ static const double MOUSE_SPEED_DIVISOR = 1.25; index:1]; } } + + leftStickX = self->controllerToMouseStick != LeftStickToMouse ? gamepad.leftThumbstick.xAxis.value * 0x7FFE : 0; + leftStickY = self->controllerToMouseStick != LeftStickToMouse ? gamepad.leftThumbstick.yAxis.value * 0x7FFE : 0;; - leftStickX = gamepad.leftThumbstick.xAxis.value * 0x7FFE; - leftStickY = gamepad.leftThumbstick.yAxis.value * 0x7FFE; + rightStickX = self->controllerToMouseStick != RightStickToMouse ? gamepad.rightThumbstick.xAxis.value * 0x7FFE : 0; + rightStickY = self->controllerToMouseStick != RightStickToMouse ? gamepad.rightThumbstick.yAxis.value * 0x7FFE : 0; - rightStickX = gamepad.rightThumbstick.xAxis.value * 0x7FFE; - rightStickY = gamepad.rightThumbstick.yAxis.value * 0x7FFE; + 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; @@ -1077,6 +1144,7 @@ static const double MOUSE_SPEED_DIVISOR = 1.25; [self updateTriggers:voidController left:leftTrigger right:rightTrigger]; [self updateFinished:voidController]; }; + */ } } else { Log(LOG_W, @"Tried to register controller callbacks on NULL controller"); @@ -1416,6 +1484,18 @@ static const double MOUSE_SPEED_DIVISOR = 1.25; _oscEnabled = _oscEnabled || (OnScreenControlsLevel)[tempSettings.onscreenControls integerValue] != OnScreenControlsLevelOff || streamConfig.gyroMode != GyroModeOff; _gyroSensitivity = tempSettings.gyroSensitivity.floatValue; + + _stickToMouseEnabled = true; + controllerToMouseStick = LeftStickToMouse; + stickToMouseExpo = 1.1; + stickToMouseVelocity = 21*60/tempSettings.framerate.intValue; + stickToMouseInputX = 0; + stickToMouseInputY = 0; + [self stopDisplayLink]; + if(_stickToMouseEnabled){ + _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkCallBack)]; + [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; + } } - (void)resetGyroInputForController:(VoidController* )voidController{ @@ -1746,6 +1826,8 @@ static const double MOUSE_SPEED_DIVISOR = 1.25; [self unregisterMouseCallbacks:mouse]; } } + + [self stopDisplayLink]; } @end diff --git a/VoidLink/Input/ControllerUtil.swift b/VoidLink/Input/ControllerUtil.swift new file mode 100644 index 00000000..c633d011 --- /dev/null +++ b/VoidLink/Input/ControllerUtil.swift @@ -0,0 +1,170 @@ +// +// ControllerUtil.swift +// VoidLink +// +// Created by True砖家 on 2025/11/19. +// Copyright © 2025 True砖家 on Bilibili. All rights reserved. +// + + +import GameController + +@objc enum ControllerButton: Int { + // Face buttons + case a = 0x1000 + case b = 0x2000 + case x = 0x4000 + case y = 0x8000 + + // DPad + case dpadUp = 0x0001 + case dpadDown = 0x0002 + case dpadLeft = 0x0004 + case dpadRight = 0x0008 + + // Shoulder + case leftShoulder = 0x0100 + case rightShoulder = 0x0200 + + // Stick click + case leftStickButton = 0x0040 + case rightStickButton = 0x0080 + + // Menu/Back/Special + case menu = 0x0010 // PLAY_FLAG + case back = 0x0020 // BACK_FLAG + case special = 0x0400 + + // Extended buttons (Sunshine only) + case paddle1 = 0x010000 + case paddle2 = 0x020000 + case paddle3 = 0x040000 + case paddle4 = 0x080000 + case touchpad = 0x100000 + case misc = 0x200000 + + case leftTrigger, rightTrigger +} + + +@objc class ControllerUtil: NSObject { + + @objc static func listen( + controller: GCController, + swapABXY: Bool, + handler: @escaping (_ buttonDict: NSDictionary, + _ gamepad: GCExtendedGamepad, + _ element: GCControllerElement) -> Void + ) { + guard let gamepad = controller.extendedGamepad else { return } + + // 内部生成 map + var tempMap: [NSNumber: GCControllerButtonInput] = [:] + let swiftMap = buildMapping(for: controller, swapABXY: swapABXY) + for (button, input) in swiftMap { + tempMap[NSNumber(value: button.rawValue)] = input + } + let buttonDict = tempMap as NSDictionary + + // 单一 gamepad.valueChangedHandler + gamepad.valueChangedHandler = { gamepad, element in + handler(buttonDict, gamepad, element) + } + } + + + // MARK: - 构建按钮映射 + private static func buildMapping(for controller: GCController, swapABXY:Bool) + -> [ControllerButton : GCControllerButtonInput] + { + var result: [ControllerButton : GCControllerButtonInput] = [:] + + if let pad = controller.extendedGamepad { + // Face buttons + if swapABXY { + result[.a] = pad.buttonB + result[.b] = pad.buttonA + result[.x] = pad.buttonY + result[.y] = pad.buttonX + } + else{ + result[.a] = pad.buttonA + result[.b] = pad.buttonB + result[.x] = pad.buttonX + result[.y] = pad.buttonY + } + + // Shoulders & triggers + result[.leftShoulder] = pad.leftShoulder + result[.rightShoulder] = pad.rightShoulder + result[.leftTrigger] = pad.leftTrigger + result[.rightTrigger] = pad.rightTrigger + + // Stick buttons + if #available(iOS 12.1, *) { + result[.leftStickButton] = pad.leftThumbstickButton + result[.rightStickButton] = pad.rightThumbstickButton + } + + // DPad + result[.dpadUp] = pad.dpad.up + result[.dpadDown] = pad.dpad.down + result[.dpadLeft] = pad.dpad.left + result[.dpadRight] = pad.dpad.right + + // Menu / Options / Share + if #available(iOS 13.0, *) { + result[.menu] = pad.buttonMenu + if let options = pad.buttonOptions { result[.back] = options } + } + + if #available(iOS 14.0, tvOS 14.0, *) { + if let home = pad.buttonHome { + result[.special] = home + } + + if let controller = pad.controller { + let profile = controller.physicalInputProfile + if let paddle1 = profile.buttons[GCInputXboxPaddleOne] { + result[.paddle1] = paddle1 + } + if let paddle2 = profile.buttons[GCInputXboxPaddleTwo] { + result[.paddle2] = paddle2 + } + if let paddle3 = profile.buttons[GCInputXboxPaddleThree] { + result[.paddle3] = paddle3 + } + if let paddle4 = profile.buttons[GCInputXboxPaddleFour] { + result[.paddle4] = paddle4 + } + if let touchpadBtn = profile.buttons[GCInputDualShockTouchpadButton] { + result[.touchpad] = touchpadBtn + } + if #available(iOS 15.0, tvOS 15.0, *) { + if let share = profile.buttons[GCInputButtonShare] { + result[.misc] = share + } + } + } + } + } + + /* + if let micro = controller.microGamepad { + if #available(iOS 13.0, *) { + result[.menu] = micro.buttonMenu + result[.a] = micro.buttonA + result[.x] = micro.buttonX + result[.dpadUp] = micro.dpad.up + result[.dpadDown] = micro.dpad.down + result[.dpadLeft] = micro.dpad.left + result[.dpadRight] = micro.dpad.right + result[.menu] = micro.buttonMenu + } + }*/ + + return result + } + + +}