Merge pull request #22723 from hrydgard/ios-generic-controllers

iOS: Accept controllers without thumbsticks
This commit is contained in:
Henrik Rydgård authored and GitHub committed 2026-10-22 12:03:07 +02:00
commit bc785fa07c
2 files changed
+114 -113

No files matched your search

+1 -1
View File
@@ -17,7 +17,7 @@
bool InitController(GCController *controller);
void ShutdownController(GCController *controller);
// Returns false unless the controller is a Siri Remote (a micro gamepad profile and no extended one).
// Returns false unless the controller is a Siri Remote (a micro gamepad profile, no extended one and no B button).
bool InitSiriRemote(GCController *controller);
struct TouchTracker {
+113 -112
View File
@@ -28,132 +28,129 @@ static void analogTriggerPressed(InputAxis axis, float value) {
NativeAxis(&axisInput, 1);
}
static void bindButton(GCControllerButtonInput *input, InputKeyCode keyCode) {
// Optional buttons are nil on controllers that don't have them.
if (input == nil) {
return;
}
input.valueChangedHandler = ^(GCControllerButtonInput *button, float value, BOOL pressed) {
controllerButtonPressed(pressed, keyCode);
};
}
static void bindTrigger(GCControllerButtonInput *trigger, InputAxis axis) {
if (trigger == nil) {
return;
}
trigger.valueChangedHandler = ^(GCControllerButtonInput *button, float value, BOOL pressed) {
analogTriggerPressed(axis, value);
};
}
static void bindDpad(GCControllerDirectionPad *dpad) {
if (dpad == nil) {
return;
}
bindButton(dpad.up, NKCODE_DPAD_UP);
bindButton(dpad.down, NKCODE_DPAD_DOWN);
bindButton(dpad.left, NKCODE_DPAD_LEFT);
bindButton(dpad.right, NKCODE_DPAD_RIGHT);
}
static void bindStick(GCControllerDirectionPad *stick, InputAxis xAxis, InputAxis yAxis) {
if (stick == nil) {
return;
}
stick.xAxis.valueChangedHandler = ^(GCControllerAxisInput *axis, float value) {
AxisInput axisInput;
axisInput.deviceId = DEVICE_ID_PAD_0;
axisInput.axisId = xAxis;
axisInput.value = value;
NativeAxis(&axisInput, 1);
};
stick.yAxis.valueChangedHandler = ^(GCControllerAxisInput *axis, float value) {
AxisInput axisInput;
axisInput.deviceId = DEVICE_ID_PAD_0;
axisInput.axisId = yAxis;
axisInput.value = -value;
NativeAxis(&axisInput, 1);
};
}
// Newer iOS versions (17.4+) also have GCInputDirectionalDpad as a name for it.
static GCControllerDirectionPad *findDpad(GCPhysicalInputProfile *profile) {
GCControllerDirectionPad *dpad = profile.dpads[GCInputDirectionPad];
if (dpad == nil) {
if (@available(iOS 17.4, tvOS 17.4, *)) {
dpad = profile.dpads[GCInputDirectionalDpad];
}
}
return dpad;
}
bool InitController(GCController *controller) {
GCExtendedGamepad *extendedProfile = controller.extendedGamepad;
if (extendedProfile == nil) {
GCPhysicalInputProfile *profile = controller.physicalInputProfile;
// Controllers without thumbsticks (like the 8BitDo FlipPad) have no extended profile, only the
// physical one. Take those if they at least look like a gamepad.
NSDictionary<NSString *, GCControllerButtonInput *> *buttons = profile.buttons;
NSDictionary<NSString *, GCControllerDirectionPad *> *dpads = profile.dpads;
if (extendedProfile == nil && (buttons[GCInputButtonA] == nil || findDpad(profile) == nil)) {
INFO_LOG(Log::System, "Ignoring controller without a gamepad layout: %s", [controller.vendorName ?: @"(unnamed)" UTF8String]);
return false;
}
for (GCControllerElement* element in controller.physicalInputProfile.allElements) {
for (GCControllerElement* element in profile.allElements) {
element.preferredSystemGestureState = GCSystemGestureStateDisabled;
}
const std::string controllerName = [controller.vendorName ?: @"Gamepad" UTF8String];
KeyMap::NotifyPadConnected(DEVICE_ID_PAD_0, controllerName);
extendedProfile.buttonA.valueChangedHandler = ^(GCControllerButtonInput *button, float value, BOOL pressed) {
controllerButtonPressed(pressed, NKCODE_BUTTON_2); // Cross
};
extendedProfile.buttonB.valueChangedHandler = ^(GCControllerButtonInput *button, float value, BOOL pressed) {
controllerButtonPressed(pressed, NKCODE_BUTTON_3); // Circle
};
extendedProfile.buttonX.valueChangedHandler = ^(GCControllerButtonInput *button, float value, BOOL pressed) {
controllerButtonPressed(pressed, NKCODE_BUTTON_4); // Square
};
extendedProfile.buttonY.valueChangedHandler = ^(GCControllerButtonInput *button, float value, BOOL pressed) {
controllerButtonPressed(pressed, NKCODE_BUTTON_1); // Triangle
};
extendedProfile.leftShoulder.valueChangedHandler = ^(GCControllerButtonInput *button, float value, BOOL pressed) {
controllerButtonPressed(pressed, NKCODE_BUTTON_L1); // LTrigger
};
extendedProfile.rightShoulder.valueChangedHandler = ^(GCControllerButtonInput *button, float value, BOOL pressed) {
controllerButtonPressed(pressed, NKCODE_BUTTON_R1); // RTrigger
};
extendedProfile.dpad.up.valueChangedHandler = ^(GCControllerButtonInput *button, float value, BOOL pressed) {
controllerButtonPressed(pressed, NKCODE_DPAD_UP);
};
extendedProfile.dpad.down.valueChangedHandler = ^(GCControllerButtonInput *button, float value, BOOL pressed) {
controllerButtonPressed(pressed, NKCODE_DPAD_DOWN);
};
extendedProfile.dpad.left.valueChangedHandler = ^(GCControllerButtonInput *button, float value, BOOL pressed) {
controllerButtonPressed(pressed, NKCODE_DPAD_LEFT);
};
extendedProfile.dpad.right.valueChangedHandler = ^(GCControllerButtonInput *button, float value, BOOL pressed) {
controllerButtonPressed(pressed, NKCODE_DPAD_RIGHT);
};
extendedProfile.leftTrigger.valueChangedHandler = ^(GCControllerButtonInput *button, float value, BOOL pressed) {
INFO_LOG(Log::System, "ltrigger: %f %d", value, (int)pressed);
analogTriggerPressed(JOYSTICK_AXIS_LTRIGGER, value);
};
extendedProfile.rightTrigger.valueChangedHandler = ^(GCControllerButtonInput *button, float value, BOOL pressed) {
INFO_LOG(Log::System, "rtrigger: %f %d", value, (int)pressed);
analogTriggerPressed(JOYSTICK_AXIS_RTRIGGER, value);
};
// The optional buttons are nil on controllers that don't have them.
if (extendedProfile.leftThumbstickButton != nil) {
extendedProfile.leftThumbstickButton.valueChangedHandler = ^(GCControllerButtonInput *button, float value, BOOL pressed) {
controllerButtonPressed(pressed, NKCODE_BUTTON_THUMBL);
};
}
if (extendedProfile.rightThumbstickButton != nil) {
extendedProfile.rightThumbstickButton.valueChangedHandler = ^(GCControllerButtonInput *button, float value, BOOL pressed) {
controllerButtonPressed(pressed, NKCODE_BUTTON_THUMBR);
};
if (extendedProfile != nil) {
bindButton(extendedProfile.buttonA, NKCODE_BUTTON_2); // Cross
bindButton(extendedProfile.buttonB, NKCODE_BUTTON_3); // Circle
bindButton(extendedProfile.buttonX, NKCODE_BUTTON_4); // Square
bindButton(extendedProfile.buttonY, NKCODE_BUTTON_1); // Triangle
bindButton(extendedProfile.leftShoulder, NKCODE_BUTTON_L1); // LTrigger
bindButton(extendedProfile.rightShoulder, NKCODE_BUTTON_R1); // RTrigger
bindDpad(extendedProfile.dpad);
bindTrigger(extendedProfile.leftTrigger, JOYSTICK_AXIS_LTRIGGER);
bindTrigger(extendedProfile.rightTrigger, JOYSTICK_AXIS_RTRIGGER);
bindButton(extendedProfile.leftThumbstickButton, NKCODE_BUTTON_THUMBL);
bindButton(extendedProfile.rightThumbstickButton, NKCODE_BUTTON_THUMBR);
bindButton(extendedProfile.buttonOptions, NKCODE_BUTTON_SELECT);
bindButton(extendedProfile.buttonMenu, NKCODE_BUTTON_START);
bindButton(extendedProfile.buttonHome, NKCODE_HOME);
bindStick(extendedProfile.leftThumbstick, JOYSTICK_AXIS_X, JOYSTICK_AXIS_Y);
// Map right thumbstick as another analog stick, particularly useful for controllers
// like the DualShock 3/4 when connected to an iOS device
bindStick(extendedProfile.rightThumbstick, JOYSTICK_AXIS_Z, JOYSTICK_AXIS_RZ);
return true;
}
if (extendedProfile.buttonOptions != nil) {
extendedProfile.buttonOptions.valueChangedHandler = ^(GCControllerButtonInput *button, float value, BOOL pressed) {
controllerButtonPressed(pressed, NKCODE_BUTTON_SELECT);
};
}
if (extendedProfile.buttonMenu != nil) {
extendedProfile.buttonMenu.valueChangedHandler = ^(GCControllerButtonInput *button, float value, BOOL pressed) {
controllerButtonPressed(pressed, NKCODE_BUTTON_START);
};
}
if (extendedProfile.buttonHome != nil) {
extendedProfile.buttonHome.valueChangedHandler = ^(GCControllerButtonInput *button, float value, BOOL pressed) {
controllerButtonPressed(pressed, NKCODE_HOME);
};
}
extendedProfile.leftThumbstick.xAxis.valueChangedHandler = ^(GCControllerAxisInput *axis, float value) {
AxisInput axisInput;
axisInput.deviceId = DEVICE_ID_PAD_0;
axisInput.axisId = JOYSTICK_AXIS_X;
axisInput.value = value;
NativeAxis(&axisInput, 1);
};
extendedProfile.leftThumbstick.yAxis.valueChangedHandler = ^(GCControllerAxisInput *axis, float value) {
AxisInput axisInput;
axisInput.deviceId = DEVICE_ID_PAD_0;
axisInput.axisId = JOYSTICK_AXIS_Y;
axisInput.value = -value;
NativeAxis(&axisInput, 1);
};
// Map right thumbstick as another analog stick, particularly useful for controllers
// like the DualShock 3/4 when connected to an iOS device
extendedProfile.rightThumbstick.xAxis.valueChangedHandler = ^(GCControllerAxisInput *axis, float value) {
AxisInput axisInput;
axisInput.deviceId = DEVICE_ID_PAD_0;
axisInput.axisId = JOYSTICK_AXIS_Z;
axisInput.value = value;
NativeAxis(&axisInput, 1);
};
extendedProfile.rightThumbstick.yAxis.valueChangedHandler = ^(GCControllerAxisInput *axis, float value) {
AxisInput axisInput;
axisInput.deviceId = DEVICE_ID_PAD_0;
axisInput.axisId = JOYSTICK_AXIS_RZ;
axisInput.value = -value;
NativeAxis(&axisInput, 1);
};
// The same layout, looked up by element name. Anything the controller lacks is nil.
NSMutableArray<NSString *> *names = [NSMutableArray arrayWithArray:buttons.allKeys];
[names addObjectsFromArray:dpads.allKeys];
INFO_LOG(Log::System, "Controller %s has no extended profile, using its elements: %s", controllerName.c_str(), [[names componentsJoinedByString:@", "] UTF8String]);
bindButton(buttons[GCInputButtonA], NKCODE_BUTTON_2);
bindButton(buttons[GCInputButtonB], NKCODE_BUTTON_3);
bindButton(buttons[GCInputButtonX], NKCODE_BUTTON_4);
bindButton(buttons[GCInputButtonY], NKCODE_BUTTON_1);
bindButton(buttons[GCInputLeftShoulder], NKCODE_BUTTON_L1);
bindButton(buttons[GCInputRightShoulder], NKCODE_BUTTON_R1);
bindDpad(findDpad(profile));
bindTrigger(buttons[GCInputLeftTrigger], JOYSTICK_AXIS_LTRIGGER);
bindTrigger(buttons[GCInputRightTrigger], JOYSTICK_AXIS_RTRIGGER);
bindButton(buttons[GCInputLeftThumbstickButton], NKCODE_BUTTON_THUMBL);
bindButton(buttons[GCInputRightThumbstickButton], NKCODE_BUTTON_THUMBR);
bindButton(buttons[GCInputButtonOptions], NKCODE_BUTTON_SELECT);
bindButton(buttons[GCInputButtonMenu], NKCODE_BUTTON_START);
bindButton(buttons[GCInputButtonHome], NKCODE_HOME);
bindStick(dpads[GCInputLeftThumbstick], JOYSTICK_AXIS_X, JOYSTICK_AXIS_Y);
bindStick(dpads[GCInputRightThumbstick], JOYSTICK_AXIS_Z, JOYSTICK_AXIS_RZ);
return true;
}
@@ -251,6 +248,10 @@ bool InitSiriRemote(GCController *controller) {
if (micro == nil || controller.extendedGamepad != nil) {
return false;
}
// A gamepad without thumbsticks can have a micro profile too. Remotes have no B button.
if (controller.physicalInputProfile.buttons[GCInputButtonB] != nil) {
return false;
}
micro.allowsRotation = NO;