223 lines
7.9 KiB
Swift
223 lines
7.9 KiB
Swift
//
|
|
// 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 touchpadButton = 0x100000
|
|
case misc = 0x200000
|
|
|
|
case leftTrigger = 0x400000
|
|
case rightTrigger = 0x800000
|
|
|
|
case null = 0xFFFFFF
|
|
}
|
|
|
|
|
|
@objc class ControllerUtil: NSObject {
|
|
|
|
static private let stickMaxOffset:CGFloat = 0x7FFE
|
|
|
|
@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[.touchpadButton] = touchpadBtn
|
|
}
|
|
if #available(iOS 15.0, tvOS 15.0, *) {
|
|
if let share = profile.buttons[GCInputButtonShare] {
|
|
result[.misc] = share
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
@objc static func string(for button: ControllerButton) -> String {
|
|
switch button {
|
|
case .a: return "A"
|
|
case .b: return "B"
|
|
case .x: return "X"
|
|
case .y: return "Y"
|
|
|
|
case .dpadUp: return SwiftLocalizationHelper.localizedString(forKey: "Up")
|
|
case .dpadDown: return SwiftLocalizationHelper.localizedString(forKey: "Down")
|
|
case .dpadLeft: return SwiftLocalizationHelper.localizedString(forKey: "Left")
|
|
case .dpadRight: return SwiftLocalizationHelper.localizedString(forKey: "Right")
|
|
|
|
case .leftShoulder: return "LB"
|
|
case .rightShoulder: return "RB"
|
|
|
|
case .leftStickButton: return "LS"
|
|
case .rightStickButton: return "RS"
|
|
|
|
case .menu: return "Menu"
|
|
case .back: return "Back"
|
|
case .special: return "Home"
|
|
|
|
case .paddle1: return SwiftLocalizationHelper.localizedString(forKey: "Paddle1")
|
|
case .paddle2: return SwiftLocalizationHelper.localizedString(forKey: "Paddle2")
|
|
case .paddle3: return SwiftLocalizationHelper.localizedString(forKey: "Paddle3")
|
|
case .paddle4: return SwiftLocalizationHelper.localizedString(forKey: "Paddle4")
|
|
case .touchpadButton: return SwiftLocalizationHelper.localizedString(forKey: "Touch button")
|
|
case .misc: return "Misc"
|
|
|
|
case .leftTrigger: return "LT"
|
|
case .rightTrigger: return "RT"
|
|
|
|
case .null: return SwiftLocalizationHelper.localizedString(forKey: "Null")
|
|
|
|
default: return "UNKNOWN"
|
|
}
|
|
}
|
|
|
|
@objc static func compensated(offsetVector: CGVector, minOffset: CGFloat, circulate:Bool=false) -> CGVector{
|
|
let vectorHypot = hypot(offsetVector.dx, offsetVector.dy)
|
|
guard vectorHypot > 0 else {return CGVector(dx: 0, dy: 0)}
|
|
let targetHypot = minOffset + (stickMaxOffset-minOffset)*(vectorHypot/stickMaxOffset)
|
|
var compensatedX = targetHypot * (offsetVector.dx/vectorHypot)
|
|
var compensatedY = targetHypot * (offsetVector.dy/vectorHypot)
|
|
|
|
if circulate {
|
|
return circulated(offsetVector: CGVector(dx: compensatedX, dy: compensatedY))
|
|
}
|
|
else {
|
|
compensatedX = max(min(compensatedX, stickMaxOffset),-stickMaxOffset)
|
|
compensatedY = max(min(compensatedY, stickMaxOffset),-stickMaxOffset)
|
|
return CGVector(dx: compensatedX, dy: compensatedY)
|
|
}
|
|
}
|
|
|
|
@objc static func circulated(offsetVector: CGVector) -> CGVector{
|
|
let vectorHypot = hypot(offsetVector.dx, offsetVector.dy)
|
|
guard vectorHypot > 0 else {return CGVector(dx: 0, dy: 0)}
|
|
let targetHypot = min(vectorHypot, stickMaxOffset)
|
|
let circulatedX = targetHypot*(offsetVector.dx/vectorHypot)
|
|
let circulatedY = targetHypot*(offsetVector.dy/vectorHypot)
|
|
return CGVector(dx: circulatedX, dy: circulatedY)
|
|
}
|
|
}
|