Files
goldeneye-pc-port/docs/dev/GEPD-INPUT-PLAN.md
T
James Dansereau b3b0ff74db v0.3.0 release review: watchdog re-arm, rule-2 sign-offs, GPL source removal, repo hygiene
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.
2026-09-20 10:51:12 -04:00

12 KiB
Raw Blame History

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
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.