Virtual seats, per-seat camera and input routing, aim modes, hot-plugged
controllers reopening via a coalesced rescan (D450, dolent's PR #122,
merged as the original commit). Rule-2
sign-off for D417.
Refs: #99 D416 D417 D418 D419 D422 D428 D448 D450 #122
SDL_CONTROLLERDEVICEADDED / REMOVED are dequeued by whichever of the two
event pumps polls first; fast3d's gfx_sdl_handle_events had no case for
them and dropped them, so inputRescanPads never ran after a replug (the
stale handle later aborted in malloc). Both pumps now post a request and
inputUpdate() consumes it on the pad-reading thread. Findings D435.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Port-layer only (port/src/input.c, optionsoverlay.c); zero src/game edits.
All new knobs off/neutral at default = byte-identical at defaults.
New controller input options (CONTROLLER-INPUT-PLAN Wave A):
- Input.PadLookSensX/Y: per-axis right-stick look sensitivity (100 = native)
- Input.PadSouthpaw: swap fire<->grenade triggers (applied after edge capture,
so edge-detection stays consistent)
- Input.PadDeadzoneL/R: per-stick deadzone; legacy Input.PadDeadzone key kept
registered (no row) + one-time migration in inputInit() seeds any unset L/R
from it (C-init sentinel PAD_DZ_UNSET) before the first configSave
- Input.PadLookSmooth: 0-10 per-pad EMA low-pass on the natural-pitch look
output (default 0 = off), reset on pad hot-unplug
- Input.MouseYScale: pre-existing registered key finally gets an INPUT row
Verbiage baseline (Turok PC options menu, user-provided verbatim): Mouse
horizontal/vertical sensitivity, X axis/Y axis look sensitivity, Look
smoothing, Southpaw, Vibration (was "Rumble strength"), Deadzone
(left/right stick). GE keeps one flat INPUT section (D356), so the two
Invert-look rows retain their (mouse)/(controller) suffixes.
Display baseline (Turok scales): sensitivity + deadzone rows render 0-100
with 50 = default (existing calibratedDefault path); look smoothing renders
0-10 via a new dispMax "N/M" row formatter (alongside the existing N/100 and
N/255 special cases).
Tested on a real pad (Windows build) - all six options behave as intended.
Port layer only (zero src/ edits), per docs/dev/RUMBLE-PLAN.md:
- port/src/input.c: inputRumbleSupported/inputRumble -> SDL_GameControllerRumble;
detection via SDL_GameControllerHasRumble (SDL >= 2.0.18) else
SDL_JoystickIsHaptic + known-type Windows fallback; zeroed on pad close.
Single global gRumbleScale (dflt 0.5) registered as Input.RumbleScale.
- port/src/libultra.c: the two M-87 blockers resolved - g_contStatus[i].status
now sets CONT_CARD_ON for connected rumble-capable pads (so
joyRumblePakInit arms) and osPfsInit returns PFS_ERR_DEVICE for them (so
osMotorInit is attempted); osMotorInit/Start/Stop drive the haptics API.
- port/src/optionsoverlay.c: one global 'Rumble strength' ROW_SLIDER
(Input.RumbleScale, 0.05 detents, %.2f) in the INPUT section +
kResetDefaults entry. The four per-player Input.PlayerN.RumbleScale keys
were dropped (user decision 2026-09-28); stale keys in an existing ini
log four 'unknown key' LOG_NOTE lines and self-clean on next configSave.
- Real-pad haptics confirmed working by the user (2026-09-28).
Build env: build-pc.sh self-heal guard revision - re-exec now writes a
self-contained .ps1 (msys->native argv conversion mangles $-bearing
-Command strings; file write is conversion-free), builds PATH explicitly,
picks TMP from writable candidates (GetTempPath honours the broken
inherited TMP), self-logs to build-pc/ge007-native-reexec-*.log, and
treats a null LASTEXITCODE as 'did not execute' (PS 5.1 piped native
commands error non-terminating). OPEN: the guard still fails inside
build-pc.sh while the identical standalone .ps1 invocation works - AGENTS.md
documents the workaround. docs: RUMBLE-PLAN.md (new, tracked), findings.md
D401 entry + index rows, QOL-INVENTORY D224 -> DONE (D401),
findings-index.csv regenerated.
Input (input.c/h):
- D380/D386: single effective PC layout; ini values ARE the bindings
(no hidden defaults); versioned migration materialises effective
GEPD defaults once (inputMigrateBinds, bindsVersion).
- D385: keyboard keys and pad buttons 1..5 share the two bind slots;
legacy extra ini tokens are migration-only. D386: SDL button-number
correction in the D385 defaults.
- D383/D384: opt-in inputBindingProbe (restores ini state); the PC
settings editor must not navigate with live game keys.
- D393/D394: Xbox 1.1 Jinx face map — A use, X reload, Y weapon
cycle, B owned-gadget cycle (no N64 equivalent, port-side), L3/R3
crouch, LB aim; retires LB/RB weapon cycling.
- D395: both options UIs refuse pad-initiated keyboard/mouse capture;
pad B cancels a capture started by keyboard/mouse.
Options UI (optionsoverlay.c/h):
- D388: nested Input -> Bindings / Gameplay -> HUD roots in both
F10 and front-options; redundant Low-end toggle retired (FPS cap /
MSAA remain independent, stored values preserved on migration).
- D391: hit bands follow rendered glyph baselines; dead zone +
out-of-card click rejection.
- D396: F10 pad B backs/closes a page (closes at root), A/X
accepts/toggles without shifting sliders; left/right still adjust.
All behind #ifdef PORT; N64 input/menu paths untouched. M7 (D387
AllUnlocked save-safety) intentionally NOT in this wave.
bondview2.c reads crouchDown = insightaimmode && (buttons & (D_JPAD|D_CBUTTONS))
(5318/5579): crouch is C-down WHILE IN AIM MODE, never the B button. M3's
IA_CROUCH -> GE_CONT_B (and the CrouchMode=1 latch) was inert, so crouch
never fired in the GEPD layout. Hold + latch now emit GE_CONT_D (0x0004).
N64-faithful side effect: S (IA_BACK, also C-down) crouches while aiming.
User re-test: aim (RMB/Left Shift) + Left Ctrl; hold or F10 Toggle.
M2 (the wave's only rule-2 item, sign-off recorded via user direction):
#ifdef PORT hook in gunfire.c gunDrawSight -- Video.CrosshairHide skips
the draw, Video.CrosshairColor tints the RGB (7 named tints, 0 = the
N64 white); non-PORT builds keep the original literals. F10 GRAPHICS
rows + kResetDefaults.
D374 (M3 playtest fix, user decision): GEPD is now the DEFAULT key
layout (Input.Layout 0 = GEPD, 1 = N64-era binds; a mouse FPS layout is
the natural default and the N64 never had a keyboard). The preset is
now the true effective default: only EXPLICIT per-key [Input.Bind]
overrides beat it; persisted values equal to the N64 defaults count as
unset. This fixes the playtest bug where a stale persisted
Fire=Left Ctrl masked the GEPD preset and made Ctrl fire+crouch at the
same time. Boot log reports the applied layout. One-off: pre-D374
Layout=1 (ex-GEPD) inis mean N64 now -- flip to 0 (release note).
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).
D345 (a-g): re-assert window on menu change; cursorToItem skips snap while
the 1:1 pointer is live; W/S emit stick in menus under NaturalPitch;
F10-style discrete row stepping + arrow left/right value adjust replace raw
drift (first pass shipped broken, fixed); screen-placed cursor parks in the
left gutter off the row text.
D346 (+b): labels/values aligned to Nightdive/Turok + PD-port conventions
(title-case On/Off, Frame rate cap, Anti-aliasing/None, Anisotropic
filtering, units in values not labels, deadzone as %, self-explanatory
auto-FOV toggles); VIEW section merged into VIDEO (MAX_PROWS 12->15,
ROW_DY 20->18). Display-only: config keys and stored values untouched.
Port-only; no src/game edits. User playtest verified.
Replaces the M2 panel. Built like the Cheat Options screen: open dossier
(tabs + blank CLASSIFIED page), black Zurich ink via frontPrintText, GE's
highlight box, PREVIOUS tab as back, the game's crosshair. Page 0 is a
numbered section list; each section lists the F10 overlay's rows with a
value column (ON in the cheat red) and slider bars.
Game code (Rule 2, user sign-off 2026-09-27): MENU_PC_OPTIONS enum value
and five one-line dispatch cases, all #ifdef PORT. The file-select label
now enters the screen via frontChangeMenu. The M2 button hand-off in
input.c is gone; F10/Select don't open the overlay on this screen.
The file-select Options label opens a panel drawn in the same hook:
sections as tabs, rows from the F10 overlay's table via an exported row
API, the game's crosshair as pointer (input.c passes the stick, hands the
buttons to the panel). Quit to desktop now goes through videoRequestQuit.
Also repairs two escaped-newline typos in the D343 docs.
One Rule-2 #ifdef PORT draw hook in constructor_menu05_fileselect (user
sign-off 2026-09-27). The label's hit test and open request live in
optionsoverlay.c; the open is applied on the input thread. While the
overlay is open on file select, g_MenuTimer is held at 0 so the screen no
longer drops to the legal screen after 30 s.
optionsoverlay.c called atoi and lround through implicit int declarations
(GE's stub stdlib.h/math.h shadow the CRT, D324 class); atoi joins the
other K&R decls in port/shim/stdlib.h, lround gets a local prototype.
Windows + Linux builds clean of both warnings.
GE_INPUTSCRIPT gains ZHOLD/ZREL (sustained fire) for automatic-weapon
audio repros. D207/D241: headless burst-fire at 60 vs 30 fps with the
alarm forced logged zero voice drops at both caps; owed a real-play
capture. Golden gate 3/3.
input.c called SDL_SetRelativeMouseMode / SDL_ShowCursor from the scheduler
thread (every controller poll) and from the host thread's focus/click
handling at the same time. SDL's mouse API is not thread-safe; on Windows
relative mode starts/stops the raw-input thread, and clicking back into the
window after being unfocused is exactly when both threads flip it - the D287
freeze trigger. Callers now record the wanted state; inputApplyMouseRequests()
applies it on the host thread from videoPumpEvents(). Queue-deadlock leads
ruled out statically. Win+Linux build clean, golden 3/3; freeze re-test owed.
Scales the mouse accumulator's screen mapping and edge-scroll threshold; aim
entry adopts through the game's screen mapping so it never jumps. Measured:
PC 98%/72%, N64 65%/49%. Golden 3/3; Win + Linux builds.
- Input.AimMode 0=N64 (D332 PD-integration, default) / 1=Centred (PC); legacy
AimStyle/PdMouseAim migrated; hidden Input.AimLegacyGepd fallback.
- Mouse aim routing follows RMB/Aim, Q/L (the game's second aim button), and
insightaimmode (Toggle aim, pad): removes the accidental Q 'FPS aim'.
- The PD game hook only feeds the mouse while the mouse is the active aim
device, so pad/keyboard stick aim is not swallowed.
- Harness: GE_INPUTSCRIPT MDX/MDY/RHOLD/LHOLD/RREL + GE_AIMLOG; measured old
vs new: same screen range and edge-scroll, jitter equal headlessly.
Golden 3/3; Win + Linux builds.
HUD elements now keep 4:3 proportions via fast3d's EXT aspect modes (the PD
pattern): watch, gauge bars, messages/title card, upper text and countdown
CENTER; ammo RIGHT (right hand) / LEFT (left hand). The front end is
pillarboxed (whole menu_jump_constructor_handler CENTER) and the menu
pointer is mapped into the pillarbox. port/include/hudaspect.h emits only
when native widescreen is on and the window is wider than 4:3, so 4:3 and
native-off stay byte-identical. Rule-2 approved.
Verified with 16:9/21:9 captures (HUD, watch, title card, legal, file
select, gun barrel); golden 3/3; Win + Linux builds. Gun-barrel circle
leaks into the bars while sliding in -> GRAPHICS-BACKLOG D335a.
One F10 selector ("RMB aim style") instead of stacking toggles. CENTRED
routes RMB-held mouse motion to hipDirectCompute (FOV-scaled) and emits no
aim stick, so the game's aim integrator keeps the crosshair centred: the
behaviour the reporter found on Q (game L button), now on RMB. PD DAMP is
D332 (PR #96); legacy Input.PdMouseAim=1 migrates to it. Default N64 =
unchanged. Port-only.
Windows + Linux builds clean; bunker1 golden 3/3. Feel check owed.
Opt-in Input.PdMouseAim (F10 "PD mouse aim"): in aim mode the port only
accumulates the mouse position and the game's own crosshair integrator is
driven by it with PD's mouse damp (0.01), instead of GEPD's per-poll
overwrite fighting the weapon's ~0.8 per-tick damp (write*damp^k jitter).
Hook: #ifdef PORT block in sub_GAME_7F067FBC (aim-mode branch only),
identity when off; Rule-2 sign-off given by the maintainer.
Integrated locally from PR #96 with review fixes: finding renumbered
D337 -> D332; GE_PDMOUSEAIM lookup cached (GE_ENVSTR; it ran every poll
and aim tick); "live env" comments corrected. Windows build clean; bunker1
golden 3/3. Feel A/B owed.
Auto-Aim / Look Ahead "wouldn't stay on": in the watch, current_menu is
still RUN_STAGE and hipDirectCompute declines on its !watch gate, so mouse
motion fell through to the legacy stick fallback (~3 counts/px/poll). A
~15 px flick crossed the watch's +/-0x2E threshold, which on a selected
option picks OFF/ON (game_option_toggle_input) and otherwise flips pages.
The mouse now contributes nothing while the watch is open or paused;
keyboard and pad drive the watch as before. Save/load round trip ruled out.
Build clean; bunker1 golden 3/3. Mouse feel-check owed (needs real motion).
- D302: fix a reintroduced D250-class perf regression -- 4 uncached getenv()
calls added by D236 diagnostic passes 10/12/14/16 were running on every
triangle draw in gfx_sp_tri1; cached with the established pattern.
- D304: F10 mouse-sensitivity menu overhaul -- widened Input.AimModeSens'
clamp to match Input.MouseTurnSpeed, then collapsed both plus the
aim/turn link toggle into a single Input.MouseSensitivity control per
user request (avoids decoupling into a confusing state). Fixed two real
overlay bugs found along the way: overlayUpdateScroll() bottom-anchored
the clicked row on every click (not just when off-screen), and
overlayRowAtY()'s hit-test scanned rows past the last one actually drawn,
letting a click near a scroll boundary silently hit the wrong row
(reproduced as "All unlocked" triggering "Quit to desktop" instead).
- D225: GE_DEBUG_UNLOCKALL=1 dev-tooling toggle (file2.c), default off.
- D164/D76: docs-only correction -- the legal-screen one-line fix was
already committed 8dfc578f4; findings.md just never caught up.
- D252: root-caused and fixed a 6+-session-old rainbow-particle bug --
explosionRenderPart() wrote 5 vertices into a 4-vertex allocation,
corrupting the next particle/effect's data in the shared vertex pool.
User-confirmed live: "Rainbow went away."
- D305: Steam Deck SIGSEGV recurrence past D285's own fix -- sndHandleEvent's
preempt-scan dereferenced its list-walk pointer before checking it for
NULL. Added the missing guard. User-verified crash-free on the Deck
across multiple sessions, including toggling Game.AllUnlocked off/on.
D236 pass-16 diagnostic (GE_D236RAW, bg.c) carried along from a prior
session's in-progress investigation; unchanged, env-gated, zero-cost unset.
D243/D173/D292 (Bond cutscene family) -- root cause found and fixed:
bondview2.c's cutscene body-model buffer layout reserved space using a
hardcoded 32-bit sizeof(Model) literal that never accounted for 64-bit
pointer widening (real size 232 vs reserved ~188 bytes), so the model's
own animation-data buffer overlapped its tail fields (playspeed/animrate/
unkac/unkb0/unkb4), corrupting them on every cutscene body-model creation.
Fixed by computing the real sizeof(Model), matching the identical idiom
already used one line above for ModelFileHeader. User-confirmed live.
Follow-up camera-tracking fix: replaced the old shake-workaround freeze
(which also blocked legitimate per-shot tracking) with a write-every-tick
scheme plus an instant re-seed of the look-at filter's accumulator at each
teleport, eliminating the old catch-up shake without freezing anything.
Second bug found in the same review: mouse-look was never gated during
scripted/no-control camera states (an already-documented but never
implemented safety gate in port/src/input.c's hipDirectCompute). Fixed by
adding the missing check, reusing gameScriptedCameraActive() (renamed from
D243's d243mCutsceneActive, since it's no longer D243-specific -- also
used by model.c's M-183/M-185 clamps, which were separately found to be
dead code for real players (gated on the diagnostic env var, not just
camera mode) and fixed to be unconditionally active.
D236 (Surface tree wall) pass 12: new mechanism found (both competing
texture classes are Z_UPD=0 decal-mode draws, so on-screen precedence is
decided by draw order, not depth) and a headless capture finally landed on
the actual bugged view -- but that view has zero tree-class triangles
present, so the mechanism couldn't be tested there. Two new env-gated
diagnostic probes (GE_D236ORDER, GE_D236ZFIX) left in tree for the next
pass; not a fix, not v0.3.0-blocking.
All findings.md summary-table rows touched this session cross-checked
against their full entries and corrected where stale (D219/D252/D246/D247
had been fixed in a prior session but their index rows still said
OPEN/NOT-A-BUG).
D300: new hipDirectCompute() in port/src/input.c writes vv_theta/vv_verta
directly for hipfire mouse-look, mirroring GEPD's linear px->degree model
and the existing D194 aim-mode pattern -- replaces the N64 stick-curve
routing that saturated at ~13 px/poll (issue #89). Ported GEPD's
dead/watch/pause safety gates. Input.MouseDirectLook=1 default, escape
hatch to the legacy path. User-confirmed better feel in a live playtest.
D243: added a permanent onscreen-visibility probe (D243M: onscreen,
chr.c) to test whether chrTick's posIsOnScreen gate explains the
abseil cutscene's frozen render position (M-159). A live capture
refuted that and found the real mechanism instead: chrTick overwrites
prop->pos with a stale value via its own chrUpdateAnim/modelTickAnim
animation-position logic, racing the AI script's scripted write
(M-160). A decisive throwaway experiment neutralizing that overwrite
produced no visible change in a live playtest (M-161) -- the mechanism
is real but isn't the dominant visible cause. Experiment reverted;
D243 parked pending a new lead after ~15 session-passes without a fix.
Docs: findings.md/findings-index.csv/porting-notes.md/GE-ENV-PROBES.md
updated throughout.
Menus read the same stick_x/stick_y channel as in-level gameplay, and
naturalPitchMode (SOLITARE-style, default-on) routes that channel to
the right stick for continuous look. Since front.c's menu nav shares
the channel, it silently inherited the right-stick routing, which is
unintuitive on a modern pad/Deck outside a running stage. The gamepad
poll already computed an unused padMenuMode flag for shoulder-button
gating; reused it to always source stick_x/stick_y from the left
stick outside RUN_STAGE, regardless of naturalPitchMode. Port-layer
only, inert during gameplay -- verified via -level_09/-level_20
unregressed; in-menu feel needs a human controller playtest.
Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
User playtest reports against v0.2.0:
- D259: geEepromPatchAllCheats left an all-zero fresh save at volume 0
(silence) instead of seeding max volume like fileResetSave would.
- D260: swap wheel-up/wheel-down to match the PC wheel-menu's on-screen
order (was inverted vs. user expectation).
- D261: watch-menu stick-held navigation had no repeat past the smooth-scroll
band, so holding W/S or a pegged stick crawled one item per tap; added a
bounded auto-repeat.
- D263: GE_INPUTSCRIPT stick tokens were applied after the stick was already
written out and silently discarded (test-harness fix, kept from the D263
Facility/Ourumov investigation, which itself found no port bug).
- D264: watch item-preview ModelRenderData template read across two globals
that no longer sit adjacent on PC, zeroing renderdata.flags and gating all
geometry emission in the preview's subdraw pass.
Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
Controller-only machines (Steam Deck) had no way to open or navigate the
F10 options overlay -- it was keyboard+mouse only. Now:
- Select (BACK) rising edge toggles the overlay, tracked across both the
open and closed poll paths (padSelectPrev) so a held button cannot
re-toggle on the transition. BACK is read nowhere else in the port.
- While open, D-pad or left stick up/down moves the selection, A/X step
forward (the Enter equivalent), B/Y step back, Start closes. The gamepad
bits are OR-ed into the existing keyboard edge logic, so repeat/clamp/
scroll behaviour is identical; input.c already swallows pad 0 while the
overlay is open, so nothing reaches the game.
- inputPadButton/inputPadAxis accessors expose raw SDL pad state to the
overlay (unaffected by our logic-layer swallow).
Smoke-tested headless with GE_OPTIONSOVERLAY=1 and no pad attached.
Two defects found in the local review of the unpushed v0.2.0-pre work:
1. input.c: padShoulderPrev was only updated outside menu mode, so a
shoulder held across the menu->game transition fired a spurious
weapon cycle on the first gameplay poll. Edge state is now tracked
every poll; emission stays suppressed in menus where A means select.
2. ci.yml release job: the notes template substitutes <version> with the
raw tag (v0.2.0-pre), but bundle-{linux,win}.sh strip the leading v
when building asset names -- the published notes would have linked
goldeneye-pc-port-v0.2.0-pre-* assets that don't exist. Substitute
the stripped form (NOTES_VERSION) instead.
Steam Deck target. LS move/strafe + RS look + RT fire + LT aim were
already in place (D194/D238 naturalPitchMode); this completes the
scheme with shoulder weapon cycling per the console re-releases:
RB rising edge = next weapon, LB rising edge = previous weapon.
Cycling rides the game's own A-line formulas (bondview2.c
weaponForwardOffset/weaponBackOffset): a fresh A edge cycles forward;
A+Z presented on the same tick cycles backward -- the identical trick
the mouse-wheel path uses (D223), which also guarantees no stray shot
(triggerOn is gated off while invButtons is held). Edges are emitted
for exactly one poll so holding a shoulder cannot latch A and block
firing; both are suppressed in menu mode where A means select.
A/X = action/use, B/Y = crouch/cancel (unchanged); LB no longer maps
to lean. README controls table + release notes updated.
Per the M-82/M-106 user decision: click-to-lock is now the only cursor
capture mode. Port-only deletion, no game-code touch:
port/src/input.c drop mouseCaptureMode + Input.MouseCaptureMode key;
simplify reconcileGrab/notifyClick/releaseCapture/
aimGepdCompute guards; delete the !haveAbs relative
P-controller menu path and its dead state (menuEst/Tgt,
menuCursorStep, MENU_P_GAIN/EST_DELTA/MOUSE_TO_PX);
drop the unused inputMouseCaptureActive() export
port/include/input.h update WI-1 comment, remove the dead declaration
port/src/optionsoverlay.c remove the 'Mouse capture' toggle row + labels
Existing ini files that set Input.MouseCaptureMode are ignored (config layer
skips unknown keys). Closes D192 (outer grid cells unreachable under mode 0)
and D239 (always-grab parity) by removing the bugged path rather than
patching it.
Known issue (logged in D251): intermittent ghost repeat of a top row at the
panel bottom in true 4K fullscreen; local repro clean, MSAA-off bisection
pending with the user.
Mirrors MouseInjectorPlugin (games/goldeneye.c) in model: mouse motion
drives a crosshair POSITION accumulator (clamped +/-5.16), written
straight into crosshair_x/y_pos + gun_azimuth_angle/turning via GEPD's
formulas; the view only scrolls past 72% toward the screen edge
((ratio-0.72)*475*dt*fov/basefov); no look stick is emitted while
aiming; on release the game's own damping eases everything to centre.
Key correction vs the earlier attempt: GEPD patches NOTHING in
bondview2's damped crosshair update (caclulate_gun_crosshair_position_
rotation) -- the per-frame overwrite simply wins over the damping, and
that function is what derives crosshair_angle (HUD crosshair + aim ray)
and field_FFC (arm/gun offset in gunUpdateAndFire). The temporary
portAimOverrideActive() guard is removed; bondview2.c carries only an
explanatory comment. No game-code behaviour change.
Knobs: Input.AimAbsolute (1=GEPD mirror, 0=legacy velocity stick),
Input.GepdSens (default 20). Field mapping vs GEPD's N64 RAM offsets
verified with offsetof (constant 0xBE0 delta = 32->64-bit pointer
expansion).
Open: crosshair/arm 'spazz' glitch, aim-mode FPS drop (perf.ps1),
sensitivity tuning -- see docs/HANDOFF.md M-122.
The cursor-anchored chase model swung the aim out to where the cursor
sat, which is not how GEPD feels. Replace it with the model the PD port
itself uses (mouseDX/DY + sensitivity): while RMB is held and the cursor
is free, per-poll cursor DELTA rotates the view by the same screen angle
the cursor swept (viewport width <-> full hFOV, height <-> fovy). The
crosshair stays locked at centre; stop moving and the game's own speed
decay coasts the view to rest where it is. Scope zooms compose for free
(angle-per-pixel scales with the live fovy). Dithered quantization keeps
fine sub-level movements smooth; no target, no chase, no snap-back.
Both axis signs were wrong (verified against bondview2.c plant code +
live GE_INPUTLOG trace): a right turn INCREASES vv_theta and looking
down DECREASES vv_verta. The anchor placed the frozen target at the
mirror image of the cursor point, so the controller steered through
the real point and ran away to the screen corner.
- anchor: tgtTheta = curT + offYawR, tgtVerta = curV - offPitchD
- stick map: errT>0 -> +stick (turn right), errV<0 -> +stick (look down)
- per-poll absaim log now includes cursor/ndc/fov for tuning
Injection test (RMB hold + 15 mouse steps): target tracks cursor,
error converges to deadband (0.7deg), stick->0, view holds.
Holding RMB (or the Aim bind, default Left Shift) in a stage now releases
the click-to-lock grab and drives the aim stick from a closed position
loop on the live camera (vv_theta/vv_verta read via player.h): the
crosshair chases the point under the free OS cursor and HOLDS it --
stationary cursor = stationary aim, even while walking. Release re-grabs
with no snap.
- Target derived from cursor NDC against the real render viewport rect
(viGetView*), portScaleFovY so scope zooms compose for free; re-anchors
on every cursor move or FOV change.
- Coast prediction (game's 0.05*mult/tick speed decay) prevents overshoot;
first-order dithering maps continuous demand onto the game's 10 discrete
stick levels for precision work (shoot-the-lock etc.).
- LMB fires while the cursor is out; ESC still releases capture; menus and
MouseCaptureMode=0 unchanged. New knob: Input.AimAbsolute (default 1).
Also: -I include shadows <math.h> with the game's constants-only header,
which left fabs/pow/tan in input.c on implicit declarations -- new
port/include/port_math.h pulls the real system math.h via #include_next.
D194(a): bondview2.c's aim-mode turn speed formula, (stick-60)/10 clamped
to 1.0, saturates at stick=70 -- the port's [61, 60+AimBand] mapping (up
to stick=80) was treating an effectively-capped range as proportional,
so a few px/poll already maxed out perceived sensitivity ("bang-bang").
Curve-map px/poll onto the game's real [61,70] band with a configurable
gamma (Input.MouseAimCurve, default 160) for a wider low-speed zone.
D238: added Input.MouseSensitivity as a master multiplier over both
MouseAimSpeed and MouseTurnSpeed/HipfirePitchSpeed, so one knob scales
overall feel; legacy per-axis knobs remain an independent trim.
D194/D238 vertical-vs-horizontal asymmetry: GE's default 1.1/HONEY
scheme only exposes hipfire pitch as a digital C-button, so the port
duty-cycled it (D166) to approximate continuous motion -- structurally
incapable of matching yaw's genuinely continuous analog mapping. Fix:
force GE's own 1.2/SOLITARE control style (cur_player_set_control_type,
an original player-selectable scheme, not new game logic) every poll,
which gives pitch the same continuous analog treatment as yaw
(canNaturalPitch/canNaturalTurn in bondviewProcessInput). Trade-off:
SOLITARE moves movement from the analog stick to digital step buttons,
so WASD forward/back now fires C-up/C-down instead of the stick (strafe
was already C-button-driven in both schemes, unchanged). Gamepad stick
roles swapped to match (left = digital-step movement, right =
continuous look) so the global scheme switch doesn't break pad play.
Escape hatch: Input.NaturalPitch=0 reverts to 1.1/HONEY + the D166
digital-pulse hack, same pattern as the existing MouseDtDecouple knob.
Verified: build green, tools_pc/verify.sh bunker1 PASS (worst_cell=0).
Needs a human A/B playtest for feel -- headlessly unverifiable.
Claude-Session: https://claude.ai/code/session_016DQUKGLZxFtPy52MkKXXog
Part (b) of D194 ("sensitivity correlates with movement/frame rate"):
input.c converted raw accumulated mouse px directly to stick counts,
treating "one inputComputePad(0) drain" as a fixed-size time unit even
though the real interval between drains drifts with render/scene load.
Same physical hand motion -> different emitted turn rate depending on
how ragged the poll cadence gets.
Fix: measure real elapsed time between drains with a monotonic clock
(SDL_GetPerformanceCounter) and rescale the accumulated px by
(1/60s / dtActual), clamped [0.25, 4.0], before MOUSE_TURN_GAIN/AIM_GAIN
apply -- so "px this poll" always means "px for a nominal 1/60s tick"
regardless of actual poll-to-poll timing. 1/60s matches D193's own
measured steady-state sim tick rate. Applied only to the aim-mode and
hipfire gameplay-look branches; menu-cursor paths (D165/D169's
poll-count-based P-controller) are untouched.
New Input.MouseDtDecouple config knob (default 1, escape hatch to the
prior frame-coupled behaviour) + a GE_INPUTLOG lookdt trace line.
Part (a) (aim-mode response curve) and the CONTROLLER_CONFIG_SOLITARE
controldef switch are deliberately NOT touched here -- both are design
decisions (D238 overlaps significantly with part (a)) that should get
the user's own interactive feel-testing rather than be baked in blind.
Build green (241/241), verify.sh bunker1 PASS (crash-free). Cannot be
feel-verified headlessly -- needs a human playtest (A/B against
Input.MouseDtDecouple=0).
Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG
* fix(D223): directional mouse-wheel weapon cycling
Wheel-up keeps the existing 2-poll A pulse (weaponForwardOffset: fresh
A edge, Z released). Wheel-down now runs a 2-poll backward sequence:
A-only poll, then A+Z_TRIG -- the fresh-Z-edge-while-A-held combo that
weaponBackOffset requires (bondview2.c:5337-5340); ordering matters
because the edge is computed against the game's own previous poll.
moveData.triggerOn (bondview2.c:5419) requires A to be released, so
the synthesized Z edge can never fire a shot.
Roles are stable: the port forces CONTROLLER_CONFIG_HONEY
(bondview.c:1484), whose scheme gives invButtons=A_BUTTON /
shootButtons=Z_TRIG -- the same roles the pre-fix forward pulse
already relied on. While RMB aim is held the game flips controldef to
KISSY, under which neither direction cycles -- identical to the old
forward-only behavior in that state (pre-existing, not a regression).
Port-only; no src/ changes.
* docs(D223/D196): mark D223 FIXED (M-105); correct stale D196 status
D223 row now records the implemented fix and its known aim-mode
limitation. D196 was already fixed and merged (a17d5466, the
prescribed 1-line applyCursorVisibility() after reconcileGrab()) but
its status cell still led OPEN -- same stale-claim class the roadmap
flags; corrected. findings-index.csv regenerated.
* D223 follow-up: fix wheel-down backstep racing the real game tick
User playtest (M-105 cont.) found the backward-cycle combo unreliable:
PP7->AK47->PP7->wheel-down sometimes correctly landed on melee, but
sometimes landed back on AK47, and separately AK47->wheel-down sometimes
skipped past PP7 straight to melee.
Root cause: the wheel-down pulse staggered the synthetic 'hold A, tap Z'
combo across two polls (A alone, then A+Z one poll later). Whenever those
landed in two SEPARATE game ticks -- which happens unpredictably depending
on real-time poll/tick alignment (same D117-class nondeterminism as
everywhere else in the port) -- the 'A alone' tick was itself a fresh A
edge with no Z held, which bondview2.c's weaponForwardOffset formula (both
control-scheme sites) reads as a genuine cycle-FORWARD request, firing a
real, visible weapon change *before* the correcting backward tick ran.
Confirmed with an instrumented trace build: every wheel-down after the
very first one in a session mis-fired as a forward step.
Fix (port/src/input.c only, no game-logic touched): present A and Z
together from the first poll instead of staggering them, so whichever
single tick samples the 0->(A|Z) transition sees them rising
simultaneously. weaponForwardOffset requires Z NOT held, so that's
unambiguous -- only the backward path can fire, regardless of tick
timing. moveData.triggerOn (actual fire) is separately gated off while
A/invButtons is held, so this doesn't introduce an accidental shot.
User-verified: repeated up/down cycling through PP7/AK47/melee/Skorpion
in both directions with no more mismatches.
port/src/input.c: 12 gameplay actions (Forward/Back/StrafeLeft/StrafeRight/
TurnLeft/TurnRight/Fire/Aim/Action/Cancel/LeanLeft/Start), each bound to a
comma-separated list of up to 4 SDL scancode names via Input.Bind.* keys.
inputRebuildBinds() parses them in inputInit() after configLoad();
SDL_GetScancodeFromName resolves names, unknowns warn + skip. The hardcoded
keyDown(ks, SDL_SCANCODE_*) block in inputComputePad is replaced with
actHeld(ks, IA_*). Mouse buttons (fire/aim) stay hardwired.
File-driven, no UI -- matches the PD port. A fresh ge007.ini gets the full
[Bind] block with defaults visible.
Defaults reproduce the previous FPS layout exactly: all 20 default key names
round-trip to the identical SDL_Scancode the old code used (0 mismatches in a
SDL_GetScancodeFromName test). Build clean; verify.sh dam/facility/bunker1
PASS crash-free.
findings.md D214 + index; QOL-INVENTORY row marked done.
inputSuspendForOverlay() forces the OS cursor visible while the overlay is
open; on close nothing re-hid it unless reconcileGrab() happened to re-grab
(only true if the user had already clicked-to-lock in a stage). Opening the
overlay from a menu, or from a stage not yet click-locked, left the cursor
stuck visible over the game's own crosshair.
Fix: re-assert applyCursorVisibility() once per controller-0 poll, right
after reconcileGrab(). Unreachable while the overlay is open (early return
above), so it only fires once the overlay has closed and self-heals every
poll thereafter. Port-only, no render change; verify.sh bunker1 PASS.
Owed: 5-second visual check - open F10 from a menu, close it, cursor gone.
- Fix invisible text: drawText passed the measured text w/h into textRender's
width/height args, which are the on-screen CLIP rect (not the text size), so
every glyph failed the clip test. Pass the full 2D viewport like bondview2.c's
debug-text path.
- Mouse UI: hover-to-highlight; left-click in the value column toggles/cycles;
drag sliders; red X close box (clicking a row's label only focuses it, so
selecting can't flip a toggle). inputSuspendForOverlay() forces the OS cursor
free + visible while the overlay owns the mouse.
- New rows: Fullscreen (live) and Resolution (windowed presets filtered to the
desktop, live resize + recenter). SDL window ops are posted from the
scheduler thread and drained host-side in videoPumpEvents.
- Layout recomputed for the wide BankGothic font: right-aligned values, wider
row pitch, all 13 rows fit the ~240-unit 2D space; nothing clips.
Port-layer only. Links clean; runtime-verified on a real display (front end +
-level_09): layout, hover/click/drag, live VSync/filter/fullscreen/resolution.