Files
goldeneye-pc-port/docs/dev/GEPD-INPUT-PLAN.md
T
James Dansereau 6938d5644d docs: add vendored GEPD Mouse Injector reference + input-overhaul plan
Both were already documented as standing repo content (AGENTS.md's
critical-files table, several findings.md entries citing D194/D300's
ground truth) but never actually committed -- add them so the repo
matches what's already written about it. GPLv2, read-only reference,
never compiled as part of this build (see reference/mouse-injector/README.md
for provenance).
2026-09-17 19:31:33 -04:00

214 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 (vendored in-repo)
The GEPD Edition Mouse Injector source is vendored at **`reference/mouse-injector/`**
(see its README for provenance, version-skew warning, and a per-file guide).
The file that matters is **`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). Consult it directly; no
re-extraction needed.
### 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** (`reference/mouse-injector/`) = *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`.