feat(apple): multi-controller support
Roll the pf-client-core slot pattern to the Apple client (Swift): - GamepadManager tracks all connected GCControllers, assigning each a stable lowest-free wire pad index + concrete type, emitting GamepadArrival on connect and GamepadRemove on disconnect (index freed for reuse on re-plug). - GamepadCapture binds every controller with per-controller Slot state (buttons/axes/fingers/motion), threading the pad index into flags on every event; GamepadWire/InputEvents carry the pad + the two new events. - GamepadFeedback + RumbleRenderer go per-pad (rumbleByPad, slots[pad]), routing rumble/HID back to the correct controller by wire index. - ContentView/Settings surface every forwarded controller. pad 0 => flags 0, so single-controller wire is byte-identical. Cannot build on the Linux dev box (no Swift toolchain / Apple frameworks); wire bytes hand-checked against input.rs and GamepadWireTests extended for multi-pad. CI apple.yml (swift build/test on macOS) is the compile gate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
// player-LED-bits → GCControllerPlayerIndex map. All pure functions.
|
||||
|
||||
import GameController
|
||||
import PunktfunkCore
|
||||
import XCTest
|
||||
|
||||
@testable import PunktfunkKit
|
||||
@@ -40,6 +41,43 @@ final class GamepadWireTests: XCTestCase {
|
||||
XCTAssertEqual(GamepadWire.axisRT, 5)
|
||||
}
|
||||
|
||||
func testPadIndexRidesFlagsOnEveryPerPadEvent() {
|
||||
// The wire pad index is the low byte of `flags` (punktfunk_core::input) on button + axis.
|
||||
let btn = PunktfunkInputEvent.gamepadButton(GamepadWire.a, down: true, pad: 3)
|
||||
XCTAssertEqual(btn.kind, UInt8(PUNKTFUNK_INPUT_KIND_GAMEPAD_BUTTON.rawValue))
|
||||
XCTAssertEqual(btn.code, GamepadWire.a)
|
||||
XCTAssertEqual(btn.x, 1)
|
||||
XCTAssertEqual(btn.flags, 3)
|
||||
let axis = PunktfunkInputEvent.gamepadAxis(GamepadWire.axisRT, value: 200, pad: 5)
|
||||
XCTAssertEqual(axis.kind, UInt8(PUNKTFUNK_INPUT_KIND_GAMEPAD_AXIS.rawValue))
|
||||
XCTAssertEqual(axis.code, GamepadWire.axisRT)
|
||||
XCTAssertEqual(axis.x, 200)
|
||||
XCTAssertEqual(axis.flags, 5)
|
||||
// Single-controller path stays byte-identical: pad 0 ⇒ flags 0, exactly as before.
|
||||
XCTAssertEqual(PunktfunkInputEvent.gamepadButton(GamepadWire.a, down: false, pad: 0).flags, 0)
|
||||
XCTAssertEqual(PunktfunkInputEvent.gamepadAxis(GamepadWire.axisLSX, value: 0, pad: 0).flags, 0)
|
||||
}
|
||||
|
||||
func testArrivalAndRemoveWireLayout() {
|
||||
// GamepadArrival (kind 14): code = the GamepadType wire byte, flags = pad index.
|
||||
let arrival = PunktfunkInputEvent.gamepadArrival(
|
||||
pref: PunktfunkConnection.GamepadType.dualSense.rawValue, pad: 2)
|
||||
XCTAssertEqual(arrival.kind, UInt8(PUNKTFUNK_INPUT_KIND_GAMEPAD_ARRIVAL.rawValue))
|
||||
XCTAssertEqual(arrival.code, PunktfunkConnection.GamepadType.dualSense.rawValue) // 2
|
||||
XCTAssertEqual(arrival.flags, 2)
|
||||
// The GamepadType raw values ARE the GamepadPref wire bytes the host resolves.
|
||||
XCTAssertEqual(PunktfunkConnection.GamepadType.xbox360.rawValue, 1)
|
||||
XCTAssertEqual(PunktfunkConnection.GamepadType.dualSense.rawValue, 2)
|
||||
XCTAssertEqual(PunktfunkConnection.GamepadType.xboxOne.rawValue, 3)
|
||||
XCTAssertEqual(PunktfunkConnection.GamepadType.dualShock4.rawValue, 4)
|
||||
// GamepadRemove (kind 13): flags = pad index (the core stamps the per-pad seq).
|
||||
let remove = PunktfunkInputEvent.gamepadRemove(pad: 7)
|
||||
XCTAssertEqual(remove.kind, UInt8(PUNKTFUNK_INPUT_KIND_GAMEPAD_REMOVE.rawValue))
|
||||
XCTAssertEqual(remove.flags, 7)
|
||||
// 16 addressable pads (punktfunk_core::input::MAX_PADS).
|
||||
XCTAssertEqual(GamepadWire.maxPads, 16)
|
||||
}
|
||||
|
||||
func testTouchpadConversionCorners() {
|
||||
// GC ±1 with +y up → wire 0...65535 with origin top-left, +y down.
|
||||
let topLeft = GamepadWire.touchpad(x: -1, y: 1)
|
||||
|
||||
Reference in New Issue
Block a user