Skip to content

Commit b35a13d

Browse files
committed
fix: route 8BitDo Micro d-pad as d-pad
1 parent 15e1f1d commit b35a13d

2 files changed

Lines changed: 45 additions & 15 deletions

File tree

XboxControllerMapper/XboxControllerMapper/Services/Controller/ControllerService.swift

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -735,7 +735,7 @@ class ControllerService: ObservableObject {
735735

736736
/// Identifies the small 8BitDo pads from their SDL/HID product names
737737
/// ("8BitDo Zero 2", "8BitDo Micro", "8BitDo Lite 2", "8BitDo Lite SE").
738-
static func eightBitDoMinimapModel(forControllerName name: String) -> EightBitDoMinimapModel? {
738+
nonisolated static func eightBitDoMinimapModel(forControllerName name: String) -> EightBitDoMinimapModel? {
739739
let lowered = name.lowercased()
740740
guard lowered.contains("8bitdo") else { return nil }
741741
if lowered.contains("zero") { return .zero2 }
@@ -745,10 +745,21 @@ class ControllerService: ObservableObject {
745745
return nil
746746
}
747747

748-
static func eightBitDoMinimapModel(vendorName: String?, productCategory: String) -> EightBitDoMinimapModel? {
748+
nonisolated static func eightBitDoMinimapModel(vendorName: String?, productCategory: String) -> EightBitDoMinimapModel? {
749749
eightBitDoMinimapModel(forControllerName: "\(vendorName ?? "") \(productCategory)")
750750
}
751751

752+
nonisolated static func isSticklessEightBitDoModel(vendorName: String?, productCategory: String) -> Bool {
753+
eightBitDoMinimapModel(vendorName: vendorName, productCategory: productCategory)?.isStickless == true
754+
}
755+
756+
nonisolated static func shouldUsePhysicalDirectionPadAsLeftStickFallback(
757+
vendorName: String?,
758+
productCategory: String
759+
) -> Bool {
760+
!isSticklessEightBitDoModel(vendorName: vendorName, productCategory: productCategory)
761+
}
762+
752763
private func startEightBitDoHIDMonitoringIfNeeded(for controller: GCController, reason: String) {
753764
guard Self.eightBitDoMinimapModel(
754765
vendorName: controller.vendorName,
@@ -2128,17 +2139,14 @@ class ControllerService: ObservableObject {
21282139
]
21292140

21302141
// Stickless 8BitDo pads (Zero 2 / Micro in D-input mode) have no real
2131-
// analog stick — their physical d-pad IS the directional input. The
2132-
// Micro exposes it as a "Direction Pad" (with digital sub-buttons),
2133-
// which we must NOT bind as .dpad* buttons here: that would fire d-pad
2134-
// actions AND (via the thumbstick fallback below) drive the mouse —
2135-
// the double-input. Instead we route the d-pad to the left stick so
2136-
// the left-stick mode (Mouse by default, or the D-Pad mode) governs
2137-
// it, exactly like the Zero 2 (which exposes its d-pad as a thumbstick).
2138-
let isSticklessEightBitDo = Self.eightBitDoMinimapModel(
2142+
// analog stick. When macOS exposes the physical d-pad as a
2143+
// `Direction Pad`, bind it as real .dpad* buttons and do not also use it
2144+
// as the left-stick fallback. The mapping canvas labels this control as
2145+
// a D-pad, and Android-mode Micro reports digital D-pad directions here.
2146+
let useDirectionPadAsLeftStickFallback = Self.shouldUsePhysicalDirectionPadAsLeftStickFallback(
21392147
vendorName: controller.vendorName,
21402148
productCategory: controller.productCategory
2141-
)?.isStickless == true
2149+
)
21422150

21432151
var boundCount = 0
21442152
for (inputName, controllerButton) in buttonMap {
@@ -2148,9 +2156,8 @@ class ControllerService: ObservableObject {
21482156
}
21492157
}
21502158

2151-
// D-pad (skip the direct button binding for stickless 8BitDo pads —
2152-
// their d-pad flows through the left stick instead, see above).
2153-
if !isSticklessEightBitDo, let dpad = profile.dpads[GCInputDirectionPad] {
2159+
// D-pad.
2160+
if let dpad = profile.dpads[GCInputDirectionPad] {
21542161
bindButton(dpad.up, to: .dpadUp, from: controller)
21552162
bindButton(dpad.down, to: .dpadDown, from: controller)
21562163
bindButton(dpad.left, to: .dpadLeft, from: controller)
@@ -2166,7 +2173,7 @@ class ControllerService: ObservableObject {
21662173
service.updateLeftStick(x: x, y: y)
21672174
}
21682175
}
2169-
} else if let dpad = profile.dpads[GCInputDirectionPad] {
2176+
} else if useDirectionPadAsLeftStickFallback, let dpad = profile.dpads[GCInputDirectionPad] {
21702177
// Fallback: Joy-Con stick may be exposed as a D-pad rather than a thumbstick.
21712178
// Use it for mouse movement so the user gets analog-like cursor control.
21722179
dpad.valueChangedHandler = { [weak self, weak controller] _, xValue, yValue in

XboxControllerMapper/XboxControllerMapperTests/BugFixTests.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,29 @@ final class ControllerDisconnectStateResetTests: XCTestCase {
173173
XCTAssertEqual(model, .lite2)
174174
}
175175

176+
func testEightBitDoMicroPhysicalDPadIsSticklessDPadNotLeftStickFallback() {
177+
XCTAssertTrue(ControllerService.isSticklessEightBitDoModel(
178+
vendorName: "HID",
179+
productCategory: "8BitDo Micro gamepad"
180+
))
181+
XCTAssertTrue(ControllerService.isSticklessEightBitDoModel(
182+
vendorName: nil,
183+
productCategory: "8BitDo Micro Controller"
184+
))
185+
XCTAssertFalse(ControllerService.shouldUsePhysicalDirectionPadAsLeftStickFallback(
186+
vendorName: "HID",
187+
productCategory: "8BitDo Micro gamepad"
188+
))
189+
XCTAssertFalse(ControllerService.isSticklessEightBitDoModel(
190+
vendorName: "HID",
191+
productCategory: "8BitDo Lite 2"
192+
))
193+
XCTAssertTrue(ControllerService.shouldUsePhysicalDirectionPadAsLeftStickFallback(
194+
vendorName: "HID",
195+
productCategory: "8BitDo Lite 2"
196+
))
197+
}
198+
176199
func testTriggerResetExistsInDisconnectHandler() {
177200
// Verify the actual disconnect handler code resets triggers by checking
178201
// that the production code includes the trigger reset lines.

0 commit comments

Comments
 (0)