245 lines
9.0 KiB
Swift
245 lines
9.0 KiB
Swift
import GameController
|
|
|
|
/// Manages GCController connections and input, translating events into the
|
|
/// compact wire-format arrays expected by the Dart side.
|
|
class GCControllerManager {
|
|
|
|
// MARK: - Properties
|
|
|
|
private let streamHandler: GamepadStreamHandler
|
|
|
|
/// Maps a GCController instance to its stable int gamepad ID.
|
|
private var controllerIds: [ObjectIdentifier: Int] = [:]
|
|
|
|
/// Counter for generating unique IDs.
|
|
private var nextId: Int = 0
|
|
|
|
/// Notification observers so we can remove them on dispose.
|
|
private var connectObserver: NSObjectProtocol?
|
|
private var disconnectObserver: NSObjectProtocol?
|
|
|
|
// MARK: - Init
|
|
|
|
init(streamHandler: GamepadStreamHandler) {
|
|
self.streamHandler = streamHandler
|
|
}
|
|
|
|
// MARK: - Lifecycle
|
|
|
|
/// Begin listening for GCController connect/disconnect notifications and
|
|
/// register input handlers on any controllers that are already connected.
|
|
func startObserving() {
|
|
connectObserver = NotificationCenter.default.addObserver(
|
|
forName: .GCControllerDidConnect,
|
|
object: nil,
|
|
queue: .main
|
|
) { [weak self] notification in
|
|
guard let self = self,
|
|
let controller = notification.object as? GCController else { return }
|
|
self.controllerDidConnect(controller)
|
|
}
|
|
|
|
disconnectObserver = NotificationCenter.default.addObserver(
|
|
forName: .GCControllerDidDisconnect,
|
|
object: nil,
|
|
queue: .main
|
|
) { [weak self] notification in
|
|
guard let self = self,
|
|
let controller = notification.object as? GCController else { return }
|
|
self.controllerDidDisconnect(controller)
|
|
}
|
|
|
|
// Handle controllers that were already connected before we started
|
|
// observing (e.g. wired controllers).
|
|
for controller in GCController.controllers() {
|
|
controllerDidConnect(controller)
|
|
}
|
|
}
|
|
|
|
/// Remove all observers and clear input handlers.
|
|
func stopObserving() {
|
|
if let observer = connectObserver {
|
|
NotificationCenter.default.removeObserver(observer)
|
|
connectObserver = nil
|
|
}
|
|
if let observer = disconnectObserver {
|
|
NotificationCenter.default.removeObserver(observer)
|
|
disconnectObserver = nil
|
|
}
|
|
|
|
// Remove value changed handlers from all tracked controllers.
|
|
for controller in GCController.controllers() {
|
|
controller.extendedGamepad?.valueChangedHandler = nil
|
|
}
|
|
|
|
controllerIds.removeAll()
|
|
}
|
|
|
|
// MARK: - listGamepads
|
|
|
|
/// Returns an array of dictionaries describing currently connected
|
|
/// controllers, suitable for returning over a MethodChannel.
|
|
func listGamepads() -> [[String: Any]] {
|
|
var result: [[String: Any]] = []
|
|
for controller in GCController.controllers() {
|
|
let key = ObjectIdentifier(controller)
|
|
guard let gamepadId = controllerIds[key] else { continue }
|
|
|
|
var info: [String: Any] = [
|
|
"id": gamepadId,
|
|
"name": controllerName(controller),
|
|
]
|
|
|
|
if let vendorId = vendorId(for: controller) {
|
|
info["vendorId"] = vendorId
|
|
}
|
|
if let productId = productId(for: controller) {
|
|
info["productId"] = productId
|
|
}
|
|
|
|
result.append(info)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// MARK: - Connection events
|
|
|
|
private func controllerDidConnect(_ controller: GCController) {
|
|
let gamepadId = assignId(for: controller)
|
|
|
|
// Register input handlers for the extended gamepad profile.
|
|
registerInputHandlers(for: controller, gamepadId: gamepadId)
|
|
|
|
// Wire format: [0, gamepadId, timestamp, connected, name, vendorId, productId]
|
|
let event: [Any] = [
|
|
0,
|
|
gamepadId,
|
|
currentTimestamp(),
|
|
true,
|
|
controllerName(controller),
|
|
vendorId(for: controller) as Any,
|
|
productId(for: controller) as Any,
|
|
]
|
|
streamHandler.send(event: event)
|
|
}
|
|
|
|
private func controllerDidDisconnect(_ controller: GCController) {
|
|
let key = ObjectIdentifier(controller)
|
|
guard let gamepadId = controllerIds.removeValue(forKey: key) else { return }
|
|
|
|
controller.extendedGamepad?.valueChangedHandler = nil
|
|
|
|
// Wire format: [0, gamepadId, timestamp, connected, name, vendorId, productId]
|
|
let event: [Any] = [
|
|
0,
|
|
gamepadId,
|
|
currentTimestamp(),
|
|
false,
|
|
controllerName(controller),
|
|
vendorId(for: controller) as Any,
|
|
productId(for: controller) as Any,
|
|
]
|
|
streamHandler.send(event: event)
|
|
}
|
|
|
|
// MARK: - Input handlers
|
|
|
|
private func registerInputHandlers(for controller: GCController, gamepadId: Int) {
|
|
guard let extendedGamepad = controller.extendedGamepad else { return }
|
|
|
|
extendedGamepad.valueChangedHandler = { [weak self] (gamepad, element) in
|
|
guard let self = self else { return }
|
|
self.handleValueChanged(gamepad: gamepad, element: element, gamepadId: gamepadId)
|
|
}
|
|
}
|
|
|
|
private func handleValueChanged(gamepad: GCExtendedGamepad,
|
|
element: GCControllerElement,
|
|
gamepadId: Int) {
|
|
let timestamp = currentTimestamp()
|
|
|
|
// D-pad fires as a single GCControllerDirectionPad element, not as
|
|
// individual direction buttons. Expand it into 4 button events.
|
|
if element === gamepad.dpad {
|
|
sendButtonEvent(gamepadId: gamepadId, index: 12,
|
|
pressed: gamepad.dpad.up.isPressed,
|
|
value: Double(gamepad.dpad.up.value), timestamp: timestamp)
|
|
sendButtonEvent(gamepadId: gamepadId, index: 13,
|
|
pressed: gamepad.dpad.down.isPressed,
|
|
value: Double(gamepad.dpad.down.value), timestamp: timestamp)
|
|
sendButtonEvent(gamepadId: gamepadId, index: 14,
|
|
pressed: gamepad.dpad.left.isPressed,
|
|
value: Double(gamepad.dpad.left.value), timestamp: timestamp)
|
|
sendButtonEvent(gamepadId: gamepadId, index: 15,
|
|
pressed: gamepad.dpad.right.isPressed,
|
|
value: Double(gamepad.dpad.right.value), timestamp: timestamp)
|
|
return
|
|
}
|
|
|
|
// Check buttons.
|
|
if let (index, button) = ButtonMapping.buttonIndex(for: element, in: gamepad) {
|
|
sendButtonEvent(gamepadId: gamepadId, index: index,
|
|
pressed: button.isPressed,
|
|
value: Double(button.value), timestamp: timestamp)
|
|
return
|
|
}
|
|
|
|
// Check axes (may produce multiple events when a full thumbstick fires).
|
|
let axisEvents = ButtonMapping.axisIndices(for: element, in: gamepad)
|
|
for (index, axisValue) in axisEvents {
|
|
// Wire format: [2, gamepadId, timestamp, axisIndex, value]
|
|
streamHandler.send(event: [2, gamepadId, timestamp, index, axisValue])
|
|
}
|
|
}
|
|
|
|
private func sendButtonEvent(gamepadId: Int, index: Int,
|
|
pressed: Bool, value: Double, timestamp: Int) {
|
|
// Wire format: [1, gamepadId, timestamp, buttonIndex, pressed, value]
|
|
streamHandler.send(event: [1, gamepadId, timestamp, index, pressed, value])
|
|
}
|
|
|
|
// MARK: - Helpers
|
|
|
|
private func assignId(for controller: GCController) -> Int {
|
|
let key = ObjectIdentifier(controller)
|
|
if let existing = controllerIds[key] {
|
|
return existing
|
|
}
|
|
let id = nextId
|
|
nextId += 1
|
|
controllerIds[key] = id
|
|
return id
|
|
}
|
|
|
|
private func controllerName(_ controller: GCController) -> String {
|
|
let category = controller.productCategory
|
|
if !category.isEmpty {
|
|
return category
|
|
}
|
|
return controller.vendorName ?? "Unknown Controller"
|
|
}
|
|
|
|
/// Returns the USB vendor ID for the controller, if available.
|
|
///
|
|
/// GCController does not directly expose numeric vendor/product IDs
|
|
/// through the public API. We attempt to obtain them from the
|
|
/// underlying physical device description on iOS 16+.
|
|
private func vendorId(for controller: GCController) -> Int? {
|
|
// GCController does not publicly expose numeric vendor IDs on iOS.
|
|
// Return nil; the Dart side treats this as optional.
|
|
return nil
|
|
}
|
|
|
|
/// Returns the USB product ID for the controller, if available.
|
|
private func productId(for controller: GCController) -> Int? {
|
|
// GCController does not publicly expose numeric product IDs on iOS.
|
|
// Return nil; the Dart side treats this as optional.
|
|
return nil
|
|
}
|
|
|
|
/// Returns the current timestamp in milliseconds since epoch.
|
|
private func currentTimestamp() -> Int {
|
|
return Int(Date().timeIntervalSince1970 * 1000)
|
|
}
|
|
}
|