Options overlay: native gamepad support (Select / D-pad / A / B / Start).

Controller-only machines (Steam Deck) had no way to open or navigate the
F10 options overlay -- it was keyboard+mouse only. Now:

- Select (BACK) rising edge toggles the overlay, tracked across both the
  open and closed poll paths (padSelectPrev) so a held button cannot
  re-toggle on the transition. BACK is read nowhere else in the port.
- While open, D-pad or left stick up/down moves the selection, A/X step
  forward (the Enter equivalent), B/Y step back, Start closes. The gamepad
  bits are OR-ed into the existing keyboard edge logic, so repeat/clamp/
  scroll behaviour is identical; input.c already swallows pad 0 while the
  overlay is open, so nothing reaches the game.
- inputPadButton/inputPadAxis accessors expose raw SDL pad state to the
  overlay (unaffected by our logic-layer swallow).

Smoke-tested headless with GE_OPTIONSOVERLAY=1 and no pad attached.
This commit is contained in:
James Dansereau committed 2026-09-13 18:40:52 -04:00
1 parent 97ba1e9b07
commit 72af96cf0d
3 files changed
+63 -7

No files matched your search

+6
View File
@@ -55,6 +55,12 @@ int inputReleaseCapture(void);
* owns the mouse. Safe to call every poll. */
void inputSuspendForOverlay(void);
/* Raw gamepad state for controller idx (the F10 options overlay's gamepad
* navigation). Unaffected by the overlay's pad-swallow, which happens at our
* logic layer, not SDL's. No pad open -> 0. */
int inputPadButton(int idx, SDL_GameControllerButton b);
short inputPadAxis(int idx, SDL_GameControllerAxis a);
/* Queue a mouse-wheel weapon-cycle input (one short A-button press). Sign is
* ignored -- GE only cycles forward on a bare A edge. */
void inputPostWheel(int notches);
+32 -3
View File
@@ -275,6 +275,7 @@ static int connectedMask = 0x1; /* controller 0 always present */
static SDL_GameController *pads[MAX_PADS];
static int padShoulderPrev[MAX_PADS]; /* LB/RB edge state for weapon cycling */
static int padSelectPrev = 0; /* Select (BACK) edge: overlay toggle */
static int mouseEnabled = 1;
static int mouseGrabbed = 1; /* released while the window is unfocused */
@@ -748,10 +749,17 @@ unsigned inputComputePad(int idx, signed char *stick_x, signed char *stick_y)
}
/* F10 options overlay: while it is open, controller 0 is fully swallowed
* (neutral pad, no stick) and the nav keys / wheel drive the overlay
* instead. Mirrors the WI-1 "cursor free in a stage -> withhold input"
* pattern. Controllers 1-3 are untouched. */
* (neutral pad, no stick) and the nav keys / wheel / gamepad drive the
* overlay instead. Mirrors the WI-1 "cursor free in a stage -> withhold
* input" pattern. Controllers 1-3 are untouched. */
if (idx == 0 && optionsOverlayIsOpen()) {
/* Select closes the overlay. padSelectPrev is tracked on this path
* and the open path below alike, so a button held across the
* transition cannot immediately re-toggle it. */
int selNow = pads[0] ? SDL_GameControllerGetButton(pads[0],
SDL_CONTROLLER_BUTTON_BACK) : 0;
if (selNow && !padSelectPrev) optionsOverlayToggle();
padSelectPrev = selNow;
optionsOverlayHandleInput();
if (stick_x) *stick_x = 0;
if (stick_y) *stick_y = 0;
@@ -1158,6 +1166,15 @@ unsigned inputComputePad(int idx, signed char *stick_x, signed char *stick_y)
button |= GE_CONT_LEFT;
if (SDL_GameControllerGetButton(pad, SDL_CONTROLLER_BUTTON_DPAD_RIGHT))
button |= GE_CONT_RIGHT;
/* Select (BACK) opens the F10 options overlay -- the gamepad
* equivalent of the F10 key for controller-only machines (Steam
* Deck). The game never reads BACK, so nothing is withheld. */
{
int selNow = SDL_GameControllerGetButton(pad, SDL_CONTROLLER_BUTTON_BACK);
if (selNow && !padSelectPrev) optionsOverlayToggle();
padSelectPrev = selNow;
}
}
if (sx > STICK_MAX) sx = STICK_MAX;
@@ -1256,6 +1273,18 @@ int inputReleaseCapture(void)
* stage) would persist and the mouse UI would be unusable. Force the cursor
* free + visible every poll; the normal reconcile resumes once the overlay
* closes and the early-return no longer fires. */
int inputPadButton(int idx, SDL_GameControllerButton b)
{
if (idx < 0 || idx >= MAX_PADS || !pads[idx]) return 0;
return SDL_GameControllerGetButton(pads[idx], b);
}
short inputPadAxis(int idx, SDL_GameControllerAxis a)
{
if (idx < 0 || idx >= MAX_PADS || !pads[idx]) return 0;
return SDL_GameControllerGetAxis(pads[idx], a);
}
void inputSuspendForOverlay(void)
{
if (mouseGrabbed) {
+25 -4
View File
@@ -565,11 +565,32 @@ void optionsOverlayHandleInput(void)
int lmb = (mb & SDL_BUTTON(SDL_BUTTON_LEFT)) ? 1 : 0;
int rmb = (mb & SDL_BUTTON(SDL_BUTTON_RIGHT)) ? 1 : 0;
int up = ks[SDL_SCANCODE_UP] || ks[SDL_SCANCODE_KP_8];
int dn = ks[SDL_SCANCODE_DOWN] || ks[SDL_SCANCODE_KP_2];
int lf = ks[SDL_SCANCODE_LEFT] || ks[SDL_SCANCODE_KP_4];
/* Gamepad navigation (controller-only machines, e.g. Steam Deck): the
* D-pad or left stick moves the selection; A/X step forward (the Enter
* equivalent), B/Y step back; Start closes. OR-ed into the same edge
* logic as the keyboard, so repeat/clamp/scroll behaviour is identical.
* input.c swallows the pad while we are open, so none of this reaches
* the game. (Select also closes -- handled in input.c's toggle, which
* runs before this one.) */
int gUp = inputPadButton(0, SDL_CONTROLLER_BUTTON_DPAD_UP)
|| inputPadAxis(0, SDL_CONTROLLER_AXIS_LEFTY) < -12000;
int gDn = inputPadButton(0, SDL_CONTROLLER_BUTTON_DPAD_DOWN)
|| inputPadAxis(0, SDL_CONTROLLER_AXIS_LEFTY) > 12000;
int up = ks[SDL_SCANCODE_UP] || ks[SDL_SCANCODE_KP_8] || gUp;
int dn = ks[SDL_SCANCODE_DOWN] || ks[SDL_SCANCODE_KP_2] || gDn;
int lf = ks[SDL_SCANCODE_LEFT] || ks[SDL_SCANCODE_KP_4]
|| inputPadButton(0, SDL_CONTROLLER_BUTTON_B)
|| inputPadButton(0, SDL_CONTROLLER_BUTTON_Y);
int rt = ks[SDL_SCANCODE_RIGHT] || ks[SDL_SCANCODE_KP_6]
|| ks[SDL_SCANCODE_RETURN] || ks[SDL_SCANCODE_KP_ENTER];
|| ks[SDL_SCANCODE_RETURN] || ks[SDL_SCANCODE_KP_ENTER]
|| inputPadButton(0, SDL_CONTROLLER_BUTTON_A)
|| inputPadButton(0, SDL_CONTROLLER_BUTTON_X);
static int prevStart = 0; /* not reset while closed: a Start held across
close must not re-close on the next open */
int startNow = inputPadButton(0, SDL_CONTROLLER_BUTTON_START);
if (startNow && !prevStart) optionsOverlayToggle(); /* Start closes */
prevStart = startNow;
/* ---- keyboard / D-pad nav (clamped at the ends; scroll follows) ---- */
if (up && !prevUp && s_sel > 0) s_sel--;