From 17891cc4682f3a59c072c7b4100e9c2266a03ad0 Mon Sep 17 00:00:00 2001 From: dolent Date: Tue, 29 Sep 2026 13:26:21 -0700 Subject: [PATCH] feat: D436 -- persistent crosshair, opt-in (Video.CrosshairPersistent; F10 > Gameplay > HUD > "Crosshair when not aiming") The native sight is drawn at crosshair_angle outside aim mode too: one masked condition in gunDrawSight (#ifdef PORT, beside the D373/D381 hooks) ignores GUNSIGHTREASON_NOTAIMING when the option is on; every other reason and the MP menu still hide it. Default off is byte-identical (frames 600/1200/1800); hipfire on/off differ only in the sprite region; aim mode on/off identical. Findings D436. Co-Authored-By: Claude Fable 5.1 --- docs/dev/findings-index.csv | 1 + docs/dev/findings.md | 33 +++++++++++++++++++++++++++++++++ port/src/optionsoverlay.c | 2 ++ port/src/video.c | 3 +++ src/game/gunfire.c | 12 ++++++++++++ 5 files changed, 51 insertions(+) diff --git a/docs/dev/findings-index.csv b/docs/dev/findings-index.csv index 4e1c35b0..85e4992f 100644 --- a/docs/dev/findings-index.csv +++ b/docs/dev/findings-index.csv @@ -285,3 +285,4 @@ D405,CLOSED,"fast3d/OpenGL: MSAA above the driver max black-screens; two glBindF D406,RESOLVED,front-end PC Options screen:,"RESOLVED (2026-09-28, port-layer only, Steam Deck-validated):" D407,PARTIAL,Tanks cannot be boarded/exited on PC —,"PARTIAL (fix landed in v0.4.0, port-only):" D408,OPEN,Game.SkipIntro skips the post-mission failure dossier:,"OPEN (cosmetic, not root-caused past the handoff site)." +D436,FIXED,"Persistent (hipfire) crosshair, opt-in:",FIXED (shipped 2026-09-29; rule-2 sign-off: diff --git a/docs/dev/findings.md b/docs/dev/findings.md index f23ee5ee..f36fa406 100644 --- a/docs/dev/findings.md +++ b/docs/dev/findings.md @@ -693,6 +693,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | D406 | **front-end PC Options screen: long sections (INPUT, 17 visible rows) run off the bottom of the 440x330 paper, and rows past the silent 14-row page cap (Crouch mode, Reset to defaults, Bindings…) are unreachable from the front screen** (user report 2026-09-28, v0.4.0 pre-push review) — full `## D406` entry at file tail | RESOLVED (2026-09-28, port-layer only, Steam Deck-validated): in-section pagination in `frontoptions.c` (11 rows/page; the vertical stepper crosses page boundaries both ways; dim 'Page p/N' marker + bottom hint on every page; slider bars start past long labels; F10 overlay untouched). D406b/c/d same-cycle refinements (label/track overlap, hint visibility/wording, hint position). | | D407 | **Tanks cannot be boarded/exited on PC — the v0.4.0 use/reload split (D378/D393) removed the B-button tap the engine's tank handlers in `bondview2.c` consume (user report 2026-09-28, Runway/Streets)** — full `## D407` entry at file tail | PARTIAL (fix landed in v0.4.0, port-only): present `GE_CONT_B` on the use (E / pad A) rising edge only while `g_PlayerIsInTank == 1` or `g_BondCanEnterTank != 0`; E keeps its D378 no-reload-fallback semantics elsewhere, N64 layout unaffected. D407(b) same cycle: front PC Options page-edge highlight clamp + mouse-wheel / W-S paging (wheel queue consumed on the menu, D223); its wheel mapping shipped inverted and was fixed to match W/S (wheel up = step up). D407(c): board-animation lockout, menu-accept B gate in tank states, in-tank aim routed through the legacy velocity stick + `Input.TankAimScale` knob. RESOLVED 2026-09-28: all port-only (src/game zero-diff), 10 TANKDBG probes stripped, release binary verified clean, user live tank drive signed off (board + exit OK, aim feel good). | | D408 | **`Game.SkipIntro` skips the post-mission failure dossier: with it on, entering a level then aborting (watch Z+A) or dying (KIA) returns straight to the menus — no REPORT / "Mission status: KILLED IN ACTION / ABORTED" screen** (user report 2026-09-28; save/AllUnlocked ruled out) — full `## D408` entry at file tail | OPEN (cosmetic, not root-caused past the handoff site). v0.4.0 decision: SkipIntro stays EXPERIMENTAL / not recommended for regular users — F10 row relabelled "Skip intro (EXPERIMENTAL)". | +| D436 | **Persistent (hipfire) crosshair, opt-in: `Video.CrosshairPersistent` / F10 > Gameplay > HUD > "Crosshair when not aiming" draws the native sight at `crosshair_angle` outside aim mode (2026-09-29).** — full `## D436` entry at file tail | FIXED (shipped 2026-09-29; rule-2 sign-off: user direction; presentation-only masked condition in `gunDrawSight` + `port/src/video.c`). Default off byte-identical; aim mode unchanged; netplay SH identical mixed off/on. | Phase 2 replaced the Phase-1 demo loop with the real `mainproc()` on real OS @@ -15524,3 +15525,35 @@ included; D281 keeps the shipped mitigation + disclosure, zero-saves root cause **v0.4.0 decision (user, 09-28):** `Game.SkipIntro` stays **EXPERIMENTAL — not recommended for regular users yet**, alongside its older unrooted audio-break report (D216 status note). The F10 row is relabelled "Skip intro (EXPERIMENTAL)" (`port/src/optionsoverlay.c`, mirroring the D387 AllUnlocked labelling precedent); the config var, default (off), and `lv.c` hook are unchanged. **Status:** OPEN — cosmetic (no crash, no data loss; the mission still records as failed/aborted and the menus are fully functional). Root-causing needs a side-by-side of front-end state (`current_menu`, `g_CurrentStageToLoad`, `mission_failed_or_aborted`, `g_isBondKIA`) on the fail return with SkipIntro off vs on — candidate for v0.5.0 alongside the D216 audio half. + +## D436 — persistent (hipfire) crosshair, opt-in: the native sight drawn outside aim mode (2026-09-29) + +**Status:** SHIPPED (offered from the netplay line): port-only logic + one presentation-only `#ifdef PORT` condition in `src/game/gunfire.c` `gunDrawSight` (rule-2 sign-off: user direction 2026-09-29, "constant on screen crosshair that could be enabled or disabled"; same class and same function as upstream's D373/D381 hide/colour/size/style hooks). Default off = N64. + +**Spec:** `docs/dev/RESEARCH-FREE-AIM.md` option A'. The N64 sets `GUNSIGHTREASON_NOTAIMING` every tick from `moveData.aiming` (`bondview2.c:5753`) and `gunDrawSight` draws only when `gunsightmode == 0`. Every shot, hipfire included, leaves from the screen point `crosshair_angle`, recomputed every tick in both modes (auto-aim pull, stick lag), so the native sprite drawn there in hipfire shows the true aim point. `gunsightmode` has no reader but `gunDrawSight` (draw-only state). + +**Implementation:** +- `port/src/video.c`: `Video.CrosshairPersistent` (0/1) next to the other crosshair options; `portCrosshairPersistent()`. +- `src/game/gunfire.c` `gunDrawSight`: under `#ifdef PORT` the visibility test becomes `(gunsightmode & ~(persistent ? NOTAIMING : 0)) == 0 && !mpmenuon`. Every other reason still hides the sprite: `GUNSIGHTREASON_1` (the "Sight on screen" option in solo, the lobby-agreed `sight` flag in MP/netplay), `NOCONTROL` (cutscenes), `DAMAGE` (the damage/death sequence) and the MP menu. In aim mode nothing changes (one sprite, the game's own). Non-PORT builds keep the original condition byte-identical. +- `port/src/optionsoverlay.c`: HUD row "Crosshair when not aiming" (Off/On) after "Show crosshair", plus its reset default (0). The front-end PC Options screen picks the row up through `optionsRowCount()`. + +**Netplay / fairness:** render-only and per machine (nothing is written; `texSelect` + a texrect, the same variance upstream's "Show crosshair" off already introduces). A hipfire sight that visibly slides onto auto-aim targets is extra information in a match; today it follows the lobby's agreed per-player `sight` flag (a player whose sight is off sees nothing either way). Whether the lobby should pin the persistent mode too is a **user decision** (RESEARCH-FREE-AIM decision 3). + +**Verification** (headless, silent, llvmpipe, mp-sandbox data, `GE_FIXEDSTEP=1 GE_TICKINPUT=1 GE_RSEED=12345`; sandboxes `build-pc/xh-*`): + +| Check | Result | +|---|---| +| F10 > Gameplay > HUD auto-opened (`GE_OPTIONSOVERLAY=3 GE_OPTIONSOVERLAY_SECTION=__HdrHUD`), frame 240 | row renders between "Show crosshair" and "Crosshair colour", value Off; page self-check count 12 <= 14 | +| Solo `-level_09` hipfire, option ON, frame 1800 (unseeded first pass) | the red sight at the centre of the view, ammo/gun unchanged (PNG viewed) | +| Solo `-level_09` hipfire, option OFF, frame 1800 | no sight (PNG viewed) | +| Solo `-level_09`, option OFF, frames 600/1200/1800, new binary vs the pre-change binary (ad7b63d9) | **BYTE-IDENTICAL** all three; frame 1800 md5 `1561622c8eed` = the golden in CONTROLLER-PAGES-RESULTS | +| Solo `-level_09` hipfire, OFF vs ON, frames 600/1200/1800 | 2 561 px differ, bbox x 391-475 y 278-373 on the 869x652 canvas (the sprite at the view centre); nothing else | +| Solo `-level_09`, R held from frame 700 (aim mode), OFF vs ON, frames 1200/1800 | **identical** (one sprite, the game's own; no double draw) | +| Sanity: OFF hipfire vs OFF aim mode, frame 1800 | the same 2 561 px / bbox as the row above: the persistent sprite is pixel-for-pixel the aim-mode sprite | +| Netplay 2P loopback (Temple, MULTIPLAYER-DEV script), both peers OFF | SH identical on 4 615 common ticks, 0 DESYNC | +| Both peers ON | SH identical on 4 616 ticks, 0 DESYNC | +| **Mixed: A OFF, B ON** | SH identical on 4 610 common ticks, 0 DESYNC (render-only confirmed) | + +**Not verified:** a real play session (feel, the sight over the watch while paused: the N64 draws its own aim-mode sight the same way, so it is fidelity-consistent), split-screen and the own-view transform (the D13 texrect signature should match the same sprite; check on the next couch/own-view pass). + +**Confidence: [H]** for correctness and neutrality (measured); [M] for the user-facing wording of the row. diff --git a/port/src/optionsoverlay.c b/port/src/optionsoverlay.c index 68122989..da6b3009 100644 --- a/port/src/optionsoverlay.c +++ b/port/src/optionsoverlay.c @@ -331,6 +331,7 @@ static struct Row rows[] = { /* D226: scales the ammo counter, pickup/status text and dialogue. */ { .key="Game.HudScale", .label="HUD scale", .kind=ROW_SLIDER, .step=5, .unit="%" }, { .key="Video.CrosshairHide", .label="Show crosshair", .kind=ROW_TOGGLE, .step=1, .names=kOnOffRev }, + { .key="Video.CrosshairPersistent", .label="Crosshair when not aiming", .kind=ROW_TOGGLE, .step=1, .names=kOnOff }, /* D436 */ { .key="Video.CrosshairColor", .label="Crosshair colour", .kind=ROW_ENUM, .step=1, .names=kCrosshairColor }, { .key="Video.CrosshairRed", .label="Red", .kind=ROW_SLIDER, .step=1, .shownWhen="Video.CrosshairColor", .shownValue=8 }, { .key="Video.CrosshairGreen", .label="Green", .kind=ROW_SLIDER, .step=1, .shownWhen="Video.CrosshairColor", .shownValue=8 }, @@ -1316,6 +1317,7 @@ static const struct { const char *key; double def; } kResetDefaults[] = { { "Video.DrawDistance", 250 }, /* midpoint: 50/100 */ { "Video.LodDistance", 250 }, /* midpoint: 50/100 */ { "Video.CrosshairHide", 0 }, /* = 0 (on, N64) */ + { "Video.CrosshairPersistent", 0 }, /* D436: off = N64 (aim mode only) */ { "Video.CrosshairColor", 0 }, /* = 0 (authored red sprite) */ { "Video.CrosshairRed", 255 }, { "Video.CrosshairGreen", 255 }, diff --git a/port/src/video.c b/port/src/video.c index 1739110c..18d66812 100644 --- a/port/src/video.c +++ b/port/src/video.c @@ -106,6 +106,8 @@ s32 portNoHitFlash = 0; * (gunfire.c gunDrawSight, #ifdef PORT). Default keeps the original * authored red sprite; hide is opt-in. */ s32 portCrosshairHide = 0; +static int cfgCrosshairPersistent = 0; /* D436: 1 = draw the sight outside aim mode too */ +int portCrosshairPersistent(void) { return cfgCrosshairPersistent; } static int cfgCrosshairColor = 0; /* 0 = authored sprite; 1..7 = presets; 8 = custom RGB */ static int cfgCrosshairRed = 255, cfgCrosshairGreen = 255, cfgCrosshairBlue = 255; static int cfgCrosshairSize = 100; /* 100% retains the original 32x32 drawing */ @@ -413,6 +415,7 @@ PD_CONSTRUCTOR static void videoConfigInit(void) configRegisterInt("Game.SkipIntro", &portSkipIntro, 0, 1); configRegisterInt("Game.NoHitFlash", &portNoHitFlash, 0, 1); configRegisterInt("Video.CrosshairHide", &portCrosshairHide, 0, 1); /* v0.4.0 M2 (D373) */ + configRegisterInt("Video.CrosshairPersistent", &cfgCrosshairPersistent, 0, 1); /* D436 */ configRegisterInt("Video.CrosshairColor", &cfgCrosshairColor, 0, 8); /* 8 = custom; old ini values unchanged */ configRegisterInt("Video.CrosshairRed", &cfgCrosshairRed, 0, 255); configRegisterInt("Video.CrosshairGreen", &cfgCrosshairGreen, 0, 255); diff --git a/src/game/gunfire.c b/src/game/gunfire.c index d21ff3bc..69cff368 100644 --- a/src/game/gunfire.c +++ b/src/game/gunfire.c @@ -6518,7 +6518,19 @@ void gunDrawSight(s32 *gdl) { f32 xypos[2]; f32 halfedxy[2]; +#ifdef PORT + /* D436: opt-in persistent sight. With Video.CrosshairPersistent the + * NOTAIMING reason is ignored so the sprite is also drawn in hipfire, at + * the same crosshair_angle the shots use. Every other reason (Sight + * option / MP sight policy, cutscene, damage) and the MP menu still hide + * it. Default off = the N64 test. */ + extern int portCrosshairPersistent(void); + s32 sightmode = g_CurrentPlayer->gunsightmode + & ~(portCrosshairPersistent() ? GUNSIGHTREASON_NOTAIMING : 0); + if ((sightmode == 0) && (g_CurrentPlayer->mpmenuon == FALSE)) { +#else if ((g_CurrentPlayer->gunsightmode == 0) && (g_CurrentPlayer->mpmenuon == FALSE)) { +#endif #ifdef PORT if (portCrosshairHide) return;