From 72af96cf0dcf851a02ae4def86266685cecf4605 Mon Sep 17 00:00:00 2001 From: James Dansereau <106631954+jkdansereau@users.noreply.github.com> Date: Sun, 13 Sep 2026 18:40:52 -0400 Subject: [PATCH] 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. --- port/include/input.h | 6 ++++++ port/src/input.c | 35 ++++++++++++++++++++++++++++++++--- port/src/optionsoverlay.c | 29 +++++++++++++++++++++++++---- 3 files changed, 63 insertions(+), 7 deletions(-) diff --git a/port/include/input.h b/port/include/input.h index 52f448cb..27fa4ab2 100644 --- a/port/include/input.h +++ b/port/include/input.h @@ -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); diff --git a/port/src/input.c b/port/src/input.c index 3d87cc8c..3d8b361b 100644 --- a/port/src/input.c +++ b/port/src/input.c @@ -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) { diff --git a/port/src/optionsoverlay.c b/port/src/optionsoverlay.c index 79f2c1e8..b7627025 100644 --- a/port/src/optionsoverlay.c +++ b/port/src/optionsoverlay.c @@ -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--;