From 40ef53e39539c6418f85fac5e962e7ddcce66a2a Mon Sep 17 00:00:00 2001 From: James Dansereau <106631954+jkdansereau@users.noreply.github.com> Date: Sun, 27 Sep 2026 16:29:18 -0400 Subject: [PATCH] v0.4.0 M3: GEPD key-layout preset + crouch/reload bindings (D371) Input.Layout (0=FPS default, 1=GEPD) + Input.CrouchMode (hold/toggle) rows in F10 INPUT (Turok-standard functional section; the planned MODERN bucket is dropped per D356). The GEPD preset acts as the per-key effective default under [Input.Bind] overrides; new IA_RELOAD/IA_CROUCH actions emit GE_CONT_B (unbound by default, N64-original). Toggle crouch latches B with a menu-boundary latch drop. optionsoverlay rowSetCommit hook re-derives the binds via inputLayoutApply(). inputRebuildBinds no longer warns on intentionally-empty defaults. Boot + ini round-trip + F10 page verified; in-game feel-check owed (D371). --- docs/dev/findings-index.csv | 1 + docs/dev/findings.md | 48 ++++++++++++++++++++++ port/include/input.h | 5 +++ port/src/input.c | 80 +++++++++++++++++++++++++++++++++++-- port/src/optionsoverlay.c | 29 ++++++++++++++ 5 files changed, 160 insertions(+), 3 deletions(-) diff --git a/docs/dev/findings-index.csv b/docs/dev/findings-index.csv index c815ba9e..d068e1b3 100644 --- a/docs/dev/findings-index.csv +++ b/docs/dev/findings-index.csv @@ -247,3 +247,4 @@ D367,?,F10 full-width control boxes misaligned with their contents (2026-09-27). D368,?,Simplify F10 click targets:,"INPUT RULE KEPT; tinted strips user-rejected, removed in D369." D369,FIXED,Remove F10 row boxes/strips entirely (2026-09-27).,FIXED (awaiting live acceptance): D370,PARTIAL,F10 distance/volume drags hurt frame rate (2026-09-27).,PARTIAL (user live test): +D371,FIXED,"GEPD key-layout preset + reload/crouch bindings, v0.4.0 modern options wave M3 (2026-09-27).",FIXED (headless-verified; awaiting user live accept): diff --git a/docs/dev/findings.md b/docs/dev/findings.md index 1472de2f..3e7b44a9 100644 --- a/docs/dev/findings.md +++ b/docs/dev/findings.md @@ -655,6 +655,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | D368 | **Simplify F10 click targets: setting name selects, the rest changes (2026-09-27).** — full `## D368` entry at file tail | INPUT RULE KEPT; tinted strips user-rejected, removed in D369. | | D369 | **Remove F10 row boxes/strips entirely (2026-09-27).** — full `## D369` entry at file tail | FIXED (awaiting live acceptance): no row backgrounds, control rectangles or hover paint; selected setting distinguished by brighter text, slider tracks remain. Input rule from D368 unchanged. | | D370 | **F10 distance/volume drags hurt frame rate (2026-09-27).** — full `## D370` entry at file tail | PARTIAL (user live test): mostly improved; rapid spam can still lower FPS. Unchanged-detent/audio and Draw/LOD reapply fixed; other image sliders still request full video pass (live log shows frequent FOV passes). No further churn without focused repro. | +| D371 | **GEPD key-layout preset + reload/crouch bindings, v0.4.0 modern options wave M3 (2026-09-27).** full `## D371` entry at file tail | FIXED (headless-verified; awaiting user live accept): `Input.Layout` (0=FPS, 1=GEPD) + `Input.CrouchMode` (hold/toggle) in F10 INPUT; GEPD preset acts as per-key effective default under `[Input.Bind]` overrides; new IA_RELOAD/IA_CROUCH emit B (unbound by default). MODERN section dropped per D356 Turok-standard rule (rows land in functional INPUT). Boot/ini-round-trip/F10-page verified; in-game feel-check owed. | Phase 2 replaced the Phase-1 demo loop with the real `mainproc()` on real OS threads, compiled GE's real `src/sched.c`, and brought in PD's fast3d software @@ -14309,3 +14310,50 @@ Residual not isolated: the latest playtest log shows repeated full video config passes while changing FOV (not Draw/LOD) and several watch EEPROM persists; sustained high Draw/LOD also increases actual GPU work. Parked as polish until a focused repro identifies which slider/spam pattern dominates. + +## D371 — GEPD key-layout preset + reload/crouch bindings (v0.4.0 modern options wave, M3) (2026-09-27) + +**Spec:** `docs/dev/notes/MODERN-OPTIONS-PLAN.md` M3. **Deviation from plan +(user decision, 2026-09-27):** the planned "MODERN" F10 section is DROPPED — +it is a provenance bucket, not a functional category, which conflicts with +the D356 Turok-standard "every section is functional, baseline names" rule +(the same rule that retired the D353 BOND FILE section). The rows land in +the functional section (INPUT); no new category, no `__ResetModern` — the +existing per-section reset rows cover the new keys via `kResetDefaults`. + +**Implementation (port-only; `port/src/input.c`, `port/include/input.h`, +`port/src/optionsoverlay.c`):** +- New actions `IA_RELOAD` / `IA_CROUCH` (emits `GE_CONT_B` both; on N64 the + single B button does reload-tap vs crouch-hold, and the game's own + `bond_interact_object` context logic makes that choice — so the port + emits B and leaves the decision to the game, as on the N64). Defaults are + empty strings = unbound (byte-identical N64 layout out of the box). +- `Input.Layout` (0 = FPS defaults, 1 = GEPD) + `Input.CrouchMode` + (0 = hold, 1 = toggle/latched). The GEPD preset + (GEPORT-REFERENCE-DEEPDIVE.md section 7.3: Action=Q, Cancel=E, Reload=R, + Crouch=Left Ctrl, Fire="" — LMB fires/RMB aims, LeanLeft="" — lean is + GEPD-removed) acts as the EFFECTIVE DEFAULT only for actions the user has + not individually overridden in `[Input.Bind]`; per-key ini values layer + on top, so switching layouts never clobbers explicit binds. Revert = + switching back. +- Toggle crouch latches B; the latch is dropped on entering a menu (a + latched B would read as back/cancel) and on `inputLayoutApply()`. +- F10 rows: "Key layout" (kKeyLayout) + "Crouch mode" (reuses kHold) under + INPUT, before its reset row. +- `inputRebuildBinds`: an intentionally-empty string no longer logs the + "action unbound" warning; only a NON-empty string that yields no valid + keys warns (D214's warning was written when no default was ever empty). +- `optionsoverlay.c` `rowSetCommit` hook: `Input.Layout` / + `Input.CrouchMode` -> `inputLayoutApply()` (scheduler thread; a mid-poll + rebuild can at worst drop/add one scancode for one frame — the D214 + thread-pairing, just at F10 commit time instead of boot). + +**Verification:** NTSC build clean. GEPD boot (`Layout=1`, `CrouchMode=1`, +`Bind.Reload=R`, `Bind.Crouch=Left Ctrl`): no crash, no spurious warnings. +ini round-trip via orderly `GE_QUITFRAME` exit: `Layout`/`CrouchMode` +persist under `[Input]`; `Reload`/`Crouch` under `[Input.Bind]` (config.c +`splitKey` splits on the LAST dot — pre-existing D214 behaviour, binds were +always saved there with bare leaf names). F10 INPUT page +(`GE_OPTIONSOVERLAY=2`) boots clean, both rows resolve. **Owed:** live F10 +layout-switch feel-check (GEPD vs FPS), in-game crouch toggle/reload B +semantics + menu-latch-reset, PAL/JP. diff --git a/port/include/input.h b/port/include/input.h index 48ef1be0..d8536435 100644 --- a/port/include/input.h +++ b/port/include/input.h @@ -68,6 +68,11 @@ short inputPadAxis(int idx, SDL_GameControllerAxis a); * ignored -- GE only cycles forward on a bare A edge. */ void inputPostWheel(int notches); +/* v0.4.0 M3: re-derive the keyboard binds after an F10 change to + * Input.Layout / Input.CrouchMode (optionsoverlay rowSetCommit hook). + * Also drops the latched-crouch state. Scheduler thread only. */ +void inputLayoutApply(void); + /* D345(b): 1 while the 1:1 menu pointer owns cursor_h/v_pos (in a menu, * abs pointer available, mouse used within the re-assert window). Port * screens that teleport the crosshair should skip their snap when set. */ diff --git a/port/src/input.c b/port/src/input.c index ea58a662..96c9bb06 100644 --- a/port/src/input.c +++ b/port/src/input.c @@ -868,7 +868,12 @@ static int keyDown(const Uint8 *ks, SDL_Scancode sc) * ---------------------------------------------------------------------- */ enum { IA_FORWARD, IA_BACK, IA_STRAFE_L, IA_STRAFE_R, IA_TURN_L, IA_TURN_R, - IA_FIRE, IA_AIM, IA_ACTION, IA_CANCEL, IA_LEAN_L, IA_START, IA_COUNT + IA_FIRE, IA_AIM, IA_ACTION, IA_CANCEL, IA_LEAN_L, IA_START, + /* v0.4.0 M3 (modern options wave): GEPD-layout actions. Both map to + * the N64 B button -- the game's own B-context logic + * (bond_interact_object) picks reload (tap, gun in hand, no + * interact target) vs crouch (held) exactly as on the N64. */ + IA_RELOAD, IA_CROUCH, IA_COUNT }; static const struct { const char *key; const char *def; } kBindDefs[IA_COUNT] = { @@ -884,8 +889,30 @@ static const struct { const char *key; const char *def; } kBindDefs[IA_COUNT] = [IA_CANCEL] = { "Input.Bind.Cancel", "X,R,F,Escape" }, [IA_LEAN_L] = { "Input.Bind.LeanLeft", "Q" }, [IA_START] = { "Input.Bind.Start", "Return,Tab" }, + [IA_RELOAD] = { "Input.Bind.Reload", "" }, + [IA_CROUCH] = { "Input.Bind.Crouch", "" }, }; +/* v0.4.0 M3: GEPD key-layout preset (docs/dev/notes/GEPORT-REFERENCE- + * DEEPDIVE.md section 7.3). The preset is the EFFECTIVE DEFAULT for any action + * the user has not overridden per-key in [Bind]; a per-key ini value + * layers on top, so switching layouts never clobbers explicit binds. + * Fire loses its Left Ctrl key in this layout (LMB fires, RMB aims; + * Ctrl becomes crouch); LeanLeft is unbound (lean is GEPD-removed). + * NULL entries keep the FPS default. */ +static const char *const kGepdPreset[IA_COUNT] = { + [IA_FIRE] = "", + [IA_ACTION] = "Q", + [IA_CANCEL] = "E", + [IA_LEAN_L] = "", + [IA_RELOAD] = "R", + [IA_CROUCH] = "Left Ctrl", +}; +static int keyLayout = 0; /* 0 = FPS defaults, 1 = GEPD preset */ +static int crouchMode = 0; /* 0 = hold, 1 = toggle (latched B) */ +static int s_crouchLatch = 0; +static int s_crouchHeldPrev = 0; + #define BIND_MAX_KEYS 4 static char g_bindStr[IA_COUNT][64]; static SDL_Scancode g_bind[IA_COUNT][BIND_MAX_KEYS]; @@ -896,9 +923,18 @@ static void inputRebuildBinds(void) for (int k = 0; k < BIND_MAX_KEYS; k++) { g_bind[a][k] = SDL_SCANCODE_UNKNOWN; } + /* v0.4.0 M3: the GEPD preset fills the actions the user has not + * individually overridden (empty g_bindStr); FPS layout = the + * kBindDefs defaults, byte-identical to the pre-wave binds. */ + const char *def = kBindDefs[a].def; + if (keyLayout == 1) { + const char *preset = kGepdPreset[a]; + if (preset) def = preset; + } char buf[64]; - strncpy(buf, g_bindStr[a][0] ? g_bindStr[a] : kBindDefs[a].def, sizeof(buf) - 1); + strncpy(buf, g_bindStr[a][0] ? g_bindStr[a] : def, sizeof(buf) - 1); buf[sizeof(buf) - 1] = 0; + int hadInput = (buf[0] != 0); /* before strtok clobbers buf */ int n = 0; for (char *tok = strtok(buf, ","); tok && n < BIND_MAX_KEYS; tok = strtok(NULL, ",")) { @@ -914,13 +950,32 @@ static void inputRebuildBinds(void) } g_bind[a][n++] = sc; } - if (n == 0) { + /* v0.4.0 M3: an intentionally-empty string (a preset or default + * that leaves an action unbound, e.g. GEPD Fire) is silent -- + * only a non-empty string that produced no valid keys warns. */ + if (n == 0 && hadInput) { sysLogPrintf(LOG_WARNING, "input: %s has no valid keys; action unbound", kBindDefs[a].key); } } } +/* v0.4.0 M3: re-derive the binds after an F10 change to Input.Layout / + * Input.CrouchMode (optionsoverlay rowSetCommit hook, scheduler thread). + * g_bind is read per-frame by the game thread's actHeld(): a mid-poll + * rebuild can at worst drop or add one scancode for one frame (the + * D214 startup-rebuild is the same thread-pairing, just at boot). + * Also drops the crouch latch so a latched crouch never carries B into + * a menu (where B = back/cancel). */ +void inputLayoutApply(void) +{ + s_crouchLatch = 0; + s_crouchHeldPrev = 0; + inputRebuildBinds(); + sysLogPrintf(LOG_INFO, "input: key layout %s, crouch %s applied", + keyLayout ? "GEPD" : "FPS", crouchMode ? "toggle" : "hold"); +} + static int actHeld(const Uint8 *ks, int act) { for (int k = 0; k < BIND_MAX_KEYS; k++) { @@ -1126,6 +1181,21 @@ unsigned inputComputePad(int idx, signed char *stick_x, signed char *stick_y) } if (actHeld(ks, IA_CANCEL)) /* D145: Escape is in the default Cancel bind */ button |= GE_CONT_B; + /* v0.4.0 M3: reload (B tap, same as any B press) and crouch + * (hold, or a latched toggle). Both are unbound by default (the + * N64-original layout), so this is a no-op out of the box; the + * game's B-context logic decides reload-vs-crouch as on N64. */ + if (actHeld(ks, IA_RELOAD)) + button |= GE_CONT_B; + int crouchNow = actHeld(ks, IA_CROUCH); + if (crouchMode == 1) { + if (crouchNow && !s_crouchHeldPrev) s_crouchLatch ^= 1; + if (menuMode) s_crouchLatch = 0; /* never carry B into a menu */ + if (s_crouchLatch) button |= GE_CONT_B; + s_crouchHeldPrev = crouchNow; + } else if (crouchNow) { + button |= GE_CONT_B; + } if (actHeld(ks, IA_LEAN_L)) button |= GE_CONT_L; if (actHeld(ks, IA_START)) @@ -1998,6 +2068,10 @@ PD_CONSTRUCTOR static void inputConfigInit(void) configRegisterInt("Input.PadDeadzone", &padDeadzone, 0, 30000); configRegisterInt("Input.PadTriggerPct", &padTriggerPct, 1, 99); configRegisterInt("Input.PadLookInvertY", &padLookInvertY, 0, 1); + /* v0.4.0 M3 (modern options wave): key-layout preset + crouch bind + * mode; both default to the N64-original FPS layout / hold. */ + configRegisterInt("Input.Layout", &keyLayout, 0, 1); + configRegisterInt("Input.CrouchMode", &crouchMode, 0, 1); /* D214: keyboard rebinding. Seed each buffer with its default so the knob * is visible/editable in a fresh ge007.ini; configLoad() overwrites any the diff --git a/port/src/optionsoverlay.c b/port/src/optionsoverlay.c index c4abc349..e1eecd0e 100644 --- a/port/src/optionsoverlay.c +++ b/port/src/optionsoverlay.c @@ -91,6 +91,17 @@ static const char *const kTexFilter[] = { "Nearest", "Bilinear", "3-Point", NULL static const char *const kAimMode[] = { "N64", "Centred (PC)", NULL }; /* D337 */ static const char *const kAimRange[] = { "PC", "N64", NULL }; /* D338 */ static const int kMsaaSeq[] = { 1, 2, 4, 8 }; +/* v0.4.0 modern options wave: value names for the new rows (they land + * in the functional sections -- Turok standard, D356 -- not a bucket). */ +static const char *const kOnOffRev[] = { "On", "Off", NULL }; /* 0 = On */ +/* M2: named crosshair tints; index 0 = the N64 white (no tint applied). */ +static const char *const kCrosshairColor[] = { + "White", "Green", "Red", "Blue", "Yellow", "Cyan", "Magenta", + NULL, +}; +/* M3: key-layout preset (0 = the FPS default binds, byte-identical to the + * current defaults; 1 = the GEPD mouse-injector layout) + crouch bind mode */ +static const char *const kKeyLayout[] = { "FPS (default)", "GEPD", NULL }; /* D186: the sim's own tick pacemaker is hardcoded to the console's native VI * rate (60Hz NTSC / 50Hz PAL, port/src/libultra.c) -- Video.FpsCap can only * throttle down from there, never past it, and throttling it below 30 @@ -190,6 +201,13 @@ static struct Row rows[] = { { .key="Input.PadLookInvertY", .label="Invert look (controller)", .kind=ROW_TOGGLE, .step=1, .names=kOnOff }, { .key="Input.PadDeadzone", .label="Stick deadzone", .kind=ROW_SLIDER, .step=500 }, { .key="Input.PadTriggerPct", .label="Trigger threshold", .kind=ROW_SLIDER, .step=1 }, + /* v0.4.0 M3 (modern options wave, D371): the GEPD mouse-injector + * key-layout preset (docs/dev/notes/GEPORT-REFERENCE-DEEPDIVE.md + * section 7.3) + the crouch bind's fire mode. Turok standard: these are + * control bindings, so they live in INPUT -- no provenance bucket. + * Defaults are the N64-original FPS layout / hold. */ + { .key="Input.Layout", .label="Key layout", .kind=ROW_ENUM, .step=1, .names=kKeyLayout }, + { .key="Input.CrouchMode", .label="Crouch mode", .kind=ROW_ENUM, .step=1, .names=kHold }, { .key="__ResetInput", .label="Reset to defaults", .kind=ROW_ACTION }, { .key="__HdrGameplay", .label="GAMEPLAY", .kind=ROW_HEADER }, @@ -726,6 +744,15 @@ static void rowSetCommit(struct Row *r, double v, int commit) videoRequestLiveConfig(); } + /* v0.4.0 M3: key-layout preset / crouch mode -- re-derive the binds + * (inputRebuildBinds via inputLayoutApply; also drops the crouch latch). + * Safe at scheduler-thread commit time; a mid-poll rebuild can only + * drop/add one scancode for one frame (D214 thread-pairing). */ + if (strcmp(r->key, "Input.Layout") == 0 || + strcmp(r->key, "Input.CrouchMode") == 0) { + inputLayoutApply(); + } + /* Linked aim/turn sensitivity (Input.SensLink, default on): moving either * knob scales the other to hold the stock default ratio -- AimModeSens 38 * : MouseTurnSpeed 50 (the D194/D238 calibrated defaults). */ @@ -871,6 +898,8 @@ static const struct { const char *key; double def; } kResetDefaults[] = { { "Input.PadLookInvertY", 0 }, /* = 0 */ { "Input.PadDeadzone", 7000 },/* = STICK_DEADZONE (7000) */ { "Input.PadTriggerPct", 23 }, /* = 23 */ + { "Input.Layout", 0 }, /* = 0 (FPS defaults) */ + { "Input.CrouchMode", 0 }, /* = 0 (hold) */ /* GRAPHICS (port/src/video.c initializers) */ { "Video.MSAA", 2 }, /* = 2 */ { "Video.TextureFilter", 1 }, /* = 1 (bilinear) */