forked from Ports/goldeneye-pc-port
Per the 2026-09-19 adversarial pre-ship review (findings.md M-192): - d318watchdog.c: one-shot per OCCURRENCE, not per process -- re-arm after 600 ticks of signature absence so a Facility replay in the same session (AllUnlocked) is still covered. - model.c: RULE-2-SIGNOFF records for the retained M-183/M-185 and D311 defensive clamps (user-approved retention as the stability-maximizing option; stale 'Threshold: 50.0' comment corrected to 10.0f; both marked post-release removal candidates pending full cutscene re-verification). - reference/mouse-injector/: removed from the public repo (GPLv2 source + prebuilt discord-rpc.lib in an MIT project; never compiled here). Provenance stub README remains; path gitignored for local re-vendoring. AGENTS.md / GEPD-INPUT-PLAN.md / optionsoverlay.c refs updated. - scratch/: untracked + gitignored (19 session logs/probe sources were committed in the release window). - findings-index.csv: D317 (and 5 other stale rows) now CLOSED -- added CLOSED to gen_findings_index.py's verdict vocabulary. - docs: D310 perf gauge (no change warranted), GE_DEBUG_UNLOCKALL noted in security-and-fidelity-status.md, release notes gain the missing D318 watchdog bullet + FOV-clamp clarification. Build verified: ./build-pc.sh ntsc-final clean; gen_findings_index.py --check and gen_env_probes.py --check pass.
218 lines
12 KiB
Markdown
218 lines
12 KiB
Markdown
# GEPD-inspired input overhaul — FINAL PLAN (issues #89, #90)
|
||
|
||
Status: **plan only** — no code changes yet. Companion to `port/src/input.c`
|
||
(current state: D118/D165/D166/D194/D214/D223/D238 lineage).
|
||
|
||
## 1. The reference (no longer vendored in the public repo)
|
||
|
||
The GEPD Edition Mouse Injector source was vendored at `reference/mouse-injector/`
|
||
and **removed from the public repo on 2026-09-19** (GPLv2 code + a prebuilt
|
||
binary inside an MIT project; never compiled here). See
|
||
`reference/mouse-injector/README.md` for provenance and how to re-vendor it
|
||
locally if you need the original. The file that mattered was
|
||
**`games/goldeneye.c`** (~380 lines, the whole GE aim model), plus `device.c`
|
||
(raw mouse polling) and `maindll.h` (tickrate — the units every per-tick
|
||
constant assumes). The model is quoted inline below and in the D194 findings;
|
||
the ported result lives in `port/src/input.c`.
|
||
|
||
### What the Mouse Injector does (GE)
|
||
|
||
### What the Mouse Injector actually does (GE)
|
||
|
||
| Aspect | GEPD implementation (`games/goldeneye.c`) |
|
||
|---|---|
|
||
| Mouse source | Raw driver deltas via ManyMouse (`device.c`), cursor locked by periodic `SetCursorPos`. No OS pointer accel, no deadzone. |
|
||
| **Hipfire look** | **Direct camera write**: `camx += XPOS/10 * (SENS/40) * (fov/basefov)` degrees per tick; `camy` likewise, clamped ±90 (−20 in tank). Linear in px — this is why GEPD "just feels right". |
|
||
| **Aim mode (R)** | Crosshair *position* accumulator `crosshairpos += XPOS/10 * SENS/292`, clamped to ±`CROSSHAIRLIMIT` (5.159° = screen edge). Gun/arm pose derived per-weapon from a 33-entry offset table. **View scrolls only past 72% of the way to the edge**: `aimx = (ratio−0.72) * 475 * timestep`, scaled by `fov/basefov`. |
|
||
| FOV coupling | Every term carries `(fov/basefov)` — scopes automatically get finer control. `basefov` = min(60, override). |
|
||
| Tank | Separate direct write to the tank x-rotation global (`0x80036484`), different scale (`360/TANKXROT * 2.5`); crosshair X locked to 0 when driving with tank equipped. |
|
||
| Menus | Mouse moves the menu cursor directly (`menux/y += px/10*SENS*6`, clamped to screen rect); aim button acts as B (back) in menus; wheel = weapon cycle macro. |
|
||
| Safety gates | Injection only when `camera==4||0 && menupage==11 && !dead && !watch && !pause && !mproundend`. |
|
||
| Extras | Crouch *toggle* mode, dedicated reload key (ROM hack — not portable to us), FOV override + viewmodel repositioning (ROM hacks — we have the clean equivalents in `port/`), intro skip on FIRE+AIM. |
|
||
|
||
### Already inherited (do not redo)
|
||
|
||
- **D194 GEPD-mirror aim** (`aimGepdCompute()`): crosshair-position model,
|
||
±5.159° clamp, 72% edge scroll at 475/s, fov scaling, per-tick overwrite of
|
||
`crosshair_x/y_pos` + `gun_azimuth_angle/turning`. This *is* GEPD's aim mode.
|
||
- D223 mouse-wheel weapon cycling (A / A+Z synthesis) — mirrors
|
||
`GE_Controller()`'s weapon macro; D214 ini key binds; click-to-lock capture;
|
||
F10 options overlay with aim/turn sensitivity sliders; D194(b) dt-decoupled
|
||
look; menu pointer via direct `cursor_h/v_pos` writes (mirrors GEPD's
|
||
`menux/y` writes).
|
||
|
||
### Remaining inheritances (this plan)
|
||
|
||
GEPD's *hipfire* is a direct linear camera write — we still route hipfire
|
||
through the N64 stick curve (#89). Also un-inherited: raw-input-by-default,
|
||
crouch toggle, GEPD-style safety gates on our always-live look paths, and its
|
||
sensitivity/FOV coupling discipline. That's WIs 1–3 + 6 below.
|
||
|
||
### Reference split (GEPD vs PD port)
|
||
|
||
Two references, two jobs (decided during #89/#90 triage):
|
||
|
||
- **GEPD Mouse Injector** (was `reference/mouse-injector/`, removed from the
|
||
public repo 2026-09-19 — see its README stub) = *what the mouse does to the
|
||
camera*. The only reference with GE-specific knowledge (camera fields,
|
||
crosshair limits, tank case, safety gates). Primary for WI-1.
|
||
- **PD PC port checkout** (standing reference, see AGENTS.md) = *how a PC game
|
||
presents and captures input*: cursor-lock behavior, raw input handling,
|
||
sensitivity units/scaling, and the shipped in-game options/keybind UI.
|
||
Primary for WI-2 conventions and WI-5's menu design. PD knows nothing about
|
||
GE — never use it for camera math.
|
||
|
||
## 2. Issue #89 — "mouse movement is not uniform; slow motion barely registers"
|
||
|
||
### Diagnosis (high confidence)
|
||
|
||
The complaint is the **hipfire** path. Today hipfire converts mouse px into an
|
||
*analog stick value* (`sx += (int)(hipEdx * hipSens * MOUSE_TURN_GAIN)`), and
|
||
the game then runs that stick through its own N64-era natural-turn response —
|
||
a **quadratic-ish curve with a soft floor** (D238: "saturates the game's
|
||
quadratic natural-turn curve at ~13 px/poll"). Consequences, exactly as the
|
||
reporter describes:
|
||
|
||
1. Slow mouse motion → small stick values → squared-off tiny turn rates →
|
||
"almost unrecognised".
|
||
2. Fast motion saturates the curve at full 315°/s — so the usable band is
|
||
narrow and the feel is non-uniform (bang-bang-ish).
|
||
3. `(int)` truncation of the stick adds a quantized dead zone on top.
|
||
|
||
GEPD/PD-PC never hit this because **they write the camera angles directly** —
|
||
the stick curve is bypassed entirely, and sensitivity is linear in px with
|
||
FOV scaling. We already do exactly that in aim mode (D194); hipfire is the one
|
||
leftover N64-stick-shaped path.
|
||
|
||
Secondary contributors worth folding in:
|
||
|
||
- `Input.MouseRawInput` defaults to **0** — on X11/Wayland (the #90 reporter
|
||
is on Linux) OS pointer acceleration still shapes relative deltas; GEPD
|
||
reads the raw driver. Windows is largely unaffected.
|
||
- Sensitivity is spread over 4 knobs (`MouseSensitivity`, `MouseTurnSpeed`,
|
||
`AimModeSens`, legacy `MouseAimSpeed`) — the reporter "tried adjusting
|
||
ge007.ini" and couldn't find the one that mattered for hipfire.
|
||
|
||
### Work items
|
||
|
||
**WI-1 (core fix): direct camera writes for hipfire look.**
|
||
Mirror GEPD's hipfire branch in `input.c`.
|
||
|
||
*Pre-flight:* skim the PD port's `port/src/input.c` mouse path first and match
|
||
its conventions (sensitivity units, dt handling, focus-loss behavior) so the
|
||
result feels like PD PC rather than a reinvention. GEPD supplies the GE camera
|
||
math; PD supplies the UX shape.
|
||
|
||
Implementation:
|
||
|
||
- New path `hipDirectCompute()` next to `aimGepdCompute()`: convert this
|
||
poll's dt-scaled px to degrees using the *current* viewport
|
||
(deg/px = fovy/viewH, or hfov/viewW — reuse whatever D194 absolute aim used),
|
||
scale by `MouseTurnSpeed * MouseSensitivity`, and add straight to
|
||
`g_CurrentPlayer->vv_theta` / `vv_verta` (clamp ±90). Same confinement as
|
||
today: only when `idx==0`, cursor captured, in a running stage.
|
||
- **GEPD's safety gates** must be ported over (aim mode got away without them
|
||
because it requires RMB held; hipfire is always live): check the decomp
|
||
equivalents of GEPD's `camera==4||0 && menupage==11 && !dead && !watch &&
|
||
!pause` before writing. `menupage` → `current_menu` (already have); the rest
|
||
are `player`/game globals — read-only, no logic change.
|
||
- **Tank**: GEPD writes a separate tank-rotation global with its own scale and
|
||
locks crosshair X to 0 when driving with the tank equipped. Find the decomp
|
||
symbol for `0x80036484` (tank.c) and mirror it; if too fiddly, phase 2 —
|
||
tanks are rare in single-player.
|
||
- Keyboard turn (arrows → stick) keeps the existing stick path; only mouse px
|
||
goes direct. Escape hatch: `Input.MouseDirectLook=1` default, `0` reverts to
|
||
the current stick path (kept as-is).
|
||
- This makes hipfire and aim mode share one linear mental model: px → screen
|
||
angle, FOV-scaled, dt-normalized. Expect to then simplify or delete the
|
||
D166/D238 curve-tuning knobs that become inert (`MouseAimCurve`, `AimBand`,
|
||
legacy `MouseAimSpeed`) — *after* playtest, and keep the config keys as
|
||
no-op aliases so existing inis don't break.
|
||
|
||
**WI-2: raw input by default.** Flip `Input.MouseRawInput` default to 1 (SDL
|
||
hints already handled in `inputInit`). Verify Windows relative-mode deltas are
|
||
unaffected; this mainly fixes Linux feel. Keep the escape hatch.
|
||
|
||
**WI-3: one honest sensitivity number.** With WI-1, hipfire and aim both scale
|
||
off px→degrees, so `MouseSensitivity` becomes genuinely master. Add it to the
|
||
F10 overlay (see WI-5), document the knob hierarchy in the generated ini
|
||
header comment, and re-calibrate defaults against GEPD's SENS≈40 ≈ 1:1
|
||
screen-angle mapping (our current 38/50/100 trio should land close; measure,
|
||
don't guess).
|
||
|
||
**Verification:** `GE_INPUTLOG` linearity check — log px-in vs deg-out at slow
|
||
and fast speeds (ratio must be constant); headless script harness for the
|
||
stick-fallback path; manual playtest checklist (hipfire slow/fast, aim mode,
|
||
scope zoom, tank, menu, death/cutscene safety gates). New finding `Dxxx` in
|
||
`docs/dev/findings.md`, index row added.
|
||
|
||
## 3. Issue #90 — in-game key bindings + "ini binds don't work" + sensitivity menu
|
||
|
||
### Diagnosis candidates for "binds don't work properly" (repro before fixing)
|
||
|
||
1. **Not a bug, GE design:** in-game, a fresh A edge *without* Z held is the
|
||
weapon-cycle-forward signal (D223 documents `weaponForwardOffset`). The
|
||
default Action bind includes E — so "pressing E changes weapon" is the
|
||
game's own behavior, and "Escape reloads/activates switches" is the default
|
||
Cancel=Escape → B button. The user's mental model (PC: E = interact)
|
||
collides with GE's A/B semantics. Address via documentation + possibly a
|
||
PC-idiomatic default set (product decision; GEPD itself uses Q=A, E=B).
|
||
2. **Ini syntax footgun:** keys must be written as `Input.Bind.Action=E` (or
|
||
`Bind.Action=E` under `[Input]`). A bare `Action=E` under `[Input]` resolves
|
||
to `Input.Action`, which is unregistered and silently ignored. `configLoad`
|
||
also ignores unknown keys with no warning — add a log line for unrecognized
|
||
keys so users get feedback.
|
||
3. **Key-name format:** binds use SDL scancode names (`"Left Ctrl"`, `"Up"`,
|
||
`"Space"`); nothing documents this in the ini. Add per-key comments in
|
||
`configSave` output and/or a `# valid names: ...` header.
|
||
4. There may still be a real parse bug lurking (`strtok` on the seeded buffer,
|
||
constructor ordering vs `configLoad`) — **repro first**: write an ini with
|
||
`Input.Bind.Action=E`, launch, confirm via `GE_INPUTLOG` that E alone
|
||
produces A. Fix whatever actually breaks; don't fix hypotheticals.
|
||
|
||
### Work items
|
||
|
||
**WI-4: keybind audit + ini ergonomics.** Repro (above), fix real bugs, add
|
||
unknown-key warnings, document scancode names in the saved ini. Cheap, do first
|
||
— it unblocks #90's half regardless of the UI work.
|
||
|
||
**WI-5: F10 overlay — bindings section + master sensitivity.**
|
||
Model the UX on the PD port's shipped in-game options menu (standing reference
|
||
checkout) where anything is player-facing. `optionsoverlay.c` already has
|
||
row/hit-test/scroll infrastructure and owns all input while open; what's missing:
|
||
|
||
- A new row kind `ROW_BIND`: label = action, value = first bound key name;
|
||
click → "press a key…" state that captures the *next* scancode (keyboard or
|
||
mouse button), writes it into `g_bindStr[a]` (replacing or appending —
|
||
decide: GEPD/PD use one primary + optional secondary; simplest v1 = replace
|
||
single key, keep multi-key only for ini power users), then `inputRebuildBinds()`
|
||
+ persist via the existing config save path.
|
||
- Expose `Input.MouseSensitivity` (master), and after WI-1 trim the panel to
|
||
master + per-mode fine-trim + invert Y (drop inert legacy rows).
|
||
- Bind capture must swallow all input while armed (overlay already does this)
|
||
and handle ESC = cancel, Backspace/Delete = clear.
|
||
|
||
**WI-6 (nice-to-have, from GEPD): crouch toggle.** GEPD's `CROUCHTOGGLE` state
|
||
machine (`GE_Crouch`) is ~20 lines and pure port-side (stance flag write).
|
||
Low priority; include only if WI-1..5 land cleanly.
|
||
|
||
## 4. Suggested order
|
||
|
||
1. **WI-4** (keybind repro/fix + ini docs) — small, unblocks #90 partially.
|
||
2. **WI-1** (direct hipfire) + **WI-2** (raw input default) — the #89 fix;
|
||
playtest-heavy, needs the safety gates done right.
|
||
3. **WI-5** (overlay binds + master sens) — completes #90.
|
||
4. **WI-3** (sensitivity consolidation/cleanup) — after WI-1 defaults settle.
|
||
5. **WI-6** (crouch toggle) — optional garnish.
|
||
|
||
## 5. Constraints reminders
|
||
|
||
- All work is `port/`-only: direct struct writes are the established D194
|
||
pattern (read/write live game state from the input poll, no logic change).
|
||
Any temptation to "fix" the natural-turn curve in `src/game` = rule-2 sign-off
|
||
territory — don't.
|
||
- Keep every removed/renamed ini key registered as an alias so shipped inis
|
||
keep loading; log deprecations at startup.
|
||
- Findings: new `Dxxx` entries for WI-1 (hipfire direct look), the #90 bind
|
||
repro outcome, and the overlay bind UI; update `findings-index.csv`.
|