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).
12 KiB
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 |
| 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 ofcrosshair_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 directcursor_h/v_poswrites (mirrors GEPD'smenux/ywrites).
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:
- Slow mouse motion → small stick values → squared-off tiny turn rates → "almost unrecognised".
- Fast motion saturates the curve at full 315°/s — so the usable band is narrow and the feel is non-uniform (bang-bang-ish).
(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.MouseRawInputdefaults 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, legacyMouseAimSpeed) — 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 toaimGepdCompute(): 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 byMouseTurnSpeed * MouseSensitivity, and add straight tog_CurrentPlayer->vv_theta/vv_verta(clamp ±90). Same confinement as today: only whenidx==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 && !pausebefore writing.menupage→current_menu(already have); the rest areplayer/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=1default,0reverts 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, legacyMouseAimSpeed) — 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)
- 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). - Ini syntax footgun: keys must be written as
Input.Bind.Action=E(orBind.Action=Eunder[Input]). A bareAction=Eunder[Input]resolves toInput.Action, which is unregistered and silently ignored.configLoadalso ignores unknown keys with no warning — add a log line for unrecognized keys so users get feedback. - Key-name format: binds use SDL scancode names (
"Left Ctrl","Up","Space"); nothing documents this in the ini. Add per-key comments inconfigSaveoutput and/or a# valid names: ...header. - There may still be a real parse bug lurking (
strtokon the seeded buffer, constructor ordering vsconfigLoad) — repro first: write an ini withInput.Bind.Action=E, launch, confirm viaGE_INPUTLOGthat 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 intog_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), theninputRebuildBinds()- 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
- WI-4 (keybind repro/fix + ini docs) — small, unblocks #90 partially.
- WI-1 (direct hipfire) + WI-2 (raw input default) — the #89 fix; playtest-heavy, needs the safety gates done right.
- WI-5 (overlay binds + master sens) — completes #90.
- WI-3 (sensitivity consolidation/cleanup) — after WI-1 defaults settle.
- 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 insrc/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
Dxxxentries for WI-1 (hipfire direct look), the #90 bind repro outcome, and the overlay bind UI; updatefindings-index.csv.