diff --git a/.github/SECURITY.md b/.github/SECURITY.md index 6e6b6d2e..b018d9ef 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -1,7 +1,12 @@ # Security Policy This project is a non-commercial, fan-made research port. It has no server -component, no networking of any kind, and no telemetry. Each release ships +component and no telemetry, and no networking unless you turn on the opt-in +update check (`Game.CheckUpdates`, off by default), which makes one HTTPS +request to GitHub's releases API per launch (GitHub sees that request and +your IP, as with any web request). On Windows it loads the system +`winhttp.dll` from System32 only then; on Linux/Steam Deck it runs the +system `curl`. It never downloads or installs anything. Each release ships prebuilt Windows and Linux binaries, but never a ROM or any game asset — you supply those yourself from a copy you legally own. The realistic security surface is: diff --git a/.github/release-notes.md b/.github/release-notes.md index f9562a6a..bcb78515 100644 --- a/.github/release-notes.md +++ b/.github/release-notes.md @@ -12,9 +12,8 @@ Platforms: **Windows x86-64** and **Linux x86-64 (including Steam Deck)**. Region: **NTSC-U (US) only**. See [Known issues](#known-issues). - +Played on Windows and Steam Deck: campaign spot-checks, 2-4 player +split-screen and controller-only sessions. The headline is **one real options system**: both options screens merged into a single menu, laid out like the Perfect Dark PC port's, @@ -74,6 +73,31 @@ list. ## Fidelity and stability fixes +- **Dam ending camera:** the first shot of the ending cutscene swivels up onto + Bond again, as on the N64; an old port workaround snapped it straight onto + him (D552). +- **Fog rework (Surface 2, from Deck playtest):** ground fog no longer "chunks" + away tile by tile, and the sky fades into fog toward the horizon as on the + N64 (D540). Large near-camera triangles are now clipped against the near + plane the way the N64's RSP does, with per-vertex fog recomputed on the + clipped edge, so the haze around Bond stays steady as he moves instead of + brightening and darkening (D543, D553; compared against hardware-level + emulation). A new **Fog distance** setting (100% = N64) is independent of + Draw distance, and Draw and Fog distance now apply live instead of on the + next level load. +- **Banded walls near explosions:** in Aztec's dark corridor, walls near rocket + fire turned into black/white/yellow/blue bands (also seen on the Deck); fixed + by clamping colour-combiner inputs to the range the N64 can produce (D548). + Build-verified; a live Aztec re-check is owed. +- **Rocket and thrown-item crashes/clipping:** a player-fired rocket could pass + through the ground when fired near Bond's feet (a stack-layout difference + from the N64, D545), and the same class of bug is fixed in thrown grenades, + knives and objects (D549). +- **Menu double-trigger (Steam Deck):** one A press could advance two menu + screens, and one B could back out two overlay pages. A short release + hold-off on menu A/B/Start and a single back action per press are now in + place. This is a mitigation: the exact cause is unconfirmed (the Deck + re-check passed) (D541). - **Fog:** objects far in fog are hidden as on the N64 (fog snap); in widescreen the wider view can still show a faint distant building edge (e.g. Surface's dish from the start area) — 4:3 matches the N64 (D503). @@ -117,6 +141,40 @@ percent, frames, seconds, pixels — standardised across the two menus (D506); a **one-line description** of the selected option in the same menu (D507); and menu spelling standardised to one convention across both menus. +- **Settings standardised (D546):** Draw and LOD distance default to 2.0x + (N64 = 1.0x), stick deadzone to 25%, and the mouse sensitivity maximum is 4x. + **FOV** is now vertical degrees, 30-90, with 60 = the N64 view. The old + "Guard AI uses full wide view" row is now **Gameplay view area** + (Original / Extended). Untouched old values migrate once; existing + `ge007.ini` files still load. +- **Optional update check (D551):** a new **Check for updates** option in Game, + **off by default**. When on, it makes one HTTPS request to GitHub per launch + and shows an "Update available" row linking to the releases page. +- **Settings menu wording pass (D554):** clearer tips that wrap to two lines + instead of being cut off, one `Key: Action` style for the control hints, + consistent casing and names (Original / Extended, "Original layout"). + *Skip intro* and *All unlocked* are no longer marked experimental. + The game's own *Look up/down* option is gone from the menu; use *Invert look* + on the Mouse and Controller pages (D564). +- **Crosshair mouse pointer and scrollbar in the settings menu (D555, D556):** + the game's own crosshair is the pointer while F10 / PC Options is open (Mouse → + Crosshair pointer to turn it off), long pages get a draggable scrollbar, and + tips are kept only where a setting needs explaining. Long tips are no longer + cut off inside a level (D558). +- **New defaults (D556, D557):** Crouch mode defaults to Toggle (new installs and + Reset to defaults), and new profiles start with Look ahead off, which fights + mouse look on PC; an existing save's untouched empty folders (no progress, + factory options) get the same when first played (D559). Played or + customised profiles and existing settings are not changed. +- **All unlocked** now applies live from the options menu, with no restart + (D547). +- **F10 overlay mouse:** the OS cursor is always shown (no more flicker on the + main menu), right-click goes back, and the tip line follows the hovered row + (D544). +- **Window:** the title bar is now static (no per-second "NN fps" refresh; the + FPS readout is still in the F10 overlay), and the window and taskbar icon is a + new project mark instead of a game still (D550). Live icon/title check owed. + --- ## Known issues @@ -127,36 +185,25 @@ and menu spelling standardised to one convention across both menus. | Changing aspect ratio inside a level can briefly glitch the gun/hand model, rarely | Cosmetic, one-off | Change the ratio from the front-end PC Options, or accept it | | No macOS or ARM builds | Platform | — | | Saves from builds before v0.4.1 can hold fake unlocks from `All unlocked` | Save data | Not repaired automatically. Since v0.4.1 the option never writes the save; keep a backup of `data/ge007.eep` from before you used it | -| `Skip intro` and `All unlocked` are experimental | Opt-in options | Leave them off for a normal playthrough | | The first frame of a level takes a little longer while its textures upload | Brief FPS-counter dip | None needed | | Far objects almost fully in fog are now hidden as on the N64, except in widescreen where a faint distant building edge can still show (e.g. Surface's dish from the start area); 4:3 matches the N64 | Cosmetic | Higher Draw/LOD distance shows more | The full list, with workarounds, is the [known-issues table](https://github.com/jkdansereau/goldeneye-pc-port/blob/main/docs/ROADMAP.md#known-issues). -**Not yet playtested live** (merged, build- and review-verified, owed a play -session before this release is announced): the four new settings (D511), -`Game.HudScale` (D512), Control style Original on a pad (D513/D516 per seat), -emulator-save import (D514), and the D519 profile-chooser row / no-click-through -follow-ups. - - -**Reference-frame gate for this release:** both platforms carry the full 21-level -reference-frame set (63 frames each) at the same stems. The Windows gate is green -against its own goldens (21/21, three consecutive clean sweeps); on Linux the -crash gate is green and the pixel verdict is still owed (see below). The recipe pins the run for you: the save file's +**Reference-frame gate for this release:** Windows, Linux and Steam Deck each +carry the full 21-level reference-frame set (63 frames each) at the same stems, +re-captured for this release's fog and near-plane changes and confirmed by two +independent capture passes per platform. The full pixel gate is green on all +three against their own goldens on the final build: Windows 21/21, Linux +(Intel HD 3000) 21/21 and Steam Deck 21/21. The recipe pins the run for you: the save file's CONTENT is pinned (the gate installs its own canonical save per level, the same way it pins the display ini — D529) and the PRNG seed is hard-pinned (a stray `GE_RSEED` in your environment warns and is ignored rather than re-seeding the gate — D532), so a gate run and a capture run see the same starting state. The -cross-platform spread measured on the shipped pairs (0.380-5.120% of pixels over -tol 2 on the 20 non-Cuba levels, structural tier 21/21 clean) is -informational only — it is not a cross-platform pixel-parity claim, and the -linux *pixel* gate did not run on the capture box (its GL probe fails closed -there; the linux crash gate did). The Dam ending cutscene has not been checked -by eye in a live session. +cross-platform spread on the shipped sets (0.188-4.553% of pixels over tol 2 on +the 20 non-Cuba levels) is informational only — it is not a cross-platform +pixel-parity claim. --- diff --git a/.github/social-preview.html b/.github/social-preview.html index 3aa0c427..518a7a77 100644 --- a/.github/social-preview.html +++ b/.github/social-preview.html @@ -55,11 +55,9 @@ .tab span { writing-mode:vertical-rl; text-orientation:mixed; font-family:'Arimo',sans-serif; font-weight:700; font-size:15px; letter-spacing:4px; color:#3b3b33; transform:rotate(180deg); } - .reticle { position:absolute; right:34px; top:246px; width:44px; height:44px; z-index:5; } - .reticle::before, .reticle::after { content:""; position:absolute; background:#c62828; } - .reticle::before { left:50%; top:-6px; width:3px; height:56px; margin-left:-1.5px; } - .reticle::after { top:50%; left:-6px; height:3px; width:56px; margin-top:-1.5px; } - .reticle i { position:absolute; inset:4px; border:3px solid #c62828; border-radius:50%; } + /* the project mark: in-game default crosshair proportions (ring outer r 18, + bars reach ~1.43x r with an open centre ~0.24x r); see tools_pc/make_icon.py */ + .reticle { position:absolute; right:30px; top:242px; width:52px; height:52px; z-index:5; } /* the parchment card */ .card { position:absolute; left:40px; top:40px; right:96px; bottom:40px; @@ -124,12 +122,12 @@
REPORT:
-
Status: RELEASED v0.4.0 — WINDOWS · LINUX · STEAM DECK
+
Status: RELEASED v0.5.0 — WINDOWS · LINUX · STEAM DECK
Statistics:All 20 solo missions — full campaign completed in playtesting - ~230 game-code units, compiled unmodified - ~25k lines C/C++  ·  ~14k Python (port layer + tools) - 286 findings root-caused  ·  two coding agents, one human, part-time + ~210 game-code units, compiled unmodified + ~34k lines C/C++  ·  ~10k Python (port layer + tools) + 407 findings logged  ·  two coding agents, one human, part-time
@@ -142,6 +140,6 @@
NEXT
PREVIOUS
-
+ diff --git a/.github/social-preview.png b/.github/social-preview.png index d9ebd2c9..c0701e6a 100644 Binary files a/.github/social-preview.png and b/.github/social-preview.png differ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2e92a89e..f19d3d6a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -404,6 +404,15 @@ jobs: with: persist-credentials: false + # The pushed tag must match the version baked into the exe (the update + # check compares against it); bump GE007_VERSION in CMakeLists.txt first. + - name: Check tag matches GE007_VERSION + run: | + ver=$(sed -n 's/^set(GE007_VERSION "\([^"]*\)").*/\1/p' CMakeLists.txt) + if [ -z "$ver" ] || [ "${GITHUB_REF_NAME}" != "v${ver}" ]; then + echo "tag ${GITHUB_REF_NAME} != v${ver} (CMakeLists.txt GE007_VERSION)"; exit 1 + fi + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: goldeneye-pc-port-win64 diff --git a/.github/workflows/selfhosted.yml b/.github/workflows/selfhosted.yml index 374e8b80..3e5f391c 100644 --- a/.github/workflows/selfhosted.yml +++ b/.github/workflows/selfhosted.yml @@ -1,7 +1,7 @@ # Self-hosted runtime (ROM-gated) # # Runs the build + the 21-level headless sweep (tools_pc/verify.sh) on the -# self-hosted runner "corpyvt6agent01", the one machine that has a legal ROM. +# self-hosted runner, the one machine that has a legal ROM. # This is the job class ci.yml explicitly does NOT do: CI is build-only, no # ROM, so runtime checks live here. Setup + rollout plan (gitignored, local): # docs/dev/notes/SELFHOSTED-RUNNER-PLAN.md diff --git a/AGENTS.md b/AGENTS.md index a7f6d5ff..080a26d9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -73,8 +73,9 @@ mostly agent-authored. bulk ROM-derived captures: capture output dirs are gitignored (`tools_pc/sweep-captures/cap-*/`, `tools_pc/sweep-captures/**/*.ppm`); the only bulk imagery in the tree is `tools_pc/golden/**/*.png` - (63 frames × 2 platforms, 126 total, nested per level at - `tools_pc/golden//{win,linux}/`), which exists because `verify.sh` + (63 frames × 3 platforms, 189 total, nested per level at + `tools_pc/golden//{win,linux,deck}/`; the `deck` set added + 2026-10-05 with maintainer approval), which exists because `verify.sh` consumes it. Downloads ship no game content at all. 5. **Batch, don't spam.** Accumulate approved doc/tree fixes across a session into ONE commit; no per-item branches. diff --git a/CMakeLists.txt b/CMakeLists.txt index 59ed90a4..3da73806 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -149,6 +149,7 @@ endif() # Provenance, printed by --version and at startup (port/src/main.c): a build # keeps pointing at this repository no matter who compiles or renames it. set(VERSION_CODENAME "larkspur") +set(GE007_VERSION "0.5.0") # bump on every release set(ORIGIN_URL "https://github.com/jkdansereau/goldeneye-pc-port") configure_file( @@ -600,6 +601,11 @@ target_link_libraries(ge007 ${LIBS}) set_target_properties(ge007 PROPERTIES OUTPUT_NAME "${BIN_NAME}") if(WIN32) set_target_properties(ge007 PROPERTIES WIN32_EXECUTABLE TRUE) + # Explorer/taskbar icon: the original crosshair mark (cmake/icon/ge007.rc, + # a static resource script; the ge007.ico it references is generated by + # tools_pc/make_icon.py). Compiled by the resource compiler; ge007.ico is + # resolved relative to this .rc file. + target_sources(ge007 PRIVATE cmake/icon/ge007.rc) endif() message(STATUS "Output binary: ${BIN_NAME}") diff --git a/README.md b/README.md index dfcb658f..be7aea99 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # GoldenEye 007 PC Port +

project mark: a generic version of the game's default aim cross -- a circle with N/S/E/W lines crossing its edge (original generated art, see tools_pc/make_icon.py -- no game content)

+ [![CI](https://github.com/jkdansereau/goldeneye-pc-port/actions/workflows/ci.yml/badge.svg)](https://github.com/jkdansereau/goldeneye-pc-port/actions/workflows/ci.yml) [![Latest release](https://img.shields.io/github/v/release/jkdansereau/goldeneye-pc-port?include_prereleases&label=download)](https://github.com/jkdansereau/goldeneye-pc-port/releases) ![Platforms](https://img.shields.io/badge/platforms-Windows%20%7C%20Linux%20%7C%20Steam%20Deck-blue) @@ -218,7 +220,8 @@ Select again) closes. With a keyboard, use `F10` and the arrow keys/Enter. saved automatically. - **Faithful N64 progression by default.** Opt-in extras: *Skip intro* and *All unlocked* (every level, 007 mode and the full cheat menu, without - touching your save; both experimental). + touching your save), and *Check for updates* (off + by default; one request to GitHub per launch when on, see Security). - **Saves in a plain file** next to the game, so backing up is a file copy. ## Status @@ -240,7 +243,7 @@ only in the release packages, and no macOS/ARM builds yet. > those are not repaired; if you used it on an older build, keep a backup of > `data/ge007.eep`. -What a release installs (no networking, no telemetry, no ROM or game assets) +What a release installs (no networking unless you turn on the opt-in update check, which is off by default and makes one HTTPS request to GitHub per launch; no telemetry, no ROM or game assets) and how faithfully the port tracks the original game's logic: [Security](docs/security.md) and [fidelity](docs/fidelity.md) status. The binaries are not code-signed; from v0.5.0 on each release artifact also @@ -651,7 +654,7 @@ release, is useless without a ROM you supply. Textures, audio, models, level data and in-game text are extracted from a ROM *you already own*, on *your* machine, the first time you start the game. The -port has no network access and never uploads anything. +port has no network access unless you turn on the opt-in *Check for updates* option (`Game.CheckUpdates`, off by default). When on, it makes one HTTPS request per launch to GitHub's releases API to look for a newer release (GitHub sees the request and your IP, as with any web request), never downloads or installs anything, and never uploads anything. ### This repository @@ -660,6 +663,10 @@ The repository is a fork of the public its contents (see [`NOTICE`](NOTICE) for what that includes). The few port-specific edits inside the decompiled code are marked `#ifdef PORT`. +The project mark / favicon is original *generated* art -- a generic version +of the game's default aim cross (a circle with N/S/E/W lines crossing its +edge), no game screenshots or logos in it. + ### Your part - Only use a ROM that you dumped from a cartridge you own. diff --git a/cmake/icon/ge007.ico b/cmake/icon/ge007.ico new file mode 100644 index 00000000..4eaaad63 Binary files /dev/null and b/cmake/icon/ge007.ico differ diff --git a/cmake/icon/ge007.rc b/cmake/icon/ge007.rc new file mode 100644 index 00000000..350d9f8e --- /dev/null +++ b/cmake/icon/ge007.rc @@ -0,0 +1,4 @@ +/* Windows executable icon (Explorer / taskbar). + * Original crosshair mark generated by tools_pc/make_icon.py (cmake/icon/ + * ge007.ico) -- same art as the site favicon (docs/img/icon/). */ +IDI_ICON1 ICON "ge007.ico" diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 42ed6205..979b96c4 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -120,7 +120,6 @@ below). | Changing Aspect ratio in a level (mostly Fill window to Original or a forced ratio) can briefly glitch the gun / hand model, rarely | Cosmetic, one-off | Change the ratio from the front-end PC Options, or accept it | D509 (frame hold fixed the common case; the rare remainder is logged, not investigated) | | No macOS or ARM builds | Platform | — | #88, #95, #101, §4 | | Saves from builds before v0.4.1 can hold fake unlocks from `All unlocked` | Save data | Not repaired automatically. Since v0.4.1 the option never writes the save; keep a backup of `data/ge007.eep` from before you used it | D442, D387 | -| `Skip intro` and `All unlocked` are experimental | Opt-in options | Leave them off for a normal playthrough | D408, D442 | | The first frame of a level takes a little longer while its textures upload | Brief FPS-counter dip | None needed | D475, D480 | | Far objects almost fully in fog are now hidden as on the N64, except in widescreen, where the wider view can still show a faint distant building edge (e.g. Surface's dish from the start area). 4:3 matches the N64 | Cosmetic | None; higher Draw/LOD distance shows more | D503, §5c | @@ -140,13 +139,14 @@ below). | Item | Status | Refs | Notes | |---|---|---|---| | Third-person Bond offset residual | partial; intro puppet explained (2026-10-02) | D173, D292 | Mostly fixed. On the level-start puppet the variance is tick/PRNG-dependent idle timing (D13 class, faithful, no sign-off requested); one 1-frame first-tick Y transient is untraced. If the residual is seen elsewhere (cutscene body model), the maintainer should note where. | -| Dam ending cutscene: Bond placement; railing transparency | partial (railing/grate transparency confirmed faithful 2026-10-05; Bond-placement re-check pending) | D173, D292, D526; maintainer 2026-10-02 | **Railing/grate transparency CLOSED as faithful (D526):** the 2D edge-railing (grate) texture in the Dam ending shows "odd" transparency between its two anchors, confirmed identical on 1964/GEPD (port == N64) → the original 2D texture, not a port bug (D306/D291 class). **Remaining: re-check Bond placement at the end of Dam** (the D173 level-start probe found no defect; the last place a Bond offset was seen). | -| Title-screen attract demo runs too fast; F10 box rescales while it plays | backlogged (after v0.5.0, maintainer 2026-10-04) | D87 (ramrom file struct); timing: D13, D248, D427, D451, D486 | **Reported 2026-10-04 (maintainer live pass):** the attract segment plays visibly too fast; not investigated. Lead: it is a ramrom demo replay whose recordings carry a per-frame tick count, which the port's 60 fps / 1-tick pacing may override. Rule out PC causes, then compare with 1964. Same pass: the F10 box briefly rescaled during the demo (not reproduced; candidate fix = overlay reads fast3d's native viewport, parked locally). | +| Dam ending cutscene | done (2026-10-06) | D526, D552 | Railing/grate transparency faithful (D526); first-shot swivel restored (D552, maintainer-verified). | +| Title-screen attract demo runs too fast; F10 box rescales while it plays | backlogged (after v0.5.0, maintainer 2026-10-04) | D87 (ramrom file struct); timing: D13, D248, D427, D451, D486 | **Reported 2026-10-04 (live pass):** attract plays visibly too fast; not investigated. Lead: it is a ramrom demo replay whose recordings carry a per-frame tick count, which the port's 60 fps / 1-tick pacing may override. Rule out PC causes, then compare with 1964. Same pass: the F10 box briefly rescaled during the demo (not reproduced; candidate fix = overlay reads fast3d's native viewport, parked locally). | | Sniper rifle: zoom level and speed vs 1964 | verify | #136, D484, D485, D494 | Crouch: **D494** (2026-10-03) makes the PC crouch key crouch/stand freely with the sniper, as GEPD does (superseding D483's freeze); maintainer-verified. Zoom level (D484, 20 -> 7 degrees) and speed (D485, tick-scaled, Rule-2) fixed 2026-10-02. **Owed:** 1964 A/B of zoom level and speed; close #136 after it. | | Crash on save slots 1/3/4, plus a host BSOD (fltmgr.sys) | decided | #94 | Needs a repro. Two maintainer retest requests (v0.3.0, v0.4.0) got no reply. Likely D299/D301 (save-slot crash, fixed) and D344 (BSOD on exit, fixed). **Decided 2026-10-01: close as stale when the release ships**, pointing at D299/D301/D344; no GitHub activity before then. | | Authored mip chains not sampled for explicit-LOD textures | post-1.0 | D323 | Distant textures use driver mips instead of Rare's hand-authored chain (heaviest on Statue/Aztec). Parked as a fidelity enhancement. **Decided 2026-10-01:** after 1.0, together with the LOD-fraction approximation as one "real RDP LOD" project. | | Approximations in the software RSP | post-1.0 | `gfx_pc.cpp` `G_CCMUX_LOD_FRACTION`; D323 | The LOD-fraction CC input is an eyeballed approximation, bundled with D323 as one "real RDP LOD" project after 1.0 (2026-09-30 triage). **WrapFix settled 2026-10-02:** the maintainer checked the Depot building ceilings against 1964 with the default `Video.WrapFix = 0` and they look right, so the default stays off (the opt-in knob remains; RC3/D167). | -| Latent 64-bit ABI hazards | partial | D441, #107, #108; AUDIT-M6 `struct player`/`struct hand` raw offsets (porting-notes §A1) | **State (2026-10-03, D441):** complete census (local `scratch/d441/`; many compiler hits are intentional `(s32)&ANIM_DATA_*` offsets, D34); 17 files widened to `uintptr_t` under `#ifdef PORT`; 35 sign-extension sites fixed; CI ratchet `tools_pc/abi_ratchet.py` (baseline 389); arena windows centralised (`port/include/portaddr.h`); tier-1 high-arena build (`GE_HIGHARENA_BASE=0x90000000`) runs the menu and `-level_09` pixel-identical to default. D478: nothing live on the current layout; the rest is harmless today but would bite on macOS/ARM/ASLR. **Sweep (2026-10-05):** tier-1 all-level sweep done, 21/21 PASS, no fix batches, ratchet unchanged (389) — recipe + caveats in the D441 entry. **Owed:** tier 2 (above 4 GiB; u32 slots widened); store-to-u32 vs arithmetic-only tag per site (`portHostToN64` under macOS port_addr); `mema.c` widening (D453); `initanitable.c` heap slots; GE_DETERM wall-clock timers (D117 follow-up, blocks using determinism mode for frame gates). **#108:** widening the `sndPlaySfx` bound disables the guard everywhere; prefer a range check. Detail: findings D441, D457, D478. | +| Latent 64-bit ABI hazards | partial | D441, #107, #108; AUDIT-M6 `struct player`/`struct hand` raw offsets (porting-notes §A1) | **State (2026-10-03, D441):** complete census (local `scratch/d441/`; many hits are intentional `(s32)&ANIM_DATA_*` offsets, D34); 17 files widened to `uintptr_t` under `#ifdef PORT`; 35 sign-extension sites fixed; CI ratchet `tools_pc/abi_ratchet.py` (baseline 389); arena windows centralised (`port/include/portaddr.h`); tier-1 high-arena build (`GE_HIGHARENA_BASE=0x90000000`) runs the menu and `-level_09` pixel-identical to default. D478: nothing live on the current layout; the rest would bite only on macOS/ARM/ASLR. **Sweep (2026-10-05):** tier-1 all-level sweep done, 21/21 PASS, no fix batches, ratchet unchanged (389), recipe in the D441 entry. **Owed:** tier 2 (above 4 GiB; u32 slots widened); store-to-u32 vs arithmetic-only tag per site (`portHostToN64` under macOS port_addr); `mema.c` widening (D453); `initanitable.c` heap slots; GE_DETERM wall-clock timers (D117 follow-up, blocks using determinism mode for frame gates). **#108:** widening the `sndPlaySfx` bound disables the guard everywhere; prefer a range check. | +| Port-memory vertex/pool bounds hardening | post-1.0 (decided; not a v0.5.0 blocker) | D545, D549 | D545 (rockets through floor, fixed) left one residual: ~1 dropped rocket in ~60 spawns (Archives, akimbo), consistent with the room-pool hypothesis (`bgBuildRoomVtxBounds` allocation failing leaves no bounds) but unproven; D549 (same stack-overflow class in grenade/knife/object throws) is fixed. Both shipped. Proposed hardening: a port-memory bounds table validating vtx/pool pointers against the known arena windows. | ## 3. Verification debt (fixed; a live check is owed) @@ -174,9 +174,9 @@ A single campaign pass with these on the checklist would clear most of them. | Item | Status | Refs | Notes | |---|---|---|---| -| macOS (Intel + Apple Silicon) / ARM64 Linux | open (target 0.9.0/1.0, maintainer 2026-10-02) | PRs #88, #95; #101 | Outside drafts, parked for `main` until after 1.0. **Address model decided (2026-09-30): port_addr (#95)**, a `portN64ToHost`/`PORT_N64PTR` chokepoint with a `PORT_ADDR_STRICT` validator. The `macos` integration branch is on origin with #88 (merged), #132 and #95 (retargeted, open); nothing merges to `main` before the post-1.0 pass. **Open against `macos`:** #131 (self-hosted sweep scaffold, never run on a Mac; needs #95 first). **On the next rebase onto `main`:** take #131's `selfhosted.yml`; drop #95's `crouched_rifle` hunk (our #120/D445 is correct); relabel #95's D29x/D405-D421 labels. Needs the §2 ABI-hazard sweep; the maintainer tests firsthand before merging. | +| macOS (Intel + Apple Silicon) / ARM64 Linux | open (target 0.9.0/1.0, maintainer 2026-10-02) | PRs #88, #95; #101 | Outside drafts, parked until after 1.0. **Address model decided (2026-09-30): port_addr (#95)**, a `portN64ToHost`/`PORT_N64PTR` chokepoint with a `PORT_ADDR_STRICT` validator. The `macos` integration branch is on origin with #88 (merged), #132 and #95 (open); nothing merges to `main` before the post-1.0 pass. **Open against `macos`:** #131 (self-hosted sweep scaffold, never run on a Mac; needs #95 first). **On the next rebase onto `main`:** take #131's `selfhosted.yml`; drop #95's `crouched_rifle` hunk (our #120/D445 is correct); relabel #95's D29x/D405-D421 labels. Needs the §2 ABI-hazard sweep; the maintainer tests firsthand before merging. | | Platform parity gate (Linux co-equal with Windows) | open | — | **Decided 2026-09-30:** yes. Every released platform carries the same gate: a 21-level sweep plus a campaign pass. Needs the golden baselines on both platforms first (§7). The last known Linux crash (D190) was closed as not-a-bug. | -| Low-end GPUs (~30 fps on Celeron/HD 400) | fixed, verify on N3060 class | D339, D481, D482, #92 | **D481 (2026-10-02):** fast3d ran inline on the scheduler thread, delaying retrace delivery so the tick gate skipped frames; with the render worker a Lenovo X220 (HD 3000) went 44-46 → 60.0 fps. **D482:** Atom/Celeron-class GPUs (HD 400-605, UHD 600/605, software renderers) get DrawDistance/LodDistance 100 and MSAA 1 once, where still at defaults. **D473:** uncached per-batch getenv fixed. Direction: real optimizations, not a preset. **Next only if N3060-class hardware still drops:** one `GE_PERFSTAT=1` run there (`gpu=` vs `run=`), then a lighter renderer-keyed default (GPU) or fast3d per-batch cuts (CPU; local `PERF-HOTSPOTS.md`). | +| Low-end GPUs (~30 fps on Celeron/HD 400) | fixed, verify on N3060 class | D339, D481, D482, #92 | **D481 (2026-10-02):** fast3d ran inline on the scheduler thread, delaying retrace delivery so the tick gate skipped frames; the render worker took a Lenovo X220 (HD 3000) from 44-46 to 60.0 fps. **D482:** Atom/Celeron-class GPUs (HD 400-605, UHD 600/605, software renderers) get DrawDistance/LodDistance 100 and MSAA 1 once, where still at defaults. **D473:** uncached per-batch getenv fixed. Direction: real optimizations, not a preset. **Next only if N3060-class hardware still drops:** one `GE_PERFSTAT=1` run (`gpu=` vs `run=`), then a lighter renderer-keyed default (GPU) or fast3d per-batch cuts (CPU; local `PERF-HOTSPOTS.md`). | | Statue/Cradle hitching (maintainer, 2026-09-30) | parked (not player-visible, 2026-10-02) | `GE_PERFSTAT` spike counters | **Diagnosed (D475, D477, D480):** no sustained load — the ~33 ms frames are the frame limiter skipping a retrace after a late gfx task (`boss.c`, D13-class timing); CPU bursts were CI-palette re-imports (ruled out, D477) and in-play shader compiles (fixed, D480). **Parked 2026-10-02:** a play session with the timeline probe felt no hitching. Reopen only if a visible hitch is reported (lever: pre-warm at level load). | | Steam Deck preset skipped when first launched from Desktop Mode | verify | D283 | Fixed on `release/v0.4.1`: the Deck is detected by DMI hardware id (Valve Jupiter/Galileo; `STEAMOS` as a second signal) and the preset applies once per ini, only while Fullscreen/Maximized are untouched. Only a Windows build and a Linux `-fsyntax-only` check ran. **Owed: one run on a real Deck** (Desktop-Mode-created ini, then Game Mode; expect fullscreen 1280x800 and the log line `video: Steam Deck (DMI Valve ...)`). The Known-issues row goes when this ships. | | Windows 7 | open | #98 | **Re-checked 2026-10-02: likely feasible around 1.0.** The exe imports no Windows 8+ functions and the bundled SDL2 2.32.10 still supports Windows 7. The real blocker is the first-run converter `ge007-convert.exe` (PyInstaller, Python 3.9+): the native C converter (#6 Part B) removes it, or a separate Python 3.8 build. Work: one Windows 7 VM test, then manual per-release checks (CI cannot test it). Supersedes the earlier 'won't-fix' call. | @@ -189,8 +189,8 @@ ultrawide, FOV, MSAA/anisotropic/texture-filter options (including N64 3-point), draw and LOD distance, VSync and a frame cap, show-FPS, keyboard/mouse rebinding with a GEPD-style layout, pad deadzone, sensitivity, smoothing, southpaw and triggers, rumble, crosshair and HUD -customisation, HUD scale, music/FX volume, no-hit-flash, SkipIntro -(experimental), All unlocked (experimental, see D387), and the F10 overlay. +customisation, HUD scale, music/FX volume, no-hit-flash, SkipIntro, +All unlocked (see D387, D442), and the F10 overlay. The modern defaults (native widescreen, overscan crop, 250% draw/LOD distance, bilinear filtering) are **deliberate**. They are not a fidelity @@ -204,20 +204,23 @@ Background research lives in the maintainer's local notes (gitignored): | Item | Status | Refs | Notes | |---|---|---|---| -| F10 / PC Options menu: PD-style dialog refactor + watch-consolidation review | verify | D504-D512, `MENU-PD-ALIGNMENT-PLAN.md` (maintainer's local notes) | **Built 2026-10-04 (D504); follow-ups merged into `release/v0.4.1`:** D505/D506/D507 (wording, real-unit sliders, descriptions); D508 forced aspect ratios (16:9 / 21:9); D509 2-frame hold after an aspect change (rare gun glitch = known issue); D510 overlay + FPS counter same size front end vs level; D512 `Game.HudScale` scales the F10 overlay. **Owed (live pass, `docs/dev/notes/MENU-LIVE-CHECKLIST.md`, launches A/B/C):** look-and-feel vs PD; mouse, keyboard and pad in a level and on the front end; HudScale 75/100/150; 21:9; display-mode round trip across a restart; split-screen 2P with the overlay open. **Gaps:** background blur (post-1.0, §5c); per-seat control scheme is #139 / D513. Watch consolidation: not cheaply feasible (parked as T5). | +| Log file (next release) | open | maintainer 2026-10-06 | Write `ge007.log` next to the ini (console output is lost on a normal launch). Crosshair pointer done in v0.5.0 (D555). | +| F10 / PC Options menu: PD-style dialog refactor + watch-consolidation review | verify | D504-D512, `MENU-PD-ALIGNMENT-PLAN.md` (maintainer's local notes) | **Built 2026-10-04 (D504); follow-ups merged:** D505/D506/D507 (wording, real-unit sliders, descriptions); D508 forced aspect ratios (16:9 / 21:9); D509 2-frame hold after an aspect change (rare gun glitch = known issue); D510 overlay + FPS counter same size front end vs level; D512 `Game.HudScale` scales the F10 overlay. **Owed (live pass, `docs/dev/notes/MENU-LIVE-CHECKLIST.md`, launches A/B/C):** look-and-feel vs PD; mouse, keyboard and pad in a level and on the front end; HudScale 75/100/150; 21:9; display-mode round trip across a restart; split-screen 2P with the overlay open. **Gaps:** background blur (post-1.0, §5c); per-seat control scheme is #139 / D513. Watch consolidation: not cheaply feasible (parked as T5). | | Defaults/presets consolidation: one `Display mode` switch | verify | D504, D440, `MENU-PD-ALIGNMENT-PLAN.md` §5 (local notes) | **Landed 2026-10-04 (D504):** the derived `__DisplayMode` row shows Modern / Original N64 / Custom (no ini key); F10 dropdown + left/right stepping, front end steps too; one reset row per dialog covering exactly its keys; editing a non-bundled key does not flip Display mode. **Decided 2026-10-03: tri-state**, flipping to Custom when the user edits a bundled key. **Owed (live):** display-mode round trip across a restart; read of the regrouped pages and wording (MENU-LIVE-CHECKLIST.md A1/A2/A4). | | Control style Original shows the game's own N64 styles | verify | #139 follow-up, D516, D517 | **Landed 2026-10-04 (D516, merged @ 91595597):** "Controller style" row (Original only; the game's own 1.1-2.4 per seat, saved to the Bond file; front end reads "(in a mission)"); D498 presets hidden in Original. **D517:** hold-repeat on any Bond row dropped fps (EEPROM rewrite per detent); now staged like sliders. Live passes 2026-10-04 (maintainer): single player + split-screen 2P with different styles per seat. **Owed:** the 2.x second-pad pass (real second controller). | -| Pad layout preset per player (setting P2's no longer moves P1) | verify | D518, D498 follow-up | **Landed 2026-10-04 (D518, merged @ 91595597):** `Input.PadPreset` kept as seat 1's key + new `Input.PadPreset2/3/4`; the F10/front-end preset row follows the Select-player seat. Headless: GE_PADMAPTEST PASS; **live passes 2026-10-04** (P2 change leaves P1 alone; split-screen: each seat ran its own preset). | +| Pad layout preset per player (setting P2's no longer moves P1) | verify | D518, D498 follow-up | **Landed 2026-10-04 (D518):** `Input.PadPreset` kept as seat 1's key + new `Input.PadPreset2/3/4`; the F10/front-end preset row follows the Select-player seat. Headless GE_PADMAPTEST PASS; **live passes 2026-10-04** (P2 change leaves P1 alone; split-screen: each seat ran its own preset). | | Control scheme selectable: Ext / Original control style | verify | #139, D513, D498, D166 | **Landed 2026-10-04 (D513, merged into `release/v0.4.1`).** `Input.ControlScheme` (row "Control style", Ext default / Original) on the Controller page, both UIs. Original leaves the game's own style alone; D498 presets set layout and stick swap only in Original; 2.x runs faithfully; global in v1, per seat with the Select Player work. **Owed:** pad pass in both modes, watch-selected 1.1-1.4, split-screen 2P with different styles, 2.x with a second pad. | -| Outside PRs #107–#109, #122–#124 | decided | #122 hot-plugged pads never reopen, #123 persistent crosshair (`gunfire.c`), #124 compact health/armour bars (`bondview2.c`); #107/#108/#109 (italoarruda) | **Decided 2026-10-01 (local `PR-REVIEW-*.md`):** **#120/#121/#122** merged locally (#122 = D450); close on push. **#123**: accept-with-changes, deferred (Original N64 preset, relabel D436, Rule-2 line, 2P + sniper-zoom check). **#124**: deferred (ignores HudScale, overlaps status text, ultrawide anchoring, split-screen untested). **#133**: duplicates D472; close as superseded on push. **#109** (pad rebinding): decline as-is; supersede with our pad presets (§5a), crediting the author (Co-authored-by). **#107**: folded into D441; close on push. **#108**: belongs with `macos`; ask for a range check. | +| Outside PRs #107–#109, #122–#124 | decided | #122 hot-plugged pads never reopen, #123 persistent crosshair (`gunfire.c`), #124 compact health/armour bars (`bondview2.c`); #107/#108/#109 (italoarruda) | **Decided 2026-10-01:** **#120/#121/#122** merged locally (#122 = D450); close on push. **#123**: accept-with-changes, deferred (Original N64 preset, relabel D436, Rule-2 line, 2P + sniper-zoom check). **#124**: deferred (ignores HudScale, overlaps status text, ultrawide anchoring, split-screen untested). **#133**: duplicates D472; close as superseded on push. **#109** (pad rebinding): decline as-is; supersede with our pad presets (§5a), crediting the author (Co-authored-by). **#107**: folded into D441; close on push. **#108**: belongs with `macos`; ask for a range check. | | Gamepad presets: duplicate-binding warning, Xbox stick behaviours | open (after v0.5.0; documented in the README) | D469, D498, D518, D394 | D469 (pad action table, Jinx 1.1 default, Custom per-seat bindings), **D498** (Xbox 1.2 Christmas / 1.3 Frost / 1.4 Elektra) and the per-seat preset (**D518**) are done and maintainer-verified with pads. Not done (maintainer 2026-10-04: skip in game for v0.5.0, document only): a warning when two actions share a button; the Xbox "left stick aims the crosshair" and "D-pad copies the left stick" behaviours (D394). | | Pad button names on a Nintendo pad | verify | D471; local note `PAD-MENU-AUDIT.md` | Audit done 2026-10-01 (no controller-only trap; D472 fixed G1-G3). D471 family button names + player LEDs: **PS5 DualSense verified 2026-10-03.** **Owed:** a Nintendo (Switch) pad check; the maintainer has no such pad, so this waits for one or for a user report. | | Mute and auto-pause on focus loss; PNG screenshots to `screenshots/` | parked | D231, PR #56 (closed unmerged), `docs/dev/notes/parked/0001-QoL-*.patch` (local) | Deprioritised by the maintainer on 2026-09-25. F12 today is the dev PPM dump. | | Frame rate above 60 (120/144/uncapped with interpolation) | decision | [`dev/UNLOCKED-FPS-PLAN.md`](dev/UNLOCKED-FPS-PLAN.md) | The menu cap is {30, 60} (the ini accepts 0 = uncapped, but the sim ticks at the VI rate). The plan's Phase 0 is resolved by D248 (the default was a port bug; it is a real 60 fps) and its Phase 1 audit and Phase 2 slice scope are written (landed 2026-09-30), but the header still reads parked. Next step is a standing sim/render-rate measurement, not the old 30 fps investigation. Must stay compatible with netplay determinism. | -| PD Extended Options parity: settings that need new dev work | done (reviewed 2026-10-04; two rows deferred) | `MENU-PARITY-SONNET-PLAN.md` §3 (local notes); pd_port `port/src/optionsmenu.c` @ 32a1cb9 | **Reviewed 2026-10-04 (maintainer decisions; local notes `PD-PARITY-DESIGN-REVIEW.md`, `WATCH-SCREEN-RATIO-DESIGN.md`).** **Done:** D511 fullscreen mode, Center window, crosshair opacity and color by health (live check owed, MENU-LIVE-CHECKLIST.md A4/A5/B3). **Watch Screen size/Ratio:** stay on the watch, no PC rows; the game saves watch settings on watch close (D349 correction) and they don't fight Aspect ratio (README). **Deferred past v0.5.0:** mouse lock mode (Auto/Always/Off; aim lock itself = `Input.AimMode`, D337); per-seat controller device select (needs a stable pad id; MP input layer). **Closed:** edge deadzone = `Input.AimRange` (D338); crosshair sway N/A (GE has none; gun sway is D428); use key reloads, no toggle (Ext keeps D378's GEPD split, Control style Original gives the N64 behaviour); per-seat options/binds (pad binds already per seat, D469/D518; keyboard/mouse is one device); no Detail textures toggle (`Video.DetailBaseTile` is the D236 fix); N/A confirmed: HUD centering (automatic, D335b), glare, overexposure, GE64 muzzle flashes, MP death music, radial menu speed, language filter, crosshair speed. **Other rows:** analog movement/stick scale/crouch → #139, T7; tickrate/framerate limit → frame rate above 60. | -| One PC options UI: front-end "PC Options" opens the F10 overlay over the folder graphic | verify | D519, D343, D504, D510, D512 | **Landed 2026-10-04 (D519, branch `feat/one-options-ui`):** the file-select "PC Options" entry opens the F10 overlay over file select; the `MENU_PC_OPTIONS` screen and four old probes are gone; Bond-file rows and the watch chooser are keyed to file select. Headless checks pass (D519). **Owed:** by-eye mouse, keyboard and pad pass on file select; profile chooser + Controller style with a real profile; footer text overlaps the Copy/Erase bar. | -| Emulator saves load directly in the game (no tool for import) | verify | D514, D492; `port/src/eepimport.c` called from `geEepromLoad`; D297 `legacycrc.c` same pattern | **Landed 2026-10-04 (D514, merged into `release/v0.4.1`).** Copying a 1964/Project64 `.eep` in as `data/ge007.eep` works: on first launch each region that validates in emulator format and not port format is converted in place (same rules as `eep_convert.py`); regions invalid in both are left to the game's wipe; port saves untouched; the original is backed up to `ge007.eep.emulator.bak`. Export stays with `eep_convert.py`. Headless verify 6/6 byte-identical to the in-game-proven reference. **Owed:** by-eye mission select after importing a real 1964 save. | +| PD Extended Options parity: settings that need new dev work | done (reviewed 2026-10-04; two rows deferred) | `MENU-PARITY-SONNET-PLAN.md` §3 (local notes); pd_port `port/src/optionsmenu.c` @ 32a1cb9 | **Reviewed 2026-10-04 (maintainer decisions; local notes).** **Done:** D511 fullscreen mode, Center window, crosshair opacity and color by health (live check owed, MENU-LIVE-CHECKLIST.md A4/A5/B3). **Watch Screen size/Ratio:** stay on the watch, no PC rows; the game saves watch settings on watch close (D349 correction) and they don't fight Aspect ratio (README). **Deferred past v0.5.0:** mouse lock mode (Auto/Always/Off; aim lock itself = `Input.AimMode`, D337); per-seat controller device select (needs a stable pad id; MP input layer). **Closed:** edge deadzone = `Input.AimRange` (D338); crosshair sway N/A (GE has none; gun sway is D428); use key reloads, no toggle (Ext keeps D378's GEPD split, Control style Original gives the N64 behaviour); per-seat options/binds (pad binds already per seat, D469/D518; keyboard/mouse is one device); no Detail textures toggle (`Video.DetailBaseTile` is the D236 fix); N/A confirmed: HUD centering (D335b), glare, overexposure, GE64 muzzle flashes, MP death music, radial menu speed, language filter, crosshair speed. **Other rows:** analog movement/stick scale/crouch → #139, T7; tickrate/framerate limit → frame rate above 60. | +| One PC options UI: front-end "PC Options" opens the F10 overlay over the folder graphic | verify | D519, D343, D504, D510, D512 | **Landed 2026-10-04 (D519):** the file-select "PC Options" entry opens the F10 overlay over file select; the `MENU_PC_OPTIONS` screen and four old probes are gone; Bond-file rows and the watch chooser are keyed to file select. Headless checks pass. **Owed:** by-eye mouse, keyboard and pad pass on file select; profile chooser + Controller style with a real profile; footer text overlaps the Copy/Erase bar. | +| Emulator saves load directly in the game (no tool for import) | verify | D514, D492; `port/src/eepimport.c` called from `geEepromLoad`; D297 `legacycrc.c` same pattern | **Landed 2026-10-04 (D514).** Copying a 1964/Project64 `.eep` in as `data/ge007.eep` works: on first launch each region that validates in emulator format and not port format is converted in place (same rules as `eep_convert.py`); regions invalid in both are left to the game's wipe; port saves untouched; the original is backed up to `ge007.eep.emulator.bak`. Export stays with `eep_convert.py`. Headless verify 6/6 byte-identical to the reference. **Owed:** by-eye mission select after importing a real 1964 save. | | DexDrive / GameFAQs `.n64` saves | idea (easy, post-1.0) | D514; sample `scratch/eepinv/gamefaqs_dexdrive_2001.eep` (local) | **Noted 2026-10-04.** Old GameFAQs GoldenEye saves are DexDrive Controller Pak dumps (`123-456-STD` header, then a 32 KB pak) whose note (game code `3BADD1E5`, 2 pages) holds the 512-byte cartridge EEPROM. Extracting it and zero-padding to 2048 bytes gives a valid emulator-format save (TenebrusoM sample: all 6 CRCs OK). Support = walk the note table, pull the note, pad, hand to the D514 importer (or an `eep_convert.py dexdrive-to-pc` subcommand). Not tested in game. | +| Title bar and window icon (no FPS counter; static "GoldenEye 007" title + GE-crosshair window icon) | done (v0.5.0 polish) | maintainer request 2026-10-17; D550; `port/src/video.c`, `port/fast3d/gfx_sdl2.cpp`, `cmake/icon/ge007.rc`; icon art = `docs/img/icon/ge007-icon.png` (`tools_pc/make_icon.py`) | **DONE 2026-10-17 (D550):** static title bar (live FPS stays on F10), `SDL_SetWindowIcon` from a generated 32x32 header, exe icon via `ge007.rc`. Original generated mark, no game imagery. Build-verified; maintainer live window/taskbar check owed. | +| Opt-in update check (`Game.CheckUpdates`, off by default) | verify | D551; `port/src/updatecheck.c` | **Landed 2026-10-06:** Game-page toggle; one HTTPS GET per launch to `/releases/latest` (no pre-releases); Windows loads `winhttp.dll` at runtime only. Live-verified (Win/Linux/Deck), security-reviewed. | ### 5b. GE+ / Dab's-mod gameplay options (opt-in; touch game code) @@ -271,19 +274,19 @@ and a stage loader. | 1964-vs-port comparison harness | partial | `tools_pc/abpad/` | One millisecond timeline drives 1964 (virtual XInput pad) and the port (`GE_INPUTSCRIPT_MS`); both windows captured on one clock with side-by-side sheets, verified through intro skip, file select and folder open. Not frame-identical (1964 has no seed pin or frame dump). **Left:** in-level timelines (e.g. the D486 turret check); the two sides use different save files. | | Scripted-playthrough harness | post-1.0 (maintainer 2026-10-04: not a good use of current LLMs, revisit with newer models) | `GE_INPUTSCRIPT`, D117 | Input script + `GE_PCDUMP` + crash-log check for unattended regression runs. Levers: Invincibility + Invisibility cheats, closed-loop waypoint steering, pinned seed. Spike: one Dam route x5, measure end-position spread, then go/no-go. | | Cheap worker for mechanical, build-checked tasks | open | `delegate-local` MCP; memory of past Qwen issues | Birdman64's fast worker model wrote 44% of its commits while the lead planned and reviewed. Ours: pattern-applied edits (D441 fix batches, scaffolding strips, small tools), each gated by a clean build + a specific check + the lead's diff review, in the worker's own worktree. Local Qwen for edit-only tasks (it fabricates on reading tasks), or a hosted fast model via `delegate_to_provider` (the maintainer's cost call). | -| Golden baselines | partial | `tools_pc/verify.sh`, `tools_pc/golden/README.md`, D117/D522/D529/D531 | **Windows gate GREEN (2026-10-05 re-base):** 21/21 PASS honest sequential. The recipe pins the save CONTENT (D529: `verify.sh` installs the canonical in-tree save `tools_pc/golden/ge007.eep` per level, the same way it pins the ini — D523's "presence" claim was refuted by the D529 A/B) and per-level windows — `900-1500:300` on 14 levels, `1000-1400:200` on Surface 2/Streets/Depot/Cradle/Frigate, `1500-1700:100` on Dam — because the 900 stem sits inside the wall-clock-paced intro flyby (D117; dam measured 1.6–7.2 % run to run). No tol limit raised. **Linux set (D525 → D530 → D531):** the 2026-10-04 round cleared the save, the box re-rounded 2026-10-05 (D530 — but under the box-LOCAL save: `capture_p7.sh`'s per-level re-pin silently overwrote D529's canonical install, the D531 blocker), and D531 (2026-10-05) fixed the tool and re-captured all 21 levels under the canonical eep (47/63 frames moved). **Cross-platform spread recomputed 2026-10-05** from the committed pairs (`framediff.py` win-vs-linux at the stems, tol 2): exact **0.380–5.120 %** on the 20 non-Cuba levels (silo's 900 stem is the outlier: cell mean 34.3, phash 10; streets 3.223–4.566), structural **21/21 clean**, max phash 40 (cradle, at threshold); Cuba 1.357–12.663 % is the structural-tier-only level. The D525 figures above predate the D531 linux re-capture. **Owed:** the pixel-gated linux verify — the box's WSLg GPU display is session-state-dependent (its 10-05 capture was GPU-backed; 10-07's session reads back black even with `DISPLAY=:0`, so `verify.sh`'s probe fails closed to crash-detect); run `verify.sh sweep` on the box when its GPU display is up and check the four D117 flaky levels' first stems (never-do 6: exclude/widen, don't re-capture). | -| Flicker scan (A/B/A frame alternation) | parked (2026-10-03: tooling, nothing blocked on it; spec in the local investigation report `scratch/birdman64-rows-investigation.md` §4) | `GE_PCDUMP`, `GE_INPUTSCRIPT` | Dump **every** frame of a scripted run at low resolution and flag frame n when it differs from n-1 but matches n-2. For any old-vs-new pixel diff outside the golden gate, run a same-commit control first. Reference: Birdman64 `tools/flicker_scan.py`. | +| Golden baselines | partial | `tools_pc/verify.sh`, `tools_pc/golden/README.md`, D117/D522/D529/D531 | **2026-10-06: all three sets (win/linux/deck) re-based for D543/D546**, A/B-confirmed; history in the golden README. **Windows gate GREEN (2026-10-05 re-base):** 21/21 PASS honest sequential. The recipe pins the save CONTENT (D529: `verify.sh` installs the canonical in-tree save `tools_pc/golden/ge007.eep` per level) and per-level windows — `900-1500:300` on 14 levels, `1000-1400:200` on Surface 2/Streets/Depot/Cradle/Frigate, `1500-1700:100` on Dam — because the 900 stem sits inside the wall-clock-paced intro flyby (D117; dam measured 1.6–7.2 % run to run). No tol limit raised. **Linux set (D525 → D530 → D531):** D530's re-round ran under the box-local save (`capture_p7.sh`'s re-pin overwrote D529's canonical install); D531 fixed the tool and re-captured all 21 levels under the canonical eep (47/63 frames moved). **Cross-platform spread recomputed 2026-10-05** from the committed pairs (`framediff.py` win-vs-linux at the stems, tol 2): exact **0.380–5.120 %** on the 20 non-Cuba levels (silo's 900 stem is the outlier: cell mean 34.3, phash 10), structural **21/21 clean**, max phash 40 (cradle, at threshold); Cuba 1.357–12.663 % is the structural-tier-only level. **Pixel gate green on all three, final v0.5.0 build (2026-10-06):** win 21/21; linux 21/21 on the X220 (`DISPLAY=:1` makes the GL probe see the real GPU; one Jungle flyby-jitter miss passed twice solo); deck 21/21 (needs `XAUTHORITY` for the probe over ssh). | +| Flicker scan (A/B/A frame alternation) | parked (2026-10-03: tooling, nothing blocked on it; local spec `scratch/birdman64-rows-investigation.md` §4) | `GE_PCDUMP`, `GE_INPUTSCRIPT` | Dump **every** frame of a scripted run at low resolution and flag frame n when it differs from n-1 but matches n-2. For any old-vs-new pixel diff outside the golden gate, run a same-commit control first. Reference: Birdman64 `tools/flicker_scan.py`. | | Release build-provenance attestation | verify | `.github/workflows/ci.yml` release job; `.github/SECURITY.md`; README Status section | **Implemented 2026-10-04:** SHA-pinned `actions/attest-build-provenance` attests `dist/*.zip[.sha256]` + `dist/*.tar.gz[.sha256]` in the tag-only release job (`id-token: write` + `attestations: write` on that job only). Backs SECURITY.md's "unsigned, but reproducible" stance with `gh attestation verify --repo jkdansereau/goldeneye-pc-port`; documented in SECURITY.md and the README. **Owed:** the v0.5.0 tag build is the first real run; confirm the step passes and verification succeeds on a downloaded asset, then delete this row. | | Self-hosted CI runner link failure | open | D244 | Needs a `pacman -Syu` on the runner (maintainer-only access). Release tag builds use GitHub runners, so it doesn't block a release. | | Remove temporary scaffolding before a release | partial | [`dev/GE-ENV-PROBES.md`](dev/GE-ENV-PROBES.md), D302 | Passes 2026-09-30 to 2026-10-03 removed the closed-finding probe suites (registry in `GE-ENV-PROBES.md`, `gen_env_probes.py --check` passing); TEMP probes `GE_TESTSTYLE` / `GE_D516` removed 2026-10-04. **Left:** D104 (kept while D294 is in verify); the M-183/M-185 clamps, **in scope 2026-10-03 (maintainer)**: re-verify all scripted-camera cutscenes with the clamps compiled out (intro flyover, per-level endings incl. the Dam abseil), then delete both blocks in `model.c`. | -| P12 U5 review of `tools_pc/**` (release-diff, B10) | partial | D531/D532/D533 | Reviewed 2026-10-05 (the unit that owed the P7 merge). **Blocker found + fixed (D531):** `capture_p7.sh`'s per-level re-pin restored the local save, so the D530 box re-round ran under the box-local save — fixed + re-captured under the canonical eep (47/63 frames moved). **Should-fixes applied as D532 (tooling-only):** `verify.sh`'s `pin_ini_640x480` fails closed like `pin_eep`; `GOLDEN_SEED` hard-pinned with a warning for a stray `GE_RSEED`; `audiodebug.ps1`'s dead `-AB`/`-Old` switches removed. **Still open:** the pixel-gated box verify in a WSLg GPU-display window (D531) — crash-detect only until then. | +| P12 U5 review of `tools_pc/**` (release-diff, B10) | partial | D531/D532/D533 | Reviewed 2026-10-05 (the unit that owed the P7 merge). **Blocker found + fixed (D531):** `capture_p7.sh`'s per-level re-pin restored the local save (see Golden baselines). **Should-fixes applied as D532 (tooling-only):** `verify.sh`'s `pin_ini_640x480` fails closed like `pin_eep`; `GOLDEN_SEED` hard-pinned with a warning for a stray `GE_RSEED`; `audiodebug.ps1`'s dead `-AB`/`-Old` switches removed. Box pixel verify done 2026-10-06 (see Golden baselines). | | Findings log and doc size | partial | `tools_pc/docs_budget.py` | Budget checker (per-file and tier-1 token budgets; `--toc FILE`). `findings.md` is still about 2 MB (~500k tokens), so agents must keep using the index. Nothing blocks; trim only when a budget check fails. | | Branch and PR housekeeping | open | — | 30 landed local branches pruned 2026-09-30 (tips in `prune-branches-2026-09-30.txt`, notes archive). **On hold by the maintainer (do not prune yet):** probe-only `d243-verify-local`, `investigate/d243-cutscene-race`, `park/d236-probes-m139`. Local `main` and `release/v0.4.1` are deliberately not pushed. PR #62 (mouse dt-decouple) stays held until mouse feel is fundamentally better. | | Docs site: Web 1.0 phase 2 | decision | — | Phase 1 shipped. Phase 2 (centred serif page) is plan-only. | -| Docs site: press/SEO pass (2026-10-05) | partial | `docs/` site pages | Landed on the pure site pages + presentation layer: real `

` masthead on every page, `color-scheme`/`theme-color`/`max-image-preview`/`rel=me` in the layout, ink-mute >=4.5:1, focus-visible + selection, 15px body, mobile padding; `date`/`modified_time` front matter on the pure pages. Home-page additions (elevator-pitch pull-quote, Overview fact table, press/coverage section) were **removed on review 2026-10-05** — each restated the tagline/intro or duplicated a section below. The five short topic notes (the-software-rsp, the-asset-pipeline, framerate-and-pacing, input-and-aim, saves) were dropped from the published set (the PD port documents the same subjects; files stay in the repo as working material). **Open:** the same front matter on the *shared* docs (`internals.md`, `porting-notes.md`, `building.md`, `dev-process.md`, `dev/agentic-development.md`) is deliberately untouched — authoritative working docs, not edited for site polish. | -| Agentic-development deep dive (doc) | queued — not yet (2026-10-05) | `docs/dev/agentic-development.md` | Write-up owed for the public case study: per-agent commit estimates / work estimates (the manual attribution pass over the session log, incl. the D350-style commit-date divergences), a code-quality audit of the trial models (GPT-6 (Sol) et al.), and the process feedback / findings. Deferred by the maintainer; pick it up when scheduled — nothing here blocks. | +| Docs site: press/SEO pass (2026-10-05) | partial | `docs/` site pages | Landed on the pure site pages + layout: real `

` masthead, `color-scheme`/`theme-color`/`max-image-preview`/`rel=me`, ink-mute >=4.5:1, focus-visible + selection, 15px body, mobile padding; `date`/`modified_time` front matter on the pure pages. Home-page additions (pull-quote, Overview fact table, press section) **removed on review 2026-10-05** as duplicates. The five short topic notes (the-software-rsp, the-asset-pipeline, framerate-and-pacing, input-and-aim, saves) were dropped from the published set (files stay as working material). **Open:** the same front matter on the *shared* docs is deliberately untouched (authoritative working docs, not edited for site polish). | +| Agentic-development deep dive (doc) | queued — not yet (2026-10-05) | `docs/dev/agentic-development.md` | Write-up owed for the public case study: per-agent commit/work estimates (manual attribution pass over the session log, incl. D350-style commit-date divergences), a code-quality audit of the trial models (GPT-6 (Sol) et al.), and process feedback / findings. Deferred by the maintainer; nothing here blocks. | | Docs site: split Security & fidelity -> two pages + v0.5.0 update pass (2026-10-05) | done (local, unpushed) | `docs/security.md`, `docs/fidelity.md` (was `security-and-fidelity-status.md`) | Split on review (the homepage pointer sat near Known issues and read as if there were security *issues*); both pages cross-linked and linked from the hub and index.md's Download section. Update pass 2026-10-05 against the `release/v0.4.1` tree: no network includes/symbols/HTTP literals in compiled `src/`+`port/`; no registry or library-loading calls; converter imports no network module; bundle manifests re-checked (allowlist + ROM/oversized-blob hard-fail); region macros re-compared vs the Makefile (all three regions); library classification re-checked (CMake globs libultra+libultrare audio, gu, `io/vitbl.c` only). Full game-logic audit not re-run; recent `src/game` edits are finding-gated. | -| Docs site: final sanity sweep + anchor fixes (2026-10-05) | done (local, unpushed) | `docs/porting-notes.md` (3 TOC hrefs), `docs/404.html` (permalink) | Audit of the 10-page published set: one h1 per page, no heading-level skips, all 42 in-page anchors resolve, titles/descriptions unique and <=160 chars, og:image/canonical present, JSON-LD parses, no local paths/usernames, served == built. Three `porting-notes.md` TOC hrefs carried a double hyphen where kramdown emits one (headings whose punctuation leaves a single separator: `(32->64):`, `D114/D116):`, `"off":`) — dead on the live GitHub Pages site, corrected at source against the live ids. `docs/404.html` needed `permalink: /404.html` (else canonical/og:url read `/404.html.html`). Preview-builder slug rule fixed to kramdown: strip punctuation, map **each** whitespace char to `-` (so ` / ` -> `--`), no run-collapsing; headings with inline markup now get ids too. | +| Docs site: final sanity sweep + anchor fixes (2026-10-05) | done (local, unpushed) | `docs/porting-notes.md` (3 TOC hrefs), `docs/404.html` (permalink) | Audit of the 10-page published set (one h1 per page, no heading skips, in-page anchors resolve, unique titles/descriptions, og:image/canonical, JSON-LD, no local paths, served == built). Three `porting-notes.md` TOC hrefs had a double hyphen where kramdown emits one: corrected at source against the live ids. `docs/404.html` got `permalink: /404.html`. Preview-builder slug rule fixed to kramdown's. | ## Community forks (not ours; reference only) diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html index 79acc088..ebdc4942 100644 --- a/docs/_layouts/default.html +++ b/docs/_layouts/default.html @@ -11,7 +11,10 @@ large image preview in search cards, which is what makes the in-engine stills legible in a coverage roundup. --> - + + + {%- seo -%} diff --git a/docs/dev/GE-ENV-PROBES.md b/docs/dev/GE-ENV-PROBES.md index dcb31d3e..41eef687 100644 --- a/docs/dev/GE-ENV-PROBES.md +++ b/docs/dev/GE-ENV-PROBES.md @@ -98,6 +98,12 @@ Two classes: | `GE_PADMAPTEST` | `port/src/input.c` (`inputInit` -> `padMapSelfTest`) | D469: boot-time self-test of the gamepad action table. Proves preset 0 == the legacy hard-wired mapping (20,971,520 synthetic cases), logs the per-source mask table, a Custom remap, per-seat independence, the Start guard and the binding API; restores config afterwards (nothing persisted); the game then continues. Cached (`GE_ENVFLAG`). | **live** (harness) | | `GE_D466LOG=` | `port/src/drawdistgameplay.c` (`portD466FrameLog`, called from `bgDetermineVisibleRooms`) | D466 dev probe: every `` calls (per player view) logs `D466 f=.. active= ext= auth= subset= hext= hauth= chrOn= chrGp= seen= hgame=` to stderr: extended vs authored room-set counts + hashes, ONSCREEN chrs vs gameplay-on-screen chrs, `HAS_BEEN_ON_SCREEN` count, and a chr-state hash. Cached. | **live** (D466 dev probe) | | `GE_D468_SCALE=<0.05..1>` | `port/src/drawdistgameplay.c` (`portD468UpdateClamp`) | D468 dev falsifier: multiplies the AI-view-clamp fractions (and so turns the clamp on even at 16:9 / FOV 100), so a headless `GE_D466LOG` run can show the authored room set and gameplay on-screen chrs shrinking. Cached; out-of-range values ignored. | **live** (D468 dev probe) | +| `GE_SHADEPERSP=1` | `port/fast3d/gfx_opengl.cpp` (`gfx_opengl_init`, shader build) | D540 A/B switch: perspective-correct shade-colour interpolation instead of the default RDP-style screen-affine (`noperspective`) one (the sky horizon fade). Read once at GL init. | **live** (D540 A/B) | +| `GE_FOGPIXEL=1` | `port/fast3d/gfx_opengl.cpp` (`gfx_opengl_init`), `port/fast3d/gfx_pc.cpp` (`gfx_sp_vertex`) | D543 A/B switch: the D540 exact per-pixel fog (fog * w, divided by w in the shader) instead of the default per-vertex RSP fog (honest negative-w divide, CPU near clip lerps it). Read once at GL init. | **live** (D543 A/B) | +| `GE_FOGVERTEX` | (removed) | **removed** -- D540's per-vertex-fog opt-in; per-vertex fog is the D543 default, the opt-out is `GE_FOGPIXEL=1`; tombstone kept. | **removed** | +| `GE_NEARCLIP=0` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_tri1`) | D543 A/B switch: disable the CPU RSP-style near-plane clip (default on) and leave near-plane clipping to the GPU. Cached on first triangle. | **live** (D543 A/B) | +| `GE_NEARCLIPFOG=recompute` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_tri1`) | D543 A/B switch: recompute the per-vertex RSP fog value of near-clip-created vertices from their new z/w (as `gfx_sp_vertex`) instead of lerping it (default). Cached on first triangle. | **live** (D543 A/B) | +| `GE_FOGSNAP=1` | `port/fast3d/gfx_opengl.cpp` (fragment shader build) | D540 A/B switch: re-enables the D503 full-fog snap (fog >= 247/255 -> full), off by default since D540 (its hard edge swept across the ground in chunks). Cached on first shader build. | **live** (D540 A/B) | | `GE_PADFAMILY=xbox\|ps\|nintendo` | `port/src/input.c` (`inputPadFamily`) | D471 dev override: force the controller family used for button names in the PC menus (headless screenshots without the hardware). Cached. | **live** (D471 dev knob) | | `GE_D466_FORCE=1` | `port/src/drawdistgameplay.c` (`portD466Active`) | D466: run the authored-distance split even at DrawDistance 100% (identity test: authored set must equal extended). Cached. | **live** (D466 test hook) | | `GE_D466_OFF=1` | `port/src/drawdistgameplay.c` (`portD466Active`) | D466 kill switch: disable the gameplay/render split (every gameplay reader falls back to the extended-range state, i.e. the pre-fix #125 behaviour). For A/B only. Cached. | **live** (D466 kill switch) | @@ -112,6 +118,8 @@ Two classes: | `GE_OPTIONSOVERLAY_SECTION` | `port/src/optionsoverlay.c:989` | Isolated screenshot/probe of a nested F10 page by row key (opens that header; never a menu mode). | **live** (diagnostic) | | `GE_OPTIONSOVERLAY_SELECTBACK` | `port/src/optionsoverlay.c:995` | Screenshot-only: pins the selection to the Back row when the overlay auto-opens, to check it against a normally selected setting on the same page. | **live** (diagnostic) | | `GE_OPTIONSOVERLAY_DROPDOWN` | `port/src/optionsoverlay.c:1195` | Screenshot-only: with the overlay auto-opened on a page, opens the dropdown popup of the named row key (e.g. `Video.TextureFilter`, `__Resolution`) and focuses it. | **live** (diagnostic) | +| `GE_OPTIONSOVERLAY_ATFRAME` | `port/src/optionsoverlay.c:3362` | Re-applies the GE_OPTIONSOVERLAY / _SECTION / _SELECT request at emit frame n so it survives a level load (in-level screenshots, D558). Read once. | **live** (diagnostic) | +| `GE_OPTIONSOVERLAY_SELECT` | `port/src/optionsoverlay.c:1221` | Screenshot-only: selects the named row key on the auto-opened page (e.g. `Game.NoHitFlash`) so its tip shows (D558). | **live** (diagnostic) | | `GE_OPTIONTREEPROBE` | `port/src/optionsoverlay.c:888` | D388: opt-in in-memory walk of the real F10 page tree; writes no settings, binds or profile bytes; restores UI state afterward. | **live** (diagnostic) | | `GE_PRESETPROBE` | `port/src/optionsoverlay.c:957` (`presetProbe`) | D440: one-shot at overlay init — drives the "Original N64" and "Port defaults" preset rows through the real arm -> confirm activation and checks the "Active" state; logs `GE_PRESETPROBE: PASS/FAIL`. **Writes the live config** (persisted on exit) — use a throwaway data dir. | **live** (diagnostic) | | `GE_OVPOINTERMAP` | `port/src/optionsoverlay.c:1726` | D391 pointer-map diagnostic: on LMB-down (and every 120 samples) logs SDL mouse coords vs GL drawable pixel coords + the hover row, to separate hit-band/scroll errors from coordinate-space mismatch. | **live** (diagnostic) | diff --git a/docs/dev/HANDOFF-ARCHIVE.md b/docs/dev/HANDOFF-ARCHIVE.md index 197f8952..976c2e46 100644 --- a/docs/dev/HANDOFF-ARCHIVE.md +++ b/docs/dev/HANDOFF-ARCHIVE.md @@ -6242,7 +6242,7 @@ Local-only commits on top of `442eb2ff`: `aa19b354` (site SEO/redundancy), ## Environment - Preview: `:8777` (PID may be stale after a reboot — `node scratch/build_preview.mjs site && node scratch/serve_preview.mjs 8777`). -- Box ssh: `ssh the test box` (Windows→box works; box has no sshd — +- Box ssh: `ssh` to the X220 box (Windows→box works; box has no sshd — everything rides the same session's stdin/stdout; pushes via `cat f | ssh ... 'cat > dest'` or tar pipes). - 3 parked worktrees (ci/d441/modpatch) untouched. diff --git a/docs/dev/findings-index.csv b/docs/dev/findings-index.csv index b0f7250f..31a776db 100644 --- a/docs/dev/findings-index.csv +++ b/docs/dev/findings-index.csv @@ -40,7 +40,7 @@ D230,FIXED,In-level music quality:,"FIXED (M-121, v0.2.0) —" D231,CLOSED,"QoL batch: mute-on-focus-loss + screenshot-hotkey productization (M-105, roadmap Tier 3, port-only).","CLOSED — PARKED/DEPRIORITIZED (user, 2026-09-25)." D233,FIXED,All levels (intermittent):,"FIXED (port/fast3d/gfx_pc.cpp, gfx_sp_tri1) —…" D234,FIXED,"Dam: guard towers along the dam structure invisible except the first one (M-106, user QA report).",VISUALLY VERIFIED FIXED (M-108) —… -D244,OPEN,Self-hosted CI runner (corpyvt6agent01) link failure:,"OPEN, not yet fixed. Next…" +D244,OPEN,Self-hosted CI runner link failure:,"OPEN, not yet fixed. Next…" D228,FIXED,AK47 white missing-texture patches after D217's texpool fix —,"FIXED (port/fast3d/gfx_pc.cpp, palette_to_rgba32) —" D204,FIXED,PC audio ran ~2 % below real time permanently (AI feedback loop's 3 ms setpoint vs PC jitter).,"FIXED (port/src/audio.c, port/src/libultra.c, port/include/audio.h) —" D203,CLOSED,"Steam Deck (SteamOS, x86_64 Linux) v0.1.0 release bundle:",CLOSED — root cause + fix in D253. @@ -283,11 +283,35 @@ D403,RESOLVED,Deck v0.4.0-pre test:,"RESOLVED (2026-09-27, user-confirmed on the D404,RESOLVED,"""Aim style: Centred (PC)"" (Input.AimMode=1, D333/D337) has no effect when aiming with a gamepad; the row's own doc comment over-promises (""applies to every aim…","RESOLVED (2026-09-28, port-layer only):" D405,CLOSED,"fast3d/OpenGL: MSAA above the driver max black-screens; two glBindFramebuffer calls pass an index where a GL name is required (PR #106, italoarruda, 2026-09-25…","CLOSED (2026-09-28). Merged from PR #106 (code file only; security-reviewed clean - port-layer GL state fixes, no external input, no allocations)." D406,RESOLVED,front-end PC Options screen:,"RESOLVED (2026-09-28, port-layer only, Steam Deck-validated):" +D541,PARTIAL,One A press advanced two menu screens on the Steam Deck (Desktop Mode) —,"PARTIAL 2026-10-05 (mitigation landed, port-only:" +D542,OPEN,"Stray polygons left of Bond's gun, frequent on foggy levels (Streets, Surface 2) —",OPEN 2026-10-05 (triage: +D543,FIXED,Surface 2: lit area following Bond brighter than 1964 —,"FIXED 2026-10-06 (CPU RSP-style near clip in gfx_sp_tri1 + per-vertex fog default; build-verified, by-eye vs 1964 at next playtest)" +D544,FIXED,OS cursor flickers with F10 open on the main menu —,"FIXED 2026-10-05 (port-only, input.c; build-verified, maintainer mouse check owed)" +D545,FIXED,Player rockets can pass through the ground when fired near Bond's feet —,"FIXED 2026-10-06 (AVOID_UB array, gun.c; maintainer Archives preset A/B:" +D546,FIXED,PC settings standardization:,FIXED 2026-10-05 (port-only; build + maintainer check owed; golden re-base batched with D543) +D547,FIXED,Game.AllUnlocked applies live (no restart) —,"FIXED 2026-10-05 (port-only, main.c/optionsoverlay.c; build-verified, maintainer mission-select check owed)" +D548,FIXED,Walls near rocket fire/explosions banded black/white/yellow/blue (Aztec dark corridor; also seen on Deck) —,FIXED 2026-10-06 (gfx_opengl.cpp input clamp; root fix = D543 CPU near clip; maintainer Aztec re-check owed) +D549,FIXED,Thrown grenade/knife/object:,"FIXED 2026-10-06 (AVOID_UB coord3d, gun.c x3; build-verified)" +D550,FIXED,"Static title bar (no more per-second ""NN fps"" refresh) + project mark as favicon / window / taskbar icon —","FIXED 2026-10-17 (port + tooling only, video.c/gfx_sdl2.cpp/make_icon.py; ninja-command-verified + full build 249/249; maintainer live window/taskbar check owed)" +D551,?,"Opt-in update check (Game.CheckUpdates, default off):",ADDED 2026-10-06 (port layer only; live-verified Windows/Linux/Deck + F10 UI capture; security review fixes applied) +D552,FIXED,Dam ending cutscene: the first camera shot no longer swivels up onto Bond —,FIXED 2026-10-06 (port-only addition removed; code path back to the decomp; maintainer-verified live) +D553,FIXED,Surface 2: the haze around Bond brightened/darkened as he moved —,FIXED 2026-10-06 (fast3d: +D554,OPEN,F10 settings menu: wording/casing pass + two-line tips —,DONE 2026-10-06 (port/src/optionsoverlay.c; maintainer by-eye check owed) +D555,OPEN,Crosshair mouse pointer over the F10 / PC Options overlay —,"DONE 2026-10-06 (port/src/input.c, optionsoverlay.c, tools_pc/make_icon.py; maintainer by-eye check owed)" +D556,OPEN,"F10 feedback pass (tips trimmed, conditional-row order, wording fixes) + mouse scrollbar —","DONE 2026-10-06 (port/src/optionsoverlay.c; build only, by-eye check owed)" +D557,FIXED,"New/blank profiles start with Look ahead OFF on PC (Rule-2 sign-off, maintainer 2026-10-06) —",FIXED 2026-10-06 (src/game/file2.h DEFAULT_OPTIONS under #ifdef PORT; existing saves untouched; win gate 21/21) +D558,FIXED,F10 overlay tips cut off in a level (not on the front end) —,FIXED 2026-10-06 (optionsoverlay.c textRenderWide; verified by in-level capture) +D559,FIXED,"Look ahead off for untouched existing folders (maintainer-approved, port layer only) —",FIXED 2026-10-06 (watchsettings.c migrateUntouchedLookAhead) +D560,FIXED,F10 copy pass 3 (maintainer line review) —,FIXED 2026-10-06 (optionsoverlay.c tips/labels/value names; display text only) +D561,FIXED,"F10: ""Menu pointer"" row removed (front-end-only legacy option; confused players) —","FIXED 2026-10-06 (row/tip/names removed; Input.MenuPointerMode ini key, Direct default and the legacy code path kept for one release)" +D562,FIXED,F10 crosshair pointer hidden while a pad/keyboard drives the menu —,FIXED 2026-10-06 (drawCrosshairPointer draws only while s_mouseActive) +D563,FIXED,F10 root hub: Left/Right no longer open pages; held nav inputs latched on open/page change —,FIXED 2026-10-06 +D564,FIXED,"F10: game's own ""Look up/down"" row removed (dead for mouse and centred pad, stacked with Invert look); value pinned to factory; tip polish —","FIXED 2026-10-06 (port only; Mouse / Controller ""Invert look"" are the inversion controls)" D407,CLOSED,Tanks cannot be boarded/exited on PC —,CLOSED (2026-09-28 bookkeeping: D408,FIXED,Game.SkipIntro skips the post-mission failure dossier:,FIXED 2026-09-30 (SkipIntro hook gated to first boot; live dossier check owed). D415,FIXED,"Shooting a light fixture does not ""kill"" its flickering light (Bunker, Caverns):",FIXED (2026-09-29 bookkeeping: D416,RESOLVED,"2-player split-screen crashes (issue #99, v0.3.0 4827bc8, fault addr 0xa43a0ed4, PC 0x14009e758):",RESOLVED in-harness (converter + D417 + D418 fix the 2P crash and split-screen flicker; 18/18 2P/3P/4P x 6-stage load runs clean; live pad play still owed). -D417,RESOLVED,enum PROP unsigned on GCC:,"RESOLVED (src/bondconstants.h PROP__PORT_SIGNED = -1, D3/D142 precedent; Rule-2 review owed)." +D417,RESOLVED,enum PROP unsigned on GCC:,"RESOLVED (src/bondconstants.h PROP__PORT_SIGNED = -1, D3/D142 precedent; Rule-2 review done 2026-10-06:" D418,RESOLVED,"Split-screen: fast3d safe-area crop stretched the last-set (half-height) viewport over the whole window, so each player's view filled the screen and the last-d…","RESOLVED (gfx_set_split_screen, port-only)." D419,RESOLVED,Split-screen input shared between players / P1 lost solo controls:,"RESOLVED (per-slot slotPlayer(), port-only; no current-player swaps from the poll thread; live-tested OK)." D420,FIXED,MP score / stats screen shows an odd big number (issue #99) —,FIXED 2026-10-01 (mp/virtual-seats); 2P/3P/4P watch-menu runs show 0s. @@ -394,4 +418,5 @@ D531,FIXED,capture_p7.sh's D529 adaptation was incomplete —,"FIXED 2026-10-05 D532,FIXED,The three P12 U5 should-fixes applied —,FIXED 2026-10-05 (tooling-only: D533,FIXED,"The probe-doc drift that would have failed CI, plus three tracked-file accuracy/hygiene fixes (release-sweep spot-check) —",FIXED 2026-10-05 (GE-ENV-PROBES.md missing the GE_D526 family → gen_env_probes.py --check rc 1 = a CI-failing step; build-pc.sh local MSYS2 root in two messages; docs/fidelity.md og line; release-not… D534,FIXED,Pre-release review of the 2026-10-05 local batch —,FIXED 2026-10-05 (verify.sh/capture_p7.sh INT/TERM trap; release-notes linux-gate wording; dead security-and-fidelity-status.md links; future-dated 2026-10-07 labels normalised; D526 probe dangling-e… +D540,FIXED,Fog rework (Deck playtest):,FIXED 2026-10-05 (port: D450,FIXED,"Unplugging and replugging a controller was not picked up (user, GameSir-G7 Pro; 2026-09-29).","FIXED — port-only: the render thread's gfx_sdl_handle_events drains the same SDL queue as video.c's host pump but had no case for SDL_CONTROLLERDEVICEADDED/REMOVED, so it usually swallowed them and i…" diff --git a/docs/dev/findings.md b/docs/dev/findings.md index 4350da47..9be94ce6 100644 --- a/docs/dev/findings.md +++ b/docs/dev/findings.md @@ -424,7 +424,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | D231 | **QoL batch: mute-on-focus-loss + screenshot-hotkey productization (M-105, roadmap Tier 3, port-only).** `Audio.MuteOnFocusLoss` (drop mixed blocks while the window is unfocused; pause-on-focus-loss deferred because of the D204-family AI feedback loop) plus `Video.ScreenshotKey`/`Video.ScreenshotDir` (scancode-based key, timestamped `screenshots/` PPMs). Implemented M-105 as PR #56 (`port/src/audio.c`, `port/src/video.c`, `port/include/audio.h`), never merged. | **CLOSED — PARKED/DEPRIORITIZED (user, 2026-09-25).** PR #56 closed and the `feat/qol-mute-screenshot` branch was deleted from GitHub. The code is in no release branch; it's kept locally only (local branch `feat/qol-mute-screenshot`, commit `3f6e8d98`, plus a patch in the gitignored `docs/dev/notes/parked/`). If it's revived: rebase onto the current release branch and playtest alt-tab mid-level plus the custom screenshot key/dir. | | D233 | **All levels (intermittent): indoor level geometry invisible near doors — you can see through walls/floors into rooms you can't see into, while world objects…** — full `## D233` entry at file tail | **FIXED (port/fast3d/gfx_pc.cpp, `gfx_sp_tri1`) —… | | D234 | **Dam: guard towers along the dam structure invisible except the first one (M-106, user QA report).** — full `## D234` entry at file tail | **VISUALLY VERIFIED FIXED (M-108) —… | -| D244 | **Self-hosted CI runner (`corpyvt6agent01`) link failure: `undefined reference to 'objectiveGetStatus_WEAK'` (2026-09-12, first-ever build attempt on the…** — full `## D244` entry at file tail | **OPEN, not yet fixed.** Next… | +| D244 | **Self-hosted CI runner link failure: `undefined reference to 'objectiveGetStatus_WEAK'` (2026-09-12, first-ever build attempt on the…** — full `## D244` entry at file tail | **OPEN, not yet fixed.** Next… | | D228 | **AK47 white missing-texture patches after D217's texpool fix — separate failure mode (M-97, `-level_34` FACILITY).** — full `## D228` entries at file tail | **FIXED (`port/fast3d/gfx_pc.cpp`, `palette_to_rgba32`)** — byte-order bug in the G_TT_IA16 branch; user-verified live. Fix write-up: second `## D228` section. | | D204 | **PC audio ran ~2 % below real time permanently (AI feedback loop's 3 ms setpoint vs PC jitter).** — full `## D204` entry at file tail | FIXED (`port/src/audio.c`, `port/src/libultra.c`, `port/include/audio.h`) — measured; a by-ear pass on a real playthrough still owed | | D203 | **Steam Deck (SteamOS, x86_64 Linux) v0.1.0 release bundle: Facility (`-level_34`) crashes "after loading in as James Bond" (user bug report).** — full `## D203` entry at file tail | CLOSED — root cause + fix in D253. | @@ -691,11 +691,35 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | D404 | **"Aim style: Centred (PC)" (`Input.AimMode=1`, D333/D337) has no effect when aiming with a gamepad; the row's own doc comment over-promises ("applies to every aim input … pad trigger") but the centred path is mouse-only (Steam Deck, 2026-09-27).** Full `## D404` entry at file tail. | RESOLVED (2026-09-28, port-layer only): the toggle now genuinely works on a pad. First logged as a communication bug and closed by relabel ("Aim style (mouse)"), then reopened on user challenge the same day: the pad path emits stick *deflection* into the game's crosshair integrator, so the pad was actually always in the **N64** model (a flick travels the crosshair; edge-scroll + spring-back only made a hold *feel* direct), and the "stick has no crosshair to travel" rationale was wrong. Fix: new `padDirectCompute()` in `input.c` (the pad twin of `hipDirectCompute`'s CENTRED branch -- same dead/watch/pause/cutscene gates, minus the mouse-grab gate; revised same day to the N64 path's verbatim quadratic curve -- `sign(v)\u00b7clamp(defl/70,\u00b11)\u00b2 \u00b7 (fov/60) \u00b7 3.5 deg/poll` (bondview2.c canNaturalTurn/canNaturalPitch), full-stick parity with the N64 path, pending retest on the refreshed build); the natural-pitch pad branch routes CENTRED through it and keeps N64's classic deflection behaviour (also the default, so no pad default changes). Labels reverted to device-neutral ("Aim style" / "Aim range"). **Steam Deck (2026-09-27/28): "centered aim is broken on deck too" -- fixed for real; the first-pass linear gain was measured as a dramatic sensitivity loss (2026-09-28) and replaced with the N64 path's verbatim quadratic curve (fov/60 base, 3.5 deg/poll top rate); retest on the refreshed build: user "good". RESOLVED.** | | D405 | **fast3d/OpenGL: MSAA above the driver max black-screens; two `glBindFramebuffer` calls pass an index where a GL name is required** (PR #106, italoarruda, 2026-09-25; label rebased from the PR's D323, which collides with our D323 LOD finding) — full `## D405` entry at file tail | **CLOSED (2026-09-28).** Merged from PR #106 (code file only; security-reviewed clean - port-layer GL state fixes, no external input, no allocations). `resolve_msaa_color_buffer` clamps `msaa_level` to `GL_MAX_SAMPLES`; the resolve/copy paths bind `current_framebuffer ? framebuffers[i].fbo : 0` (the default framebuffer is GL name 0). Both Windows (opengl32) and Linux use this backend. Post-merge sweep 21/21 PASS + MSAA=16 boot check render-verified; link ritual clean. | | 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). | +| D541 | **One A press advanced two menu screens on the Steam Deck (Desktop Mode) — cause unconfirmed (Steam desktop layout A=Enter or a pad reseat)** — Deck playtest; full `## D541` entry at file tail | PARTIAL 2026-10-05 (mitigation landed, port-only: 70 ms menu release hold-off in `input.c` + overlay accept; one back per 150 ms) — mitigation, cause unconfirmed; Deck re-test owed | +| D542 | **Stray polygons left of Bond's gun, frequent on foggy levels (Streets, Surface 2)** — D540-build playtest; full `## D542` entry at file tail | OPEN 2026-10-05 (triage: GE_SHADEPERSP / GE_FOGVERTEX / rc1 A/B, then 1964) | +| D543 | **Surface 2: lit area following Bond brighter than 1964** — full `## D543` entry at file tail | FIXED 2026-10-06 (CPU RSP-style near clip in gfx_sp_tri1 + per-vertex fog default; build-verified, by-eye vs 1964 at next playtest) | +| D544 | **OS cursor flickers with F10 open on the main menu** — show/hide requests race (inputSuspendForOverlay vs the D519 front-end pointer); full `## D544` entry at file tail | FIXED 2026-10-05 (port-only, input.c; build-verified, maintainer mouse check owed) | +| D545 | **Player rockets can pass through the ground when fired near Bond's feet** — `gunInitProjectileFromPlayer` passes a single `s32` as a 20-entry room out-array (N64 overflow absorbed by `pad_a`; D490 / porting-notes §D24 class); full `## D545` entry at file tail | FIXED 2026-10-06 (AVOID_UB array, gun.c; maintainer Archives preset A/B: 1 drop in ~60 at wide view, residual = room-pool hypothesis, hardening backlogged) | +| D546 | **PC settings standardization: round defaults, vertical-degree FOV, "Gameplay view area" wording** — draw/LOD 2.0x, deadzone 25 %, mouse sens max 4x, FOV 30–90° in 5° steps (60 = N64, PD "Vert FOV" model), one-time ini migration; full `## D546` entry at file tail | FIXED 2026-10-05 (port-only; build + maintainer check owed; golden re-base batched with D543) | +| D547 | **Game.AllUnlocked applies live (no restart)** — F10 row seeds/clears the RAM unlock flags it owns; full `## D547` entry at file tail | FIXED 2026-10-05 (port-only, main.c/optionsoverlay.c; build-verified, maintainer mission-select check owed) | +| D548 | **Walls near rocket fire/explosions banded black/white/yellow/blue (Aztec dark corridor; also seen on Deck)** — D540 `noperspective` combiner inputs extrapolated past 1.0 by GPU near-clipping, then the CC wrap emulation wrapped them; full `## D548` entry at file tail | FIXED 2026-10-06 (gfx_opengl.cpp input clamp; root fix = D543 CPU near clip; maintainer Aztec re-check owed) | +| D549 | **Thrown grenade/knife/object: `s32 sp94` receives a 12-byte `coord3d` from `bullet_path_from_screen_center`** — D24 class (N64 overflow hit a dead slot, PC can hit live locals); full `## D549` entry at file tail | FIXED 2026-10-06 (AVOID_UB coord3d, gun.c x3; build-verified) | +| D550 | **Static title bar (no more per-second "NN fps" refresh) + project mark as favicon / window / taskbar icon** — original generated crosshair art (no game content); full `## D550` entry at file tail | FIXED 2026-10-17 (port + tooling only, video.c/gfx_sdl2.cpp/make_icon.py; ninja-command-verified + full build 249/249; maintainer live window/taskbar check owed) | +| D551 | **Opt-in update check (`Game.CheckUpdates`, default off): one HTTPS GET per launch to GitHub `/releases/latest`; `GE007_VERSION` added** — full `## D551` entry at file tail | ADDED 2026-10-06 (port layer only; live-verified Windows/Linux/Deck + F10 UI capture; security review fixes applied) | +| D552 | **Dam ending cutscene: the first camera shot no longer swivels up onto Bond — the D243 M-190 port-only look-at filter re-seed snapped it onto him** — full `## D552` entry at file tail | FIXED 2026-10-06 (port-only addition removed; code path back to the decomp; maintainer-verified live) | +| D553 | **Surface 2: the haze around Bond brightened/darkened as he moved — near-plane-clipped vertices lerped fog from a behind-the-eye corner that flips 0/255** — full `## D553` entry at file tail | FIXED 2026-10-06 (fast3d: fog recomputed on clip-created vertices by default, as the RSP; `GE_NEARCLIPFOG=lerp` = old A/B) | +| D554 | **F10 settings menu: wording/casing pass + two-line tips** — display text only, config keys unchanged; tips wrap instead of ".." — full `## D554` entry at file tail | DONE 2026-10-06 (port/src/optionsoverlay.c; maintainer by-eye check owed) | +| D555 | **Crosshair mouse pointer over the F10 / PC Options overlay** — the game's own crosshair sprite is drawn at the mouse, last in the overlay display list (OS cursor hidden; reworked 2026-10-06 from an SDL colour cursor); opt-out `Input.CrosshairCursor` — full `## D555` entry at file tail | DONE 2026-10-06 (port/src/input.c, optionsoverlay.c, tools_pc/make_icon.py; maintainer by-eye check owed) | +| D556 | **F10 feedback pass (tips trimmed, conditional-row order, wording fixes) + mouse scrollbar** — standard PC settings lose their tips, Controller page keeps Control style first (Profile chooser now sits above the first profile row, not row 1), No hit flash / Skip intro tips corrected, thin draggable scrollbar on overflowing pages — full `## D556` entry at file tail | DONE 2026-10-06 (port/src/optionsoverlay.c; build only, by-eye check owed) | +| D557 | **New/blank profiles start with Look ahead OFF on PC (Rule-2 sign-off, maintainer 2026-10-06)** — full `## D557` entry at file tail | FIXED 2026-10-06 (`src/game/file2.h` DEFAULT_OPTIONS under `#ifdef PORT`; existing saves untouched; win gate 21/21) | +| D558 | **F10 overlay tips cut off in a level (not on the front end)** — full `## D558` entry at file tail | FIXED 2026-10-06 (`optionsoverlay.c` `textRenderWide`; verified by in-level capture) | +| D559 | **Look ahead off for untouched existing folders (maintainer-approved, port layer only)** — full `## D559` entry at file tail | FIXED 2026-10-06 (`watchsettings.c` `migrateUntouchedLookAhead`) | +| D560 | **F10 copy pass 3 (maintainer line review)** — full `## D560` entry at file tail | FIXED 2026-10-06 (`optionsoverlay.c` tips/labels/value names; display text only) | +| D561 | **F10: "Menu pointer" row removed (front-end-only legacy option; confused players)** — full `## D561` entry at file tail | FIXED 2026-10-06 (row/tip/names removed; `Input.MenuPointerMode` ini key, Direct default and the legacy code path kept for one release) | +| D562 | **F10 crosshair pointer hidden while a pad/keyboard drives the menu** — full `## D562` entry at file tail | FIXED 2026-10-06 (`drawCrosshairPointer` draws only while `s_mouseActive`) | +| D563 | **F10 root hub: Left/Right no longer open pages; held nav inputs latched on open/page change** — full `## D563` entry at file tail | FIXED 2026-10-06 | +| D564 | **F10: game's own "Look up/down" row removed (dead for mouse and centred pad, stacked with Invert look); value pinned to factory; tip polish** — full `## D564` entry at file tail | FIXED 2026-10-06 (port only; Mouse / Controller "Invert look" are the inversion controls) | | 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 | **CLOSED (2026-09-28 bookkeeping: tank board/exit accepted by the user; shipped in v0.4.0).** Earlier: 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 | FIXED 2026-09-30 (SkipIntro hook gated to first boot; live dossier check owed). | | D415 | **Shooting a light fixture does not "kill" its flickering light (Bunker, Caverns): the fixture takes the hit but the flicker keeps going (community report, issue #87 comment by the reporter, 2026-09-28, 00 Agent playthrough of v0.4.0).** — full `## D415` entry at file tail | FIXED (2026-09-29 bookkeeping: root-caused and fixed as D430 + D431, user-verified vs GEPD/1964 on Caverns 2026-09-29; #119). Earlier: ROOT-CAUSED as D430 (see there); fix in tree, live check owed. | | D416 | **2-player split-screen crashes (issue #99, v0.3.0 `4827bc8`, fault addr 0xa43a0ed4, PC 0x14009e758): MP setup files `Ump_setup*Z` were never converted N64→PC** — full `## D416` entry at file tail | RESOLVED in-harness (converter + D417 + D418 fix the 2P crash and split-screen flicker; 18/18 2P/3P/4P x 6-stage load runs clean; live pad play still owed). Also D417, D418. -| D417 | **`enum PROP` unsigned on GCC: `prop >= 0` passed for `getPropForHeldItem()`'s -1 (fist) -> `modelLoad(-1)` null deref in any 2P+ stage (player.c `sub_GAME_7F09B398`)** — full `## D417` entry at file tail | RESOLVED (src/bondconstants.h `PROP__PORT_SIGNED = -1`, D3/D142 precedent; **Rule-2 review owed**). | +| D417 | **`enum PROP` unsigned on GCC: `prop >= 0` passed for `getPropForHeldItem()`'s -1 (fist) -> `modelLoad(-1)` null deref in any 2P+ stage (player.c `sub_GAME_7F09B398`)** — full `## D417` entry at file tail | RESOLVED (src/bondconstants.h `PROP__PORT_SIGNED = -1`, D3/D142 precedent; Rule-2 review done 2026-10-06: compiler-ABI class, no behaviour change). | | D418 | **Split-screen: fast3d safe-area crop stretched the last-set (half-height) viewport over the whole window, so each player's view filled the screen and the last-drawn (shuffled) player won -> alternating full-screen flicker** — full `## D418` entry at file tail | RESOLVED (`gfx_set_split_screen`, port-only). | | D419 | **Split-screen input shared between players / P1 lost solo controls: poll thread used g_CurrentPlayer for per-slot aim/stance/style/use** — full `## D419` entry at file tail | RESOLVED (per-slot `slotPlayer()`, port-only; no current-player swaps from the poll thread; live-tested OK). | | D420 | **MP score / stats screen shows an odd big number (issue #99)** — `char rankbuffer[4]` overrun ("Rank: 1st") clobbered the adjacent `scores[]` (D8 class); widened under `#ifdef PORT` — full `## D420` entry at file tail | FIXED 2026-10-01 (`mp/virtual-seats`); 2P/3P/4P watch-menu runs show 0s. | @@ -802,6 +826,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | D532 | **The three P12 U5 should-fixes applied — the gate's ini pin now fails closed, the seed is hard-pinned (stray `GE_RSEED` warns), and `audiodebug.ps1`'s dead A/B switches are gone** — golden gate + tooling; D531/U5 follow-up; full `## D532` entry at file tail | FIXED 2026-10-05 (tooling-only: `pin_ini_640x480` fails closed like `pin_eep`; `GOLDEN_SEED` literal + warning, capture `eval` unaffected; `-AB`/`-Old`/`-OldMode` removed with a note) | | D533 | **The probe-doc drift that would have failed CI, plus three tracked-file accuracy/hygiene fixes (release-sweep spot-check)** — release gate (P12 U2/U6/U7 spot-checks); full `## D533` entry at file tail | FIXED 2026-10-05 (`GE-ENV-PROBES.md` missing the `GE_D526` family → `gen_env_probes.py --check` rc 1 = a CI-failing step; `build-pc.sh` local MSYS2 root in two messages; `docs/fidelity.md` og line; release-notes gate paragraph quoted a pre-D531 spread) | | D534 | **Pre-release review of the 2026-10-05 local batch — Ctrl-C in the golden gate could leave the user's save replaced, plus doc accuracy fixes** — release gate; full `## D534` entry at file tail | FIXED 2026-10-05 (`verify.sh`/`capture_p7.sh` INT/TERM trap; release-notes linux-gate wording; dead `security-and-fidelity-status.md` links; future-dated `2026-10-07` labels normalised; D526 probe dangling-else; verify.sh usage range) | +| D540 | **Fog rework (Deck playtest): ground fog chunked (D503 snap) and cycled light/dark at Bond's feet (behind-camera vertex fog), sky horizon fade squeezed (perspective shade), fog tied to Draw distance** — full `## D540` entry at file tail | FIXED 2026-10-05 (port: exact per-pixel fog, snap off, noperspective shade, new `Video.FogDistance` default 1.0x = N64 (far clip = min(Fog, Draw) x authored), Draw/Fog distance live; maintainer A/B vs 1964 OK; goldens re-based win/linux/deck); Deck re-check owed | | 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)". | | D450 | **Unplugging and replugging a controller was not picked up (user, GameSir-G7 Pro; 2026-09-29).** — full `## D450` entry at file tail | FIXED — port-only: the render thread's `gfx_sdl_handle_events` drains the same SDL queue as video.c's host pump but had no case for `SDL_CONTROLLERDEVICEADDED/REMOVED`, so it usually swallowed them and `inputRescanPads` never ran. Both pumps now post a request; the rescan runs at the top of `inputUpdate`, on the thread that reads the pads. | @@ -11794,9 +11819,9 @@ D173 (level-start floating), D292 (death sinking), and the Cradle spin (also und **2026-10-01: diagnostics stripped (ROADMAP §7 scaffolding strip, batch 3, branch `chore/strip-d243-probes`).** D243 is FIXED (M-189/M-190); the investigation scaffolding is removed, behaviour unchanged. Removed: `GE_D243M` and its plumbing (`d243mEnabled`/`d243mProbeActive`/`d243mGetFrameCounter`, `D243M:` CREATE/REMOVE/tick/draw/f488write/firinganim lines in `bondview2.c`; chrdraw census in both `drawjointlist` variants of `objecthandler.c`; animrestart in `chraction.c`; playanim in `chrai.c`; setanim_caller/setanimspeed/setanimplayspeed/tickstart/postadv/biganim and the clamp log lines in `model.c`; prebefore/onscreen in `chr.c`), `GE_D243X2` (prop->pos save/restore) and `GE_D243X4` (render_pos freeze) in `chr.c`, `GE_D243CAM` (3 sites) and `GE_D243SWIRL` in `bondview2.c`, and the `GE_D243` menu-transition trace + `d243MenuName` in `front.c`. (`GE_D243X3` was already gone.) Kept (the shipped fix and its supports): the `sizeof(Model)` buffer-layout fix in `bondview2.c` (M-189); the unconditional `field_488.pos` write + teleport-epoch filter re-seed (M-190: `d243NotifyTeleport`/`d243GetTeleportEpoch`, the call in `chrai.c` `AI_TRYTeleportingChrToPad`); `gameScriptedCameraActive()` and its two users (`model.c` clamps, `port/src/input.c` mouse-look gate); the M-183/M-185 playspeed/endframe clamps in `model.c` (Rule-2 retention sign-off, M-192) minus their log lines. Left alone: `GE_DEBUGMENU` in `src/boss.c` (a debug-menu enabler whose comment cites D243; not a D243 probe). -## D244 — Self-hosted CI runner (`corpyvt6agent01`) link failure: `undefined reference to 'objectiveGetStatus_WEAK'` (2026-09-12, first-ever build attempt on the… +## D244 — Self-hosted CI runner link failure: `undefined reference to 'objectiveGetStatus_WEAK'` (2026-09-12, first-ever build attempt on the… -**Self-hosted CI runner (`corpyvt6agent01`) link failure: `undefined reference to 'objectiveGetStatus_WEAK'` (2026-09-12, first-ever build attempt on the reinstalled box, run `34707798707`).** `src/game/objective_status.c:162` uses `#pragma weak objectiveGetStatus_WEAK = get_status_of_objective` (a decomp ground-truth alias, referenced 2-arg from `chrai.c:2538` — comment calls it "horrible hack to get ai matching"). The box's `g++`/`ld` reported as `x86_64-w64-mingw32/16.1.0`; a minimal local repro of the identical `#pragma weak` cross-TU function alias **links cleanly** on this session's own machine (`mingw-w64-x86_64-gcc 16.2.0-3`, `binutils 2.47-3`). Not a game-logic bug (the alias is unmodified decomp code, ground truth) and not a CMakeLists/link-flag issue (no `--gc-sections`/LTO in play) — the leading, unverified explanation is the self-hosted box's MSYS2 packages predate a PE/COFF weak-external fix or behavior change versus current MSYS2, consistent with rollout step 2's earlier finding that the box's mingw64 package set was stale/incomplete at initial setup. +**Self-hosted CI runner link failure: `undefined reference to 'objectiveGetStatus_WEAK'` (2026-09-12, first-ever build attempt on the reinstalled box, run `34707798707`).** `src/game/objective_status.c:162` uses `#pragma weak objectiveGetStatus_WEAK = get_status_of_objective` (a decomp ground-truth alias, referenced 2-arg from `chrai.c:2538` — comment calls it "horrible hack to get ai matching"). The box's `g++`/`ld` reported as `x86_64-w64-mingw32/16.1.0`; a minimal local repro of the identical `#pragma weak` cross-TU function alias **links cleanly** on this session's own machine (`mingw-w64-x86_64-gcc 16.2.0-3`, `binutils 2.47-3`). Not a game-logic bug (the alias is unmodified decomp code, ground truth) and not a CMakeLists/link-flag issue (no `--gc-sections`/LTO in play) — the leading, unverified explanation is the self-hosted box's MSYS2 packages predate a PE/COFF weak-external fix or behavior change versus current MSYS2, consistent with rollout step 2's earlier finding that the box's mingw64 package set was stale/incomplete at initial setup. **Status (from §F table):** **OPEN, not yet fixed.** Next: on the box, `pacman -Syu` (or at minimum `pacman -S mingw-w64-x86_64-gcc mingw-w64-x86_64-binutils`) to bring the toolchain current, then re-dispatch `selfhosted.yml`. If updating doesn't clear it, the fallback is a narrow `#ifdef PORT` replacement of the alias with an explicit thin wrapper function in `objective_status.c` (ABI-preserving, no logic change) — but try the toolchain update first since the local repro suggests this is a version/environment gap, not a fundamental PE/COFF limitation. @@ -12744,7 +12769,7 @@ This confirms the M-160/M-164/M-165 hypothesis (`## D173`'s own consolidated-nex 1. *Change:* `src/game/chrai.c`, `chraiitemsize()`, `case AI_PRINT:` — replace the NUL-scan with `return 1;` (the record's true on-disk size), under `#ifdef PORT`; the `#else` arm is byte-identical to the original decomp. 2. *Proof of divergence:* mechanism verified live (gdb symbol + per-tick probe lines above, exact offsets/sizes/landing bytes). Intended-behavior references: (a) the list source itself — PRINT sits between two real commands and is a no-op by construction; (b) user's direct N64 experience — guards do not stand frozen in Caverns on real hardware (same standard as M-144's user comparison that settled D243). Honest caveat recorded at request time: the ROM *code* would mis-skip on N64 too if reached — this is a latent original defect, and the fix restores the list author's intent rather than provable N64 behavior. 3. *Why not `port/`:* no N64 hardware dependency to shim — the sizer function's own logic operating on game data is the defect; nothing upstream (ABI/layout §A1 suspects, port-layer) is involved. *Why not the ABI exception:* nothing pointer-width-driven; the 32-bit original has identical code and data. -4. *Same-engine precedent:* Perfect Dark removed `PRINT` from the AI language entirely and **reassigned slot 0xAD** to a real command (`aiChrCopyProperties`, `pd_port/src/game/chraicommands.c:8159`) — Rare themselves retired this broken record class in the next title on the same engine. +4. *Same-engine precedent (CORRECTED 2026-10-05, 3g review):* the original text here claimed Perfect Dark removed `PRINT` and reassigned slot 0xAD to `aiChrCopyProperties`; that was wrong (`aiChrCopyProperties` is PD `@cmd 0173`, and PD's 16-bit opcodes are numbered independently of GE's). PD **kept** PRINT (`CMD_PRINT 0x00b5`) with the same NUL-scan sizer (`chraiGetCommandLength`, `pd_port/src/game/chrai.c:796`), and its global lists carry the strings inline (`pd_port/src/game/gailists.c`, e.g. `dprint 'd','y','e','n','g',0` = GE's commented-out `PRINT("dyeng")`). So Rare fixed the *data* in the next title, not the sizer. The v2 split still follows the real encoding: `chraidata.c` is in the byte-matching N64 link (`ld/game.data.ld.inc`), so the cartridge's global lists genuinely hold 1-byte PRINTs (the `PRINT(STRING)` macro drops its argument), while all 398 level-list PRINTs in the US setups carry cmd + string + NUL (static scan through the real `chraiitemsize`, 21 setups, 0 mismatches). An equivalent data-side alternative (PD style: emit the two strings in `chraidata.c` under `#ifdef PORT` via `debug_log` chars, drop `d310ItemSize` and the per-label-jump list lookup) is noted as a possible post-release cleanup, not needed for correctness. **Grant (2026-09-18, in-thread):** "I want to proceed with your implementation plan here to fix this bug." (User's reply to the full §7 write-up; asked only whether a fresh session was needed — answered no, granted in this session.) @@ -12785,6 +12810,8 @@ This confirms the M-160/M-164/M-165 hypothesis (`## D173`'s own consolidated-nex **2026-09-25 playtest (user, Windows, one ~46-min session Silo → Caverns, `GE_D322=1 GE_D204=1`, build `7baf4900`):** Caverns combat ~2 min, plus Silo onwards: zero `D311: … ANOMALY` lines (the probe logs via `osSyncPrintf` into the same log, and D318W lines from the same path prove it is live). Combined with D336 (upstream poison source) this closes D311. Guard stays (Rule-2 sign-off 2026-09-19). +**2026-10-06 (3f behaviour audit, maintainer asked to revisit "AI hacks"):** keep as-is. The guard acts only on frame values that are non-finite or beyond +/-1e6, which no legitimate animation reaches, so it cannot alter a real animation; it stops a hang that already-corrupt state would cause. The corruption source is fixed (D336, ABI), so on a healthy run it never fires; cost is two compares per anim tick. Not an AI/behaviour workaround. + ## D312 — Throwable items (mines, grenades, etc.) can clip through walls in certain spots (user report, v0.3.0 pre-release playtest, 2026-09-18) — FIXED: `bgBuildRoomVtxBounds` mis-reads the G_VTX vertex count on PC (Gfx_le `dma.par` bit-layout shift), collapsing whole batches of wall geometry out of the collision query (diagnosed + fixed, 2026-09-18). **Reported:** user's last-minute v0.3.0 playtest: thrown/placed throwable items — mines, grenades, "etc." — can pass through wall geometry in certain spots rather than colliding with it. Spot-dependent ("certain spots"), not universal. @@ -15859,6 +15886,8 @@ flickering fixture), then repro on PC. **Fix:** `PROP__PORT_SIGNED = -1` sentinel under `#ifdef PORT` at the head of `enum PROP` (`src/bondconstants.h`), following the `SP_LEVEL__PORT_SIGNED` precedent. `PROP_ALARM1` stays 0, sizeof unchanged, no stored value changes. Also corrects `drop_inventory` (`propid >= 0`). **This is a `src/game`-header edit: Rule-2 review owed.** +**Rule-2 review (2026-10-06, 3f behaviour audit):** not a behavioural change. IDO gives `enum PROP` a signed int type, so the decomp's `prop >= 0` test rejected -1 on the N64; GCC's unsigned choice for an all-non-negative enum is compiler-ABI drift (porting-notes §D3, D142, `SP_LEVEL__PORT_SIGNED` precedent). The unused sentinel restores IDO's signedness: no value, size or logic change, N64 semantics exact. Closed as ABI class; no sign-off needed. + **Status:** RESOLVED. ## D418 — Split-screen alternating full-screen views: safe-area crop remapped sub-rect viewports (issue #99) @@ -17376,3 +17405,803 @@ in as calendar dates; every commit is dated 2026-10-05) normalised to `if`, so with no `GE_D526BOX` the off-screen filter never ran) — braced; `verify.sh`'s usage print (`sed -n '2,32p'`) cut the `--json`/`--against` lines — now `2,34p`; an unbalanced bold in the ROADMAP Golden-baselines row. + +## D540: Fog rework from the Deck playtest — ground fog "chunked" and cycled light/dark around Bond, sky horizon fade squeezed, fog tied to Draw distance; new Fog distance slider, Draw/Fog distance apply live (2026-10-05) + +**Reports (v0.5.0-rc, maintainer, Deck + Windows A/B against 1964/GEPD on +Surface 2):** ground fog "blocky — updates one piece of the ground at a time / +chunks away" as Bond walks; the ground around Bond's feet cycled bright/dark +as he moved or turned; the sky's clouds stayed bright down to the horizon +where 1964 fades them into fog; no way to set fog independently ("fog, draw +and LOD distance all seem independent"); Draw distance only applied on the +next level load. + +**Causes (port renderer, `port/fast3d/`):** +1. *Chunking* — the D503 full-fog snap (fog >= 247/255 -> full). Its hard 97% + edge followed the per-vertex fog's triangle-shaped iso-lines and swept + across the ground tile by tile. Maintainer A/B with the snap disabled: + smooth. (The rc1 per-vertex form of the snap was worse.) +2. *Light/dark cycling at Bond's feet* — per-vertex fog for triangle corners + behind the camera. The old code forced `winv = +32767` for `w < 0` + (sign flip -> no-fog/full-fog jumps); even with the RSP's honest + negative-w divide (full fog), that corner's value is blended into the + visible ground after GPU near-clipping, so the area at the player's feet + changed with whichever corners sat behind the camera. +3. *Sky horizon fade* — the RDP interpolates shade colour affinely in screen + space; the GPU default is perspective-correct. On a sky triangle running + from overhead to the horizon that kept the bright cloud colour almost to + the horizon and squeezed the authored fade into a thin band. +4. *Fog vs Draw distance* — D218 scaled the far clip and kept the authored + per-mille fog positions, so fog moved out with Draw distance (2.5x + default): fogged levels were not N64 at the defaults. Both were computed + only at level load (`fogLoadCurrentEnvironment`). + +**Fixes:** +- Fog is exact per pixel: the vertex sends `z*fog_mul + w*fog_offset` + (fog x w, linear in clip space, clips correctly) and the fragment shader + divides by `gl_FragCoord.w`. Same fog function, colour and positions as the + RSP; corners behind the camera no longer contribute. Differs from the N64 + only in the in-between shading of large triangles (the N64 is per-vertex). +- D503 snap off by default (`GE_FOGSNAP=1` restores it). +- Shade colour (`vInputN`) is `noperspective` (desktop GLSL >= 1.30; GLSL ES + keeps perspective) — RDP-style screen-affine Gouraud; textures stay + perspective-correct. +- `Video.FogDistance` (new, 50..800 %, default 100 = N64; F10 Graphics + "Fog distance", both display presets 100): `bgfog.c` (`#ifdef PORT`, the + D218 site) now scales the level's far clip/fog by + `min(FogDistance, DrawDistance)` instead of DrawDistance alone, and keeps + the game's own per-mille fog positions and intensity math unchanged (the + D218 mechanism, driven by fog). At Fog 1.0x that is the authored far: exact + N64 fog at any Draw distance (Cradle: Draw 2.5x vs 1.0x 0.05% px, the + distant truss a ghost as on 1964). Draw distance still scales prop draw + distance (`propobj.c`), the room pool and the D466 split within that clip. + **Rejected first cut (same evening):** keep the far clip at Draw x and + convert the per-mille fog positions to distances and back. Two defects: + (1) the game reads the two fractions as LINEAR distance for culling but as + DEPTH-BUFFER per-mille for the prop/chr fade (`sp20`/`sp1C` -> + `fogGetPropDistColor`), and feeding both the linear rescale faded props and + characters out (Train attract: no crates, no Bond); (2) with the clip far + out, the fog sits within a few per-mille of 1000, so the s32 rounding + pulled the fog visibly in (Cradle truss fogged out), and the NDC conversion + assumed `viSetZRange`'s first argument (`BlendMultiplier`) is the + projection near plane -- Fog 2.0x fogged the walkway at Bond's feet. Lesson: + extend the clip only as far as the fog needs and leave the game's fog math + alone. +- Live apply: `fogSetRenderFogColor` re-runs `fogLoadCurrentEnvironment` on + the game thread when either multiplier changed (pointer reset per level). +- A/B switches (registered): `GE_FOGVERTEX=1` (per-vertex RSP fog), + `GE_SHADEPERSP=1` (perspective shade), `GE_FOGSNAP=1`. + +**Verified:** maintainer side-by-side vs 1964/GEPD on Surface 2 at Bond's +start and while moving: ground fog smooth, no cycling at Bond's feet, horizon +fade matches; "everything else looks solid". Maintainer 1964 stills of the +Frigate and Cradle starts: Frigate's sea is dark navy (the new build; the old +goldens' teal was the 2.5x fog), Cradle's distant truss is a faint ghost +(matched by the final clip design above). Runway/Surface 1/Dam/Jungle +headless captures inspected during the per-pixel step. Goldens re-based on +all three platforms the same evening (win 21/21 confirming pass; linux on the +X220; first `deck` set, see tools_pc/golden/README.md). **Owed:** maintainer +re-check on the Deck (fog walk on Surface 2; Fog/Draw sliders live). A remaining "slight scale or rotation" in +the sky clouds vs 1964 was attributed by the maintainer to 1964's non-4:3 +window aspect; re-check only against a correct-aspect reference (simple64 / +ParaLLEl-RDP) before opening a row. + +## D541: one A press advanced two menu screens on the Steam Deck (Desktop Mode) — two OR'd input sources for one press (Deck playtest, 2026-10-05) + +**Report:** v0.5.0-rc on the Deck, launched in Desktop Mode — A "clicks forward +too fast" on file select, level select and the F10 / PC Options overlay +(maintainer: a double trigger from single quick presses). + +**Cause: UNCONFIRMED.** The maintainer had switched Steam to controller +(gamepad) mode in Desktop Mode, which should disable the desktop key mapping +below, so that mapping is only one candidate; the other is a Steam virtual-pad +re-enumeration (`inputOpenPads` reseat) mid-press (not read). Tracer +(per-source A edges + pad add/remove) deferred by the maintainer. Candidate 1: outside a +Steam-launched game, Steam applies its Desktop Configuration to the Deck's +controls (A types Enter, B types Escape) while SDL also reads the raw pad. The +menu accept is an OR of pad A/X, Enter and the left mouse button +(`input.c` front-end mapping; overlay `accept`), so the two sources of one +press, a few ms out of step, gave a 1→0→1 level = two edges. In the overlay +Escape (key event → `s_backPending`) and the pad-B edge are separate back +paths, so one B could back out two pages. Ruled out (Sonnet triage, high +confidence): the `joyPoll` (scheduler thread) / `joyConsumeSamples` (game +thread) ring in `src/joy.c` — a clean edge lands in exactly one consume +window; the full-ring overwrite path can only drop a press, not repeat one. + +**Fix (port-only, menus only):** a ~70 ms release hold-off on menu A/B/Start +in `inputComputePad` (applied before the GE_INPUTSCRIPT harness) and on the +overlay's `accept`; one back action per 150 ms across Esc and pad B. Press +edges are untouched, gameplay is unaffected. Covers candidate 1 and any +multi-source press; may not cover a reseat. **Owed:** Deck re-test in Desktop +Mode (and Game Mode) — one press = one screen; B backs out one page. + +## D542: stray polygons drawn to the left of Bond's gun, frequent on foggy levels (Streets, Surface 2) — OPEN (Deck/Windows playtest of the D540 build, 2026-10-05) + +**Report:** with the D540 fog build, polygons "randomly draw to the left of +Bond's gun", very often, especially on foggy levels (Streets, Surface 2). + +**Not yet triaged.** Candidates: (a) a D540 regression in near-camera geometry +(the viewmodel and nearby room triangles: `noperspective` shade, or the +per-pixel fog's `fog * w` path); (b) the pre-existing Streets "1-frame polygon +explosion near the body armour / grenade launcher" from the same playtest (its +1964 check is still owed), now seen more often. **First step (cheap A/B, no +code):** same spot with `GE_SHADEPERSP=1`, then `GE_FOGVERTEX=1`, then the rc1 +binary (Deck: `rc1-backup/ge007.x86_64`); whichever makes it vanish names the +layer. A 1964 look at the same spot settles faithful vs port. + +## D543: Surface 2 — the lit area that follows Bond looks brighter than on 1964 — OPEN (2026-10-05) + +**Report:** after D540, the ground around Bond on Surface 2 reads as a bright +light following him; 1964/GEPD shows it dimmer. Maintainer wants the +accurate result, not a tuned one. + +**Leading hypothesis:** D540's exact per-pixel fog leaves the near ground +unfogged, while the N64 (and 1964) shade per vertex and the RSP near-clips in +clip space, so the near-plane edge of a big ground triangle inherits fog lerped +from a corner behind the camera — a haze at the player's feet. D540 dropped +per-vertex fog on the GPU because GPU clipping of noperspective/RSP values +made that area cycle light/dark. **Accurate fix to evaluate:** RSP-style +near-plane clipping in fast3d (clip fogged triangles against the near plane on +the CPU, lerping per-vertex fog in clip space like the RSP), then per-vertex +fog screen-affine — N64 behaviour without the cycling; compare against 1964 +at Bond's start on Surface 2. Second hypothesis to rule out: GE's own near-fog +record (`Visibility.Nfd` / `g_NearFogValuesP`, bgfog.c). `GE_FOGVERTEX=1` +shows the per-vertex look today (with the cycling). + +**Implementation (2026-10-06):** CPU near-plane clip added in `gfx_sp_tri1` (`port/fast3d/gfx_pc.cpp`, D543 comment block before the emit loop): after the reject/cull gates, triangles are Sutherland-Hodgman clipped against z+w>eps in clip space (3 or 4 verts, fanned to 1-2 triangles), new vertices lerp x/y/z/w/u/v/colour/fog_n linearly (stack storage, not `rsp.loaded_vertices`); fully-behind triangles are dropped. Skipped for rects, `G_NO_CLIPPING_EXT`, `GE_NEARCLIP=0`. `GE_NEARCLIPFOG=recompute` re-derives the per-vertex fog byte from the new z/w instead of lerping it. Not built or run by the implementer; maintainer A/B owed on Surface 2 at Bond's start vs 1964 (default / `GE_FOGVERTEX=1` / `GE_NEARCLIPFOG=recompute` / `GE_NEARCLIP=0`). Confidence it compiles: high (read-checked); correct: medium until A/B. + + +**Decision (2026-10-06, lead, from code + maintainer feedback; no further +A/B by maintainer choice):** per-vertex RSP fog is the default again, on top +of the CPU near clip, with the clip-created vertex fog **lerped** (not +recomputed). Reasoning: the maintainer's report is "1964 is dimmer around +Bond"; the D540 per-pixel fog is zero at the feet (bright), and the recompute +rule gives zero at the near plane too (bright), while the RSP model (per-vertex +fog, behind-camera corner = full fog via the honest negative-w divide, lerped +into the near-plane vertex) leaves a haze at the feet -- the dimmer look. The +D540 reason for dropping per-vertex fog (light/dark cycling) was GPU +near-clipping of behind-camera corners, which the CPU clip removes. The fog +varying now shares the shade qualifier (`noperspective`): the RDP interpolates +fog screen-affine like shade. Switches: `GE_FOGPIXEL=1` = D540 per-pixel fog +(replaces `GE_FOGVERTEX`, tombstoned), `GE_NEARCLIP=0`, `GE_NEARCLIPFOG=recompute`. +Build-verified; by-eye check rides on the golden re-base contact sheets and +the next playtest (Surface 2 at the start vs 1964). Fogged-level goldens +change (re-base batched with D546). +## D544: OS mouse cursor flickers with the F10 overlay open on the main menu — FIXED 2026-10-05 (maintainer report, same day) + +**Report:** "sometimes the mouse cursor flickers when the F10 menu is open on +the main menu". + +**Cause (code read, not yet run):** two requests race every overlay tick. +`inputSuspendForOverlay()` (`port/src/input.c` ~3155, called from +`overlayHandleInputLocked`, `optionsoverlay.c` ~2300) always calls +`mouseRequestCursor(1)` (show the OS cursor); on the front end the D519 path +(`input.c` ~2174: `overlayFrontEndCrosshair(); mouseRequestCursor(0);`) +hides it because the game crosshair is the pointer there. Whichever request is +applied last on the event thread (`inputApplyMouseRequests`) wins, so the +cursor toggles. In a level only the show request runs (no flicker). + +**Proposed fix (port-only, ~3 lines):** in `inputSuspendForOverlay`, skip +`mouseRequestCursor(1)` when the D519 front-end pointer is active +(`mouseEnabled && current_menu != GE_MENU_RUN_STAGE && current_menu != +GE_MENU_INVALID`), so only one owner sets the cursor per context. Verify: +F10 on the main menu with a mouse -- one pointer (the crosshair), no flicker; +F10 in a level -- OS cursor visible as before. + +**Fix (2026-10-05/06, revised after the maintainer check):** the first cut +(skip the show request while the D519 pointer is active) stopped the flicker, +but the maintainer wants the pointer drawn OVER the overlay for mouse users. +Final: `inputSuspendForOverlay` always shows the OS cursor, and the D519 +front-end path no longer hides it (the game crosshair keeps tracking the +mouse underneath the box); one owner, so no race. Same pass, F10 mouse QoL +(`optionsoverlay.c`): right-click = Esc/back (closes a dropdown, then one +page, then the overlay; replaces the old right-click "cycle value back"; +blocked until release after a binding capture so a bound right-click does +not also back out), and the tip line follows the hovered row while the mouse +is in use. Maintainer check owed. Mouse scrollbar: backlog (post-release QoL). + +## D545: player rockets can pass through the ground when fired near Bond's feet — FIXED 2026-10-05 (maintainer report; 3e triage) + +**Report:** with the (dual) rocket launcher on Aztec and most maps, aiming down +near Bond's feet while running sometimes sends the rocket through the ground +or a solid object. + +**Cause (static, high confidence it is a real defect):** +`gunInitProjectileFromPlayer` (`src/game/gun.c`) declares `s32 sp54;` and +passes `&sp54` to `bgFindRoomsAlongSegment(..., &sp54, &sp50, 0x14)`, which +writes up to 0x14 traversed room numbers there. On the N64 the 19 extra slots +landed in the decomp's `u8 pad_a[0x4c]` (exactly 19 more `s32`); GCC lays the +frame out differently, so on PC the writes can hit live locals (`pos`, `tile`, +`usedfallback`) -- a spawn position/tile corrupted to just under the floor +gives a rocket that never meets the ground. Only the player launch path does +this; NPC rockets and the per-tick flight (`handles_projectile_motion`, full +`roomNums[]` buffers) are clean. Same class as D490 (porting-notes §D24). + +**Fix:** `s32 sp54[0x14]` under `#ifdef AVOID_UB` (array passed directly); +the `#else` arm keeps the original. No reads change. Build-verified. +**Verify (maintainer):** Aztec, dual rocket launchers, aim at Bond's feet +while running; if it still happens, the same shot on 1964/GEPD decides +faithful vs port. + +**Residual suspects from the triage (not proven to fire):** (a) the +`bgBuildRoomVtxBounds` safety-net `continue` (bg.c) leaves a skipped batch +with impossible bounds = a silent collision hole (worth a counter if the +report persists); (b) projectile room lists only grow via portal crossings, +so a stale `unkCC` skips world tests (faithful logic). Object/chr collision +(`projectileTestObjectCollisionRecursive`) was not traced. + +**Maintainer re-test (2026-10-06):** near Bond mostly fixed; some rockets +(akimbo) still pass through **walls and floor** (not doors), e.g. Archives. +Second static triage: no ABI/UB defect in the object/door path (door-only +suspect `sub_GAME_7F041400` `bestedge > 0` set aside, since it isn't doors). Leading +hypothesis, port-caused: the room pool starves (D294 documents mema starving +on PC when Draw/LOD/FOV widen the resident room set; D294's pool bump only +partly covers 2.0x), and `bgBuildRoomVtxBounds`'s `memaAlloc` failing leaves +`vtx_batch_bounds` NULL, so `bgTestBulletHitBackground` returns no hit for +EVERY wall/floor of a room that still renders. Test (no diagnostics, by +maintainer choice): same Archives shots under Modern vs the Original N64 +preset (level restarted). If confirmed, the proposed fix allocates the bounds table +from port memory instead of the room pool (a few KB per room, freed with the +room; no game-logic change). Status stays PARTIAL until that test. + +**Preset A/B result (maintainer, 2026-10-06, Archives, akimbo rockets):** fine +under Modern, Original N64 and custom high FOV; one rocket dropped in the whole +session (roughly 1 in 60), most likely under Modern or high FOV. D545 fixed the +main case. The single residual is consistent with the wide-view room-pool +hypothesis but not proven; the port-memory bounds table stays a proposed +hardening (backlog), not a release item. + +## D546: PC settings standardization -- round defaults, vertical FOV, view-area wording — FIXED 2026-10-05 (maintainer-approved table) + +**Ask:** N64 value = 1.0x (Original N64 preset); PC defaults on round steps +that every slider lands on; wording that reads as a setting, not a warning. + +**Changes (port-only):** +- `Video.DrawDistance` / `Video.LodDistance` default 250 -> **200** (2.0x): + initializers, `kVideoPresets` port column, reset table; D482 low-end lowering + now keys on 200. Fog stays 1.0x. +- Stick deadzones 7000 (23 %, off the 300 step) -> **7500 (25 %)** + (`STICK_DEADZONE`, reset table). +- Mouse horizontal/vertical sensitivity UI max 3x -> **4x**; controller look + stays 2x (PD port: stick scale 0..2.0x; PD mouse 0..30 at default 2.5). +- **FOV:** shown as **vertical degrees, 30..90 in 5° steps, 60 = N64** in the + middle (PD's "Vert FOV" model: vertical degrees, default 60, range 15..170). + Horizontal degrees (D443/D357) moved with the window aspect (N64 = 75° at + 4:3, 91.5° at 16:9) and never landed round. Storage stays + `Video.FovScale` % but is now a float so each step (100/12 %) is exact; + old integer inis load unchanged and snap to the next mark on the first step. +- `Game.AIWideView` (D468) row relabelled **"Gameplay view area"**, values + **Original / Extended**, tip "Screen area game logic counts as in view" + (the old "Guard AI uses full wide view" read as a vision cheat; the setting + picks which screen area on-screen gameplay tests use). Ini key unchanged. +- **Migration:** one-time, untouched values only (`Video.DefaultsRev`, + `Input.DefaultsRev`, D482 "considered" pattern): draw/LOD 250 -> 200, + deadzones 7000 -> 7500. Runs before D482. + +**Goldens:** captures use the defaults, so draw/LOD 2.0x changes frames; the +re-base is batched with D543 (one round after the fog design is final). + +## D547: Game.AllUnlocked applies live — FIXED 2026-10-05 (maintainer ask) + +The file2.c query-time hooks (D442) already read `portAllUnlocked` live; only +the two RAM flags `debug_enable_all_levels_flag` / `debug_007_unlock_flag` +were boot-seeded. `portAllUnlockedApply()` (`port/src/main.c`) now runs at +boot and from `rowSetCommit` (F10 change and section reset): On sets each flag +that is clear and remembers it; Off clears only flags it set, so GE_UNLOCK_ALL +/ debug-menu unlocks survive. Row values are plain Off/On ("(restart)" +dropped). LEFTOVERDEBUG builds only, as before. Maintainer check owed: on at +file select -> mission select shows all levels + 007; off -> back to the save's +progress (screens refresh on re-entry). + +## D548: walls near rocket fire banded black/white/yellow/blue in dark areas (Aztec; also seen on the Deck) — FIXED 2026-10-06 (maintainer screenshots, A/B) + +**Report:** in Aztec's dark corridor, walls beside Bond near rocket fire and +explosions turned into posterised white/yellow/blue/black bands following the +texture detail. `GE_SHADEPERSP=1` (perspective shade) did not reproduce it. + +**Cause:** D540 made every combiner input varying (`vInputN`) `noperspective`. +When the GPU clips a big near-camera triangle, those screen-affine values can +be extrapolated past 1.0; the GLideN64-style CC wrap emulation +(`WRAP(texel, -1.01, 1.01)` / `(-0.51, 1.51)`, gfx_opengl.cpp) then wrapped the +out-of-range result, per channel -> the bands. + +**Fix:** the fragment shader clamps each combiner input to 0..1 into a local +(`cInputN`) before the combine. N64 shade/prim/env are 8-bit, so this only +removes values the hardware cannot produce. Root fix: D543's CPU near-plane +clip (no GPU near-clipping of affine attributes). Maintainer Aztec re-check owed. + +## D549: thrown grenade / knife / object -- `s32 sp94` receives a 12-byte coord3d — FIXED 2026-10-06 (D545 follow-up triage) + +`generate_player_thrown_grenade`, `generate_player_thrown_knife` and +`generate_player_thrown_object` (`src/game/gun.c`) declare `s32 sp94` and pass +`&sp94` to `bullet_path_from_screen_center(coord3d *, ...)`, which writes x/y/z. +On the N64 (sp94 at 0x94) the extra 8 bytes ended at `spA0_a` on a slot that is +dead at that point (the mine path's `if (wor == NULL)` after the call proves +`wor` was not hit there); with GCC's layout they can hit live locals. Fix: +`struct coord3d sp94` under `#ifdef AVOID_UB`, original under `#else`; nothing +else reads sp94. Same class as D545 / D490 (porting-notes §D24). Possibly +related to older "thrown items through walls" reports; not demonstrated. + +## D550: static title bar (no per-second "NN fps" refresh) + project mark as favicon / window / taskbar icon — FIXED 2026-10-06 (v0.5.0 finalize, maintainer request 2026-10-06) + +Two small surface items, both port/tooling-only (zero `src/game` diff, no +goldens): + +**1. Static title bar.** `videoPumpEvents` (`port/src/video.c`) refreshed the +window title to `"GoldenEye 007 - NN fps"` about once a second (the +`wmAPI->set_window_title` block). Removed; the title is now set once at +window creation. The live-FPS readout is still available on the F10 overlay. + +**2. Project mark in place of the game-still favicon.** The favicon / link +card / window icon used in-engine stills (shot-28.jpg et al.) — derivative +game content in every browser tab and the taskbar. Replaced with an ORIGINAL +generated mark: a generic version of the game's *default aim crosshair* +(circle + N/S/E/W lines crossing its edge), GE red `#c62828` on near-black +`#050607` with a faint glow — a generic functional shape, no logo art, +wordmark or screenshot (tree-hygiene rule 4). One generator, +`tools_pc/make_icon.py` (Pillow, deterministic; `--check` for CI staleness), +emits: +- `docs/img/icon/ge007-icon.png` (512, site favicon + README icon), +- `cmake/icon/ge007.ico` (16–256 multi-size, PNG-compressed entries; 16/24/32 + use a chunkier stroke variant so the mark survives tiny sizes), +- `port/fast3d/ge007_icon.h` (32×32 RGBA, plain R,G,B,A per pixel). + +Wiring: `cmake/icon/ge007.rc` (static; the exe icon) via `target_sources` in +the CMakeLists WIN32 branch; `gfx_sdl2.cpp` builds an `SDL_Surface` against +explicit `0x00ff00ff`-style masks (portable, no endianness assumption) and +`SDL_SetWindowIcon`s it after the GL context; `docs/_layouts/default.html` +favicon + apple-touch-icon; `README.md` gets the mark above the badges. The +jekyll-seo `og:image` link-preview card keeps `shot-28.jpg` (a still is fine +there — it is a published page asset, not a tab/favicon) and +`scratch/build_preview.mjs` was restored to the original image constant. + +**Implementation bugs found only at build verification (both mine):** +(a) the first `make_icon.py` emitted the C array with *flat* cells per row +(no per-row braces): all 1024 `{r,g,b,a}` cells then sit at the TOP level of +the `[32][32][4]` initializer — 1024 > first dim 32 → "too many +initializers". Both gcc AND clang reject it, correctly: it is genuinely +ill-formed C, not a toolchain bug (braced/nested forms of the same data +compile cleanly in both). (b) `gfx_sdl2.cpp` packed the channels unshifted +(`p[1] & 0x0000ff00` is always 0); masked only by (a) never having compiled. + +**Verification-trap note:** in the mixed msys/native agent shell, `g++ ... +| head` reports the PIPE's rc (always 0), so earlier "compiles fine" claims +were false while the TU actually failed. Verified properly: `ninja -t +commands` exact command via cmd.exe with a sane `PATH`/`TMP` (rc 0, only the +pre-existing `gfx_pre_swap_hook` warning) + full `./build-pc.sh ntsc-final` +(249/249 link). Window/taskbar icon + static title owed a maintainer live +check. + +**Mark re-proportioned 2026-10-06 (maintainer: "right direction, not accurate +to the in-game crosshair").** Measured off an in-game capture of the default +reticle: the four bars do NOT meet in the centre (open gap ~0.24x the ring's +outer radius) and reach ~1.43x the outer radius (the first cut crossed the +centre and reached ~1.27x). `make_icon.py` now draws eight bar segments from +the gap to the reach (512 master: ring r 150, reach 214, gap 36, stroke 24; +small variant: r 140, reach 200, gap 46, stroke 40 so the centre stays open at +16 px). All three outputs regenerated (deterministic re-run identical). The +`.github/social-preview.html` card's CSS reticle (same old proportions) was +replaced with an inline SVG at the new proportions and the PNG re-rendered. +Still a generic geometric reticle (circle + four bars), no game art copied. +Full `./build-pc.sh ntsc-final` rc 0 with the regenerated header. + +**Re-drawn 2026-10-06 (gold).** The colour is now site gold `#e8a13a` (was +GE red `#c62828`), glow included; the proportions were re-measured from the +game's own default crosshair sprite (thin ring about 0.11x the ring radius, +bars about 0.09x the radius, reach about 1.45x the radius, open centre gap +about 0.30x the radius). There is a chunkier small-size variant (16/24/32 px +.ico slots + the 32 px window icon). The `.github/social-preview.html` +card's reticle stays red on purpose — the gold disappears on its manila +background. **Centring fix (same day):** shapes are now drawn symmetric about the exact centre (the line between the two middle supersample pixels; even bar widths, inclusive boxes `[c-r, c+r-1]`), which removes a half-pixel right/down shift at 48/24/16 px; `make_icon.py` (and `--check`) now fail if any emitted size is not pixel-exact left-right and top-bottom symmetric. + + +## D551: opt-in update check (`Game.CheckUpdates`, default off) -- ADDED 2026-10-06 (v0.5.0 finalize) + +**What.** `port/src/updatecheck.c` (+ `updatecheck.h`). `updateCheckStart()` is +the only network entry point; its first line is `if (!cfgCheckUpdates) return;`, +so with the ini value 0 there is no thread, no DNS and no library load. It is +called once from `main.c` right after `configLoad()`. When on: one detached +thread makes a single HTTPS GET to `api.github.com/repos//releases/latest` +(5 s timeouts, fixed User-Agent, nothing else sent), parses `tag_name`, and +compares it with the new `GE007_VERSION` (CMake `set(GE007_VERSION "0.5.0")`, +bump on every release; `versioninfo.h.in`). Failure is silent (one log line). +`/releases/latest` excludes drafts and pre-releases on purpose (maintainer +decision). + +**Windows.** No link-time winhttp dependency: `winhttp.dll` is `LoadLibraryW`'d +inside the opt-in path, nine functions resolved with `GetProcAddress`, +`FreeLibrary` at the end, so the exe's import table never lists it. Linux runs +`curl` via `popen` (URL single-quoted); no curl = no result. + +**UI.** Game page toggle "Check for updates" (+ reset-table entry, tip text) and +an action row "Update available: open page" shown only once a newer tag is +known (`SDL_OpenURL` on the releases page). CI: the tag-only `release` job now +fails if the tag differs from `v` + `GE007_VERSION`. + +**Status.** Port layer only, no game logic touched. Build-verified; exe +imports no winhttp (objdump). **Live-verified 2026-10-06 (Windows, isolated +work dir, orderly GE_QUITFRAME quit):** Off -> no update-check activity; +On -> real HTTPS GET, `update check: up to date (latest v0.4.0, running +v0.5.0)`; On with a temporary GE007_VERSION 0.3.0 build -> `v0.4.0 is +available (running v0.3.0)`. **Linux + Steam Deck (2026-10-06, system +curl):** off -> no activity, on -> "up to date". **UI:** scripted F10 run +(GE_OVNAV) on the 0.3.0 build: the "Check for updates" toggle and the +"Update available: open page" row render at the bottom of the Game page +within the column widths, and the row appears live while the page is open +(visibility is rebuilt every emit). **Security review (2026-10-06):** +SHIP-WITH-FIXES, fixes applied: winhttp.dll loaded with +`LOAD_LIBRARY_SEARCH_SYSTEM32` (no DLL planting from the exe folder); +curl run as `curl -q --proto =https` (ignores ~/.curlrc, HTTPS only); +tag restricted to `[0-9A-Za-z._-]` before it reaches the log; +`snprintf` instead of a 32-byte `memcpy`; help text says the change +applies from the next launch; security docs name the curl process, the +System32 load, and that GitHub sees the request and the IP. Confirmed +fine: gate ordering (configLoad before updateCheckStart), constant URLs +(no injection), TLS validation left at system defaults, bounded parsing, +SDL_OpenURL with a constant URL, CI tag check uses the env var quoted. +Owed: nothing beyond pressing the row once in a live session (opens the +browser). + +## D552: Dam ending cutscene — first camera shot no longer swivelled up onto Bond (D243 M-190 re-seed removed) — FIXED 2026-10-06 + +**Report (maintainer playtest 2026-10-06, vs 1964/GEPD):** in the Dam ending +(the run to the dam edge / bungee), the first camera shot should swivel up +onto Bond, ending pointed at his front right; on the port it started +already aimed. + +**Cause.** A port-only addition from D243 (M-190, 2026-09-18) in the +player-chr tick (`src/game/bondview2.c`, `#ifdef PORT`): on every +shot-change teleport during `CAMERAMODE_POSEND` it re-seeded the look-at +filter (`field_3B8` accumulator and `field_3C4/3C8/3CC`) to Bond's new +position, so the camera snapped onto him. The decomp's own leaky integrator +(`bondviewUpdatePlayerCollisionPositionFields`) instead converges over about +20 ticks from the stale pre-teleport position — that convergence is the +swivel (D243 M-143 logged the same sweep, pos2.y -22832 -> +691, and read it +as shake). D243's actual shake cause was the `sizeof(Model)` buffer overlap +(M-189), fixed independently, so neither M-170's freeze nor M-190's re-seed +was still needed. + +**Fix.** Removed the re-seed block, its teleport-epoch locals, the epoch +counter (`d243NotifyTeleport` / `d243GetTeleportEpoch`) and the notify call in +`src/game/chrai.c` (`AI_TRYTeleportingChrToPad`) — all `#ifdef PORT` +additions. The path is now the decomp, unmodified (the unconditional +`field_488.pos` write is original code). No Rule-2 sign-off needed: this +removes a port deviation. **Verified:** build rc 0; maintainer replayed the +Dam ending live — swivel present, no shake. + +## D553: Surface 2 haze around Bond popping dark/bright as he moves — near-clip fog now recomputed (RSP behaviour) — FIXED 2026-10-06 + +**Report (maintainer playtest 2026-10-06):** after D543 the lit area around +Bond on Surface 2 was dimmer (good) but changed with his position: dark, +step forward bright, step again dark. 1964 (HLE) shows a similar pulse; +the maintainer then compared against hardware-level emulation +(RetroArch Mupen64Plus-Next, cxd4 LLE RSP + angrylion LLE RDP): there the +light around Bond is static, apart from the game's own small shading +artefacts. So the pulse is a port deviation. + +**Cause.** D543's CPU near-plane clip (`port/fast3d/gfx_pc.cpp`) gave each +clip-created vertex a fog value LERPED from its two parents. One parent is +behind the eye; `gfx_sp_vertex` computes its fog as `z * (1/w) * mul + off`, +which jumps between the 0 and 255 clamps as that corner's w crosses 0. The +near-edge fog therefore depended on where a big ground triangle's corners +fell relative to Bond, and flipped as he walked. The RSP's clipper runs +new vertices through its vertex-finish code (gmain.s clip routine ends in +the same finish call), i.e. fog is recomputed from the new vertex's own +clip-space position; at the near plane that is a constant. + +**Fix.** The existing `GE_NEARCLIPFOG=recompute` path is now the default +(`GE_NEARCLIPFOG=lerp` keeps the old behaviour for A/B). Port layer only. +**Measured:** a scripted Surface 2 walk (`GE_INPUTSCRIPT="20:START;1400:UP"`, +`GE_RSEED` pinned) gives the same ground brightness in both modes (mean 86.8 +vs 86.5 of 255; D543's "brighter feet" concern for recompute does not show), +step-to-step change <2/255 in both — that path does not trigger the pulse, +so the by-eye check where the maintainer saw it is the real test. Goldens: +see the D553 note in tools_pc/golden/README.md if any set moved. + + +## D554: F10 settings menu wording/casing pass + two-line tips — DONE 2026-10-06 + +**What.** Display text only in `port/src/optionsoverlay.c` (config keys, the +kRowHelp lookup keys and every value array's order/count are untouched; values +are stored as indices, never as display text). No `src/game` change. + +**Style rules.** American spelling. Page titles and hub entries Title Case +(hub actions now "Quit to Desktop", "Restart Game"); row labels and value +names sentence case ("3-point"); parenthesised tags lower case ("(per +profile)", "(no profile)", "(restart)"); tips sentence case, no trailing +period, plain third-person English, never repeating the label; control hints +are `Key: Action` pairs, three spaces between pairs, `/` between alternative +keys with no spaces ("A/Enter: Select", "B/Esc/F10: Close"); slashes inside +labels have no spaces ("Pause/start"). "Original" = the stock N64 behaviour +wherever a player picks it; "N64" only names hardware; "PC" is never a value; +"Extended" is the opposite of Original. "deg" kept for FOV (the font has no +degree sign). + +**Maintainer decisions applied.** Aim style / Aim range values are now +Original / Centered and Original / Extended. The "(EXPERIMENTAL)" tag is +removed from Skip intro and All unlocked, with the red-ink path +(`strstr(label,"EXPERIMENTAL")`, `PD_EXP_*`) deleted so nothing depends on the +tag. "Controller style" is now "Original layout". Binding action names: the +pad row "Use / action" is renamed to match the keyboard's "Use/back". "Ext" +is now "Extended" (Control scheme), "Velocity (legacy)" is "Velocity (old)", +"(in a mission)" is "Not in a mission" (the value shows when no live player +exists), the ini warning reads "n unknown settings in the ini, see the log". +All 44 changed tips follow the plan's table; a tip was added for +`Video.CrosshairHide`. + +**Code facts confirmed.** Esc closes the hub: `optionsOverlayBack()` sets +`s_backPending`, and the update loop calls `optionsOverlayToggle()` when no +page is open (so the hub hint is "B/Esc/F10: Close"). "Show crosshair" +(`Video.CrosshairHide`, portCrosshairHide) is a port-level override: +`gunDrawSight` returns early when it is set, regardless of "Sight on screen" +(`Bond.Sight`, the game's own per-profile sight-visible reason). The tip says +exactly that. + +**Wrap.** Tips no longer go through a 64-byte buffer with ".." chopping. +`tipWrap()` measures with the overlay's own `bodyWidth()` against +`cardW - 8`; if too wide it splits at the last space that fits and the second +centred line is drawn in the hint panel. The panel was already sized for it +(`hintLines*12+2`; 2 tip + 2 key-hint lines = 50 <= `OV_HINT_H` 53), so no +layout constant changed; the key hints move down by the actual tip line count. +A line 2 that still overflows is chopped with ".." (cannot happen for current +tips). `tipSelfCheck()` runs once on the first emit that has fonts and logs +a warning by key for any tip needing more than 2 lines at HUD 150 (budget +189), plus one summary line. + +**Review fixes (same day, from the lead's HUD 150 screenshots).** +(1) Control hints clipped at HUD 150. Every hint line is now a `{full, compact}` +pair (`kHint[]`); the compact form is used only when the full one exceeds the +card budget (189 at HUD 150), then the Xbox letters as a last resort. Compact +forms: "A: Select L/R: Change", "B: Back F10: Close", "A: Bind L/R: Slot + Y: Clear", "Keys/mouse only L/R: Slot", "Enter: Bind B: Back", "Press a +button Hold B: Bind B", "Tap B: Cancel Hold BACK: Clear", "Press key or +mouse 1-5", "B: Cancel Del: Clear"; the hub lines already fit. The one-time +check now also logs any hint whose full form (Xbox names) exceeds 296 or whose +compact form exceeds 189. +(2) Game page tip showed only "Reverse makes pushing up look" at HUD 150. +Root cause was not the page: `drawBody` passed `textRender` the raw canvas +width/height as its clip limits, but `textRender` clips in unscaled glyph space +while the string is drawn through the 78% span scale about its own origin. +A line that starts right of centre and is wide enough (here a 187-unit +one-line tip, which `tipWrap` correctly kept on one line) ran past 213 in +unscaled space and lost its tail. `drawBody` now maps the limits back into +unscaled space (`x + (ovW - x) * 100 / 78`, same for y). This affects every +overlay string, none for the worse. + +**Owed.** By-eye contact sheet of every page at HUD 100 and 150. Not done: +the slider-bar shrink at HUD 150 (labels still ellipsise there). + +## D555: crosshair mouse pointer over the F10 / PC Options overlay (maintainer ask) + +**Ask.** The red in-game crosshair should be on top of the F10 / PC Options box +on the main menu, i.e. the mouse pointer there should be the crosshair. Until +now (D519) the game's own crosshair tracked the mouse UNDER the overlay and the +OS arrow was shown on top (D544). + +**Fix (port layer only; no src/game change).** One mechanism: while the overlay +is open and the OS cursor is shown, the OS cursor image is a colour cursor +shaped like the project crosshair, hotspot at the centre. The OS composites +the cursor above the game frame, so it is always over the overlay box. +- Art: `tools_pc/make_icon.py` `_cursor()` / `make_cursor_header()` emit + `port/fast3d/ge007_cursor.h` (32x32 and 48x48 RGBA, transparent background, + same proportions as `_crosshair`, red #c62828 with a 1px dark outline; + row-braced arrays like `ge007_icon.h`; deterministic, run twice = same bytes). +- `port/src/input.c`: `crosshairCursorBuild()` packs the pixels against explicit + masks, shifting each channel into its lane before masking (the D550 bug), picks + 48 px when the drawable is >= 1.5x the window size (high DPI), else 32. + `crosshairCursorApply()` does SDL_SetCursor(crosshair) / SDL_SetCursor( + SDL_GetDefaultCursor()). Both run from `inputApplyMouseRequests()`, the + host/event thread (videoPumpEvents), next to the SDL_ShowCursor application + (D287) -- never the game thread. Condition: `Input.CrosshairCursor` on, the + applied cursor state is "shown", and `optionsOverlayIsOpen()`; otherwise the + default arrow is restored (overlay closed, focus lost). The cursor object is + built lazily once and kept for the process lifetime. +- The game's under-overlay crosshair (D519, `overlayFrontEndCrosshair`) is + unchanged. It tracks the same window position, so outside the box it sits + directly beneath the pointer and the pointer covers it; whether it visibly + doubles (slight lag halo) needs a by-eye check. If so, the follow-up is to + skip `overlayFrontEndCrosshair()` while the custom cursor is active. +- Setting: `Input.CrosshairCursor` (default 1), F10 Mouse page row "Crosshair + pointer" (toggle), tip "Shows the red crosshair as the mouse pointer in this + menu", in the reset-defaults table. + +**Verification.** Build only (ntsc-final rc=0); visual check owed to the +maintainer. + +**Reworked 2026-10-06 (maintainer feedback: the front-end crosshair showed +underneath as a duplicate, and the SDL cursor was neither the game's graphic nor +its size).** The custom SDL colour cursor is REMOVED (`crosshairCursorBuild/Apply`, +`port/fast3d/ge007_cursor.h`, `make_icon.py` `_cursor`/`make_cursor_header`; the +other make_icon outputs are unchanged). New mechanism, port layer only: +- `optionsoverlay.c` `drawCrosshairPointer()` runs last in `overlayEmitLocked()` + (after the card, outside the centred-aspect block) and draws the game's own + sprite `crosshairimage` (image_bank.c; 32x32, loaded from the user's ROM + at runtime -- nothing baked into the tree) with the same + `texSelect` + `display_image_at_position` as `frontDrawCursor` (white, alpha + 220), at the OS mouse position mapped through `gfx_get_ui_screen_rect`. +- Position/size, FRONT END: the first cut parked `cursor_h_pos/cursor_v_pos` at + -1000, but the game's own front-end tick clamps the cursor back into bounds + before `frontDrawCursor`, so a copy appeared at the clamp corner (found in a + headless capture). Park removed. Now D519's `overlayFrontEndCrosshair()` keeps + the game cursor tracking the mouse (option on or off) and + `drawCrosshairPointer()` draws the pointer exactly over it: `input.c` + `inputFrontEndCursorUiFrac()` is the inverse of D519's window->canvas mapping + (x = (cursor_h_pos - screenleft)/screenwidth, then the D335 pillarbox + x*vis + (1-vis)/2 when native widescreen; y likewise), giving fractions of the + UI screen rect; canvas px = frac * (W,H); half-extent = (image->width/2)/viGetX() + * W * vis and (image->height/2)/viGetY() * H, i.e. frontDrawCursor's size. The + sprite lands on the one underneath, so one crosshair is visible. +- IN LEVEL (no front-end cursor): position from `SDL_GetMouseState` through + `gfx_get_ui_screen_rect`; half-extent `16/ovScale()` x `(4/3)/portNativeAspect()` + (gunDrawSight's 16.0 and widescreen x fix). It does NOT follow the Video.Crosshair + colour/size/opacity options (UI pointer: plain sprite, front-end opacity). +- OS cursor: `inputApplyMouseRequests()` (host thread) hides it while the option is + on and the overlay is open; restores on close / option off. +- Option off: previous behaviour (OS arrow over the overlay, D519 crosshair under it). +- Also: `Game.NoHitFlash` tip is now "Hides the white flash when you take damage". +Headless captures (1280x720, front end, overlay open; frame 1100): AspectMode=1, AspectMode=1 + NativeWidescreen=1 (the front end stays 4:3 pillarboxed), AspectMode=0 (stretched, the pointer stretches with the game, as its own cursor does): exactly one crosshair each. Live mouse feel owed to the maintainer. + +## D556: F10 feedback pass (tips trimmed, conditional-row order, wording fixes) + mouse scrollbar + +Port layer only (`port/src/optionsoverlay.c`); no game-code edits. Maintainer playtest feedback on the D554/D555 menu. + +- **Tips.** A tip is kept only where the setting is port/GE specific or not obvious. Removed (no tip): `Video.FullscreenMode`, `__CenterWindow`, `Video.CrosshairAlpha`, `Video.MSAA`, `Video.VSync`, `Video.FpsCap`, `Video.TextureFilter`, `Video.Anisotropy`, `Game.ScreenShakeIntensity`, `Audio.MasterVolume`, `Input.MouseSensitivity`, `Input.MouseYScale`, `Input.MouseInvertY`, `Input.MouseSmoothing`, `Input.MouseRawInput`, `Input.PadLookInvertY`, `Input.PadDeadzoneL/R`, `Input.PadLookSensX/Y`, `Input.PadLookSmooth`, `Input.PadTriggerPct`, `Input.RumbleScale`, `Bond.AimControl`, `Bond.Ammo`, `Input.CrouchMode`, `__UpdateOpen`. An absent tip leaves its tip area empty (see the hint-panel fix below). Rewritten: AspectMode, WidescreenAuto, SafeAreaCrop, CrosshairCursor, Look up/down, NoHitFlash, SkipIntro, CheckUpdates. Anti-aliasing's "higher costs speed" tip is gone with the rest of the standard settings. +- **Conditional-row order.** Rule: a row whose visibility depends on another row sits directly below it. Audited every page: Fullscreen -> Fullscreen mode / Center window, Aim style -> Aim range, Crosshair color -> Red/Green/Blue, Layout preset -> pad binds, Check for updates -> update link were already right. One violation: on file select the D519 profile chooser was injected as row 1 of any page with profile-backed rows, so choosing Original put "Profile" above "Control style" on the Controller page. It is now injected directly above the first profile-backed row (Controller: Control style, Profile, Original layout; Game page unchanged). Grouping moves (display order only): Widescreen auto FOV now follows Native widescreen; Mouse Smoothing/Raw input sit with the other mouse-feel rows, before Aim style. +- **Look up/down "Reverse/Upright"** is GoldenEye's own N64 wording (`assets/obseg/text/LoptionsE.c` OPTION_STR_1B/1C, used by `src/game/options.c`), so it is kept; the tip says Reverse inverts like Invert look. PD's port has no such string. +- **No hit flash.** Code: `currentPlayerSetFadeColour` in `src/game/bondview2.c` (`portNoHitFlash`, D232) zeroes any non-black fade. The two non-black fades are the damage envelope in bondviewPlayerTickDamageAndHealth (WHITE 0xFF, alpha scaled by health) and the red (150,0,0) overlay after the death blood. The old "red flash" tip was wrong for the common case; the tip now says "damage flash and the red tint on death". +- **Skip intro.** D408 (post-mission failure screen skipped) is FIXED (boot-only gate in `src/game/lv.c`); the claim is removed. The option only boots to file select. +- **Mouse scrollbar.** `overlayScrollbar()` computes a 3-unit track in the card's right margin (x = right-7..right-4; value text ends at right-10, so no overlap at HUD 100/150) over the body rows, only while the page overflows (same test as the "^ v" marker); thumb size = visible/total rows, position from the eased `s_scrollF`. Drawn after the row fills in the dim/bright green palette. Input (in `overlayHandleInputLocked`, before the row-click path, using the same ox/oy mapping): press on the thumb drags it, press on the track pages up/down by one window; both go through `overlayScrollTo()`, which sets `s_scroll` and pulls `s_sel` into the window (as the wheel moves the selection, since `overlayUpdateScroll` would otherwise snap back). A scrollbar press never reaches the row click. + +- **Hint-panel regression fix (lead review).** First cut drew the control-hint lines only while `!s_mouseActive`, and the OS cursor merely resting over the window sets `s_mouseActive`, so hints vanished per page depending on pointer position (and would flicker in play). Now the control hints are always drawn on every page, and a section page reserves a fixed two-line tip area (`tipArea` = 24 units) above them so a row without a tip leaves it empty and the hints never move (2 + 2 lines = 50 <= OV_HINT_H 53); the hub has no tips and reserves none. Compact-hint fallback unchanged. +- **Crouch mode default -> Toggle** (maintainer approved): `crouchMode` initial value 1 in `port/src/input.c` (the configRegisterInt default) and the F10 reset table entry `Input.CrouchMode` = 1 in `optionsoverlay.c`. Existing ini values are kept; no DefaultsRev migration. +- **Look ahead OFF for new profiles: NOT done.** New files are built from the `BLANKSAVEDATA` macro (`DEFAULT_OPTIONS`) at nine sites inside `src/game/file2.c` (fileResetSave, fileBuildWriteNewSave, fileDeleteSaveForFolder, fileClearSavefileForFolder, ...), mostly called from within that same file, so there is no port-side creation hook (a linker wrap would miss the intra-file calls, and detecting "new" by save contents would risk flipping existing profiles). It needs either a `#ifdef PORT` edit of `DEFAULT_OPTIONS` (Option B, src/game, needs rule-2 sign-off) or an agreed new hook; left for the maintainer. + +**Verification.** Build only (ntsc-final rc=0); no game launch. By-eye check owed (tips, Controller page order, scrollbar and hint panel at HUD 100 and 150). + +## D557: new/blank profiles start with Look ahead OFF on PC — RULE-2 SIGN-OFF (maintainer, 2026-10-06) — FIXED 2026-10-06 + +**Decision.** Defaults review against the Perfect Dark PC port +(`docs/dev/notes/DEFAULTS-VS-PD.md`, local): Look ahead (per-profile +`OPTION_LOOKAHEAD`) is the one N64 default that is clearly hostile on PC — +its pitch auto-centring on slopes fights mouse and right-stick look; PD's +port keeps the equivalent on only under `PLATFORM_N64`. Maintainer approved +turning it OFF for new profiles, with the explicit Rule-2 sign-off this +`src/game` edit needs (it is a deliberate PC default, not an ABI/layout +fix). + +**Why src/game.** New profiles are built from `BLANKSAVEDATA` +(`src/bondconstants.h`, using `DEFAULT_OPTIONS`) at nine sites inside +`src/game/file2.c` (new save, erase/clear, copy-to-blank, ...); there is no +port-side "profile created" hook, and inferring "new" from save contents +risks flipping existing profiles. So the change is one macro: +`src/game/file2.h` `DEFAULT_OPTIONS` drops `OPTION_LOOKAHEAD` under +`#ifdef PORT` (`#else` keeps the original for the byte-matching N64 build). +The port's F10 "Reset to defaults" mirror (`kBlankSave`, +`port/src/watchsettings.c`) is built from the same macro, so it follows. + +**Scope.** Existing profiles keep their stored option bits — no migration. +Only profiles initialised from `BLANKSAVEDATA` afterwards (new/erased/cleared) +start with Look ahead off; players can still turn it on per profile. Same +batch: `Input.CrouchMode` default -> Toggle (D556; ini default, new installs +and Reset to defaults only). **Verified:** build rc 0; Windows gate sweep +21/21 PASS (the canonical golden save's profiles keep their bits). + + +## D558: F10 overlay tips cut off in a level, fine on the front end — FIXED 2026-10-06 + +**Symptom.** At 1280x720, AspectMode=1, in a level, long Game-page tips ("Hides the +white flash when you take dama", "Reverse inverts up and down, like Invert lo") were +cut off; the front end showed every tip whole. + +**Root cause (not the aspect/HUD geometry).** Card geometry is identical in both +contexts (the overlay lays out on its own canvas: `ovW()` x `ovH()`, card/tip budget +from the same `overlayLayout()`; tip wrap, centring and clip all use it). +The cut is in the game's `textRenderGlyph` (`src/game/textrelated.c:241`): it drops any +glyph whose UNSCALED x is `> viGetX()` (or y `> viGetY()`) -- the game's 2D canvas, 320x240 +in a level, 440x330 on the front end. `drawBody` draws through the 78% span scale about +the string start, so the x the game tests is the start plus the unscaled advance (drawn +width / 0.78): a tip starting at x=46 died at 320 (214 drawn units in), on the front end +the 440 limit hid it. Right-aligned values and, at HUD 75 (ovW 427), more text were exposed +to the same limit. + +**Fix (port layer only).** `port/src/optionsoverlay.c` `textRenderWide()` widens the +canvas the game tests (`viSetXY`, restored right after) for the duration of each +`textRender` call, to the overlay's unscaled layout extent (`ovW()*100/OV_TEXT_PCT`). +`tipSelfCheck` already used the narrowest reachable budget (HUD 150 -> card 197, av 189); +unchanged, still 0 over 2 lines. + +**Test hooks.** `GE_OPTIONSOVERLAY_ATFRAME=` re-applies the GE_OPTIONSOVERLAY / +_SECTION request at emit n (survives level load; read once, cached); +`GE_OPTIONSOVERLAY_SELECT=` selects a row on the opened page. + +**Verified** by in-level captures (`-level_09`, GE_INPUTSCRIPT="20:START", ATFRAME=200): +before: NoHitFlash and Bond.Look tips clipped at the same screen x; after: whole and +centred at HudScale 100 (AspectMode 1 and 0) and wrapped to two centred lines at HudScale 150. + +## D559: Look ahead off for untouched existing folders — FIXED 2026-10-06 (maintainer-approved, port layer only) + +D557 only changes folders initialised from now on. `port/src/watchsettings.c` +`migrateUntouchedLookAhead()` handles existing ones with no further `src/game` edit: +when a stage is starting (first game-thread tick with `stageActive()`, before the stage +load's `fileLoadSettingsForFolder` re-reads the save; also applied live if the load already +ran) and `selected_folder_num` is a valid save with options EXACTLY the old factory default +`AUTOAIM|SIGHTONSCREEN|LOOKAHEAD|DISPLAYAMMO` (0x3a, control-type bits 0), `flag_007 == 0`, +no unlocked cheats and every `times[]` byte zero (no stage completed at any difficulty), +it clears `OPTION_LOOKAHEAD` and persists with `fileWriteSave` (same path as the F10 +per-profile rows), logging one INFO line. Folders with any progress or any other option bit +are never touched. Once per stage activation. **Verified** with a temporary forced-state +hook (removed): untouched folder fires and logs; with progress it does not. + +## D560: F10 copy pass 3 (maintainer line review) + +Display text only (`port/src/optionsoverlay.c`); config keys, value-array order and count unchanged. +Every tip is now a complete sentence ending in a period. Changes: Display mode tip "Modern, original or custom graphics presets."; Aspect ratio tip shown only while Fill window is selected (`optionsRowHelp` checks the value; Fill window = `videoOutputAspect()` 0, the output rect is the whole window); tips removed for Native widescreen, Crop overscan, Draw/Fog/LOD distance and Look up/down; Explosion shake tip restored ("Sets how strongly explosions shake the screen.", it scales `viShake()` in `src/fr.c` via `portScreenShakeScale`); Control style, Layout presets (both the Extended `Input.PadPreset` row and the Original `Bond.Control` row now carry the label "Layout presets"), Auto-aim, Gameplay view area (D468), No hit flash, Skip intro ("Skips the intro and opens the file select screen." -- `lv.c` boots to MENU_FILE_SELECT, D408), Check for updates reworded. +Southpaw: swaps only the fire and aim TRIGGER actions (`input.c` `padMapPure`, `c->southpaw` G<->R swap), not the sticks (the stick swap belongs to the 1.3 Frost / 1.4 Elektra presets, D498); tip "Swaps the fire and aim triggers." +Look up/down values renamed Inverted/Normal (index 0 = the game's "Reverse", 1 = "Upright"); mapping from the game's own option strings and the options-screen preview (`options.c` ~3103: value 1 shows up for D-pad up). Note a fresh profile (value 0) therefore reads "Inverted"; compare against 1964 if in doubt. +Two tips were shortened to keep `tipSelfCheck` at 0 over 2 lines at HUD 150 (Control style: "Choose the port's modern controls or the original game's."; Gameplay view area: "...widescreen and any field of view."; layout presets drops "port"). Menu pointer row kept; analysis in the session report (Input.MenuPointerMode, D165, default Direct). +**Verified** by in-level captures (`-level_09`, 1280x720, AspectMode 0/1, HudScale 100): all whole, centred, punctuated; Gameplay view area wraps to two lines at HUD 100. + +## D561: F10 "Menu pointer" row removed — FIXED 2026-10-06 (maintainer request) + +`Input.MenuPointerMode` (Direct = 1:1 mouse position, default; Velocity (old) = +D165's mouse-as-analog-stick nudge) only affects the GAME'S front-end menu +cursor: it is read solely under `menuMode` (`port/src/input.c`, "not in a +stage"), never in gameplay, never by the F10 overlay (which handles the mouse +itself), never on pad/Deck paths. Maintainer: an option only a legacy user would +want "will only confuse and cast doubt". Removed the F10 row, its tip and its +value names (`port/src/optionsoverlay.c`); kept the ini key (default Direct, and +its Reset-to-defaults entry) and the Velocity code path so a hand-set ini still +works for this release. Follow-up (next release): delete the Velocity path and +`Input.MenuPointerSpeed`. Verified: build rc 0; in-level Mouse page capture at +1280x720 shows the row gone; tip/hint checks 0 over. + +## D562: F10 crosshair pointer hidden while a pad/keyboard drives the menu + +Maintainer: the pointer "just hovers over and blocks UI elements" when a controller drives the menu. +`drawCrosshairPointer()` (`port/src/optionsoverlay.c`) now returns early unless `s_mouseActive` +(the existing overlay state: set by mouse move/click, cleared by pad/D-pad/stick/keyboard nav, +accept, wheel and on open), so the pointer reappears on the next mouse movement. No second tracker. +The OS cursor stays hidden while the crosshair pointer mode is on (`inputApplyMouseRequests` +hides it whenever `cfgCrosshairCursor && optionsOverlayIsOpen()`), so no arrow appears. +Front end: the game's own D519 cursor (`overlayFrontEndCrosshair`, `input.c`) is left as is: it sits +UNDER the overlay card (the card does not let it block rows) and is clamped by the game each tick, so +there is no clean port-side park (the -1000 attempt is not repeated). Build rc 0. + +## D563: F10 root hub: Left/Right no longer open pages; held nav inputs latched + +Maintainer: holding the stick/D-pad/arrow on the root menu could enter a page (e.g. PC Options) and +keep acting inside it. (a) On the root hub (`s_section < 0`) header/link and action rows now activate +only on A / Enter (or a click); Left/Right/stick/D-pad/arrows do nothing there. Pages keep Left/Right +value changes. (b) Held-input latch: `s_latchArm` is set when the overlay opens (closed-state branch +of `overlayHandleInputLocked`, covers F10 and the front-end PC Options entry) and on every page +enter/back (`overlayOpenHeader`, `overlayBackOne`); the next poll snapshots held up/down/left/right/ +accept/back and each stays masked (`s_latchMask`) until released once, so auto-repeat cannot fire +from a latched input. Verified by code reading + build rc 0 only: the overlay reads SDL pad/keyboard +state directly, which GE_INPUTSCRIPT does not drive, so a headless held-input repro is not possible; maintainer +to check front end (file select -> PC Options) and in-level F10 by hand. + +## D564: F10 "Look up/down" removed; game value pinned to factory; tip polish — FIXED 2026-10-06 (maintainer decision) + +**Report:** maintainer: changing Look up/down (in-level F10 and front end) had +no visible effect. **Trace (code, file:line in the D562/D563 session report):** +the row wrote the game's `OPTION_INVERTLOOK` (`set_cur_player_look_vertical_inverted`), +read only at `src/game/bondview2.c:4938` (`invertPitch`) on the game's own +stick/C-button pitch path. The port's mouse look (D300 `hipDirectCompute`, +aim models) and centred-aim pad look (`padDirectCompute`) write `vv_verta` +directly and never consult it; only pad look in the N64 aim style passes +through it, where it stacks with Controller "Invert look". Perfect Dark's port +instead routes mouse and pad through its single "Reverse Pitch" option; GE's +port already has per-device Invert look rows (the usual PC model), so the +maintainer chose removal. + +**Fix (port only):** removed the `Bond.Look` row and its value names +(`port/src/optionsoverlay.c`); `port/src/watchsettings.c` pins the game's +value to its factory default 0 each stage tick (blank profiles load 0: +`DEFAULT_OPTIONS` has no `OPTION_INVERTLOOK`, `src/game/file2.c:1357`), so +every default profile behaves exactly as before and a value saved by an older +build or set in the in-game watch menu cannot silently flip pad pitch. Mouse / +Controller "Invert look" are the only inversion controls. Same pass, tip +polish: All unlocked "Unlocks all levels, 007 mode and every cheat.", Show +crosshair, FOV and Aim style tips rewritten as plain sentences. Verified: both +TUs compile (link was blocked by a running game; full build + re-cut follow). + diff --git a/docs/documentation.md b/docs/documentation.md index 3364199c..3f27d396 100644 --- a/docs/documentation.md +++ b/docs/documentation.md @@ -28,6 +28,6 @@ separate [Building](building.md) page.) how the two-AI-agent setup ran, its timeline and numbers, and an honest read on what did and didn't work. - **[Security status](security.md)** — what the release packages install - (and don't): no networking, no telemetry, no ROM, no system writes. + (and don't): no networking unless you opt in to the update check, no telemetry, no ROM, no system writes. - **[Fidelity status](fidelity.md)** — how faithfully the port reproduces the N64 game's logic, and what was audited to back that claim. diff --git a/docs/img/animation-glitch-1964gepd.png b/docs/img/animation-glitch-1964gepd.png deleted file mode 100644 index f744f666..00000000 Binary files a/docs/img/animation-glitch-1964gepd.png and /dev/null differ diff --git a/docs/img/animation-glitch-pc-port.png b/docs/img/animation-glitch-pc-port.png deleted file mode 100644 index b156aecc..00000000 Binary files a/docs/img/animation-glitch-pc-port.png and /dev/null differ diff --git a/docs/img/bugs/20260912-cullingbug-bugged.png b/docs/img/bugs/20260912-cullingbug-bugged.png index a4a8887f..64f42913 100644 Binary files a/docs/img/bugs/20260912-cullingbug-bugged.png and b/docs/img/bugs/20260912-cullingbug-bugged.png differ diff --git a/docs/img/bugs/D288-silo-stray-triangle-closeup.png b/docs/img/bugs/D288-silo-stray-triangle-closeup.png deleted file mode 100644 index d9b64429..00000000 Binary files a/docs/img/bugs/D288-silo-stray-triangle-closeup.png and /dev/null differ diff --git a/docs/img/bugs/d176-surface-exterior-our-build.png b/docs/img/bugs/d176-surface-exterior-our-build.png deleted file mode 100644 index b12a097f..00000000 Binary files a/docs/img/bugs/d176-surface-exterior-our-build.png and /dev/null differ diff --git a/docs/img/bugs/d182-file-select-backout-20260902.png b/docs/img/bugs/d182-file-select-backout-20260902.png deleted file mode 100644 index 12035e0b..00000000 Binary files a/docs/img/bugs/d182-file-select-backout-20260902.png and /dev/null differ diff --git a/docs/img/bugs/d227-surface-emulator-ref.png b/docs/img/bugs/d227-surface-emulator-ref.png index 80bf5269..d7f33fa1 100644 Binary files a/docs/img/bugs/d227-surface-emulator-ref.png and b/docs/img/bugs/d227-surface-emulator-ref.png differ diff --git a/docs/img/bugs/d227-surface-starburst-ours.png b/docs/img/bugs/d227-surface-starburst-ours.png index 346ab045..53d2a4bc 100644 Binary files a/docs/img/bugs/d227-surface-starburst-ours.png and b/docs/img/bugs/d227-surface-starburst-ours.png differ diff --git a/docs/img/icon/ge007-icon.png b/docs/img/icon/ge007-icon.png new file mode 100644 index 00000000..afaeb031 Binary files /dev/null and b/docs/img/icon/ge007-icon.png differ diff --git a/docs/img/rareware logo - 1964 gepd.png b/docs/img/rareware logo - 1964 gepd.png deleted file mode 100644 index 22891689..00000000 Binary files a/docs/img/rareware logo - 1964 gepd.png and /dev/null differ diff --git a/docs/index.md b/docs/index.md index be07e7b7..eeb9f219 100644 --- a/docs/index.md +++ b/docs/index.md @@ -72,7 +72,7 @@ single tracker for what comes next.
  • NTSC-U ROMs only in the release packages. PAL and JP ROMs convert, build and run from source, but release packages ship the US (NTSC-U) ROM. On the roadmap.
  • Changing the aspect ratio in a level can briefly glitch the gun/hand model (cosmetic, one-off). Change the ratio from the front-end PC Options, or accept it (D509).
  • No macOS or ARM builds yet. Today the release ships Windows, Linux and Steam Deck (keyboard, mouse and controller-button rebinding are all supported, defaults in the README's Controls section); macOS and ARM are on the roadmap.
  • -
  • Skip intro and All unlocked are experimental — leave them off for a normal playthrough. All unlocked has not written your save since v0.4.1, but saves made while it was on in builds before v0.4.1 can hold fake unlocks and are not repaired; back up data/ge007.eep if you used it on an older build (D442, D387).
  • +
  • Saves from builds before v0.4.1 can hold fake unlocks from All unlocked — All unlocked has not written your save since v0.4.1, but saves made while it was on in builds before v0.4.1 can hold fake unlocks and are not repaired; back up data/ge007.eep if you used it on an older build (D442, D387).
  • The first frame of a level takes a little longer while its textures upload — a brief FPS-counter dip; nothing to fix (D475, D480).
  • In widescreen, far objects almost fully in fog can still show a faint distant building edge (4:3 matches the N64). Cosmetic (D503).
  • diff --git a/docs/internals.md b/docs/internals.md index 48d24bc9..3ec587dd 100644 --- a/docs/internals.md +++ b/docs/internals.md @@ -382,6 +382,51 @@ sync/flush) is shared. rumble/mempak. This is simpler than a full Memory Pak/PFS emulation. +* **F10 settings overlay: mouse and scrolling** (`port/src/optionsoverlay.c`, + `port/src/input.c`; D544, D314, D304): + * *Cursor.* `inputSuspendForOverlay()` (`input.c`) drops relative-mouse + grab and calls `mouseRequestCursor(1)`. With `Input.CrosshairCursor` on + (default, D555) `inputApplyMouseRequests()` (host thread) then hides the OS + cursor while the overlay is open and `drawCrosshairPointer()` in + `optionsoverlay.c` draws the game's own `crosshairimage` sprite at the mouse, + last in the overlay DL, at the game's on-screen size; the front end's own + crosshair keeps tracking the mouse (D519) and the pointer is drawn exactly + over it (`inputFrontEndCursorUiFrac()`), so only one shows; in a level it + follows the OS mouse. With the option off the OS arrow shows over the box. One owner of the cursor state (no show/hide race). + * *Hover vs selection.* `overlayHandleInputLocked()` maps the pointer to a + visible row with `overlayRowAtY()` (follows the eased scroll position, so + a click lands on the row it visually covers). Hover sets `s_hover` only + while `s_mouseActive` (mouse moved or clicked); it is a focus cue, never + the selection. The tip line (`helpVis` in the emit path) follows the + hovered row while the mouse is in use, else the selected row. The + wheel clears `s_mouseActive`. + * *Clicks.* Left-click on a row selects it; headers, links and Back act on + the whole row, otherwise only a click in the control span to the right of + the name changes the setting (toggle/cycle, open a dropdown, start a + binding capture, or set + drag a slider). With a dropdown open + (`s_ddRow >= 0`) it owns the pointer: hover picks, click confirms, click + outside or right-click cancels. Right-click elsewhere = Esc/back (closes a + dropdown first, then one page, then the overlay). `s_rmbBlock` holds that + off until release after a binding capture, so a right-click just bound + does not also back out. + * *Scrolling.* Long pages scroll as a window + over the visible-row list (`s_visIdx`/`s_visN`, first displayed entry + `s_scroll`, section header pinned). `overlayUpdateScroll()` moves the + window only when the selection leaves it (D304). The wheel arrives on the + host thread: `optionsOverlayScroll()` only queues notches in + `s_wheelPending` (SDL atomic, D314) and `overlayApplyWheel()` applies them + on the scheduler thread. The drawn position `s_scrollF` eases toward + `s_scroll` each emit (snaps on page change via `s_scrollSnap`). + * *Scrollbar (D556).* A page that overflows (same test as the "^ v" title + marker) draws a thin track + thumb in the card's right margin + (`overlayScrollbar()`: x = right-7..right-4, clear of the value text that + ends at right-10); thumb size = visible/total rows, position from + `s_scrollF`. Mouse: a press on the thumb drags it, a press on the track + pages up/down; both call `overlayScrollTo()`, which sets `s_scroll` and + moves `s_sel` into the window (like the wheel; otherwise + `overlayUpdateScroll()` would snap back). A scrollbar press never reaches + the row-click path. + --- ## 8. Recommended plan diff --git a/docs/security.md b/docs/security.md index cd040941..3cef98ea 100644 --- a/docs/security.md +++ b/docs/security.md @@ -1,6 +1,6 @@ --- title: Security status -description: What a GoldenEye PC port release installs and doesn't: no networking, no telemetry, no ROM, no system writes — re-verified against the v0.5.0 source. +description: What a GoldenEye PC port release installs and doesn't: no networking unless you opt in to the update check, no telemetry, no ROM, no system writes — re-verified against the v0.5.0 source. date: '2026-10-05' modified_time: '2026-10-05' --- @@ -20,7 +20,9 @@ bundle contents were not re-checked then. **v0.5.0 update pass, 2026-10-05:** the source checks were re-run against the current tree — no socket/network includes, no network symbols and no HTTP string literals in the compiled `src/` and `port/` sets; no registry -or library-loading calls in either; the converter's Python sources +or library-loading calls in either (since D551 the one exception is the +opt-in update check: a System32 `winhttp.dll` load on Windows or a `curl` +process on Linux, only when `Game.CheckUpdates` is on); the converter's Python sources (`prepare-assets.py` and its emit-script imports) import no network module. The bundle contents were re-checked against the packaging manifests (`tools_pc/bundle-win.sh` / `bundle-linux.sh` — the scripts CI @@ -44,8 +46,15 @@ legally-owned copy. At runtime, the engine: -- has **no networking of any kind** (no sockets, no HTTP, checked directly - against the source), +- has **no networking unless you turn on the opt-in update check** + (`Game.CheckUpdates`, off by default): then it makes one HTTPS request to + GitHub's releases API per launch (a fixed User-Agent; GitHub sees the + request and your IP, as with any web request) and never downloads or + installs anything. On Windows it loads the system `winhttp.dll` from + System32 at runtime only in that case (it is not in the import table); on + Linux/Steam Deck it runs the system `curl`. With the setting off there + are no sockets and no HTTP (D551; checked against the source and run live + on Windows, Linux and Steam Deck), - has **no telemetry, crash reporting, or analytics** of any kind, - **never touches the Windows registry** and **never requests elevated permissions**, diff --git a/port/fast3d/ge007_icon.h b/port/fast3d/ge007_icon.h new file mode 100644 index 00000000..07bceb84 --- /dev/null +++ b/port/fast3d/ge007_icon.h @@ -0,0 +1,46 @@ +/* Generated by tools_pc/make_icon.py -- do not edit by hand. + * The project mark (a generic version of the GE default aim + * crosshair) as a 32x32 RGBA array, row-major, plain R,G,B,A + * byte order per pixel; used by gfx_sdl2.cpp for + * SDL_SetWindowIcon. Regenerate: python tools_pc/make_icon.py */ +#ifndef GE007_ICON_H +#define GE007_ICON_H + +#include + +#define GE007_ICON_SIZE 32 +static const uint8_t ge007_icon_rgba[GE007_ICON_SIZE][GE007_ICON_SIZE][4] = { + {{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 6, 7, 7,255},{ 6, 7, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255}}, + {{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 2, 4, 6,255},{ 2, 4, 6,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255}}, + {{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 6, 6, 7,255},{ 17, 14, 10,255},{ 17, 14, 10,255},{ 6, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255}}, + {{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 7, 7,255},{ 2, 4, 6,255},{ 16, 14, 10,255},{211,147, 53,255},{211,147, 53,255},{ 16, 14, 10,255},{ 2, 4, 6,255},{ 5, 7, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255}}, + {{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 8, 7, 7,255},{ 5, 5, 7,255},{ 21, 17, 11,255},{226,157, 57,255},{226,157, 57,255},{ 21, 17, 11,255},{ 5, 5, 7,255},{ 8, 7, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255}}, + {{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 8, 7, 8,255},{ 9, 8, 8,255},{ 13, 11, 9,255},{ 10, 9, 8,255},{ 17, 14, 9,255},{222,154, 56,255},{222,154, 56,255},{ 17, 14, 9,255},{ 10, 9, 8,255},{ 13, 11, 9,255},{ 9, 8, 8,255},{ 8, 7, 8,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255}}, + {{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 6, 7, 7,255},{ 9, 8, 8,255},{ 10, 9, 8,255},{ 10, 9, 8,255},{ 0, 1, 5,255},{ 0, 0, 3,255},{ 29, 22, 12,255},{224,156, 56,255},{224,156, 56,255},{ 29, 22, 12,255},{ 0, 0, 3,255},{ 0, 1, 5,255},{ 10, 9, 8,255},{ 10, 9, 8,255},{ 9, 8, 8,255},{ 6, 7, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255}}, + {{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 6, 7, 7,255},{ 9, 8, 8,255},{ 12, 10, 9,255},{ 6, 6, 7,255},{ 3, 3, 6,255},{ 70, 50, 22,255},{130, 91, 35,255},{216,150, 54,255},{233,162, 58,255},{233,162, 58,255},{216,150, 54,255},{130, 91, 35,255},{ 70, 50, 22,255},{ 3, 3, 6,255},{ 6, 6, 7,255},{ 12, 10, 9,255},{ 9, 8, 8,255},{ 6, 7, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255}}, + {{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 6, 7, 7,255},{ 9, 8, 8,255},{ 10, 9, 8,255},{ 1, 2, 6,255},{ 20, 15, 10,255},{162,113, 42,255},{249,173, 62,255},{255,184, 66,255},{226,157, 57,255},{231,160, 58,255},{231,160, 58,255},{226,157, 57,255},{255,184, 66,255},{249,173, 62,255},{162,113, 42,255},{ 20, 15, 10,255},{ 1, 2, 6,255},{ 10, 9, 8,255},{ 9, 8, 8,255},{ 6, 7, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255}}, + {{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 9, 8, 8,255},{ 12, 10, 9,255},{ 5, 4, 6,255},{ 74, 52, 23,255},{222,154, 56,255},{251,174, 62,255},{218,151, 55,255},{112, 78, 31,255},{ 35, 25, 14,255},{223,155, 56,255},{223,155, 56,255},{ 35, 25, 14,255},{112, 78, 31,255},{218,151, 55,255},{251,174, 62,255},{222,154, 56,255},{ 74, 52, 23,255},{ 5, 4, 6,255},{ 12, 10, 9,255},{ 9, 8, 8,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255}}, + {{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 8, 7, 8,255},{ 11, 9, 8,255},{ 6, 6, 7,255},{ 17, 13, 10,255},{223,155, 56,255},{255,180, 64,255},{164,114, 43,255},{ 20, 15, 10,255},{ 0, 0, 3,255},{ 16, 13, 10,255},{222,155, 56,255},{222,155, 56,255},{ 16, 13, 10,255},{ 0, 0, 3,255},{ 20, 15, 10,255},{164,114, 43,255},{255,180, 64,255},{223,155, 56,255},{ 17, 13, 10,255},{ 6, 6, 7,255},{ 11, 9, 8,255},{ 8, 7, 8,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255}}, + {{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 9, 8, 8,255},{ 10, 9, 8,255},{ 3, 3, 6,255},{161,112, 42,255},{251,174, 62,255},{165,115, 43,255},{ 8, 6, 7,255},{ 5, 5, 7,255},{ 10, 9, 8,255},{ 21, 17, 10,255},{226,157, 57,255},{226,157, 57,255},{ 21, 17, 10,255},{ 10, 9, 8,255},{ 5, 5, 7,255},{ 8, 6, 7,255},{165,115, 43,255},{251,174, 62,255},{161,112, 42,255},{ 3, 3, 6,255},{ 10, 9, 8,255},{ 9, 8, 8,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255}}, + {{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 6, 7, 7,255},{ 8, 7, 7,255},{ 13, 11, 9,255},{ 3, 3, 5,255},{ 70, 50, 22,255},{248,173, 62,255},{218,152, 55,255},{ 17, 13, 10,255},{ 6, 6, 7,255},{ 14, 11, 9,255},{ 6, 6, 7,255},{ 19, 16, 10,255},{212,147, 53,255},{212,147, 53,255},{ 19, 16, 10,255},{ 6, 6, 7,255},{ 14, 11, 9,255},{ 6, 6, 7,255},{ 17, 13, 10,255},{218,152, 55,255},{248,173, 62,255},{ 70, 50, 22,255},{ 3, 3, 5,255},{ 13, 11, 9,255},{ 8, 7, 7,255},{ 6, 7, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255}}, + {{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 2, 4, 6,255},{ 5, 5, 7,255},{ 10, 9, 8,255},{ 0, 0, 3,255},{130, 91, 35,255},{254,184, 65,255},{112, 79, 31,255},{ 0, 0, 4,255},{ 10, 9, 8,255},{ 6, 6, 7,255},{ 9, 8, 8,255},{ 10, 8, 8,255},{ 20, 16, 11,255},{ 20, 16, 11,255},{ 10, 8, 8,255},{ 9, 8, 8,255},{ 6, 6, 7,255},{ 10, 9, 8,255},{ 0, 0, 4,255},{112, 79, 31,255},{254,184, 65,255},{130, 91, 35,255},{ 0, 0, 3,255},{ 10, 9, 8,255},{ 5, 5, 7,255},{ 2, 4, 6,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255}}, + {{ 5, 6, 7,255},{ 5, 6, 7,255},{ 6, 6, 7,255},{ 16, 13, 9,255},{ 21, 16, 11,255},{ 17, 14, 10,255},{ 30, 22, 12,255},{216,150, 54,255},{225,157, 57,255},{ 34, 26, 13,255},{ 17, 13, 9,255},{ 21, 17, 11,255},{ 20, 15, 10,255},{ 10, 8, 8,255},{ 9, 8, 8,255},{ 6, 6, 7,255},{ 6, 6, 7,255},{ 9, 8, 8,255},{ 10, 8, 8,255},{ 20, 15, 10,255},{ 21, 17, 11,255},{ 17, 13, 9,255},{ 34, 26, 13,255},{225,157, 57,255},{216,150, 54,255},{ 30, 22, 12,255},{ 17, 14, 10,255},{ 21, 16, 11,255},{ 16, 13, 9,255},{ 6, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255}}, + {{ 6, 7, 7,255},{ 2, 4, 6,255},{ 17, 14, 10,255},{212,147, 53,255},{226,157, 57,255},{222,154, 56,255},{224,156, 56,255},{233,162, 58,255},{231,160, 58,255},{223,155, 56,255},{223,154, 56,255},{226,157, 57,255},{212,147, 53,255},{ 21, 16, 11,255},{ 6, 6, 7,255},{ 11, 10, 8,255},{ 11, 10, 8,255},{ 6, 6, 7,255},{ 21, 16, 11,255},{212,147, 53,255},{226,157, 57,255},{223,154, 56,255},{223,155, 56,255},{231,160, 58,255},{233,162, 58,255},{224,156, 56,255},{222,154, 56,255},{226,157, 57,255},{212,147, 53,255},{ 17, 14, 10,255},{ 2, 4, 6,255},{ 6, 7, 7,255}}, + {{ 6, 7, 7,255},{ 2, 4, 6,255},{ 17, 14, 10,255},{212,147, 53,255},{226,157, 57,255},{222,154, 56,255},{224,156, 56,255},{233,162, 58,255},{231,160, 58,255},{223,155, 56,255},{223,154, 56,255},{226,157, 57,255},{212,147, 53,255},{ 21, 16, 11,255},{ 6, 6, 7,255},{ 11, 10, 8,255},{ 11, 10, 8,255},{ 6, 6, 7,255},{ 21, 16, 11,255},{212,147, 53,255},{226,157, 57,255},{223,154, 56,255},{223,155, 56,255},{231,160, 58,255},{233,162, 58,255},{224,156, 56,255},{222,154, 56,255},{226,157, 57,255},{212,147, 53,255},{ 17, 14, 10,255},{ 2, 4, 6,255},{ 6, 7, 7,255}}, + {{ 5, 6, 7,255},{ 5, 6, 7,255},{ 6, 6, 7,255},{ 16, 13, 9,255},{ 21, 16, 11,255},{ 17, 14, 10,255},{ 30, 22, 12,255},{216,150, 54,255},{225,157, 57,255},{ 34, 26, 13,255},{ 17, 13, 9,255},{ 21, 17, 11,255},{ 20, 15, 10,255},{ 10, 8, 8,255},{ 9, 8, 8,255},{ 6, 6, 7,255},{ 6, 6, 7,255},{ 9, 8, 8,255},{ 10, 8, 8,255},{ 20, 15, 10,255},{ 21, 17, 11,255},{ 17, 13, 9,255},{ 34, 26, 13,255},{225,157, 57,255},{216,150, 54,255},{ 30, 22, 12,255},{ 17, 14, 10,255},{ 21, 16, 11,255},{ 16, 13, 9,255},{ 6, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255}}, + {{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 2, 4, 6,255},{ 5, 5, 7,255},{ 10, 9, 8,255},{ 0, 0, 3,255},{130, 91, 35,255},{254,184, 65,255},{112, 79, 31,255},{ 0, 0, 4,255},{ 10, 9, 8,255},{ 6, 6, 7,255},{ 9, 8, 8,255},{ 10, 8, 8,255},{ 20, 16, 11,255},{ 20, 16, 11,255},{ 10, 8, 8,255},{ 9, 8, 8,255},{ 6, 6, 7,255},{ 10, 9, 8,255},{ 0, 0, 4,255},{112, 79, 31,255},{254,184, 65,255},{130, 91, 35,255},{ 0, 0, 3,255},{ 10, 9, 8,255},{ 5, 5, 7,255},{ 2, 4, 6,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255}}, + {{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 6, 7, 7,255},{ 8, 7, 7,255},{ 13, 11, 9,255},{ 3, 3, 5,255},{ 70, 50, 22,255},{248,173, 62,255},{218,152, 55,255},{ 17, 13, 10,255},{ 6, 6, 7,255},{ 14, 11, 9,255},{ 6, 6, 7,255},{ 19, 16, 10,255},{212,147, 53,255},{212,147, 53,255},{ 19, 16, 10,255},{ 6, 6, 7,255},{ 14, 11, 9,255},{ 6, 6, 7,255},{ 17, 13, 10,255},{218,152, 55,255},{248,173, 62,255},{ 70, 50, 22,255},{ 3, 3, 5,255},{ 13, 11, 9,255},{ 8, 7, 7,255},{ 6, 7, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255}}, + {{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 9, 8, 8,255},{ 10, 9, 8,255},{ 3, 3, 6,255},{161,112, 42,255},{251,174, 62,255},{165,115, 43,255},{ 8, 6, 7,255},{ 5, 5, 7,255},{ 10, 9, 8,255},{ 21, 17, 10,255},{226,157, 57,255},{226,157, 57,255},{ 21, 17, 10,255},{ 10, 9, 8,255},{ 5, 5, 7,255},{ 8, 6, 7,255},{165,115, 43,255},{251,174, 62,255},{161,112, 42,255},{ 3, 3, 6,255},{ 10, 9, 8,255},{ 9, 8, 8,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255}}, + {{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 8, 7, 8,255},{ 11, 9, 8,255},{ 6, 6, 7,255},{ 17, 13, 10,255},{223,155, 56,255},{255,180, 64,255},{164,114, 43,255},{ 20, 15, 10,255},{ 0, 0, 3,255},{ 16, 13, 10,255},{222,155, 56,255},{222,155, 56,255},{ 16, 13, 10,255},{ 0, 0, 3,255},{ 20, 15, 10,255},{164,114, 43,255},{255,180, 64,255},{223,155, 56,255},{ 17, 13, 10,255},{ 6, 6, 7,255},{ 11, 9, 8,255},{ 8, 7, 8,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255}}, + {{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 9, 8, 8,255},{ 12, 10, 9,255},{ 5, 4, 6,255},{ 74, 52, 23,255},{222,154, 56,255},{251,174, 62,255},{218,151, 55,255},{112, 78, 31,255},{ 35, 25, 14,255},{223,155, 56,255},{223,155, 56,255},{ 35, 25, 14,255},{112, 78, 31,255},{218,151, 55,255},{251,174, 62,255},{222,154, 56,255},{ 74, 52, 23,255},{ 5, 4, 6,255},{ 12, 10, 9,255},{ 9, 8, 8,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255}}, + {{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 6, 7, 7,255},{ 9, 8, 8,255},{ 10, 9, 8,255},{ 1, 2, 6,255},{ 20, 15, 10,255},{162,113, 42,255},{249,173, 62,255},{255,184, 66,255},{226,157, 57,255},{231,160, 58,255},{231,160, 58,255},{226,157, 57,255},{255,184, 66,255},{249,173, 62,255},{162,113, 42,255},{ 20, 15, 10,255},{ 1, 2, 6,255},{ 10, 9, 8,255},{ 9, 8, 8,255},{ 6, 7, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255}}, + {{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 6, 7, 7,255},{ 9, 8, 8,255},{ 12, 10, 9,255},{ 6, 6, 7,255},{ 3, 3, 6,255},{ 70, 50, 22,255},{130, 91, 35,255},{216,150, 54,255},{233,162, 58,255},{233,162, 58,255},{216,150, 54,255},{130, 91, 35,255},{ 70, 50, 22,255},{ 3, 3, 6,255},{ 6, 6, 7,255},{ 12, 10, 9,255},{ 9, 8, 8,255},{ 6, 7, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255}}, + {{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 6, 7, 7,255},{ 9, 8, 8,255},{ 10, 9, 8,255},{ 10, 9, 8,255},{ 0, 1, 5,255},{ 0, 0, 3,255},{ 29, 22, 12,255},{224,156, 56,255},{224,156, 56,255},{ 29, 22, 12,255},{ 0, 0, 3,255},{ 0, 1, 5,255},{ 10, 9, 8,255},{ 10, 9, 8,255},{ 9, 8, 8,255},{ 6, 7, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255}}, + {{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 8, 7, 8,255},{ 9, 8, 8,255},{ 13, 11, 9,255},{ 10, 9, 8,255},{ 17, 14, 9,255},{222,154, 56,255},{222,154, 56,255},{ 17, 14, 9,255},{ 10, 9, 8,255},{ 13, 11, 9,255},{ 9, 8, 8,255},{ 8, 7, 8,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255}}, + {{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 8, 7, 7,255},{ 5, 5, 7,255},{ 21, 17, 11,255},{226,157, 57,255},{226,157, 57,255},{ 21, 17, 11,255},{ 5, 5, 7,255},{ 8, 7, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255}}, + {{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 7, 7,255},{ 2, 4, 6,255},{ 16, 14, 10,255},{211,147, 53,255},{211,147, 53,255},{ 16, 14, 10,255},{ 2, 4, 6,255},{ 5, 7, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255}}, + {{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 6, 6, 7,255},{ 17, 14, 10,255},{ 17, 14, 10,255},{ 6, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255}}, + {{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 2, 4, 6,255},{ 2, 4, 6,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255}}, + {{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 6, 7, 7,255},{ 6, 7, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255},{ 5, 6, 7,255}} +}; +#endif /* GE007_ICON_H */ diff --git a/port/fast3d/gfx_opengl.cpp b/port/fast3d/gfx_opengl.cpp index e16b386f..d9a13847 100644 --- a/port/fast3d/gfx_opengl.cpp +++ b/port/fast3d/gfx_opengl.cpp @@ -63,6 +63,8 @@ static int gl_glsl_version = 130; static char gl_glsl_version_str[16] = "130"; static GLenum gl_mirror_clamp = GL_MIRROR_CLAMP_TO_EDGE; static bool gl_es = false; +extern int gfx_rdp_affine; /* D540: defined in gfx_pc.cpp */ +extern int gfx_fog_vertex; /* D540: defined in gfx_pc.cpp */ static bool gl_core_profile = false; static int gfx_opengl_get_max_texture_size() { @@ -149,13 +151,13 @@ static const char* shader_item_to_str(uint32_t item, bool with_alpha, bool only_ case SHADER_1: return with_alpha ? "vec4(1.0, 1.0, 1.0, 1.0)" : "vec3(1.0, 1.0, 1.0)"; case SHADER_INPUT_1: - return with_alpha || !inputs_have_alpha ? "vInput1" : "vInput1.rgb"; + return with_alpha || !inputs_have_alpha ? "cInput1" : "cInput1.rgb"; case SHADER_INPUT_2: - return with_alpha || !inputs_have_alpha ? "vInput2" : "vInput2.rgb"; + return with_alpha || !inputs_have_alpha ? "cInput2" : "cInput2.rgb"; case SHADER_INPUT_3: - return with_alpha || !inputs_have_alpha ? "vInput3" : "vInput3.rgb"; + return with_alpha || !inputs_have_alpha ? "cInput3" : "cInput3.rgb"; case SHADER_INPUT_4: - return with_alpha || !inputs_have_alpha ? "vInput4" : "vInput4.rgb"; + return with_alpha || !inputs_have_alpha ? "cInput4" : "cInput4.rgb"; case SHADER_TEXEL0: return with_alpha ? "texVal0" : "texVal0.rgb"; case SHADER_TEXEL0A: @@ -181,13 +183,13 @@ static const char* shader_item_to_str(uint32_t item, bool with_alpha, bool only_ case SHADER_1: return "1.0"; case SHADER_INPUT_1: - return "vInput1.a"; + return "cInput1.a"; case SHADER_INPUT_2: - return "vInput2.a"; + return "cInput2.a"; case SHADER_INPUT_3: - return "vInput3.a"; + return "cInput3.a"; case SHADER_INPUT_4: - return "vInput4.a"; + return "cInput4.a"; case SHADER_TEXEL0: return "texVal0.a"; case SHADER_TEXEL0A: @@ -277,9 +279,16 @@ static struct ShaderProgram* gfx_opengl_create_and_load_new_shader(uint64_t shad } } } + /* D540: RDP-style screen-affine shade colour (see gfx_opengl_init). + * D543: fog is the per-vertex RSP value by default, which the RDP also + * interpolates screen-affine (it rides in shade alpha), so it shares the + * shade qualifier. GE_FOGPIXEL=1 (D540 per-pixel) needs a perspective + * attribute: it carries fog * w, divided by w in the shader. */ + const char *shadeq = gfx_rdp_affine ? "noperspective " : ""; + const char *fogq = gfx_fog_vertex ? shadeq : ""; if (cc_features.opt_fog) { append_line(vs_buf, &vs_len, "INPUT vec4 aFog;"); - append_line(vs_buf, &vs_len, "OUTPUT vec4 vFog;"); + vs_len += sprintf(vs_buf + vs_len, "%sOUTPUT vec4 vFog;\n", fogq); num_floats += 4; } @@ -291,7 +300,7 @@ static struct ShaderProgram* gfx_opengl_create_and_load_new_shader(uint64_t shad for (int i = 0; i < cc_features.num_inputs; i++) { vs_len += sprintf(vs_buf + vs_len, "INPUT vec%d aInput%d;\n", cc_features.opt_alpha ? 4 : 3, i + 1); - vs_len += sprintf(vs_buf + vs_len, "OUTPUT vec%d vInput%d;\n", cc_features.opt_alpha ? 4 : 3, i + 1); + vs_len += sprintf(vs_buf + vs_len, "%sOUTPUT vec%d vInput%d;\n", shadeq, cc_features.opt_alpha ? 4 : 3, i + 1); num_floats += cc_features.opt_alpha ? 4 : 3; } @@ -360,13 +369,13 @@ static struct ShaderProgram* gfx_opengl_create_and_load_new_shader(uint64_t shad } } if (cc_features.opt_fog) { - append_line(fs_buf, &fs_len, "INPUT vec4 vFog;"); + fs_len += sprintf(fs_buf + fs_len, "%sINPUT vec4 vFog;\n", fogq); } if (cc_features.opt_grayscale) { append_line(fs_buf, &fs_len, "INPUT vec4 vGrayscaleColor;"); } for (int i = 0; i < cc_features.num_inputs; i++) { - fs_len += sprintf(fs_buf + fs_len, "INPUT vec%d vInput%d;\n", cc_features.opt_alpha ? 4 : 3, i + 1); + fs_len += sprintf(fs_buf + fs_len, "%sINPUT vec%d vInput%d;\n", shadeq, cc_features.opt_alpha ? 4 : 3, i + 1); } if (cc_features.used_textures[0]) { @@ -441,6 +450,15 @@ static struct ShaderProgram* gfx_opengl_create_and_load_new_shader(uint64_t shad } append_line(fs_buf, &fs_len, "void main() {"); + /* D548: combiner inputs are 8-bit on the N64 (shade, prim, env), so never + * above 1.0. Under the D540 noperspective shade, GPU clipping of a big + * near-camera triangle can extrapolate them past 1.0; the CC wrap + * emulation below then turned walls into black/white/yellow/blue bands + * (Aztec, near rocket fire). Clamp to the hardware range first. */ + for (int i = 0; i < cc_features.num_inputs; i++) { + fs_len += sprintf(fs_buf + fs_len, " vec%d cInput%d = clamp(vInput%d, 0.0, 1.0);\n", + cc_features.opt_alpha ? 4 : 3, i + 1, i + 1); + } for (int i = 0; i < 2; i++) { if (cc_features.used_textures[i]) { @@ -503,10 +521,22 @@ static struct ShaderProgram* gfx_opengl_create_and_load_new_shader(uint64_t shad append_line(fs_buf, &fs_len, " texel = clamp(texel, 0.0, 1.0);"); // TODO discard if alpha is 0? if (cc_features.opt_fog) { + /* D543 default: vFog.a is the per-vertex RSP value, used as-is + * (N64). GE_FOGPIXEL=1 (D540): vFog.a = fog * w, divided by w here = + * exact per-pixel fog from the pixel's own depth. */ + append_line(fs_buf, &fs_len, gfx_fog_vertex ? " float fogA = clamp(vFog.a, 0.0, 1.0);" + : " float fogA = clamp(vFog.a * gl_FragCoord.w, 0.0, 1.0);"); + /* D540: the D503 snap is OFF by default -- its hard 97% edge swept + * across the ground in tile-shaped chunks as the player walked + * (Deck playtest; maintainer-confirmed with it off). GE_FOGSNAP=1 + * restores it. */ + static int fogSnap = -1; + if (fogSnap < 0) { const char *e = getenv("GE_FOGSNAP"); fogSnap = (e && e[0] == '1'); } + if (fogSnap) append_line(fs_buf, &fs_len, " if (fogA >= 247.0 / 255.0) fogA = 1.0;"); if (cc_features.opt_alpha) { - append_line(fs_buf, &fs_len, " texel = vec4(mix(texel.rgb, vFog.rgb, vFog.a), texel.a);"); + append_line(fs_buf, &fs_len, " texel = vec4(mix(texel.rgb, vFog.rgb, fogA), texel.a);"); } else { - append_line(fs_buf, &fs_len, " texel = mix(texel, vFog.rgb, vFog.a);"); + append_line(fs_buf, &fs_len, " texel = mix(texel, vFog.rgb, fogA);"); } } @@ -1103,6 +1133,22 @@ static void gfx_opengl_init(void) { } sysLogPrintf(LOG_NOTE, "GL: using GLSL version %s", gl_glsl_version_str); + /* D540: the RDP interpolates shade colour affinely in screen space; the + * GPU default is perspective-correct, which squeezed the sky's horizon + * fade into a thin band. noperspective needs desktop GLSL 1.30+ (not + * GLSL ES). GE_SHADEPERSP=1 restores perspective-correct shade (A/B). + * Fog (D543): per-vertex RSP fog by default, as the N64. Its old + * light/dark cycling at the player's feet came from GPU near-clipping of + * behind-camera corners; gfx_sp_tri1 now near-clips on the CPU like the + * RSP. GE_FOGPIXEL=1 = the D540 exact per-pixel fog (A/B). */ + { + const char *sp = getenv("GE_SHADEPERSP"), *fp = getenv("GE_FOGPIXEL"); + gfx_rdp_affine = (!gl_es && gl_glsl_version >= 130 && !(sp && sp[0] == '1')) ? 1 : 0; + gfx_fog_vertex = (fp && fp[0] == '1') ? 0 : 1; /* D543: per-vertex RSP fog by default */ + } + sysLogPrintf(LOG_NOTE, "GL: shade %s, fog %s", gfx_rdp_affine ? "affine (RDP)" : "perspective", + gfx_fog_vertex ? "per-vertex" : "per-pixel"); + glGenBuffers(1, &opengl_vbo); glBindBuffer(GL_ARRAY_BUFFER, opengl_vbo); diff --git a/port/fast3d/gfx_pc.cpp b/port/fast3d/gfx_pc.cpp index ff94d711..7756786b 100644 --- a/port/fast3d/gfx_pc.cpp +++ b/port/fast3d/gfx_pc.cpp @@ -81,12 +81,19 @@ struct NormalColor { }; }; +/* D540: set by the GL backend at init. gfx_rdp_affine: RDP-style + * screen-affine shade colour. gfx_fog_vertex: 1 = per-vertex RSP fog + * (default, D543), 0 = the D540 exact per-pixel fog (GE_FOGPIXEL=1). */ +int gfx_rdp_affine = 0; +int gfx_fog_vertex = 0; + struct LoadedVertex { float x, y, z, w; float u, v; struct RGBA color; uint8_t fog; uint8_t clip_rej; + float fog_n; /* D540: fog sent to the GPU (0..255): the RSP value (default), or fog * w (GE_FOGPIXEL) */ }; static struct { @@ -2261,19 +2268,27 @@ static void gfx_sp_vertex(size_t n_vertices, size_t dest_index, const Vtx* verti } float winv = 1.0f / w; - if (winv < 0.0f) { + /* D540: the RSP divides by a negative w as-is: a vertex behind + * the camera gets z/w at/beyond the far end of the range (full + * fog), continuously. The old override (winv < 0 -> +32767) + * flipped the sign, so a corner crossing behind the player + * jumped no-fog <-> full-fog and big ground triangles popped + * one at a time (Deck playtest). */ + if (!gfx_fog_vertex && winv < 0.0f) { winv = std::numeric_limits::max(); } float fog_z = z * winv * rsp.fog_mul + rsp.fog_offset; d->fog = clampf(fog_z, 0.f, 255.f); - /* D503: the N64's 16-bit framebuffer hid objects ~97% fogged; - * 32-bit output reveals them as silhouettes. Snap to full fog. */ - if (d->fog >= 247.f) { - d->fog = 255.f; - } + /* D543 default: the per-vertex RSP value (as the N64; D543's CPU + * near clip lerps it for clip-created vertices like the RSP). + * GE_FOGPIXEL=1 (D540): the GPU gets fog * w (z*mul + w*off, + * linear in clip space) and the shader divides by w -- exact + * per-pixel fog. The D503 full-fog snap is opt-in (GE_FOGSNAP=1). */ + d->fog_n = gfx_fog_vertex ? d->fog : d->z * rsp.fog_mul + d->w * rsp.fog_offset; } else { d->fog = rdp.fog_color.a; + d->fog_n = gfx_fog_vertex ? d->fog : d->fog * d->w; } d->color.a = vcn->a; // can be required for SHADE_ALPHA even if fog is enabled @@ -2858,14 +2873,106 @@ static void gfx_sp_tri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx, bo d526_note_emit(videoGetFrameCount(), v_arr, use_alpha ? 1 : 0, use_modulate ? 1 : 0); // D526 census d75d_note_emit_z(videoGetFrameCount(), v_arr, (used_textures[0] || used_textures[1]) ? 1 : 0, comb->used_textures[0] ? 1 : 0); } - for (int i = 0; i < 3; i++) { - float z = v_arr[i]->z, w = v_arr[i]->w; + /* D543: RSP-style near-plane clipping on the CPU. The GPU clips the + * triangle itself against the near plane, but it interpolates the + * noperspective shade colour and the fog value in screen space (D548 + * bands, D540 light/dark cycling, D543 bright lit area at Bond's feet), + * so a big ground triangle that crosses the camera plane extrapolates + * them. The RSP instead clips in clip space and lerps every vertex + * attribute linearly there (t = dA / (dA - dB)); do the same, so GL + * receives only fully-in-front triangles and never clips. Skipped for + * rects, G_NO_CLIPPING_EXT, and GE_NEARCLIP=0 (A/B). Fog on a vertex + * the clip creates is re-derived from its own z/w (as gfx_sp_vertex + * does), like the RSP, whose clipper runs new vertices through the same + * vertex-finish code: at the near plane that is a constant, so the haze + * at the player's feet stays static (D553; maintainer compared against + * LLE mupen64plus, cxd4 + angrylion). GE_NEARCLIPFOG=lerp restores the + * old interpolation from the parents (whose behind-the-eye corner flips + * between 0 and 255 fog as w crosses 0 -> the dark/bright popping) for + * A/B. The new vertices live on the stack: rsp.loaded_vertices is + * game-visible. */ + static int nearclip_on = -1, nearclip_fog_recompute = -1; + if (nearclip_on < 0) { + const char* e = getenv("GE_NEARCLIP"); + nearclip_on = !(e && e[0] == '0'); + const char* f = getenv("GE_NEARCLIPFOG"); + nearclip_fog_recompute = (f && strcmp(f, "lerp") == 0) ? 0 : 1; /* D553: recompute by default */ + } + struct LoadedVertex* emit_v[6] = { v1, v2, v3, NULL, NULL, NULL }; + int n_out_tris = 1; + struct LoadedVertex nc_vtx[2]; + if (nearclip_on && !is_rect && (rsp.extra_geometry_mode & G_NO_CLIPPING_EXT) == 0) { + const float NEAR_EPS = 1e-4f; + float d[3]; + int n_in = 0; + for (int i = 0; i < 3; i++) { + d[i] = v_arr[i]->z + v_arr[i]->w - NEAR_EPS; + if (d[i] > 0.0f) n_in++; + } + if (n_in == 0) { + return; // entirely behind the near plane + } + if (n_in < 3) { + struct LoadedVertex* poly[4]; + int n_poly = 0, n_new = 0; + for (int i = 0; i < 3; i++) { + int j = (i + 1) % 3; + struct LoadedVertex* a = v_arr[i]; + struct LoadedVertex* b = v_arr[j]; + if (d[i] > 0.0f) poly[n_poly++] = a; + if ((d[i] > 0.0f) != (d[j] > 0.0f)) { + float t = d[i] / (d[i] - d[j]); + struct LoadedVertex* o = &nc_vtx[n_new++]; + o->x = a->x + (b->x - a->x) * t; + o->y = a->y + (b->y - a->y) * t; + o->z = a->z + (b->z - a->z) * t; + o->w = a->w + (b->w - a->w) * t; + o->u = a->u + (b->u - a->u) * t; + o->v = a->v + (b->v - a->v) * t; + o->color.r = (uint8_t)clampf(floorf(a->color.r + (b->color.r - a->color.r) * t + 0.5f), 0.f, 255.f); + o->color.g = (uint8_t)clampf(floorf(a->color.g + (b->color.g - a->color.g) * t + 0.5f), 0.f, 255.f); + o->color.b = (uint8_t)clampf(floorf(a->color.b + (b->color.b - a->color.b) * t + 0.5f), 0.f, 255.f); + o->color.a = (uint8_t)clampf(floorf(a->color.a + (b->color.a - a->color.a) * t + 0.5f), 0.f, 255.f); + o->clip_rej = 0; + o->fog_n = a->fog_n + (b->fog_n - a->fog_n) * t; + if (nearclip_fog_recompute) { + if (rsp.geometry_mode & G_FOG) { + float fw = o->w; + if (fabsf(fw) < 0.001f) fw = 0.001f; + float winv = 1.0f / fw; + if (!gfx_fog_vertex && winv < 0.0f) winv = std::numeric_limits::max(); + o->fog = clampf(o->z * winv * rsp.fog_mul + rsp.fog_offset, 0.f, 255.f); + } else { + o->fog = a->fog; + } + } else { + o->fog = (uint8_t)clampf(floorf(a->fog + ((float)b->fog - (float)a->fog) * t + 0.5f), 0.f, 255.f); + } + if (gfx_fog_vertex) o->fog_n = o->fog; // as gfx_sp_vertex + poly[n_poly++] = o; + } + } + // n_poly is 3 or 4 here (one or two vertices inside) + emit_v[0] = poly[0]; emit_v[1] = poly[1]; emit_v[2] = poly[2]; + if (n_poly == 4) { + emit_v[3] = poly[0]; emit_v[4] = poly[2]; emit_v[5] = poly[3]; + n_out_tris = 2; + } + } + } + const int n_emit_verts = n_out_tris * 3; + if (buf_vbo_num_tris + n_out_tris > MAX_BUFFERED) { + gfx_flush(); // buf_vbo holds MAX_BUFFERED triangles; make room for 1-2 + } + for (int i = 0; i < n_emit_verts; i++) { + struct LoadedVertex* vi = emit_v[i]; + float z = vi->z, w = vi->w; if (clip_parameters.z_is_from_0_to_1) { z = (z + w) / 2.0f; } - buf_vbo[buf_vbo_len++] = v_arr[i]->x; - buf_vbo[buf_vbo_len++] = clip_parameters.invert_y ? -v_arr[i]->y : v_arr[i]->y; + buf_vbo[buf_vbo_len++] = vi->x; + buf_vbo[buf_vbo_len++] = clip_parameters.invert_y ? -vi->y : vi->y; buf_vbo[buf_vbo_len++] = z; buf_vbo[buf_vbo_len++] = w; @@ -2877,8 +2984,8 @@ static void gfx_sp_tri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx, bo // TODO: fix this; for now just ignore smaller mips const uint32_t tile = gfx_lod_tile_offset(t); - float u = v_arr[i]->u / 32.0f; - float v = v_arr[i]->v / 32.0f; + float u = vi->u / 32.0f; + float v = vi->v / 32.0f; int shifts = rdp.texture_tile[rdp.first_tile_index + tile].shifts; int shiftt = rdp.texture_tile[rdp.first_tile_index + tile].shiftt; @@ -2955,7 +3062,7 @@ static void gfx_sp_tri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx, bo buf_vbo[buf_vbo_len++] = rdp.fog_color.r / 255.0f; buf_vbo[buf_vbo_len++] = rdp.fog_color.g / 255.0f; buf_vbo[buf_vbo_len++] = rdp.fog_color.b / 255.0f; - buf_vbo[buf_vbo_len++] = v_arr[i]->fog / 255.0f; // fog factor + buf_vbo[buf_vbo_len++] = vi->fog_n / 255.0f; // D540: see LoadedVertex.fog_n } if (use_grayscale) { @@ -2976,10 +3083,10 @@ static void gfx_sp_tri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx, bo color = &rdp.prim_color; break; case G_CCMUX_SHADE: - color = &v_arr[i]->color; + color = &vi->color; break; case G_CCMUX_SHADE_ALPHA: - tmp.r = tmp.g = tmp.b = v_arr[i]->color.a; + tmp.r = tmp.g = tmp.b = vi->color.a; color = &tmp; break; case G_CCMUX_ENVIRONMENT: @@ -3032,7 +3139,8 @@ static void gfx_sp_tri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx, bo } } - if (++buf_vbo_num_tris == MAX_BUFFERED) { + buf_vbo_num_tris += n_out_tris; + if (buf_vbo_num_tris >= MAX_BUFFERED) { gfx_flush(); } } @@ -3943,6 +4051,7 @@ static void gfx_sp_vertex_float(size_t n_vertices, size_t dest_index, const Port d->z = z; d->w = w; d->fog = rdp.fog_color.a; + d->fog_n = gfx_fog_vertex ? d->fog : d->fog * w; } } diff --git a/port/fast3d/gfx_sdl2.cpp b/port/fast3d/gfx_sdl2.cpp index 347d9023..ece01aad 100644 --- a/port/fast3d/gfx_sdl2.cpp +++ b/port/fast3d/gfx_sdl2.cpp @@ -8,6 +8,7 @@ #include "gfx_window_manager_api.h" #include "gfx_screen_config.h" #include "input.h" +#include "ge007_icon.h" extern "C" void videoRequestQuit(const char *why); // port/src/video.c (D344) extern "C" void inputRequestRescan(void); // port/src/input.c (D450) @@ -228,6 +229,32 @@ static void gfx_sdl_init(const struct GfxWindowInitSettings *set) { SDL_GL_MakeCurrent(wnd, ctx); SDL_GL_SetSwapInterval(1); + // D550: window / taskbar icon = the project mark (generated by + // tools_pc/make_icon.py into ge007_icon.h; R,G,B,A byte order per + // pixel, portable -- packed here against the surface's masks). + { + const Uint32 rmask = 0x000000ff, gmask = 0x0000ff00, + bmask = 0x00ff0000, amask = 0xff000000; + SDL_Surface *icon = SDL_CreateRGBSurface( + 0, GE007_ICON_SIZE, GE007_ICON_SIZE, 32, rmask, gmask, bmask, amask); + if (icon) { + for (int y = 0; y < GE007_ICON_SIZE; ++y) { + for (int x = 0; x < GE007_ICON_SIZE; ++x) { + const uint8_t *p = ge007_icon_rgba[y][x]; + // Shift each channel into its mask's byte lane before + // the & (an unshifted p[1] & 0x0000ff00 is always 0): + const Uint32 v = ((Uint32)p[0] & rmask) | ((Uint32)(p[1] << 8) & gmask) | + ((Uint32)(p[2] << 16) & bmask) | ((Uint32)(p[3] << 24) & amask); + *(Uint32 *)((Uint8 *)icon->pixels + y * icon->pitch + x * 4) = v; + } + } + SDL_SetWindowIcon(wnd, icon); + SDL_FreeSurface(icon); + } else { + sysLogPrintf(LOG_NOTE, "SDL: could not create the window icon surface: %s", SDL_GetError()); + } + } + SDL_ShowWindow(wnd); qpc_freq = SDL_GetPerformanceFrequency(); diff --git a/port/include/input.h b/port/include/input.h index f6f432c5..6cdeb58a 100644 --- a/port/include/input.h +++ b/port/include/input.h @@ -59,6 +59,8 @@ int inputReleaseCapture(void); /* F10 options overlay: force the OS cursor free + visible while the overlay * owns the mouse. Safe to call every poll. */ void inputSuspendForOverlay(void); +int inputFrontEndCursorUiFrac(double *fx, double *fy, double *xscale); /* D555 */ +int inputCrosshairCursorOn(void); /* D555: Input.CrosshairCursor (overlay draws the game crosshair as the pointer) */ /* Raw gamepad state for controller idx (the F10 options overlay's gamepad * navigation). Unaffected by the overlay's pad-swallow, which happens at our diff --git a/port/include/updatecheck.h b/port/include/updatecheck.h new file mode 100644 index 00000000..86a937fc --- /dev/null +++ b/port/include/updatecheck.h @@ -0,0 +1,15 @@ +#ifndef PORT_UPDATECHECK_H +#define PORT_UPDATECHECK_H + +/* + * Opt-in "check for updates" (Game.CheckUpdates, default OFF). + * + * updateCheckStart() does nothing at all unless the ini value is 1 -- no + * thread, no DNS, no library init. When enabled it spawns one detached + * background thread that makes a single HTTPS GET to GitHub's releases API. + */ +void updateCheckStart(void); /* call once, after configLoad() */ +const char *updateCheckAvailable(void); /* newer tag (e.g. "vX.Y.Z") or NULL */ +void updateCheckOpenReleases(void); /* SDL_OpenURL on the releases page */ + +#endif diff --git a/port/include/versioninfo.h.in b/port/include/versioninfo.h.in index a643387b..9ec7797d 100644 --- a/port/include/versioninfo.h.in +++ b/port/include/versioninfo.h.in @@ -4,6 +4,7 @@ /* Generated by CMake from versioninfo.h.in — do not edit. */ #define GE007_VERSION_HASH "@VERSION_HASH@" +#define GE007_VERSION "@GE007_VERSION@" #define GE007_VERSION_CODENAME "@VERSION_CODENAME@" #define GE007_ORIGIN_URL "@ORIGIN_URL@" #define GE007_TARGET_PLATFORM "@TARGET_PLATFORM@" diff --git a/port/src/input.c b/port/src/input.c index d115b382..0e02627a 100644 --- a/port/src/input.c +++ b/port/src/input.c @@ -122,7 +122,7 @@ extern void gunRequestHandWeaponChange(enum GUNHAND hand, s32 nextWeapon, s32 cy #define GE_CONT_F 0x0001 /* C-right */ #define MAX_PADS 4 -#define STICK_DEADZONE 7000 +#define STICK_DEADZONE 7500 /* 25 %; D546 (was 7000 = 23 %) */ #define STICK_MAX 80 /* Front-end pointer mode: GE's menu cursor ("crosshair") is stick-driven @@ -791,6 +791,14 @@ static void mouseRequestCursor(int show) SDL_AtomicUnlock(&s_mouseReqLock); } +/* D555 (reworked 2026-10-06): Input.CrosshairCursor -- while the F10 overlay / + * PC Options is open the OS cursor is HIDDEN and optionsoverlay.c draws the + * game's own crosshair sprite at the mouse position, last in its display list + * (so it is above the card). Off: the OS arrow over the overlay, as before. */ +static int cfgCrosshairCursor = 1; +int inputCrosshairCursorOn(void) { return cfgCrosshairCursor; } +static int s_cursorShown = -1; /* host thread only: last SDL_ShowCursor state applied */ + /* Host thread only (videoPumpEvents, inputInit). */ void inputApplyMouseRequests(void) { @@ -806,9 +814,15 @@ void inputApplyMouseRequests(void) SDL_GetRelativeMouseState(NULL, NULL); /* drain the accumulated jump */ } } - if (cur >= 0 && cur != s_appCursor) { - s_appCursor = cur; - SDL_ShowCursor(cur ? SDL_ENABLE : SDL_DISABLE); + if (cur >= 0) s_appCursor = cur; + if (s_appCursor >= 0) { + /* D555: with the crosshair pointer on, the overlay draws its own + * pointer, so the OS cursor stays hidden while it is open. */ + const int show = s_appCursor == 1 && !(cfgCrosshairCursor && optionsOverlayIsOpen()); + if (show != s_cursorShown) { + s_cursorShown = show; + SDL_ShowCursor(show ? SDL_ENABLE : SDL_DISABLE); + } } } @@ -1029,6 +1043,7 @@ static int padSouthpaw = 0; /* 1 = swap fire (G) / grenade (R) static int padLookSmooth = 0; /* right-stick look low-pass strength, 0-10 (0 = off, 10 = max) */ static int padTriggerPct = 25; /* trigger press point, % of travel (~30*256) */ static int padLookInvertY = 0; /* 1 = invert right-stick (look) Y */ +static int inputDefaultsRev = 0; /* D546: 1 once the deadzone default migration ran */ /* Per-pad look-smoothing EMA state (Wave A item 5), one X/Y pair per pad. Reset * on hot-unplug (inputRescanPads) so a smoothed value can't stick after the * stick snaps back; the low-pass otherwise recentres it toward 0 on its own. */ @@ -1516,7 +1531,7 @@ static void inputMigrateBinds(void); /* D380; after configLoad */ static void inputBindingProbe(void); /* D383/D384; opt-in, restores ini state */ static int bindsVersion = 0; /* D380/D386: versioned migration of effective ini binds */ -static int crouchMode = 0; /* 0 = hold, 1 = toggle (latched crouch input) */ +static int crouchMode = 1; /* 0 = hold, 1 = toggle (latched crouch input); D556: toggle by default (existing ini values kept) */ static void padMapSelfTest(void); int inputInit(void) @@ -1553,6 +1568,14 @@ int inputInit(void) * Runs before any configSave, so the saved L/R values are always valid. */ if (padDeadzoneL == PAD_DZ_UNSET) padDeadzoneL = padDeadzone; if (padDeadzoneR == PAD_DZ_UNSET) padDeadzoneR = padDeadzone; + /* D546: one-time move of the untouched old deadzone default (7000 = 23 %) + * to the standardized 25 % (7500); a value the player set is kept. */ + if (inputDefaultsRev < 1) { + if (padDeadzone == 7000) padDeadzone = STICK_DEADZONE; + if (padDeadzoneL == 7000) padDeadzoneL = STICK_DEADZONE; + if (padDeadzoneR == 7000) padDeadzoneR = STICK_DEADZONE; + inputDefaultsRev = 1; + } if (getenv("GE_BINDPROBE")) inputBindingProbe(); sysLogPrintf(LOG_INFO, "input: PC bindings, crouch mode %s", @@ -2172,8 +2195,9 @@ static unsigned inputComputePadSlot(int idx, signed char *stick_x, signed char * } optionsOverlayHandleInput(); if (mouseEnabled && current_menu != GE_MENU_RUN_STAGE && current_menu != GE_MENU_INVALID) { - overlayFrontEndCrosshair(); /* D519: crosshair stays the pointer over the overlay */ - mouseRequestCursor(0); /* ...so hide the OS cursor (one pointer, not two) */ + overlayFrontEndCrosshair(); /* D519: the game cursor tracks the mouse (D555: the overlay pointer is drawn exactly over it) */ + /* D544: the OS cursor stays visible (inputSuspendForOverlay) -- the crosshair + * draws under the overlay box, so it alone could not point at F10 rows. */ } if (stick_x) *stick_x = 0; if (stick_y) *stick_y = 0; @@ -2897,6 +2921,23 @@ static unsigned inputComputePadSlot(int idx, signed char *stick_x, signed char * } } + /* D541: front-end release hold-off. One physical press can reach the menu + * through two OR'd sources a few ms apart -- on a Steam Deck in Desktop + * Mode, Steam's desktop layout types Enter for A (Escape for B) while SDL + * also reads the raw pad -- and a gap between them is a second edge, so + * one A press advanced two screens. A menu button only counts as released + * after ~70 ms up; press edges are untouched. Menus only. */ + if (idx >= 0 && idx < MAX_PADS && + current_menu != GE_MENU_RUN_STAGE && current_menu != GE_MENU_INVALID) { + static Uint32 s_menuBtnDownAt[MAX_PADS][3]; + static const u16 kHoldBits[3] = { GE_CONT_A, GE_CONT_B, GE_CONT_START }; + Uint32 now = SDL_GetTicks(); + for (int k = 0; k < 3; k++) { + if (button & kHoldBits[k]) s_menuBtnDownAt[idx][k] = now; + else if (s_menuBtnDownAt[idx][k] && now - s_menuBtnDownAt[idx][k] < 70) button |= kHoldBits[k]; + } + } + if (idx == 0 && scriptIsActive(0)) { /* D263: apply BEFORE the stick is written out -- scripted stick * tokens (SUP/SDOWN/SLEFT/SRIGHT) used to be discarded. */ @@ -3134,6 +3175,25 @@ static void overlayFrontEndCrosshair(void) cursor_v_pos = (float)(v < loV ? loV : v > hiV ? hiV : v); } +/* D555 rework: INVERSE of overlayFrontEndCrosshair's mapping. Where the game's + * front-end cursor (cursor_h_pos/cursor_v_pos, which frontDrawCursor draws at) + * lands, as fractions of the UI screen rect (the rect the overlay's full-canvas + * DL maps to), plus the x scale of the front-end canvas inside it (D335 + * native-widescreen pillarbox: vis, else 1). 0 = not available. */ +int inputFrontEndCursorUiFrac(double *fx, double *fy, double *xscale) +{ + double sw = getPlayer_c_screenwidth(), sh = getPlayer_c_screenheight(); + double sl = getPlayer_c_screenleft(), st = getPlayer_c_screentop(); + if (!(sw > 200.0 && sw < 2000.0 && sh > 150.0 && sh < 2000.0)) return 0; + double x = ((double)cursor_h_pos - sl) / sw, y = ((double)cursor_v_pos - st) / sh; + double vis = 1.0; + extern f32 portNativeAspect(void); + f32 na = portNativeAspect(); + if (na > (4.0f / 3.0f)) { vis = (4.0 / 3.0) / (double)na; x = x * vis + (1.0 - vis) * 0.5; } + *fx = x; *fy = y; *xscale = vis; + return 1; +} + void inputSuspendForOverlay(void) { if (mouseGrabbed) { @@ -3141,6 +3201,9 @@ void inputSuspendForOverlay(void) mouseRequestRelative(0); mouseDX = mouseDY = 0.0; } + /* D544: the OS cursor is always shown while the overlay is open, so the + * pointer draws OVER the menu (maintainer). The front-end path no longer + * hides it, which also removes the show/hide race that flickered. */ mouseRequestCursor(1); } @@ -3834,6 +3897,7 @@ PD_CONSTRUCTOR static void inputConfigInit(void) } configRegisterInt("Input.MPMode", &mpInputMode, 0, 2); /* D416: 0 Auto, 1 PadsOnly, 2 KbmP1 */ configRegisterInt("Input.MouseEnabled", &mouseEnabled, 0, 1); + configRegisterInt("Input.CrosshairCursor", &cfgCrosshairCursor, 0, 1); /* D555 */ configRegisterInt("Input.MouseAimSpeed", &mouseAimSpeed, 1, 500); configRegisterInt("Input.PdMouseAim", &pdMouseAim, 0, 1); /* legacy -> AimStyle 1 */ configRegisterInt("Input.AimStyle", &aimStyleLegacy, 0, 2); /* D333 legacy -> AimMode */ @@ -3876,6 +3940,7 @@ PD_CONSTRUCTOR static void inputConfigInit(void) configRegisterInt("Input.PadDeadzone", &padDeadzone, 0, 30000); /* legacy; Wave A migrates to L/R */ configRegisterInt("Input.PadDeadzoneL", &padDeadzoneL, 0, 30000); /* Wave A: left (movement) stick */ configRegisterInt("Input.PadDeadzoneR", &padDeadzoneR, 0, 30000); /* Wave A: right (look) stick */ + configRegisterInt("Input.DefaultsRev", &inputDefaultsRev, 0, 1); /* D546 */ configRegisterInt("Input.PadLookSensX", &padLookSensX, 25, 200); /* Wave A: look horizontal sensitivity (%) */ configRegisterInt("Input.PadLookSensY", &padLookSensY, 25, 200); /* Wave A: look vertical (pitch) sensitivity (%) */ configRegisterInt("Input.PadSouthpaw", &padSouthpaw, 0, 1); /* Wave A: swap fire/grenade triggers */ diff --git a/port/src/main.c b/port/src/main.c index 5e12e863..46d3636d 100644 --- a/port/src/main.c +++ b/port/src/main.c @@ -24,6 +24,7 @@ #include "platform.h" #include "system.h" #include "config.h" +#include "updatecheck.h" #include "fs.h" #include "romdata.h" #include "dram.h" @@ -95,6 +96,33 @@ static void portAtExit(void) } } +/* D257/D442: Game.AllUnlocked applies live. Called at boot and on every F10 + * row change. On seeds the game's own RAM unlock flags (mission select offers + * every solo level at every difficulty plus 007 mode); Off clears only the + * flags this setting seeded, so GE_UNLOCK_ALL / debug-menu unlocks survive. + * Port-layer memory writes only (AGENTS rule 2); the cheat menu and COMPLETED + * checks come from D442's query-time hooks in file2.c, which read + * portAllUnlocked live. The debug flags exist only with LEFTOVERDEBUG (not + * PAL); the file2.c hooks cover All unlocked everywhere. */ +void portAllUnlockedApply(void) +{ +#if defined(LEFTOVERDEBUG) + extern s32 portAllUnlocked; /* port/src/video.c */ + extern s32 debug_enable_all_levels_flag; /* src/game/debugmenu_handler.c */ + extern s32 debug_007_unlock_flag; /* ditto */ + static int s_seededLevels, s_seeded007; + if (portAllUnlocked) { + if (!debug_enable_all_levels_flag) { debug_enable_all_levels_flag = 1; s_seededLevels = 1; } + if (!debug_007_unlock_flag) { debug_007_unlock_flag = 1; s_seeded007 = 1; } + } else { + if (s_seededLevels) { debug_enable_all_levels_flag = 0; s_seededLevels = 0; } + if (s_seeded007) { debug_007_unlock_flag = 0; s_seeded007 = 0; } + } + sysLogPrintf(LOG_INFO, "all-unlocked: Game.AllUnlocked=%d (levels flag %d, 007 flag %d)", + (int)portAllUnlocked, (int)debug_enable_all_levels_flag, (int)debug_007_unlock_flag); +#endif +} + int main(int argc, char **argv) { sysSetArgs(argc, argv); @@ -134,6 +162,7 @@ int main(int argc, char **argv) * skips it for good. */ configLoad(); videoApplySteamDeckPreset(); + updateCheckStart(); /* opt-in (Game.CheckUpdates, default off); no-op otherwise */ atexit(portAtExit); /* persist config + window geometry on clean exit */ /* 1a. D257: Game.AllUnlocked (default OFF; F10 'All unlocked' enables) @@ -151,19 +180,7 @@ int main(int argc, char **argv) * COMPLETED check come from the query-time hooks in file2.c * (fileGetIsCheatUnlocked, fileIsStageUnlockedAtDifficulty); the * save is never patched. */ -#if defined(LEFTOVERDEBUG) /* the debug unlock flags only exist with LEFTOVERDEBUG (not PAL); D442's query-time hooks cover All unlocked everywhere */ - { - extern s32 portAllUnlocked; /* port/src/video.c */ - extern s32 debug_enable_all_levels_flag; /* src/game/debugmenu_handler.c */ - extern s32 debug_007_unlock_flag; /* ditto */ - if (portAllUnlocked) { - debug_enable_all_levels_flag = 1; - debug_007_unlock_flag = 1; - sysLogPrintf(LOG_INFO, "all-unlocked: RAM unlock flags seeded " - "(Game.AllUnlocked=1)"); - } - } -#endif + portAllUnlockedApply(); /* 2. Load the ROM and map segments. */ if (romdataInit() != 0) { diff --git a/port/src/optionsoverlay.c b/port/src/optionsoverlay.c index 0a832b90..53cb1d6f 100644 --- a/port/src/optionsoverlay.c +++ b/port/src/optionsoverlay.c @@ -53,11 +53,13 @@ extern MENU current_menu; #include "video.h" #include "input.h" #include "front.h" /* selected_folder_num (D356 stage probe) */ +#include "bondtypes.h" /* sImageTableEntry (D555 crosshair pointer: typed field reads) */ #include "file.h" /* save_data (D356 reset probe: second-file isolation) */ #include "optionsoverlay.h" #include "hudaspect.h" /* D335b/D472: PORT_HUD_ASPECT */ #include "watchsettings.h" #include "audio.h" +#include "updatecheck.h" #include "../fast3d/gfx_api.h" /* file2.c; same extern as watchsettings.c (not in a header). */ @@ -83,6 +85,14 @@ extern void textMeasure(s32 *textheight, s32 *textwidth, char *text, struct fontchar *chars, struct font *font, s32 lineheight); extern s16 viGetX(void); extern s16 viGetY(void); +extern void viSetXY(s16 x, s16 y); +/* D555 rework: the game's own crosshair sprite + its draw helpers (front.c + * frontDrawCursor / gunfire.c gunDrawSight use exactly these). */ +extern struct sImageTableEntry *crosshairimage; +extern void texSelect(Gfx **gdlptr, struct sImageTableEntry *tconfig, u32 arg2, s32 arg3, u32 ulst); +extern void display_image_at_position(Gfx **DL, f32 *xypos, f32 *halfedxy, s32 width, s32 height, + s32 rotateleft90, s32 fliph, s32 flipv, s32 red, s32 green, + s32 blue, s32 alpha, s32 format, s32 param_14); /* ------------------------------------------------------------------------ */ @@ -95,33 +105,24 @@ enum { ROW_TOGGLE, ROW_SLIDER, ROW_ENUM, ROW_MSAA, ROW_RES, ROW_ACTION, ROW_FPSC /* D346: wording pass -- Nightdive/Turok + PD-port conventions: title-case * On/Off, no all-caps value strings. Display-only; config stores 0/1 either way. */ static const char *const kOnOff[] = { "Off", "On", NULL }; -static const char *const kReverse[] = { "Reverse", "Upright", NULL }; static const char *const kHold[] = { "Hold", "Toggle", NULL }; static const char *const kAspectMode[] = { "Fill window", "Original", "16:9", "21:9", NULL }; /* D447: Original = 4:3 (16:9 with the game Ratio) bars; D508: 2..3 force a ratio */ -static const char *const kTexFilter[] = { "Nearest", "Bilinear", "3-Point", "Trilinear", NULL }; /* Trilinear option (playtest 2026-10-03) */ +static const char *const kTexFilter[] = { "Nearest", "Bilinear", "3-point", "Trilinear", NULL }; /* Trilinear option (playtest 2026-10-03) */ static const char *const kPadPreset[] = { "1.1 Jinx", "1.2 Christmas", "1.3 Frost", "Custom", "1.4 Elektra", NULL }; /* D498: index = stored value; inputPadPresetStep gives the display order */ /* D516: the game's own control styles, shown in Original (index = style value). */ static const char *const kOrigStyle[] = { "1.1 Honey", "1.2 Solitaire", "1.3 Kissy", "1.4 Goodnight", "2.1 Plenty", "2.2 Galore", "2.3 Domino", "2.4 Goodhead", NULL }; static int s_padSeat = 0; /* D469: seat edited by the Controller page rows */ -static const char *const kControlScheme[] = { "Ext", "Original", NULL }; /* D513: PD names the port-driven style "Ext" */ -static const char *const kAimMode[] = { "N64", "Centered (PC)", NULL }; /* D337 */ -static const char *const kMenuPointer[] = { "Velocity (legacy)", "Direct", NULL }; /* Input.MenuPointerMode 0 / 1 */ +static const char *const kControlScheme[] = { "Extended", "Original", NULL }; /* D513/D554: display text only (index = stored value) */ +static const char *const kAimMode[] = { "Original", "Centered", NULL }; /* D337 */ static int displayModeNow(void); /* defined with the preset helpers below */ static const char *const kDisplayModeName[3]; -static const char *const kAimRange[] = { "PC", "N64", NULL }; /* D338 */ +static const char *const kAimRange[] = { "Extended", "Original", NULL }; /* D338 */ static const int kMsaaSeq[] = { 1, 2, 4, 8, 16 }; /* D443: 16x added */ #define MSAA_N ((int)(sizeof(kMsaaSeq) / sizeof(kMsaaSeq[0]))) /* v0.4.0 modern options wave: value names for the new rows (they land * in the functional sections -- Turok standard, D356 -- not a bucket). */ static const char *const kOnOffRev[] = { "On", "Off", NULL }; /* 0 = On */ -/* D442: AllUnlocked is a pure query-time/RAM override (file2.c - * fileGetIsCheatUnlocked + fileIsStageUnlockedAtDifficulty, plus the debug - * flags seeded in main.c at startup): no save bytes are patched, read or - * written, so the old D259 silent-volume quirk is gone. It applies from the - * next launch. Saves polluted by builds before D442 are not repaired. - * Display-only value names; config still stores 0/1. */ -static const char *const kAllUnlocked[] = { "Off", "On (restart)", NULL }; /* D379: the authored N64 sprite is red. Original is the identity path; * White at index 7 uses the same alpha-mask combiner as other true hues. */ static const char *const kCrosshairColor[] = { @@ -130,6 +131,7 @@ static const char *const kCrosshairColor[] = { }; static const char *const kFullscreenMode[] = { "Borderless", "Exclusive", NULL }; /* D511 */ static const char *const kCrosshairStyle[] = { "Original", "Thin cross", NULL }; +static const char *const kGameplayView[] = { "Original", "Extended", NULL }; /* D468: 0 = N64 area, 1 = everything drawn (PD port) */ /* D186: the sim's own tick pacemaker is hardcoded to the console's native VI * rate (60Hz NTSC / 50Hz PAL, port/src/libultra.c) -- Video.FpsCap can only * throttle down from there, never past it, and throttling it below 30 @@ -247,6 +249,7 @@ static struct Row rows[] = { * While on, "Widescreen auto FOV" has no effect (it was the stretch-era * vertical-FOV compensation). */ { .key="Video.NativeWidescreen", .label="Native widescreen", .kind=ROW_TOGGLE, .step=1, .names=kOnOff }, + { .key="Video.WidescreenAuto", .label="Widescreen auto FOV", .kind=ROW_TOGGLE, .step=1, .names=kOnOff }, { .key="Video.MSAA", .label="Anti-aliasing", .kind=ROW_MSAA, .restart=1 }, SEP(V2), { .key="Video.VSync", .label="VSync", .kind=ROW_TOGGLE, .step=1, .names=kOnOff }, @@ -256,11 +259,13 @@ static struct Row rows[] = { { .key="Video.TextureFilter", .label="Texture filter", .kind=ROW_ENUM, .step=1, .names=kTexFilter }, { .key="Video.Anisotropy", .label="Anisotropic filtering", .kind=ROW_SLIDER, .step=1, .unit="x" }, SEP(V4), - { .key="Video.WidescreenAuto", .label="Widescreen auto FOV", .kind=ROW_TOGGLE, .step=1, .names=kOnOff }, { .key="Video.SafeAreaCrop", .label="Crop overscan", .kind=ROW_TOGGLE, .step=1, .names=kOnOff }, /* 1.0x..8.0x the authored distance (ini 100..800 %; 2.5x default). * Legacy AutoFov ini keys remain supported but no longer hide sliders. */ { .key="Video.DrawDistance", .label="Draw distance", .kind=ROW_SLIDER, .step=25, .uiMin=100, .uiMax=800 }, + /* D540: fog start/end as a multiple of the level's own (N64) fog; + * capped at the draw distance. 0.5x..8.0x, 1.0x default. */ + { .key="Video.FogDistance", .label="Fog distance", .kind=ROW_SLIDER, .step=25, .uiMin=50, .uiMax=800 }, { .key="Video.LodDistance", .label="LOD distance", .kind=ROW_SLIDER, .step=25, .uiMin=100, .uiMax=800 }, SEP(V5), /* D181 re-exposed (PD "Explosion shake"): named for what it scales (explosions only). */ @@ -285,12 +290,14 @@ static struct Row rows[] = { /* PD parity (maintainer list 2026-10-04): the long-standing ini key finally gets a row; default on. */ { .key="Input.MouseEnabled", .label="Mouse enabled", .kind=ROW_TOGGLE, .step=1, .names=kOnOff }, { .key="Input.MouseSensitivity", .label="Horizontal sensitivity", .kind=ROW_SLIDER, .step=10, - .uiMin=10, .uiMax=300 }, /* D443/D357: shown as a multiplier, raw 100 = 1.0x; storage unchanged */ + .uiMin=10, .uiMax=400 }, /* D443/D357: shown as a multiplier, raw 100 = 1.0x; storage unchanged */ /* Wave A (v0.5.0, CONTROLLER-INPUT-PLAN item 6): Input.MouseYScale (extra * vertical/pitch sensitivity, %); input.c already applies it. */ { .key="Input.MouseYScale", .label="Vertical sensitivity", .kind=ROW_SLIDER, .step=10, - .uiMin=10, .uiMax=300 }, /* D506: shown as a multiplier like the horizontal row, raw 100 = 1.0x */ + .uiMin=10, .uiMax=400 }, /* D506: shown as a multiplier like the horizontal row, raw 100 = 1.0x */ { .key="Input.MouseInvertY", .label="Invert look", .kind=ROW_TOGGLE, .step=1, .names=kOnOff }, + { .key="Input.MouseSmoothing", .label="Smoothing", .kind=ROW_SLIDER, .step=5, .unit="%" }, + { .key="Input.MouseRawInput", .label="Raw input", .kind=ROW_TOGGLE, .step=1, .names=kOnOff }, /* D337: N64 = the N64 aim model (crosshair travels, camera edge-scrolls) * with the mouse fed through the game's integrator at PD's mouse damp -- * the default. CENTRED (PC) = opt-in FPS-style aim (#104), not N64. @@ -301,9 +308,7 @@ static struct Row rows[] = { { .key="Input.AimRange", .label="Aim range", .kind=ROW_ENUM, .step=1, .names=kAimRange, .hiddenIfOn="Input.AimMode" }, /* Existing ini-only keys exposed by the PD-parity regroup (rows only; input.c * already reads them). */ - { .key="Input.MenuPointerMode", .label="Menu pointer", .kind=ROW_ENUM, .step=1, .names=kMenuPointer }, - { .key="Input.MouseSmoothing", .label="Smoothing", .kind=ROW_SLIDER, .step=5, .unit="%" }, - { .key="Input.MouseRawInput", .label="Raw input", .kind=ROW_TOGGLE, .step=1, .names=kOnOff }, + { .key="Input.CrosshairCursor", .label="Crosshair pointer", .kind=ROW_TOGGLE, .step=1, .names=kOnOff }, { .key="__ResetMouse", .label="Reset to defaults", .kind=ROW_ACTION }, SEP_BACK(Mouse), @@ -322,19 +327,19 @@ static struct Row rows[] = { * (two slots per action, pad-driven capture). */ { .key="__HdrController", .label="CONTROLLER", .kind=ROW_HEADER }, { .key="Input.ControlScheme", .label="Control style", .kind=ROW_ENUM, .step=1, .names=kControlScheme }, /* D513 */ - { .key="Input.PadPreset", .label="Layout preset", .kind=ROW_ENUM, .step=1, .names=kPadPreset, .schemeOnly=1 }, + { .key="Input.PadPreset", .label="Layout presets", .kind=ROW_ENUM, .step=1, .names=kPadPreset, .schemeOnly=1 }, /* D516: Original shows the game's own styles (the save's, per seat), not the port presets. */ - { .key="Bond.Control", .label="Controller style", .kind=ROW_ENUM, .step=1, .names=kOrigStyle, + { .key="Bond.Control", .label="Layout presets", .kind=ROW_ENUM, .step=1, .names=kOrigStyle, .found=1, .uiMax=7, .cfgMax=7, .schemeOnly=2 }, { .key="Input.Pad.Fire", .label="Fire", .kind=ROW_PADBIND, .shownWhen="Input.PadPreset", .shownValue=3, .schemeOnly=1 }, { .key="Input.Pad.Aim", .label="Aim", .kind=ROW_PADBIND, .shownWhen="Input.PadPreset", .shownValue=3, .schemeOnly=1 }, - { .key="Input.Pad.Use", .label="Use / action", .kind=ROW_PADBIND, .shownWhen="Input.PadPreset", .shownValue=3, .schemeOnly=1 }, + { .key="Input.Pad.Use", .label="Use/back", .kind=ROW_PADBIND, .shownWhen="Input.PadPreset", .shownValue=3, .schemeOnly=1 }, { .key="Input.Pad.Reload", .label="Reload", .kind=ROW_PADBIND, .shownWhen="Input.PadPreset", .shownValue=3, .schemeOnly=1 }, { .key="Input.Pad.Crouch", .label="Crouch", .kind=ROW_PADBIND, .shownWhen="Input.PadPreset", .shownValue=3, .schemeOnly=1 }, { .key="Input.Pad.NextWeapon", .label="Next weapon", .kind=ROW_PADBIND, .shownWhen="Input.PadPreset", .shownValue=3, .schemeOnly=1 }, { .key="Input.Pad.PrevWeapon", .label="Previous weapon", .kind=ROW_PADBIND, .shownWhen="Input.PadPreset", .shownValue=3, .schemeOnly=1 }, { .key="Input.Pad.Gadget", .label="Cycle gadget", .kind=ROW_PADBIND, .shownWhen="Input.PadPreset", .shownValue=3, .schemeOnly=1 }, - { .key="Input.Pad.Start", .label="Pause / start", .kind=ROW_PADBIND, .shownWhen="Input.PadPreset", .shownValue=3, .schemeOnly=1 }, + { .key="Input.Pad.Start", .label="Pause/start", .kind=ROW_PADBIND, .shownWhen="Input.PadPreset", .shownValue=3, .schemeOnly=1 }, { .key="Input.PadLookInvertY", .label="Invert look", .kind=ROW_TOGGLE, .step=1, .names=kOnOff }, /* Wave A: per-stick deadzone + right-stick (natural-pitch) feel options. */ { .key="Input.PadDeadzoneL", .label="Left stick deadzone", .kind=ROW_SLIDER, .step=300, .unit="%", .dispDiv=300 }, @@ -361,10 +366,10 @@ static struct Row rows[] = { SEP(K1), { .key="Input.Bind.Fire", .label="Fire", .kind=ROW_BIND }, { .key="Input.Bind.Aim", .label="Aim", .kind=ROW_BIND }, - { .key="Input.Bind.Action", .label="Action / next weapon", .kind=ROW_BIND }, - { .key="Input.Bind.Cancel", .label="Use / back", .kind=ROW_BIND }, + { .key="Input.Bind.Action", .label="Action/next weapon", .kind=ROW_BIND }, + { .key="Input.Bind.Cancel", .label="Use/back", .kind=ROW_BIND }, { .key="Input.Bind.LeanLeft", .label="Lean left", .kind=ROW_BIND }, - { .key="Input.Bind.Start", .label="Pause / start", .kind=ROW_BIND }, + { .key="Input.Bind.Start", .label="Pause/start", .kind=ROW_BIND }, { .key="Input.Bind.Reload", .label="Reload", .kind=ROW_BIND }, { .key="Input.Bind.Crouch", .label="Crouch", .kind=ROW_BIND }, SEP(K2), @@ -372,15 +377,12 @@ static struct Row rows[] = { SEP_BACK(Bindings), { .key="__HdrGame", .label="GAME", .kind=ROW_HEADER }, - /* D356 hid Bond.Look and Bond.AimControl; re-exposed (menu PD alignment, - * maintainer OK 2026-10-04) under the game's own names/values ("Look - * up/down: Reverse/Upright", "Aim control: Hold/Toggle"). They are per-file - * watch rows like Auto-aim. Reverse/Upright flips pitch in the game (moveData. - * invertPitch) on top of the port's own Invert look (mouse/controller) rows, - * so turning both on cancels out. Aim control (hold vs toggle the aim - * button) is independent of Aim style: the port honours the game's toggle. */ - { .key="Bond.Look", .label="Look up/down", .kind=ROW_TOGGLE, - .names=kReverse, .found=1, .uiMax=1, .cfgMax=1, .saveScoped=1 }, + /* Aim control (hold vs toggle the aim button) is a per-file watch row + * like Auto-aim; the port honours the game's toggle. The game's own + * "Look up/down" (Bond.Look) row was removed (D564): the port's mouse and + * centred-pad look write the view directly and never consult it, so the + * Mouse / Controller "Invert look" rows are the only inversion controls; + * watchsettings.c pins the game's value to its factory default. */ { .key="Bond.AimControl", .label="Aim control", .kind=ROW_TOGGLE, .names=kHold, .found=1, .uiMax=1, .cfgMax=1, .saveScoped=1 }, { .key="Bond.AutoAim", .label="Auto-aim", .kind=ROW_TOGGLE, @@ -395,12 +397,17 @@ static struct Row rows[] = { /* Crouch mode lives here (PD keeps it in "Game"; ratified T1 gate 1). * D371/D374: GEPD layout default; crouch defaults to hold. */ { .key="Input.CrouchMode", .label="Crouch mode", .kind=ROW_ENUM, .step=1, .names=kHold }, - /* D443/D357: shown as horizontal degrees; storage stays Video.FovScale %. + /* D546: shown as VERTICAL degrees like PD's "Vert FOV" (60 = N64, the + * middle of 30..90), 5-degree steps; storage stays Video.FovScale % (a + * float, so each step is exact: 5 deg = 100/12 %). Was horizontal degrees + * (D443/D357), which moved with the window aspect and never landed round. * Moved here from Graphics (PD puts Vert FOV in Game). */ - { .key="Video.FovScale", .label="Field of view", .kind=ROW_SLIDER, .step=1 }, + { .key="Video.FovScale", .label="Field of view", .kind=ROW_SLIDER, .step=100.0 / 12.0, + .uiMin=50, .uiMax=150 }, /* D468: off = AI awareness keeps the cartridge-widest view (16:9 at the * game's FOV) under ultrawide or a raised FOV; on = AI sees the full view. */ - { .key="Game.AIWideView", .label="Guards see the wider view", .kind=ROW_TOGGLE, .step=1, .names=kOnOff }, + /* D468: which part of the screen gameplay on-screen tests use; key kept for ini compat. */ + { .key="Game.AIWideView", .label="Gameplay view area", .kind=ROW_TOGGLE, .step=1, .names=kGameplayView }, /* D232: the community "no damage flash" toggle (red/green hit-flash overlay in bondview2). */ { .key="Game.NoHitFlash", .label="No hit flash", .kind=ROW_TOGGLE, .step=1, .names=kOnOff }, SEP(G2), @@ -416,21 +423,28 @@ 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="%" }, SEP(G3), - /* D216/Game.SkipIntro: D408 -- still EXPERIMENTAL: with it on, a failed/aborted + /* D216/Game.SkipIntro: D408 -- with it on, a failed/aborted * mission skips the post-mission failure dossier (MENU_MISSION_FAILED). */ - { .key="Game.SkipIntro", .label="Skip intro (EXPERIMENTAL)", .kind=ROW_TOGGLE, .step=1, .names=kOnOff }, - /* D257: everything-unlocked goodie (D442 RAM override); applies from the next launch. */ - { .key="Game.AllUnlocked", .label="All unlocked (EXPERIMENTAL)", - .kind=ROW_TOGGLE, .step=1, .names=kAllUnlocked }, + { .key="Game.SkipIntro", .label="Skip intro", .kind=ROW_TOGGLE, .step=1, .names=kOnOff }, + /* D257: everything-unlocked goodie. D442: a pure query-time/RAM override + * (file2.c fileGetIsCheatUnlocked + fileIsStageUnlockedAtDifficulty, plus + * the debug flags set by portAllUnlockedApply in main.c); no save bytes are + * patched, read or written. Applies live (rowSetCommit). */ + { .key="Game.AllUnlocked", .label="All unlocked", + .kind=ROW_TOGGLE, .step=1, .names=kOnOff }, + SEP(G4), + /* D551: opt-in update check; the action row appears only once a newer release is known. */ + { .key="Game.CheckUpdates", .label="Check for updates", .kind=ROW_TOGGLE, .step=1, .names=kOnOff }, + { .key="__UpdateOpen", .label="Update available: open page", .kind=ROW_ACTION }, { .key="__ResetGame", .label="Reset to defaults", .kind=ROW_ACTION }, /* Hub-only rows (.hub): shown after a separator at the bottom of the F10 hub, * hidden on the Game page. The front options screen still lists them here. */ { .key="__SepHub", .label="", .kind=ROW_ACTION, .hub=1 }, /* D293: in-game quit; not config-backed. */ - { .key="__QuitToDesktop", .label="Quit to desktop", .kind=ROW_ACTION, .hub=1 }, + { .key="__QuitToDesktop", .label="Quit to Desktop", .kind=ROW_ACTION, .hub=1 }, /* D443: orderly quit (D344) + relaunch, so "(restart)" rows like * Anti-aliasing can be applied without leaving the game by hand. */ - { .key="__RestartGame", .label="Restart game", .kind=ROW_ACTION, .hub=1 }, + { .key="__RestartGame", .label="Restart Game", .kind=ROW_ACTION, .hub=1 }, SEP_BACK(Game), /* D519: the Bond-file (profile) chooser. Not a section row: overlayUpdateVisible injects it as the * first row of every page that has profile-backed rows, on file select only (the old front-end @@ -477,7 +491,7 @@ static int isSepRow(const struct Row *r) } /* On/Off rows draw as PD checkboxes; named two-way rows (Reverse/Upright, - * Hold/Toggle, the EXPERIMENTAL restart row) keep their value text. */ + * Hold/Toggle, the restart row) keep their value text. */ static int overlayRowIsCheckbox(const struct Row *r) { return r->kind == ROW_TOGGLE && (r->names == kOnOff || r->names == kOnOffRev); @@ -522,6 +536,7 @@ static int s_scroll = 0; static SDL_atomic_t s_wheelPending; /* D314: host-thread wheel notches */ static int s_mouseActive = 0; /* last input was the mouse: hover cues a row + the hint line hides */ static int s_hover = -1; /* visible-list index under the pointer while s_mouseActive (cue only, never selection) */ +static int s_rmbBlock = 0; /* D544: right button held across a binding capture; re-armed on release */ /* D383: SDL_KEYDOWN belongs to videoPumpEvents (host), while both menu * controllers consume it on their own threads. Only the one-shot scancode @@ -901,6 +916,57 @@ static int overlayRowAtY(double ox, double oy) return -1; } +/* D556: mouse scrollbar. A thin track in the card's right margin (the value text + * ends at valueR = right - 10, so nothing is overlapped at any HUD scale), drawn + * and hit-tested only while the page overflows (the same condition as the "^ v" + * title marker). The thumb follows the drawn (eased) scroll. Returns 0 when the + * page does not overflow. */ +static int overlayStepSel(int pos, int dir); +struct OvScrollbar { s32 x0, x1, hx0, hx1, ty0, ty1, th0, th1; int first, span; }; +static int overlayScrollbar(struct OvScrollbar *sb) +{ + struct OvLayout o = overlayLayout(); + int first = s_section >= 0 ? 1 : 0; + int contentN = s_visN - first; + if (contentN <= o.maxRows) return 0; + sb->first = first; + sb->span = contentN - o.maxRows; /* scroll range, in rows */ + sb->x0 = o.right - 7; sb->x1 = o.right - 4; /* drawn */ + sb->hx0 = o.right - 9; sb->hx1 = o.right - 1; /* hit area (a little wider) */ + sb->ty0 = o.contentY; sb->ty1 = o.contentY + o.maxRows * OV_LINE; + s32 trackH = sb->ty1 - sb->ty0; + s32 thumbH = (s32)lround((double)trackH * o.maxRows / contentN); + if (thumbH < 10) thumbH = 10; + double f = (s_scrollF - first) / (double)sb->span; + if (f < 0) f = 0; + if (f > 1) f = 1; + sb->th0 = sb->ty0 + (s32)lround(f * (double)(trackH - thumbH)); + sb->th1 = sb->th0 + thumbH; + return 1; +} + +/* Scroll the window to `target` (first displayed entry) from the scrollbar, then pull the + * selection into view the way the wheel does (overlayUpdateScroll would otherwise snap the + * window straight back to the old selection). */ +static void overlayScrollTo(int target) +{ + struct OvLayout o = overlayLayout(); + int first = s_section >= 0 ? 1 : 0; + int maxScroll = s_visN - o.maxRows; + if (target > maxScroll) target = maxScroll; + if (target < first) target = first; + s_scroll = target; + int lo = s_scroll, hi = s_scroll + o.maxRows - 1; + if (s_sel < lo) { + s_sel = lo; + if (isSepRow(&rows[s_visIdx[s_sel]])) s_sel = overlayStepSel(s_sel, +1); + } else if (s_sel > hi) { + s_sel = hi; + if (isSepRow(&rows[s_visIdx[s_sel]])) s_sel = overlayStepSel(s_sel, -1); + } + s_mouseActive = 1; +} + /* Opt-in geometry check on both the first and last scroll windows: every * drawn row must be hit by its own band (top and bottom edge), and the gutter * outside the card must never hit. */ @@ -958,13 +1024,16 @@ static void overlayUpdateVisible(void) } } if (s_section >= 0 && current_menu == MENU_FILE_SELECT) { /* D519: profile chooser, first row of profile-backed pages */ - int hasBond = 0, chooser = -1; - for (int q = 0; q < s_visN; q++) - if (watchSettingsFieldForKey(rows[s_visIdx[q]].key) >= 0) hasBond = 1; + /* D556: the chooser sits directly above the first profile-backed row, never above the + * row that decides whether that row exists (Controller page: Control style stays first, + * the Original-only Profile + Original layout rows follow it). */ + int firstBond = -1, chooser = -1; + for (int q = 1; q < s_visN && firstBond < 0; q++) + if (watchSettingsFieldForKey(rows[s_visIdx[q]].key) >= 0) firstBond = q; for (int i = 0; i < NUM_ROWS; i++) if (rows[i].kind == ROW_BOND_FILE) chooser = i; - if (hasBond && chooser >= 0 && s_visN >= 1 && s_visN < (int)(sizeof(s_visIdx) / sizeof(s_visIdx[0]))) { - memmove(&s_visIdx[2], &s_visIdx[1], (size_t)(s_visN - 1) * sizeof(s_visIdx[0])); - s_visIdx[1] = chooser; + if (firstBond >= 1 && chooser >= 0 && s_visN < (int)(sizeof(s_visIdx) / sizeof(s_visIdx[0]))) { + memmove(&s_visIdx[firstBond + 1], &s_visIdx[firstBond], (size_t)(s_visN - firstBond) * sizeof(s_visIdx[0])); + s_visIdx[firstBond] = chooser; s_visN++; } } @@ -1022,8 +1091,13 @@ static void ddOpen(int rowIdx); static void ddConfirm(int k); static void rowAdjust(struct Row *r, int dir); static int s_ddRow; +/* D563: nav-input latch. Armed on open / page enter / back; the next poll snapshots + * every nav input held then, and each stays ignored until released once. */ +static int s_latchArm = 0; +static unsigned s_latchMask = 0; static void overlayOpenHeader(int hdr) { + s_latchArm = 1; ddClose(); s_section = hdr; s_sel = 1; @@ -1068,6 +1142,7 @@ static void overlayBackOne(void) { int child = s_section; if (child < 0) return; + s_latchArm = 1; ddClose(); s_section = optionsRowHeaderParent(child); s_sel = 0; @@ -1112,6 +1187,46 @@ static void resolveCb(const char *key, int type, void *ptr, double min, double m static void presetProbe(void); /* D440 GE_PRESETPROBE, below */ +/* Test hook (D558): applies the GE_OPTIONSOVERLAY / _SECTION / _SELECTBACK / _DROPDOWN / + * _SELECT= request. Called from init and, with GE_OPTIONSOVERLAY_ATFRAME=, again + * at emit frame n so it survives the level load (page state resets on load). */ +static void overlayApplyTestRequest(const char *e) +{ + s_open = 1; + /* Diagnostic screenshots: 1 = root, 2..7 = the six pages in hub order; + * GE_OPTIONSOVERLAY_SECTION=__HdrController etc. targets nested pages. */ + int page = atoi(e) - 2; + if (page >= 0) { + if (optionsRootHeader(page) >= 0) overlayOpenHeader(optionsRootHeader(page)); + } + /* Isolated screenshot/probe of a nested page, never a menu mode. */ + const char *section = getenv("GE_OPTIONSOVERLAY_SECTION"); + int nested = section ? rowIndexByKey(section) : -1; + if (nested >= 0 && rows[nested].kind == ROW_HEADER) + overlayOpenHeader(nested); + /* Screenshot-only selection state for checking the pinned Back + * row against a normal selected setting on the same page. */ + if (s_section >= 0 && getenv("GE_OPTIONSOVERLAY_SELECTBACK")) + s_sel = s_visN - 1; /* Back is the last row of every page */ + /* Screenshot-only: open a dropdown popup (GE_OPTIONSOVERLAY_DROPDOWN=) and focus its row. */ + { + const char *ddk = getenv("GE_OPTIONSOVERLAY_DROPDOWN"); + int ddi = ddk ? rowIndexByKey(ddk) : -1; + if (ddi >= 0) { + for (int q = 0; q < s_visN; q++) if (s_visIdx[q] == ddi) s_sel = q; + ddOpen(ddi); + } + } + { /* D558: GE_OPTIONSOVERLAY_SELECT= selects that row on the open page. */ + const char *sk = getenv("GE_OPTIONSOVERLAY_SELECT"); + int si = sk ? rowIndexByKey(sk) : -1; + if (si >= 0) { + overlayUpdateVisible(); + for (int q = 0; q < s_visN; q++) if (s_visIdx[q] == si) s_sel = q; + } + } +} + static void overlayInit(void) { if (s_inited) { @@ -1192,7 +1307,7 @@ static void overlayInit(void) { "__HdrSelPlayerC", "__SelPlayerC1", "__SelPlayerC4" }, { "__HdrController", "Input.ControlScheme", "__ResetController" }, { "__HdrBindings", "Input.Bind.Forward", "__ResetBindings" }, - { "__HdrGame", "Bond.Look", "__ResetGame" }, + { "__HdrGame", "Bond.AimControl", "__ResetGame" }, }; for (size_t k = 0; k < sizeof(pages) / sizeof(pages[0]); k++) { int hdr = rowIndexByKey(pages[k].page), gotA = 0, gotB = 0; @@ -1265,31 +1380,7 @@ static void overlayInit(void) const char *e = getenv("GE_OPTIONSOVERLAY"); if (e && atoi(e) != 0) { - s_open = 1; - /* Diagnostic screenshots: 1 = root, 2..7 = the six pages in hub order; - * GE_OPTIONSOVERLAY_SECTION=__HdrController etc. targets nested pages. */ - int page = atoi(e) - 2; - if (page >= 0) { - if (optionsRootHeader(page) >= 0) overlayOpenHeader(optionsRootHeader(page)); - } - /* Isolated screenshot/probe of a nested page, never a menu mode. */ - const char *section = getenv("GE_OPTIONSOVERLAY_SECTION"); - int nested = section ? rowIndexByKey(section) : -1; - if (nested >= 0 && rows[nested].kind == ROW_HEADER) - overlayOpenHeader(nested); - /* Screenshot-only selection state for checking the pinned Back - * row against a normal selected setting on the same page. */ - if (s_section >= 0 && getenv("GE_OPTIONSOVERLAY_SELECTBACK")) - s_sel = s_visN - 1; /* Back is the last row of every page */ - /* Screenshot-only: open a dropdown popup (GE_OPTIONSOVERLAY_DROPDOWN=) and focus its row. */ - { - const char *ddk = getenv("GE_OPTIONSOVERLAY_DROPDOWN"); - int ddi = ddk ? rowIndexByKey(ddk) : -1; - if (ddi >= 0) { - for (int q = 0; q < s_visN; q++) if (s_visIdx[q] == ddi) s_sel = q; - ddOpen(ddi); - } - } + overlayApplyTestRequest(e); sysLogPrintf(LOG_INFO, "optionsoverlay: auto-opened (GE_OPTIONSOVERLAY=%s)", e); /* D383 isolated host->menu smoke probe. Only with explicit opt-in; * test data dir must be private because capture persists the key. */ @@ -1330,6 +1421,7 @@ static void overlayInit(void) /* D516: scheme-conditional rows (see Row.schemeOnly). */ static int rowSchemeOk(const struct Row *r) { + if (!strcmp(r->key, "__UpdateOpen")) return updateCheckAvailable() != NULL; /* D551 */ if (!r->schemeOnly || !r->schemePtr) return 1; return (*r->schemePtr != 0) == (r->schemeOnly == 2); } @@ -1446,6 +1538,13 @@ static void rowSetCommit(struct Row *r, double v, int commit) * same rebuild hook. */ if (strcmp(r->key, "Input.CrouchMode") == 0) inputBindingsApply(); + /* D257/D442: All unlocked applies live (port/src/main.c); open screens + * such as mission select refresh on re-entry. */ + if (strcmp(r->key, "Game.AllUnlocked") == 0) { + extern void portAllUnlockedApply(void); + portAllUnlockedApply(); + } + /* Linked aim/turn sensitivity (Input.SensLink, default on): moving either * knob scales the other to hold the stock default ratio -- AimModeSens 38 * : MouseTurnSpeed 50 (the D194/D238 calibrated defaults). */ @@ -1600,6 +1699,10 @@ static void rowAdjust(struct Row *r, int dir) * click / A-press -- the held-repeat paths below never re-fire them). * The quit row is the only non-reset ROW_ACTION left. */ if (isArmRow(r)) { rowActivateReset(r); break; } + if (!strcmp(r->key, "__UpdateOpen")) { /* D551 */ + if (dir > 0) updateCheckOpenReleases(); + break; + } if (!strcmp(r->key, "__CenterWindow")) { /* D511: host thread does the SDL call */ if (dir > 0) videoRequestCenterWindow(); break; @@ -1620,15 +1723,12 @@ static void rowAdjust(struct Row *r, int dir) break; default: /* ROW_SLIDER */ if (!strcmp(r->key, "Video.FovScale")) { - /* D443: step the DISPLAYED horizontal degree, not the raw %: - * walk the stored % until the rounded degree value changes. */ - int cur = (int)lround(portFovHorizDegrees((s32)lround(v))); - int p = (int)lround(v); - for (int k = 0; k < 20; k++) { - p += (dir >= 0) ? 1 : -1; - if ((int)lround(portFovHorizDegrees((s32)p)) != cur) break; - } - rowSet(r, (double)p); + /* D546: step to the next 5-degree vertical mark (60 deg = 100 %); + * an off-grid value from an older ini snaps to the next mark. */ + double d = v * 0.6; /* 30..90: positive, so (long) truncation = floor */ + long lo = (long)(d / 5.0 + 1e-6), hi = (long)(d / 5.0 + 1.0 - 1e-6); + double g = (dir >= 0) ? (lo + 1) * 5.0 : (hi - 1) * 5.0; + rowSet(r, g / 0.6); break; } { @@ -1889,22 +1989,23 @@ int optionsRowIsReset(int i) static const struct { const char *key; double def; } kResetDefaults[] = { /* INPUT (port/src/input.c initializers) */ { "Input.MouseEnabled", 1 }, /* = 1 (on) */ + { "Input.CrosshairCursor", 1 }, /* = 1 (on, D555) */ { "Input.MouseSensitivity", 100 }, /* static int mouseSensitivity = 100 */ { "Input.MouseYScale", 100 }, /* = 100 (native), Wave A item 6 */ { "Input.MouseInvertY", 0 }, /* = 0 */ { "Input.AimMode", 0 }, /* = AIMMODE_N64 (0) */ { "Input.AimRange", 0 }, /* = 0 (PC) */ { "Input.PadLookInvertY", 0 }, /* = 0 */ - { "Input.PadDeadzoneL", 7000 },/* = STICK_DEADZONE (7000), Wave A */ - { "Input.PadDeadzoneR", 7000 },/* = STICK_DEADZONE (7000), Wave A */ + { "Input.PadDeadzoneL", 7500 },/* = STICK_DEADZONE (7500 = 25%, D546), Wave A */ + { "Input.PadDeadzoneR", 7500 },/* = STICK_DEADZONE (7500 = 25%, D546), Wave A */ { "Input.PadLookSensX", 100 }, /* = 100 (native), Wave A */ { "Input.PadLookSensY", 100 }, /* = 100 (native), Wave A */ { "Input.PadLookSmooth", 0 }, /* = 0 (off), Wave A */ { "Input.PadSouthpaw", 0 }, /* = 0 (off), Wave A */ { "Input.PadTriggerPct", 25 }, /* = 25 (was 23; rounded, D508) */ { "Input.RumbleScale", 0.5 }, /* = gRumbleScale (0.5f), D401 */ - { "Input.CrouchMode", 0 }, /* = 0 (hold) */ - { "Input.MenuPointerMode", 1 }, /* = 1 (direct pointer) */ + { "Input.CrouchMode", 1 }, /* = 1 (toggle; D556) */ + { "Input.MenuPointerMode", 1 }, /* = 1 (direct pointer); no F10 row since D561 (front-end-only legacy option, ini key kept) */ { "Input.MouseSmoothing", 0 }, /* = 0 (raw) */ { "Input.MouseRawInput", 0 }, /* = 0 (off) */ { "Input.ControlScheme", 0 }, /* = 0 (Modern), D513 */ @@ -1918,8 +2019,9 @@ static const struct { const char *key; double def; } kResetDefaults[] = { { "Video.WidescreenAuto", 1 }, /* = 1 */ { "Video.SafeAreaCrop", 1 }, /* = 1 */ { "Video.AspectMode", 0 }, /* = 0 (Window), D447 */ - { "Video.DrawDistance", 250 }, /* 2.5x */ - { "Video.LodDistance", 250 }, /* 2.5x */ + { "Video.DrawDistance", 200 }, /* 2.0x (D546) */ + { "Video.FogDistance", 100 }, /* 1.0x = N64 fog (D540) */ + { "Video.LodDistance", 200 }, /* 2.0x (D546) */ { "Video.CrosshairHide", 0 }, /* = 0 (on, N64) */ { "Video.CrosshairColor", 0 }, /* = 0 (authored red sprite) */ { "Video.CrosshairRed", 255 }, @@ -1931,6 +2033,7 @@ static const struct { const char *key; double def; } kResetDefaults[] = { { "Video.CrosshairHealthColor", 0 }, /* = 0 (off, D511) */ /* GAMEPLAY ini rows (port/src/video.c initializers) */ { "Game.SkipIntro", 0 }, /* = 0 */ + { "Game.CheckUpdates", 0 }, /* = 0 (off, D551) */ { "Game.NoHitFlash", 0 }, /* = 0 */ { "Game.AllUnlocked", 0 }, /* = 0 -- D257's "default ON" note was stale */ { "Game.HudScale", 100 }, /* = 100 (D226) */ @@ -2278,6 +2381,7 @@ static void overlayHandleInputLocked(void) static int navDir = 0, navTimer = 0, adjDir = 0, adjTimer = 0; if (!s_open) { + s_latchArm = 1; /* D563: whatever is held when it opens is latched */ prevUp = prevDn = prevLf = prevRt = prevLmb = prevRmb = 0; s_dragRow = -1; SDL_AtomicSet(&s_dragWatchField, 0); @@ -2298,7 +2402,7 @@ static void overlayHandleInputLocked(void) /* The overlay owns the mouse while it is open: force the OS cursor free + * visible (a stage poll would otherwise leave it locked/hidden). */ inputSuspendForOverlay(); - if (optionsBindingCaptureTick()) return; + if (optionsBindingCaptureTick()) { s_rmbBlock = 1; return; } /* D544: a right-click bound here must not also go back */ /* D361: regression probe on the ACTUAL joyPoll/F10 thread (not the * game-thread GE_WSPROBE_FRONT hook). A front-end write used to block @@ -2319,7 +2423,12 @@ static void overlayHandleInputLocked(void) /* D356: a pending reset arm expires after 3 s or when the selection * leaves the armed row; the overlay's selection is s_visIdx[s_sel]. */ overlayUpdateVisible(); - if (SDL_AtomicSet(&s_backPending, 0)) { + /* D541: Steam's Desktop-Mode layout types Escape for the Deck's B while SDL + * also reads the pad, so one B press arrived as both back paths (Esc event + * here, pad-B edge below) and backed out two pages. One back per 150 ms. */ + static Uint32 s_lastBackAt = 0; + if (SDL_AtomicSet(&s_backPending, 0) && SDL_GetTicks() - s_lastBackAt >= 150) { + s_lastBackAt = SDL_GetTicks(); if (s_ddRow >= 0) { ddClose(); /* Esc cancels the pick, not the page */ } else if (s_section >= 0) { @@ -2360,8 +2469,29 @@ static void overlayHandleInputLocked(void) int accept = ks[SDL_SCANCODE_RETURN] || ks[SDL_SCANCODE_KP_ENTER] || inputPadButton(inputOverlayOwnerPad(), SDL_CONTROLLER_BUTTON_A) || inputPadButton(inputOverlayOwnerPad(), SDL_CONTROLLER_BUTTON_X); - int rt = rightNav || accept; + { /* D541: release hold-off -- Enter (Steam desktop layout) + pad A from + * one press must not leave a gap that reads as a second accept. */ + static Uint32 s_acceptDownAt = 0; + Uint32 now = SDL_GetTicks(); + if (accept) s_acceptDownAt = now; + else if (s_acceptDownAt && now - s_acceptDownAt < 70) accept = 1; + } int padBack = inputPadButton(inputOverlayOwnerPad(), SDL_CONTROLLER_BUTTON_B) || ks[SDL_SCANCODE_BACKSPACE]; /* QoL: Backspace goes back like B */ + if (!rmb) s_rmbBlock = 0; + if (rmb && !s_rmbBlock && !optionsBindingCaptureActive()) padBack = 1; /* D544 QoL: right-click goes back like Esc (closes a dropdown first) */ + { /* D563: held-input latch (see s_latchArm) */ + unsigned cur = (up ? 1u : 0) | (dn ? 2u : 0) | (lf ? 4u : 0) | (rightNav ? 8u : 0) | + (accept ? 16u : 0) | (padBack ? 32u : 0); + if (s_latchArm) { s_latchArm = 0; s_latchMask = cur; } + s_latchMask &= cur; + if (s_latchMask & 1u) up = 0; + if (s_latchMask & 2u) dn = 0; + if (s_latchMask & 4u) lf = 0; + if (s_latchMask & 8u) rightNav = 0; + if (s_latchMask & 16u) accept = 0; + if (s_latchMask & 32u) padBack = 0; + } + int rt = rightNav || accept; if (s_seedPrev) { /* D519: whatever opened the overlay (A / Enter / click) is still held: not a press */ s_seedPrev = 0; prevUp = up; prevDn = dn; prevLf = lf; prevRt = rt; @@ -2377,10 +2507,17 @@ static void overlayHandleInputLocked(void) return; } prevStart = startNow; + if (padBack && !prevPadBack && SDL_GetTicks() - s_lastBackAt < 150) { + prevPadBack = padBack; /* D541: same press already backed out via Esc */ + prevUp = up; prevDn = dn; prevLf = lf; prevRt = rt; + return; + } if (padBack && !prevPadBack) { + s_lastBackAt = SDL_GetTicks(); prevPadBack = padBack; navDir = adjDir = 0; prevUp = up; prevDn = dn; prevLf = lf; prevRt = rt; + prevLmb = lmb; prevRmb = rmb; /* the right-click that backed out is not also a row click */ if (s_ddRow >= 0) ddClose(); else if (s_section >= 0) overlayBackOne(); else optionsOverlayToggle(); @@ -2436,7 +2573,10 @@ static void overlayHandleInputLocked(void) adjDir = 0; } else if (s_section < 0) { struct Row *hr = &rows[s_visIdx[s_sel]]; - if (hr->kind == ROW_HEADER) + /* D563: on the root hub pages and actions open only on A / Enter + * (or a click) -- never on Left/Right/stick/D-pad. */ + if (!accept) { + } else if (hr->kind == ROW_HEADER) overlayOpenHeader(s_visIdx[s_sel]); else if (dir > 0 && !isSepRow(hr)) rowAdjust(hr, +1); /* hub actions: Restart game / Quit to desktop */ @@ -2554,7 +2694,26 @@ static void overlayHandleInputLocked(void) /* Clicking the name selects; anywhere to its right changes the * setting. Reset actions use the whole row. */ - if (lmb && !prevLmb) { + /* D556: scrollbar. Press on the thumb = drag it; press on the track = page up/down + * (toward the pointer); the drag follows the pointer until release. */ + static int sbDrag = 0, sbGrab = 0; + int sbTaken = 0; + struct OvScrollbar sb; + if (s_ddRow < 0 && overlayScrollbar(&sb)) { + if (lmb && !prevLmb && ox >= sb.hx0 && ox < sb.hx1 && oy >= sb.ty0 && oy < sb.ty1) { + if (oy >= sb.th0 && oy < sb.th1) { sbDrag = 1; sbGrab = (int)(oy - sb.th0); } + else overlayScrollTo(s_scroll + (oy < sb.th0 ? -1 : 1) * maxVisibleRows()); + sbTaken = 1; + } else if (lmb && sbDrag) { + double f = ((oy - sbGrab) - sb.ty0) / (double)((sb.ty1 - sb.ty0) - (sb.th1 - sb.th0)); + if (f < 0) f = 0; + if (f > 1) f = 1; + overlayScrollTo(sb.first + (int)lround(f * sb.span)); + } + if (sbDrag) { sbTaken = 1; s_hover = -1; hoverVis = -1; } + } + if (!lmb) sbDrag = 0; + if (lmb && !prevLmb && !sbTaken) { if (hoverVis >= 0 && !isSepRow(&rows[s_visIdx[hoverVis]])) { s_sel = hoverVis; /* explicit click -> select */ overlayUpdateScroll(); @@ -2597,17 +2756,6 @@ static void overlayHandleInputLocked(void) s_dragRow = -1; SDL_AtomicSet(&s_dragWatchField, 0); } - /* Right press in the same change region: cycle back. */ - if (rmb && !prevRmb && hoverVis >= 0 && s_section >= 0) { - int i = s_visIdx[hoverVis]; - s32 x0, x1; - if (!isArmRow(&rows[i]) && overlayControlSpan(i, &x0, &x1) && - ox >= x0 && ox < x1) { - s_sel = hoverVis; - overlayUpdateScroll(); - rowAdjust(&rows[i], -1); - } - } } prevUp = up; prevDn = dn; prevLf = lf; prevRt = rt; @@ -2640,7 +2788,7 @@ static void valueText(int i, char *out, int n) return; } if (watchSettingsFieldForKey(r->key) == WATCH_SETTING_CONTROL) { /* D516 */ - if (v < 0) snprintf(out, n, "(in a mission)"); + if (v < 0) snprintf(out, n, "Not in a mission"); else snprintf(out, n, "%s", kOrigStyle[v > 7 ? 7 : (int)v]); return; } @@ -2748,8 +2896,8 @@ static void valueText(int i, char *out, int n) return; } } - if (!strcmp(r->key, "Video.FovScale")) { /* D443/D357: horizontal degrees */ - snprintf(out, n, "%d deg", (int)lround(portFovHorizDegrees((s32)lround(v)))); + if (!strcmp(r->key, "Video.FovScale")) { /* D546: vertical degrees, 60 = N64 */ + snprintf(out, n, "%d deg", (int)lround(v * 0.6)); return; } if (!strcmp(r->key, "Input.MouseSensitivity")) { /* D443/D357: raw 100 = 1.0x */ @@ -2788,6 +2936,7 @@ static void valueText(int i, char *out, int n) /* D506: draw / LOD distance are multiples of the authored distance * (ini 100..800 %): 2.5x by default. */ if (strcmp(r->key, "Video.DrawDistance") == 0 || + strcmp(r->key, "Video.FogDistance") == 0 || strcmp(r->key, "Video.LodDistance") == 0) { char t[16]; snprintf(t, sizeof(t), "%.2f", v / 100.0); @@ -2814,6 +2963,27 @@ static void valueText(int i, char *out, int n) } } +/* D558: textRenderGlyph (src/game/textrelated.c) drops any glyph whose UNSCALED x is + * > viGetX() or whose y is > viGetY() -- the game's current 2D canvas: 320x240 in a level, + * 440x330 on the front end. The overlay lays out on its own canvas (ovW() x ovH(), up to + * 427 wide at HUD 75) and draws body text through the OV_TEXT_PCT span scale about the + * string start, so the glyph x the game tests is the start plus the UNscaled advance (up + * to 1/0.78 of the drawn width). In a level that cut every long tip at x = 320 (front end: + * 440, so it never showed). Widen the canvas the game tests for the duration of the one + * textRender call (limits maxX/maxY in unscaled space); restored right after. */ +static Gfx *textRenderWide(Gfx *gdl, s32 *px, s32 *py, char *str, struct fontchar *chars, + struct font *font, u32 colour, s32 lw, s32 lh, s32 maxX, s32 maxY) +{ + s16 ox = viGetX(), oy = viGetY(); + s16 nx = ox, ny = oy; + if (maxX + 1 > nx) nx = (s16)(maxX + 1); + if (maxY + 1 > ny) ny = (s16)(maxY + 1); + if (nx != ox || ny != oy) viSetXY(nx, ny); + gdl = textRender(gdl, px, py, str, chars, font, colour, lw, lh, 0, 0); + if (nx != ox || ny != oy) viSetXY(ox, oy); + return gdl; +} + static Gfx *drawText(Gfx *gdl, s32 x, s32 y, const char *str, u32 colour) { s32 px = x, py = y; @@ -2822,8 +2992,8 @@ static Gfx *drawText(Gfx *gdl, s32 x, s32 y, const char *str, u32 colour) * own measured size -- passing the measured w/h clipped every glyph out * (baseline+height > measured h => nothing drawn). Match the game: pass * the full 2D viewport, like bondview2.c's debug-text path. */ - return textRender(gdl, &px, &py, (char *)str, ptrFontBankGothicChars, - ptrFontBankGothic, colour, ovW(), ovH(), 0, 0); + return textRenderWide(gdl, &px, &py, (char *)str, ptrFontBankGothicChars, + ptrFontBankGothic, colour, ovW(), ovH(), ovW(), ovH()); } static s32 measureText(const char *str) @@ -2864,8 +3034,13 @@ static Gfx *drawBody(Gfx *gdl, s32 x, s32 y, const char *str, u32 colour) { s32 px = x, py = y; gSPHudScaleEXT(gdl++, OV_TEXT_PCT * 256 / 100, x * 4, y * 4); - gdl = textRender(gdl, &px, &py, (char *)str, bodyChars(), bodyFont(), - colour, ovW(), ovH(), 0, 0); + /* D554: textRender clips against its width/height in UNscaled glyph space, but the + * string is drawn through the OV_TEXT_PCT span scale about (x, y). Pass the limits + * mapped back into unscaled space, else a string starting right of centre is cut + * early when ovW() is small (HUD 150: 213) even though it fits on screen. */ + s32 lw = x + (ovW() - x) * 100 / OV_TEXT_PCT, lh = y + (ovH() - y) * 100 / OV_TEXT_PCT; + gdl = textRenderWide(gdl, &px, &py, (char *)str, bodyChars(), bodyFont(), + colour, lw, lh, ovW() * 100 / OV_TEXT_PCT, ovH() * 100 / OV_TEXT_PCT); gSPHudScaleEXT(gdl++, 256, 0, 0); return gdl; } @@ -2921,8 +3096,6 @@ static int sectionIsMixedScope(int header) #define PD_ITEM_DISABLED 0x006f00afu #define PD_FOCUS_INNER 0xffffffffu #define PD_FOCUS_OUTER 0x004400ffu -#define PD_EXP_UNFOCUSED 0xd05a50ffu /* D472 EXPERIMENTAL rows (red), same weight as unfocused green */ -#define PD_EXP_FOCUSED 0xff8a80ffu static const u8 PAL_TITLE_L[3] = { 0, 191, 0 }; /* border1 */ static const u8 PAL_TITLE_M[3] = { 0, 80, 0 }; /* titlebg */ static const u8 PAL_TITLE_R[3] = { 0, 255, 0 }; /* border2 */ @@ -3099,6 +3272,74 @@ static Gfx *drawWedge(Gfx *gdl, s32 x0, s32 x1, s32 baseY, double f, u32 ink) } static Gfx *overlayEmitLocked(void); + +/* D555 rework: Input.CrosshairCursor -- the mouse pointer over the overlay is the + * game's own crosshair sprite (crosshairimage, 32x32), drawn through the same + * texSelect + display_image_at_position the front end's frontDrawCursor uses + * (white, alpha 220). Size: 16 half-extent in the game's own VI canvas units + * (frontDrawCursor: image->width/2; gunDrawSight: 16.0), i.e. 16/ovScale() in + * this 320-wide overlay canvas, with gunDrawSight's native-widescreen x fix. + * Drawn after the card (and outside the centred-aspect block) so it floats over + * everything and follows the real mouse across the whole window. */ +static Gfx *drawCrosshairPointer(Gfx *gdl, s32 W, s32 H) +{ + if (!inputCrosshairCursorOn() || !crosshairimage || !SDL_GetMouseFocus()) return gdl; + /* D562: only while the mouse is the active input (moved/clicked since the last + * pad/keyboard nav); a pad/keyboard-driven menu has no pointer to block rows. + * The OS cursor stays hidden regardless (inputApplyMouseRequests). */ + if (!s_mouseActive) return gdl; + double px, py; + f32 halfedxy[2]; + double fx, fy, xs; + if (current_menu != MENU_RUN_STAGE && current_menu != MENU_INVALID && + inputFrontEndCursorUiFrac(&fx, &fy, &xs) && viGetX() > 0 && viGetY() > 0) { + /* Front end: sit exactly over the game's own cursor (frontDrawCursor draws at + * floor(cursor_pos + 0.5), half-extent image->width/2 in the vi canvas). */ + px = fx * W; py = fy * H; + halfedxy[0] = (f32)((crosshairimage->width * 0.5) / viGetX() * W * xs); + halfedxy[1] = (f32)((crosshairimage->height * 0.5) / viGetY() * H); + } else { + int mx = 0, my = 0; + SDL_GetMouseState(&mx, &my); + int32_t rx = 0, ry = 0, rw = 0, rh = 0; + gfx_get_ui_screen_rect(&rx, &ry, &rw, &rh); + if (rw <= 0 || rh <= 0) return gdl; + px = (double)(mx - rx) * (double)W / rw; + py = (double)(my - ry) * (double)H / rh; + f32 half = 16.0f / ovScale(); + halfedxy[0] = halfedxy[1] = half; + extern float portNativeAspect(void); + if (portNativeAspect() > 0.0f) halfedxy[0] *= (4.0f / 3.0f) / portNativeAspect(); + } + if (px < 0.0) px = 0.0; else if (px > W) px = W; + if (py < 0.0) py = 0.0; else if (py > H) py = H; + f32 xypos[2] = { (f32)(s32)(px + 0.5), (f32)(s32)(py + 0.5) }; + texSelect(&gdl, crosshairimage, 4, 0, 0); + display_image_at_position(&gdl, xypos, halfedxy, 32, 32, 0, 0, 1, 255, 255, 255, 220, + (crosshairimage->level > 0), 0); + return gdl; +} +static int tipWrap(const char *tip, s32 av, char *l1, size_t n1, char *l2, size_t n2); /* D554 */ +/* D554: every control-hint line has a full form (card budget 296) and a compact form + * used only when the full one is wider than the card (HUD 150: 189). Pad button + * placeholders stay; hintFit() also falls back to Xbox letters as a last resort. */ +struct HintLine { const char *full, *compact; }; +static const struct HintLine kHint[] = { + /* 0 */ { "Press a pad button, or hold {B} to bind {B}", "Press a button Hold {B}: Bind {B}" }, + /* 1 */ { "Tap {B}/Esc: Cancel Hold {BACK}: Clear", "Tap {B}: Cancel Hold {BACK}: Clear" }, + /* 2 */ { "Press a key or mouse button (1-5)", "Press key or mouse 1-5" }, + /* 3 */ { "{B}/Esc: Cancel Del: Clear", "{B}: Cancel Del: Clear" }, + /* 4 */ { "{A}/Enter: Select", "{A}: Select" }, + /* 5 */ { "{B}/Esc/F10: Close", "{B}/Esc/F10: Close" }, + /* 6 */ { "{A}/Enter: Bind Left/Right: Slot {Y}: Clear", "{A}: Bind L/R: Slot {Y}: Clear" }, + /* 7 */ { "{B}/Esc: Back F10: Close", "{B}: Back F10: Close" }, + /* 8 */ { "Keyboard and mouse only Left/Right: Slot", "Keys/mouse only L/R: Slot" }, + /* 9 */ { "Enter: Bind {B}/Esc: Back", "Enter: Bind {B}: Back" }, + /* 10 */ { "{A}/Enter: Select Left/Right: Change", "{A}: Select L/R: Change" }, +}; +enum { H_PADCAP1, H_PADCAP2, H_KEYCAP1, H_KEYCAP2, H_HUB1, H_HUB2, H_CTRL1, H_BACK, H_BIND1, H_BIND2, H_SEC1 }; +static void tipSelfCheck(void); + Gfx *optionsOverlayEmit(void) { SDL_AtomicLock(&s_ovLock); /* D481: see optionsOverlayHandleInput */ @@ -3115,6 +3356,22 @@ static Gfx *overlayEmitLocked(void) fpsTick(); + { /* D558 test hook: GE_OPTIONSOVERLAY_ATFRAME= re-applies the open request at emit n (read once). */ + static int s_atFrame = -2, s_emitN = 0; + if (s_atFrame == -2) { + const char *af = getenv("GE_OPTIONSOVERLAY_ATFRAME"); + s_atFrame = af ? atoi(af) : -1; + } + if (s_atFrame >= 0 && s_emitN++ == s_atFrame) { + const char *oe = getenv("GE_OPTIONSOVERLAY"); + if (oe && atoi(oe) != 0) { + s_section = -1; + overlayApplyTestRequest(oe); + sysLogPrintf(LOG_INFO, "optionsoverlay: GE_OPTIONSOVERLAY_ATFRAME applied at emit %d", s_emitN - 1); + } + } + } + if (!s_open) { if (!s_showFps || !s_fpsText[0]) { return NULL; /* nothing appended -> golden dumps byte-identical */ @@ -3186,35 +3443,47 @@ static Gfx *overlayEmitLocked(void) } /* Hint texts (computed here so the hint panel fills before any text draws). */ - const char *help1 = NULL, *help2 = NULL; + const struct HintLine *help1 = NULL, *help2 = NULL; if (optionsBindingCaptureActive() && s_padCapMode) { - help1 = "Press pad button Hold {B} = bind {B}"; - help2 = "Tap {B}/Esc cancel Hold {BACK} clear"; + help1 = &kHint[H_PADCAP1]; + help2 = &kHint[H_PADCAP2]; } else if (optionsBindingCaptureActive()) { - help1 = "Press key/mouse 1-5"; - help2 = "{B}/Esc cancel Del clear"; - } else if (!s_mouseActive) { + help1 = &kHint[H_KEYCAP1]; + help2 = &kHint[H_KEYCAP2]; + } else { /* D556: control hints are always drawn (never tied to the pointer) */ if (s_section < 0) { - help1 = "{A}/Enter select"; - help2 = "{B}/F10 close"; + help1 = &kHint[H_HUB1]; + help2 = &kHint[H_HUB2]; } else if (!strcmp(rows[s_section].key, "__HdrController")) { - help1 = "{A} bind Left/Right slot {Y} clear"; - help2 = "{B}/Esc back F10 close"; + help1 = &kHint[H_CTRL1]; + help2 = &kHint[H_BACK]; } else if (!strcmp(rows[s_section].key, "__HdrBindings")) { - help1 = "Keys and mouse only Left/Right slot"; - help2 = "Enter bind {B}/Esc back"; + help1 = &kHint[H_BIND1]; + help2 = &kHint[H_BIND2]; } else { - help1 = "{A} select Left/Right change"; - help2 = "{B}/Esc back F10 close"; + help1 = &kHint[H_SEC1]; + help2 = &kHint[H_BACK]; } } /* D507: description of the focused row (any input source), first line under * the box; the key hints, when shown, move down one line to make room. */ - const char *rowHelp = (s_section >= 0 && s_sel >= 0 && s_sel < s_visN && !optionsBindingCaptureActive()) - ? optionsRowHelp(s_visIdx[s_sel]) : NULL; + /* D544 QoL: with the mouse in use, the hovered row's tip (hover is a cue, never the selection). */ + int helpVis = (s_mouseActive && s_hover >= 0 && s_hover < s_visN) ? s_hover : s_sel; + const char *rowHelp = (s_section >= 0 && helpVis >= 0 && helpVis < s_visN && !optionsBindingCaptureActive()) + ? optionsRowHelp(s_visIdx[helpVis]) : NULL; + /* D554: the tip wraps to a second centred line instead of being chopped with "..". */ + char tipL1[160], tipL2[160]; + int tipLines = 0; + tipSelfCheck(); /* once, cached: logs tips that would need > 2 lines at HUD 150 */ + if (rowHelp) tipLines = tipWrap(rowHelp, o.right - o.left - 8, tipL1, sizeof(tipL1), tipL2, sizeof(tipL2)); + if (tipLines > 2) tipLines = 2; /* line 2 is already chopped with ".." by tipWrap when it still overflows */ + const s32 tipArea = s_section >= 0 ? 24 : 0; { /* D519: the hint text sits in its own panel (box-body style) under the card, so it never * draws bare over the game's own bar (file select's Copy/Erase) */ - int hintLines = (rowHelp ? 1 : 0) + (help1 ? 2 : 0); + /* D556: fixed panel height per page: a section page reserves two tip lines (a row without + * a tip leaves them empty) so the key hints never move; the hub has no tips. 2 + 2 lines + * = 50 <= OV_HINT_H 53. */ + int hintLines = tipArea / 12 + (help1 ? 2 : 0); if (hintLines) { s32 py1 = o.hintY + hintLines * 12 + 2; gdl = fillRectC(gdl, o.left, o.bottom + 1, o.right, py1, PD_BODY); @@ -3234,8 +3503,7 @@ static Gfx *overlayEmitLocked(void) s32 rowY = o.contentY + (s32)lround((p - s_scrollF) * OV_LINE); if (rowY < bodyTop || rowY + OV_LINE > bodyBot) continue; int isCue = (p == cueP); - int expRow = strstr(r->label, "EXPERIMENTAL") != NULL; - u32 fillInk = !r->found ? PD_ITEM_DISABLED : expRow ? PD_EXP_UNFOCUSED : + u32 fillInk = !r->found ? PD_ITEM_DISABLED : isCue ? pulseInk() : PD_ITEM_UNFOCUSED; if (isSepRow(r)) { /* PD menuitemSeparatorRender: a 1 px line */ gdl = fillRectC(gdl, o.left + 8, rowY + OV_LINE / 2, o.right - 8, rowY + OV_LINE / 2 + 1, PD_ITEM_DISABLED); @@ -3265,6 +3533,14 @@ static Gfx *overlayEmitLocked(void) } } + { /* D556: mouse scrollbar (only while the page overflows): dim track + bright thumb in the card margin */ + struct OvScrollbar sb; + if (s_section >= 0 && overlayScrollbar(&sb)) { + gdl = fillRectC(gdl, sb.x0, sb.ty0, sb.x1, sb.ty1, PD_ITEM_DISABLED); + gdl = fillRectC(gdl, sb.x0, sb.th0, sb.x1, sb.th1, PD_ITEM_UNFOCUSED); + } + } + /* ---- pass 2: text ---- */ gdl = microcode_constructor(gdl); @@ -3274,11 +3550,9 @@ static Gfx *overlayEmitLocked(void) if (rowY - OV_TEXT_DY < bodyTop || rowY - OV_TEXT_DY + OV_LINE > bodyBot) continue; if (isSepRow(r)) continue; int isCue = (p == cueP); - int expRow = strstr(r->label, "EXPERIMENTAL") != NULL; /* D472: red ink */ u32 ink = !r->found ? PD_ITEM_DISABLED : - expRow ? (isCue ? PD_EXP_FOCUSED : PD_EXP_UNFOCUSED) : isCue ? pulseInk() : PD_ITEM_UNFOCUSED; - u32 valueInk = !r->found ? PD_ITEM_DISABLED : expRow ? PD_EXP_UNFOCUSED : PD_ITEM_UNFOCUSED; /* PD: values stay unfocused */ + u32 valueInk = !r->found ? PD_ITEM_DISABLED : PD_ITEM_UNFOCUSED; /* PD: values stay unfocused */ char label[64]; if (s_section < 0) titleCase(r->label, label, sizeof(label)); else snprintf(label, sizeof(label), "%s", r->label); @@ -3312,8 +3586,8 @@ static Gfx *overlayEmitLocked(void) /* The mixed-scope Game page tags per-profile rows with a subdued word. */ if (r->saveScoped && sectionIsMixedScope(s_section)) { s32 tagX = o.labelX + bodyWidth(r->label) + 4; - if (tagX + bodyWidth("(profile)") < labelEnd - 2) - gdl = drawBody(gdl, tagX, rowY, "(profile)", PD_ITEM_DISABLED); + if (tagX + bodyWidth("(per profile)") < labelEnd - 2) + gdl = drawBody(gdl, tagX, rowY, "(per profile)", PD_ITEM_DISABLED); } if (!r->found) { gdl = drawBodyR(gdl, o.valueR, rowY, "(n/a)", PD_ITEM_DISABLED); @@ -3363,7 +3637,7 @@ static Gfx *overlayEmitLocked(void) if (s_section >= 0 && rows[s_section].saveScoped) { int f = watchSettingsActiveFolder(); char note[24]; - if (f < 0) snprintf(note, sizeof(note), "(none)"); + if (f < 0) snprintf(note, sizeof(note), "(no profile)"); else snprintf(note, sizeof(note), "(Profile %d)", f + 1); gdl = drawBody(gdl, o.left + 8 + bodyWidth(title) + 6, o.top + 1, note, PD_ITEM_UNFOCUSED); } @@ -3374,7 +3648,7 @@ static Gfx *overlayEmitLocked(void) if (s_section < 0 && configUnknownKeyCount() > 0) { /* D472: non-blocking ini warning */ char w[64]; - snprintf(w, sizeof(w), "%d unknown ini key%s - see log", configUnknownKeyCount(), + snprintf(w, sizeof(w), "%d unknown setting%s in the ini, see the log", configUnknownKeyCount(), configUnknownKeyCount() == 1 ? "" : "s"); gdl = drawBodyR(gdl, o.right - 8, o.top + 1, w, 0xe0b050ff); } @@ -3384,38 +3658,28 @@ static Gfx *overlayEmitLocked(void) * button names. Two short lines, centred. */ s32 hintDy = 0; if (rowHelp) { - char hb[64]; - s32 av = o.right - o.left - 8; - snprintf(hb, sizeof(hb), "%s", rowHelp); - for (int n = (int)strlen(hb); n > 3 && bodyWidth(hb) > av; ) { - hb[--n] = 0; - if (n >= 2) { hb[n - 1] = '.'; hb[n - 2] = '.'; } + const char *tl[2] = { tipL1, tipL2 }; + for (int L = 0; L < tipLines; L++) { + s32 hw = bodyWidth(tl[L]), hx = (o.left + o.right) / 2 - hw / 2; + s32 hy = o.hintY + L * 12; + gdl = drawBody(gdl, hx + 1, hy + 1, tl[L], 0x000000ff); + gdl = drawBody(gdl, hx, hy, tl[L], PD_ITEM_UNFOCUSED); } - s32 hw = bodyWidth(hb), hx = (o.left + o.right) / 2 - hw / 2; - gdl = drawBody(gdl, hx + 1, o.hintY + 1, hb, 0x000000ff); - gdl = drawBody(gdl, hx, o.hintY, hb, PD_ITEM_UNFOCUSED); - hintDy = 12; } + hintDy = tipArea; if (help1) { static char h1buf[80], h2buf[80]; - inputPadHelpFmt(h1buf, sizeof(h1buf), help1); /* D471: pad names for the menu pad's family */ - inputPadHelpFmt(h2buf, sizeof(h2buf), help2); + inputPadHelpFmt(h1buf, sizeof(h1buf), help1->full); /* D471: pad names for the menu pad's family */ + inputPadHelpFmt(h2buf, sizeof(h2buf), help2->full); { - /* Too wide for the card: first compact "Left/Right" to "L/R", then fall - * back to the Xbox letters, so a line never runs past the box. */ - const char *tm[2] = { help1, help2 }; + /* Too wide for the card: use the line's compact form, then the Xbox letters. */ + const struct HintLine *tm[2] = { help1, help2 }; char *bf[2] = { h1buf, h2buf }; s32 av = o.right - o.left - 8; for (int L = 0; L < 2; L++) { if (bodyWidth(bf[L]) <= av) continue; - char compact[80]; size_t o2 = 0; - for (const char *p = tm[L]; *p && o2 + 1 < sizeof(compact); ) { - if (!strncmp(p, "Left/Right", 10)) { memcpy(compact + o2, "L/R", 3); o2 += 3; p += 10; } - else compact[o2++] = *p++; - } - compact[o2] = 0; - inputPadHelpFmt(bf[L], 80, compact); - if (bodyWidth(bf[L]) > av) inputPadHelpFmtFam(bf[L], 80, tm[L], 0); + inputPadHelpFmt(bf[L], 80, tm[L]->compact); + if (bodyWidth(bf[L]) > av) inputPadHelpFmtFam(bf[L], 80, tm[L]->compact, 0); } } s32 cx = (o.left + o.right) / 2; @@ -3427,6 +3691,7 @@ static Gfx *overlayEmitLocked(void) gdl = drawBody(gdl, cx - w2 / 2, y2, h2buf, PD_ITEM_UNFOCUSED); } PORT_HUD_ASPECT(gdl, GE_HUD_ASPECT_NONE); + gdl = drawCrosshairPointer(gdl, W, H); /* D555 rework: last, so it is above the card */ gDPPipeSync(gdl++); gSPEndDisplayList(gdl++); @@ -3471,67 +3736,120 @@ int optionsRowIsShown(int i) (r->kind != ROW_BOND_FILE || current_menu == MENU_FILE_SELECT); /* front-end profile chooser */ } -/* D507: one-line descriptions of the less obvious rows (key -> text), shown for - * the focused row. Keep each <= ~44 characters: the front-end page has one line - * of paper under its rows. Rows with self-explanatory labels have no entry. */ +/* D507: short descriptions of the less obvious rows (key -> text), shown for + * the focused row under the box. D554: a tip wraps onto a second line when it is + * wider than the card (budget 296 at HUD 75/100, 189 at HUD 150); keep each tip + * <= ~55 characters so it is two lines at most (tipSelfCheck logs offenders). + * Rows with self-explanatory labels have no entry (D556: standard PC settings such as + * resolution, VSync, anti-aliasing, volumes and sensitivities carry no tip). */ static const struct { const char *key, *help; } kRowHelp[] = { - { "__DisplayMode", "Modern defaults, or the original N64 look" }, - { "Video.AspectMode", "Fill the window, or force a fixed shape" }, - { "Video.FullscreenMode", "Borderless is seamless; exclusive can run faster" }, - { "__CenterWindow", "Moves the window to the middle of the screen" }, - { "Video.CrosshairAlpha", "How see-through the crosshair is" }, - { "Video.CrosshairHealthColor", "Crosshair shifts from green to red as health drops" }, - { "Video.NativeWidescreen", "Wider view, not a stretched picture" }, - { "Video.MSAA", "Smooths jagged edges (applies on restart)" }, - { "Video.VSync", "Syncs frames to your display, no tearing" }, - { "Video.FpsCap", "Limits the game's frame rate" }, - { "Video.TextureFilter", "How textures are smoothed up close" }, - { "Video.Anisotropy", "Sharper textures seen at a steep angle" }, - { "Video.WidescreenAuto", "Widens FOV when stretched (not Native)" }, - { "Video.SafeAreaCrop", "Hides the N64 TV border at the screen edge" }, - { "Video.DrawDistance", "How far away the world is drawn" }, - { "Video.LodDistance", "How far away detailed models are used" }, - { "Game.ScreenShakeIntensity", "Strength of the camera shake from blasts" }, - { "Audio.MasterVolume", "Overall volume, on top of Music and FX" }, - { "Input.MouseSensitivity", "Mouse turning speed" }, - { "Input.MouseYScale", "Mouse look speed up and down" }, - { "Input.MouseInvertY", "Moving the mouse up looks down" }, - { "Input.AimMode", "N64 moves the crosshair; Centered locks it" }, - { "Input.AimRange", "How far the N64-style crosshair travels" }, - { "Input.MenuPointerMode", "Direct follows the mouse; Velocity drifts" }, - { "Input.MouseSmoothing", "Averages mouse motion; higher is smoother" }, - { "Input.MouseRawInput", "Bypasses OS mouse acceleration" }, - { "Input.ControlScheme", "Ext fixes the style; Original uses the watch's 1.1-1.4" }, - { "Bond.Control", "The game's own style for this player, saved with the Bond file; same as the watch" }, - { "Input.PadPreset", "Button layout; Custom lets you rebind" }, - { "Input.PadLookInvertY", "Pushing the stick up looks down" }, - { "Input.PadDeadzoneL", "Ignores small stick movement near center" }, - { "Input.PadDeadzoneR", "Ignores small stick movement near center" }, - { "Input.PadLookSensX", "Right stick turning speed" }, - { "Input.PadLookSensY", "Right stick up/down speed" }, - { "Input.PadLookSmooth", "Eases right stick turning; 0% = off" }, - { "Input.PadSouthpaw", "Swaps the movement and look sticks" }, - { "Input.PadTriggerPct", "How far a trigger must pull to count" }, - { "Input.RumbleScale", "Strength of controller vibration" }, - { "Bond.Look", "Reverse: pushing up looks down" }, - { "Bond.AimControl", "Hold the aim button, or tap to toggle" }, - { "Bond.AutoAim", "Snaps the crosshair to nearby targets" }, - { "Bond.LookAhead", "Tilts the view up or down on slopes" }, - { "Bond.Sight", "Shows the aiming sight while aiming" }, - { "Bond.Ammo", "Shows the ammo counter on screen" }, - { "Input.CrouchMode", "Hold to crouch, or tap to toggle" }, - { "Video.FovScale", "Horizontal field of view" }, - { "Game.AIWideView", "On: guards react across the full wide view" }, - { "Game.NoHitFlash", "Turns off the red flash when you are hit" }, - { "Game.HudScale", "Scales ammo, pickup text and dialogue" }, - { "Game.SkipIntro", "Skips the intro; may skip failure dossier" }, - { "Game.AllUnlocked", "Unlocks levels, cheats and weapons" }, + { "__DisplayMode", "Modern, original or custom graphics presets." }, + { "Video.AspectMode", "Fill window fits the picture to the whole window." }, /* D560: shown only while Fill window is selected (optionsRowHelp) */ + { "Video.CrosshairHide", "When off, the crosshair stays hidden, even with Sight on screen." }, + { "Video.CrosshairHealthColor", "The crosshair shifts from green to red as your health drops." }, + { "Video.WidescreenAuto", "This only applies when Native widescreen is off." }, + { "Game.ScreenShakeIntensity", "Sets how strongly explosions shake the screen." }, + { "Input.AimMode", "Original lets the crosshair move. Centered keeps it in the middle." }, + { "Input.AimRange", "Sets how far the crosshair can move from the center." }, + { "Input.CrosshairCursor", "Uses the crosshair as the mouse pointer in this menu." }, + { "Input.ControlScheme", "Choose the port's modern controls or the original game's." }, + { "Bond.Control", "The original game's control presets." }, + { "Input.PadPreset", "Xbox Series-style presets, or your own custom layout." }, + { "Input.PadSouthpaw", "Swaps the fire and aim triggers." }, + { "Bond.AutoAim", "Bond automatically aims at nearby targets." }, + { "Bond.LookAhead", "Tilts the view up or down on slopes." }, + { "Bond.Sight", "Shows or hides the game's own crosshair, saved per profile." }, + { "Video.FovScale", "Sets the vertical view angle in degrees. 60 is the original." }, + { "Game.AIWideView", "Extends guard behavior to any widescreen view." }, + { "Game.NoHitFlash", "Disables the white flash when you take damage." }, + { "Game.HudScale", "Scales ammo, pickup text, dialogue and this menu." }, + { "Game.SkipIntro", "Skips the intro and opens the file select screen." }, + { "Game.AllUnlocked", "Unlocks all levels, 007 mode and every cheat." }, + { "Game.CheckUpdates", "Notifies you when a new version is available." }, }; + +/* D554: greedy two-line wrap of a tip at width av. Returns 1 (fits), 2 (split at the last + * space that fits; line 2 fits) or 3 (needs more than 2 lines: line 2 is chopped with ".." + * so the caller still has something drawable). */ +static int tipWrap(const char *tip, s32 av, char *l1, size_t n1, char *l2, size_t n2) +{ + snprintf(l1, n1, "%s", tip); + l2[0] = 0; + if (bodyWidth(l1) <= av) return 1; + size_t len = strlen(l1), cut = 0; + for (size_t i = 1; i < len; i++) { + if (l1[i] != ' ') continue; + char c = l1[i]; l1[i] = 0; + s32 w = bodyWidth(l1); + l1[i] = c; + if (w > av) break; + cut = i; + } + if (!cut) { /* no space fits: hard-chop line 1 with ".." (should not happen for real tips) */ + for (int n = (int)len; n > 3 && bodyWidth(l1) > av; ) { + l1[--n] = 0; + if (n >= 2) { l1[n - 1] = '.'; l1[n - 2] = '.'; } + } + return 3; + } + snprintf(l2, n2, "%s", tip + cut + 1); + l1[cut] = 0; + if (bodyWidth(l2) <= av) return 2; + for (int n = (int)strlen(l2); n > 3 && bodyWidth(l2) > av; ) { + l2[--n] = 0; + if (n >= 2) { l2[n - 1] = '.'; l2[n - 2] = '.'; } + } + return 3; +} + +/* D554: one-time startup check (log only). The narrowest budget is HUD scale 150: + * overlay width 320*100/150 = 213, card = min(304, 213 - 16) = 197, av = 189. Runs + * on the first emit that has the fonts; cached, so never per frame. */ +static void tipSelfCheck(void) +{ + static int s_done = 0; + if (s_done || !bodyFont() || !bodyChars()) return; + s_done = 1; + const s32 av = (320 * 100 / 150 - 16) - 8; + char a[160], b[160]; + int bad = 0; + for (size_t k = 0; k < sizeof(kRowHelp) / sizeof(kRowHelp[0]); k++) { + if (tipWrap(kRowHelp[k].help, av, a, sizeof(a), b, sizeof(b)) > 2) { + sysLogPrintf(LOG_WARNING, "optionsoverlay: tip %s needs more than 2 lines at HUD 150 (budget %d)", + kRowHelp[k].key, (int)av); + bad++; + } + } + sysLogPrintf(LOG_INFO, "optionsoverlay: tip wrap check: %d tips, %d over 2 lines at HUD 150", + (int)(sizeof(kRowHelp) / sizeof(kRowHelp[0])), bad); + /* Hint lines: the full form (Xbox names) must fit the HUD 100/75 budget (296), the compact + * form (Xbox names, the last fallback) the HUD 150 budget. */ + const s32 avWide = 304 - 8; + int hbad = 0; + for (size_t k = 0; k < sizeof(kHint) / sizeof(kHint[0]); k++) { + inputPadHelpFmtFam(a, sizeof(a), kHint[k].full, 0); + if (bodyWidth(a) > avWide) { + sysLogPrintf(LOG_WARNING, "optionsoverlay: hint %d full form too wide at HUD 100 (%d > %d): %s", + (int)k, (int)bodyWidth(a), (int)avWide, a); + hbad++; + } + inputPadHelpFmtFam(b, sizeof(b), kHint[k].compact, 0); + if (bodyWidth(b) > av) { + sysLogPrintf(LOG_WARNING, "optionsoverlay: hint %d compact form too wide at HUD 150 (%d > %d): %s", + (int)k, (int)bodyWidth(b), (int)av, b); + hbad++; + } + } + sysLogPrintf(LOG_INFO, "optionsoverlay: hint check: %d lines, %d over budget", (int)(sizeof(kHint) / sizeof(kHint[0])), hbad); +} + const char *optionsRowHelp(int i) { struct Row *r = rowAt(i); if (!r) return NULL; + /* D560: the Aspect ratio tip describes Fill window only, so it shows only for that value. */ + if (!strcmp(r->key, "Video.AspectMode") && (int)rowGet(r) != 0) return NULL; for (size_t k = 0; k < sizeof(kRowHelp) / sizeof(kRowHelp[0]); k++) if (!strcmp(kRowHelp[k].key, r->key)) return kRowHelp[k].help; return NULL; diff --git a/port/src/updatecheck.c b/port/src/updatecheck.c new file mode 100644 index 00000000..046a370b --- /dev/null +++ b/port/src/updatecheck.c @@ -0,0 +1,221 @@ +/* + * Opt-in update check. See port/include/updatecheck.h. + * + * Off by default (Game.CheckUpdates = 0): the ONLY entry point that can + * reach the network, updateCheckStart(), returns before creating a thread + * when the setting is 0. When on: one HTTPS GET per launch, on a detached + * thread, ~5 s timeout, silent on any failure (one log line). Windows uses + * WinHTTP, LoadLibrary'd at runtime (never linked); elsewhere `curl` is run + * through popen() (skipped if missing). + * No telemetry: the request carries only a fixed User-Agent. + */ + +#include +#include +#include + +#ifdef _WIN32 +#include +#endif + +#include + +#include "platform.h" +#include "system.h" +#include "config.h" +#include "versioninfo.h" +#include "updatecheck.h" + +#define UC_HOST "api.github.com" +/* /releases/latest deliberately excludes drafts and pre-releases (maintainer + * decision): a pre-release never prompts anyone to update. */ +#define UC_PATH "/repos/jkdansereau/goldeneye-pc-port/releases/latest" +#define UC_URL "https://" UC_HOST UC_PATH +#define UC_RELEASES "https://github.com/jkdansereau/goldeneye-pc-port/releases" +#define UC_UA "ge007-pc-port" +#define UC_BODY_MAX (64 * 1024) + +static int cfgCheckUpdates = 0; /* Game.CheckUpdates, default OFF */ +static SDL_atomic_t s_ready; /* 1 once s_tag is published */ +static char s_tag[32]; + +PD_CONSTRUCTOR static void updateCheckConfigInit(void) +{ + configRegisterInt("Game.CheckUpdates", &cfgCheckUpdates, 0, 1); +} + +/* Parse "vMAJOR.MINOR.PATCH" (missing parts = 0, suffix ignored). */ +static int parseVer(const char *s, int v[3]) +{ + v[0] = v[1] = v[2] = 0; + if (*s == 'v' || *s == 'V') s++; + if (*s < '0' || *s > '9') return 0; + for (int i = 0; i < 3; i++) { + if (*s < '0' || *s > '9') break; + v[i] = (int)strtol(s, (char **)&s, 10); + if (*s != '.') break; + s++; + } + return 1; +} + +static int versionNewer(const char *tag, const char *running) +{ + int a[3], b[3]; + if (!parseVer(tag, a) || !parseVer(running, b)) return 0; + for (int i = 0; i < 3; i++) { + if (a[i] != b[i]) return a[i] > b[i]; + } + return 0; +} + +/* Minimal scan for "tag_name": "". */ +static int extractTag(const char *body, char *out, size_t n) +{ + const char *p = strstr(body, "\"tag_name\""); + if (!p) return 0; + p += 10; + while (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n') p++; + if (*p != ':') return 0; + p++; + while (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n') p++; + if (*p != '"') return 0; + p++; + size_t i = 0; + /* Tag characters only (no control/ANSI bytes reach the log). */ + while (*p && *p != '"' && i + 1 < n) { + char c = *p++; + if (!((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || + c == '.' || c == '_' || c == '-')) return 0; + out[i++] = c; + } + if (*p != '"' || i == 0) return 0; + out[i] = 0; + return 1; +} + +#ifdef _WIN32 +/* winhttp.dll is loaded at runtime, only here (after the Game.CheckUpdates + * gate), so the exe's import table never lists it and nothing networking- + * related is mapped into the process while the setting is off. */ +typedef LPVOID HINT_; +typedef HINT_ (WINAPI *PFN_Open)(LPCWSTR, DWORD, LPCWSTR, LPCWSTR, DWORD); +typedef int (WINAPI *PFN_SetTimeouts)(HINT_, int, int, int, int); +typedef HINT_ (WINAPI *PFN_Connect)(HINT_, LPCWSTR, WORD, DWORD); +typedef HINT_ (WINAPI *PFN_OpenRequest)(HINT_, LPCWSTR, LPCWSTR, LPCWSTR, LPCWSTR, LPCWSTR *, DWORD); +typedef int (WINAPI *PFN_Send)(HINT_, LPCWSTR, DWORD, LPVOID, DWORD, DWORD, DWORD_PTR); +typedef int (WINAPI *PFN_Recv)(HINT_, LPVOID); +typedef int (WINAPI *PFN_Query)(HINT_, DWORD, LPCWSTR, LPVOID, LPDWORD, LPDWORD); +typedef int (WINAPI *PFN_Read)(HINT_, LPVOID, DWORD, LPDWORD); +typedef int (WINAPI *PFN_Close)(HINT_); +#define UC_WH_ACCESS_DEFAULT_PROXY 0 +#define UC_WH_FLAG_SECURE 0x00800000 +#define UC_WH_QUERY_STATUS_CODE 19 +#define UC_WH_QUERY_FLAG_NUMBER 0x20000000 +#endif + +/* Fetch the response body into buf (NUL-terminated). Returns 1 on success. */ +static int fetchLatest(char *buf, size_t cap) +{ + size_t len = 0; +#ifdef _WIN32 + int ok = 0; + /* System32 only: never a winhttp.dll planted next to the exe (D551 review). */ +#ifndef LOAD_LIBRARY_SEARCH_SYSTEM32 +#define LOAD_LIBRARY_SEARCH_SYSTEM32 0x00000800 +#endif + HMODULE dll = LoadLibraryExW(L"winhttp.dll", NULL, LOAD_LIBRARY_SEARCH_SYSTEM32); + if (!dll) return 0; + PFN_Open pOpen = (PFN_Open)(void *)GetProcAddress(dll, "WinHttpOpen"); + PFN_SetTimeouts pTimeout = (PFN_SetTimeouts)(void *)GetProcAddress(dll, "WinHttpSetTimeouts"); + PFN_Connect pConnect = (PFN_Connect)(void *)GetProcAddress(dll, "WinHttpConnect"); + PFN_OpenRequest pOpenReq = (PFN_OpenRequest)(void *)GetProcAddress(dll, "WinHttpOpenRequest"); + PFN_Send pSend = (PFN_Send)(void *)GetProcAddress(dll, "WinHttpSendRequest"); + PFN_Recv pRecv = (PFN_Recv)(void *)GetProcAddress(dll, "WinHttpReceiveResponse"); + PFN_Query pQuery = (PFN_Query)(void *)GetProcAddress(dll, "WinHttpQueryHeaders"); + PFN_Read pRead = (PFN_Read)(void *)GetProcAddress(dll, "WinHttpReadData"); + PFN_Close pClose = (PFN_Close)(void *)GetProcAddress(dll, "WinHttpCloseHandle"); + if (!pOpen || !pTimeout || !pConnect || !pOpenReq || !pSend || !pRecv || + !pQuery || !pRead || !pClose) { + FreeLibrary(dll); + return 0; + } + HINT_ ses = pOpen(L"" UC_UA, UC_WH_ACCESS_DEFAULT_PROXY, NULL, NULL, 0); + if (!ses) { FreeLibrary(dll); return 0; } + /* resolve, connect, send, receive: 5 s each */ + pTimeout(ses, 5000, 5000, 5000, 5000); + HINT_ con = pConnect(ses, L"" UC_HOST, 443, 0); + HINT_ req = con ? pOpenReq(con, L"GET", L"" UC_PATH, NULL, NULL, NULL, + UC_WH_FLAG_SECURE) : NULL; + if (req && + pSend(req, L"Accept: application/vnd.github+json\r\n", (DWORD)-1L, NULL, 0, 0, 0) && + pRecv(req, NULL)) { + DWORD status = 0, sz = sizeof(status); + if (pQuery(req, UC_WH_QUERY_STATUS_CODE | UC_WH_QUERY_FLAG_NUMBER, NULL, + &status, &sz, NULL) && status == 200) { + for (;;) { + DWORD got = 0; + if (len + 1 >= cap) break; + if (!pRead(req, buf + len, (DWORD)(cap - 1 - len), &got) || got == 0) + break; + len += got; + } + ok = len > 0; + } + } + if (req) pClose(req); + if (con) pClose(con); + pClose(ses); + FreeLibrary(dll); + buf[len] = 0; + return ok; +#else + FILE *f = popen("curl -q -fsS --proto =https --max-time 5 -A '" UC_UA "' '" UC_URL "' 2>/dev/null", "r"); + if (!f) return 0; + size_t got; + while (len + 1 < cap && (got = fread(buf + len, 1, cap - 1 - len, f)) > 0) len += got; + int rc = pclose(f); /* nonzero if curl is missing (127) or failed */ + buf[len] = 0; + return rc == 0 && len > 0; +#endif +} + +static int updateCheckThread(void *unused) +{ + (void)unused; + static char body[UC_BODY_MAX]; /* the thread runs at most once per launch */ + char tag[32]; + if (fetchLatest(body, UC_BODY_MAX) && extractTag(body, tag, sizeof(tag))) { + if (versionNewer(tag, GE007_VERSION)) { + snprintf(s_tag, sizeof(s_tag), "%s", tag); + SDL_AtomicSet(&s_ready, 1); /* full barrier: s_tag visible first */ + sysLogPrintf(LOG_INFO, "update check: %s is available (running v%s); %s", + tag, GE007_VERSION, UC_RELEASES); + } else { + sysLogPrintf(LOG_INFO, "update check: up to date (latest %s, running v%s)", + tag, GE007_VERSION); + } + } else { + sysLogPrintf(LOG_INFO, "update check: no result (offline or unavailable)"); + } + return 0; +} + +void updateCheckStart(void) +{ + if (!cfgCheckUpdates) return; /* OFF by default: no thread, no network, no init */ + SDL_Thread *t = SDL_CreateThread(updateCheckThread, "ge007-updatecheck", NULL); + if (t) SDL_DetachThread(t); + else sysLogPrintf(LOG_INFO, "update check: could not start thread"); +} + +const char *updateCheckAvailable(void) +{ + return SDL_AtomicGet(&s_ready) ? s_tag : NULL; +} + +void updateCheckOpenReleases(void) +{ + if (SDL_OpenURL(UC_RELEASES) != 0) + sysLogPrintf(LOG_INFO, "update check: could not open browser: %s", SDL_GetError()); +} diff --git a/port/src/video.c b/port/src/video.c index e88fce2f..c3ec1a60 100644 --- a/port/src/video.c +++ b/port/src/video.c @@ -63,13 +63,14 @@ static int cfgTexFilter = 1; /* 0 = nearest, 1 = bilinear (default), 2 = N static int cfgFixMipTex = 1; /* RC2: clip mip-contaminated texture uploads to base height */ static int cfgDetailBaseTile = 1; /* D236: TEXTURETYPE_DETAIL -> sample the base image, not the detail tile */ static int cfgWrapFix = 0; /* D74 sub-tile UV pre-wrap + RC3/D167 non-PoT mask-period wrap (opt-in; GE_WRAPFIX env overrides) */ -static int cfgFovScale = 100; /* D211: percent of the original vertical FOV; 100 = unchanged (byte-identical) */ +static float cfgFovScale = 100.0f; /* D211: percent of the original vertical FOV; 100 = unchanged (byte-identical). D546: float so the 5-degree UI steps are exact */ static int cfgWidescreenAuto = 1; /* WIDESCREEN-FOV-PLAN Phase 4: auto-scale vertical FOV by window aspect ratio; on by default, no-op at 4:3 */ static int cfgNativeWidescreen = 1; /* D334 (WIDESCREEN-FOV-PLAN Phase 2): project the world at the real window aspect (Hor+); no-op at 4:3 */ static int cfgHudScale = 100; /* D226: HUD text/ammo scale %, 100 = original (no emission) */ -static int cfgDrawDistance = 250; /* % of authored far clip; 250% is the UI's 50/100 midpoint */ +static int cfgDrawDistance = 200; /* % of authored far clip; D546: 2.0x default (was 2.5x) */ +static int cfgFogDistance = 100; /* D540: % of the authored fog distances; 100 = N64 */ static int cfgDrawDistanceAutoFov = 0; /* legacy ini option, no longer exposed in the menu */ -static int cfgLodDistance = 250; /* % of authored geometry LOD distance; 50/100 in the UI */ +static int cfgLodDistance = 200; /* % of authored geometry LOD distance; D546: 2.0x default (was 2.5x) */ static int cfgLodDistanceAutoFov = 0; /* legacy ini option, no longer exposed in the menu */ static int cfgAniso = 4; /* D212: anisotropic filtering samples; 4 = the value fast3d already applied (no visual delta at default) */ static int cfgSafeAreaCrop = 1; /* crop the N64 TV-overscan safe-area margin (visible as black top/bottom bars on PC) instead of showing it; on by default */ @@ -78,6 +79,7 @@ static int cfgFullscreen = 0; /* 0 = windowed, 1 = borderless fullscreen static int cfgFullscreenMode = 0; /* D511: fullscreen flavour, 0 = borderless desktop, 1 = exclusive */ static int cfgDeckPresetApplied = 0; /* D283: 1 once the Steam Deck preset has been considered */ static int cfgLowEndConsidered = 0; /* D482: 1 once the low-end GPU defaults have been considered */ +static int cfgDefaultsRev = 0; /* D546: 1 once the 2.5x -> 2.0x draw/LOD default migration ran */ /* * [Window] persistence. W/H = 0 -> auto (gfx_sdl2 fits a 4:3 window into ~85% @@ -208,14 +210,14 @@ void portCrosshairPreview(s32 *r, s32 *g, s32 *b) } /* D257: Game.AllUnlocked — everything-unlocked goodie, OFF by default - * (faithful N64 progression: levels unlock as you complete them). Consumed - * once at startup by main.c, which sets the game's own RAM unlock flags + * (faithful N64 progression: levels unlock as you complete them). Applied + * by portAllUnlockedApply (main.c) at startup and on every F10 change: it + * sets the game's own RAM unlock flags * (debug_enable_all_levels_flag / debug_007_unlock_flag in * src/game/debugmenu_handler.c, live because the PC build defines * LEFTOVERDEBUG) — port-layer memory writes only, no game-code edits. * 1 = every solo level selectable at every difficulty plus 007 mode from - * the first launch. F10 'All unlocked' row toggles it; takes effect next - * run. Cheats are unlocked at query time via fileGetIsCheatUnlocked + * the first launch. F10 'All unlocked' row toggles it live. Cheats are unlocked at query time via fileGetIsCheatUnlocked * (src/game/file2.c, D442) -- the save file is never patched. */ s32 portAllUnlocked = 0; @@ -398,6 +400,15 @@ f32 portDrawDistanceMultiplier(void) return mult; } +/* D540: Video.FogDistance -- multiplier on the level's authored fog start/end + * DISTANCES, independent of Video.DrawDistance (which moves the far clip). + * bgfog.c caps the fog at the far clip, so the fog always hides the edge of + * the drawn world. 1.0f (100) = the N64's own fog. */ +f32 portFogDistanceMultiplier(void) +{ + return (f32)cfgFogDistance / 100.0f; +} + /* D249: Video.LodDistance -- multiplier on the *distance* term * modelUpdateDistanceRelations() (src/game/model.c) tests against each LOD * node's MinDistance/MaxDistance, composed on top of the game's own @@ -531,11 +542,12 @@ PD_CONSTRUCTOR static void videoConfigInit(void) configRegisterInt("Video.FixMipTextures", &cfgFixMipTex, 0, 1); configRegisterInt("Video.DetailBaseTile", &cfgDetailBaseTile, 0, 1); configRegisterInt("Video.WrapFix", &cfgWrapFix, 0, 1); - configRegisterInt("Video.FovScale", &cfgFovScale, 50, 150); + configRegisterFloat("Video.FovScale", &cfgFovScale, 50.f, 150.f); configRegisterInt("Video.WidescreenAuto", &cfgWidescreenAuto, 0, 1); configRegisterInt("Video.NativeWidescreen", &cfgNativeWidescreen, 0, 1); /* D334 */ configRegisterInt("Game.HudScale", &cfgHudScale, 75, 150); /* D226: capped at 150 (user: little benefit above) */ configRegisterInt("Video.DrawDistance", &cfgDrawDistance, 100, 800); + configRegisterInt("Video.FogDistance", &cfgFogDistance, 50, 800); /* D540 */ configRegisterInt("Video.DrawDistanceAutoFov", &cfgDrawDistanceAutoFov, 0, 1); configRegisterInt("Video.LodDistance", &cfgLodDistance, 25, 800); configRegisterInt("Video.LodDistanceAutoFov", &cfgLodDistanceAutoFov, 0, 1); @@ -551,6 +563,7 @@ PD_CONSTRUCTOR static void videoConfigInit(void) configRegisterInt("Window.Maximized", &cfgWinMax, 0, 1); configRegisterInt("Video.DeckPresetApplied", &cfgDeckPresetApplied, 0, 1); /* D283 */ configRegisterInt("Video.LowEndConsidered", &cfgLowEndConsidered, 0, 1); /* D482 */ + configRegisterInt("Video.DefaultsRev", &cfgDefaultsRev, 0, 1); /* D546 */ } /* D283: Steam Deck preset. Called from main() right AFTER configLoad(), so @@ -686,6 +699,20 @@ static int videoIsLowEndRenderer(const char *r, const char **why) return 0; } +/* D546: one-time move of untouched old defaults to the standardized ones + * (draw/LOD distance 2.5x -> 2.0x). A value the player changed is kept; same + * "considered" flag pattern as D482. */ +static void videoMigrateDefaults(void) +{ + int changed = 0; + if (cfgDefaultsRev >= 1) return; + if (cfgDrawDistance == 250) { cfgDrawDistance = 200; changed++; } + if (cfgLodDistance == 250) { cfgLodDistance = 200; changed++; } + sysLogPrintf(LOG_INFO, "video: defaults rev 1 (D546): %d value(s) moved to 2.0x", changed); + cfgDefaultsRev = 1; + configSave(); +} + static void videoApplyLowEndDefaults(void) { const char *r = gfx_opengl_renderer_string(); @@ -702,8 +729,8 @@ static void videoApplyLowEndDefaults(void) } if (low) { int changed = 0; - if (cfgDrawDistance == 250) { cfgDrawDistance = 100; changed++; } - if (cfgLodDistance == 250) { cfgLodDistance = 100; changed++; } + if (cfgDrawDistance == 200) { cfgDrawDistance = 100; changed++; } /* D546: the default is 2.0x */ + if (cfgLodDistance == 200) { cfgLodDistance = 100; changed++; } if (cfgMSAA == 2) { cfgMSAA = 1; gfx_msaa_level = 1; changed++; } sysLogPrintf(LOG_INFO, "video: low-end renderer \"%s\" (%s); lowered %d default(s): DrawDistance=%d LodDistance=%d MSAA=%d", r, why, changed, cfgDrawDistance, cfgLodDistance, cfgMSAA); @@ -734,8 +761,9 @@ static const struct { const char *key; double n64, port; } kVideoPresets[] = { { "Video.WidescreenAuto", 0, 1 }, /* stock vertical FOV at any window aspect */ { "Video.SafeAreaCrop", 0, 1 }, /* show the VI frame as output, borders included */ { "Video.AspectMode", 1, 0 }, /* D447: exact console aspect, bars around it */ - { "Video.DrawDistance", 100, 250 }, /* authored far clip / fog (D218) */ - { "Video.LodDistance", 100, 250 }, /* authored LOD switch distances (D249) */ + { "Video.DrawDistance", 100, 200 }, /* authored far clip (D218); D546 2.0x */ + { "Video.FogDistance", 100, 100 }, /* D540: N64 fog in both presets */ + { "Video.LodDistance", 100, 200 }, /* authored LOD switch distances (D249); D546 2.0x */ { "Video.DrawDistanceAutoFov", 0, 0 }, /* legacy coupling: off = identity */ { "Video.LodDistanceAutoFov", 0, 0 }, { "Video.FovScale", 100, 100 }, /* stock FOV (D211) */ @@ -794,7 +822,7 @@ static SDL_atomic_t liveCfgDirty; * plain float the game re-reads each frame; anisotropy goes to fast3d. */ static void videoApplyImageOptions(void) { - portFovScale = (f32)cfgFovScale / 100.0f; + portFovScale = cfgFovScale / 100.0f; gfx_set_anisotropy_level(cfgAniso); gfx_set_safe_area_crop(cfgSafeAreaCrop); } @@ -1017,6 +1045,7 @@ int videoInit(void) }; gfx_init(&set); + videoMigrateDefaults(); /* D546: before D482, which compares against the new defaults */ videoApplyLowEndDefaults(); /* D482: needs GL_RENDERER; before the first frame */ /* VSync + optional fps cap; fast3d paces the window itself. */ @@ -1190,11 +1219,11 @@ void videoStartFrame(void) if (dirty & VCFG_VSYNC) wmAPI->set_swap_interval(cfgVSync ? 1 : 0); if (dirty & VCFG_FPS) gfx_set_target_fps(cfgFpsCap); if (dirty & VCFG_FILTER) videoApplyTexFilter(); - if (dirty & VCFG_FOV) portFovScale = (f32)cfgFovScale / 100.0f; + if (dirty & VCFG_FOV) portFovScale = cfgFovScale / 100.0f; if (dirty & VCFG_ANISO) gfx_set_anisotropy_level(cfgAniso); if (dirty & VCFG_CROP) gfx_set_safe_area_crop(cfgSafeAreaCrop); sysLogPrintf(LOG_INFO, "video: live config applied mask=%02x " - "(vsync=%d fpscap=%d texfilter=%d fov=%d aniso=%d)", + "(vsync=%d fpscap=%d texfilter=%d fov=%.1f aniso=%d)", dirty, cfgVSync, cfgFpsCap, cfgTexFilter, cfgFovScale, cfgAniso); } @@ -1299,17 +1328,8 @@ void videoPumpEvents(void) } } - /* Refresh the window title with the live FPS about once a second. */ - if (wmAPI && wmAPI->set_window_title) { - static double lastTitle = 0.0; - double now = wmAPI->get_time(); - if (now - lastTitle >= 1.0) { - lastTitle = now; - char title[64]; - snprintf(title, sizeof(title), "GoldenEye 007 - %.0f fps", vidAvgFPS); - wmAPI->set_window_title(title); - } - } + /* D550: static title bar -- the "GoldenEye 007 - NN fps" per-second + * refresh used to live here (maintainer request 2026-10-17). */ /* D287: apply anything the events above (click-to-lock, focus) queued. */ inputApplyMouseRequests(); diff --git a/port/src/watchsettings.c b/port/src/watchsettings.c index 49aae10f..e879434c 100644 --- a/port/src/watchsettings.c +++ b/port/src/watchsettings.c @@ -28,6 +28,7 @@ extern u16 get_mTrack2Vol(void); extern void fileBuildWriteNewSave(u32 folder); extern void set_cur_player_look_vertical_inverted(u32 value); extern void cur_player_set_aim_control(u32 value); +extern void cur_player_set_lookahead(u32 value); extern void cur_player_set_sight_onscreen_control(u32 value); extern void cur_player_set_ammo_onscreen_setting(u32 value); @@ -532,6 +533,32 @@ static void applyValue(enum WatchSettingField field, int value) } } +/* D559 (maintainer-approved, port layer only): D557 turned Look ahead off in + * DEFAULT_OPTIONS, but folders created before it hold the OLD factory options. + * When a folder is started to play (the stage becomes active; the stage load's + * fileLoadSettingsForFolder then reads the saved options), clear OPTION_LOOKAHEAD + * iff the folder is untouched: no stage time at any difficulty (times[] all + * zero), no 007 flag, no unlocked cheats, and options EXACTLY the old factory + * default with control-type bits 0. Any progress or any other option bit + * (including a deliberately-set Look ahead on a played folder) is never touched. + * Persists through fileWriteSave, as saveFrontField does. Game thread only. + * Returns 1 when it changed the save. */ +#define OLD_FACTORY_OPTIONS (OPTION_AUTOAIM | OPTION_SIGHTONSCREEN | OPTION_LOOKAHEAD | OPTION_DISPLAYAMMO) +static int migrateUntouchedLookAhead(int folder) +{ + save_data *save = validSave(folder); + if (!save || save->options != OLD_FACTORY_OPTIONS) return 0; + if (save->flag_007 || save->unlocked_cheats_1 || save->unlocked_cheats_2 || save->unlocked_cheats_3) return 0; + for (size_t i = 0; i < sizeof(save->times); i++) + if (save->times[i]) return 0; + if (!fileGamePakProbe()) return 0; + save->options = (u16)(save->options & ~OPTION_LOOKAHEAD); + fileWriteSave(save); + sysLogPrintf(LOG_INFO, "watchsettings: Bond file %d is untouched and has the old factory options; Look ahead set off (D559)", folder + 1); + return 1; +} +static int wsLookAheadChecked = 0; /* once per stage activation */ + void watchSettingsGameTick(void) { /* Drain F10 front-end commits on the game thread. This MUST run before @@ -566,7 +593,9 @@ void watchSettingsGameTick(void) { static int wsResetProbePhase = 0; static int wsProbeFrontTicks = 0, wsProbeStageTicks = 0; - if (getenv("GE_WSPROBE_RESET")) { + static int wsResetProbeOn = -1; /* cached: this runs every tick (D250/D302 class) */ + if (wsResetProbeOn < 0) wsResetProbeOn = getenv("GE_WSPROBE_RESET") != NULL; + if (wsResetProbeOn) { if (!stageActive()) { if (wsResetProbePhase == 0 && ++wsProbeFrontTicks == 120) { optionsResetProbePrepare(); @@ -594,8 +623,22 @@ void watchSettingsGameTick(void) * during a level/menu transition. Each command's folder is re-checked * against selected_folder_num at apply time, so a stale cross-file edit * is still dropped, never misapplied. */ + /* D564: the game's own "Look up/down" option is not exposed (the port's + * Mouse / Controller "Invert look" rows are the inversion controls). Pin it + * to its factory value (0, as a blank profile has it) so a value saved by an + * older build or set via the in-game watch menu cannot silently flip the + * pad stick on the game's pitch path. RAM only; one int compare per tick. */ + if (stageActive() && g_CurrentPlayer && get_cur_player_look_vertical_inverted() != 0) + set_cur_player_look_vertical_inverted(0); + if (!stageActive() || !g_CurrentPlayer) { wsStageWasActive = 0; + if (!stageActive()) wsLookAheadChecked = 0; + else if (!wsLookAheadChecked) { /* D559: stage starting, before the stage load reads the save */ + wsLookAheadChecked = 1; + if (migrateUntouchedLookAhead(selected_folder_num) && getPlayerCount() == 1) + cur_player_set_lookahead(0); /* harmless if the stage load re-reads the save after this */ + } /* D354: one-shot fallback -- if not a single folder holds a valid * save (wiped/corrupt/fresh eeprom), build the game's own blank save for * file 1 (game thread, so the EEPROM write is on the owning thread) @@ -649,6 +692,11 @@ void watchSettingsGameTick(void) * init_watch re-applies the saved music value to the X track; resync the * BGM (track 1) to the same live value once per activation so the player's * music setting survives level starts, not just live F10 edits. */ + if (!wsLookAheadChecked) { /* D559: no tick saw the stage before its load: fix the live setting too */ + wsLookAheadChecked = 1; + if (migrateUntouchedLookAhead(selected_folder_num) && getPlayerCount() == 1) + cur_player_set_lookahead(0); + } if (!wsStageWasActive && musicTrack1GetVolume() != get_mTrack2Vol()) musicTrack1ApplySeqpVol(get_mTrack2Vol()); wsStageWasActive = 1; diff --git a/src/game/bgfog.c b/src/game/bgfog.c index f3f8c52d..264b7ebd 100644 --- a/src/game/bgfog.c +++ b/src/game/bgfog.c @@ -344,6 +344,15 @@ int portFogPositionVisibleGameplay(float *pos3, float range) #endif +#ifdef PORT +/* D540: Video.FogDistance helpers (port rendering option, like D218's draw + * distance; game logic unchanged -- identity at the defaults). */ +extern f32 portDrawDistanceMultiplier(void); +extern f32 portFogDistanceMultiplier(void); +static EnvironmentRecord *s_portFogEnv = NULL; /* last env loaded, for live re-apply */ +static f32 s_portFogDD, s_portFogFM; /* multipliers it was built with */ +#endif + /** * Address 0x7F0BA758. */ @@ -379,8 +388,17 @@ void fogLoadCurrentEnvironment(EnvironmentRecord *arg0) * a per-frame copy. Identity at DrawDistance=100/DrawDistanceAutoFov's * FovScale=100 default. */ { - extern f32 portDrawDistanceMultiplier(void); - scaledFarFog = arg0->Visibility.FarFog * portDrawDistanceMultiplier(); + /* D540: the far clip follows FOG distance, capped by Draw distance: + * beyond the fog's end everything is solid fog, so a further clip + * shows nothing -- but it pushed the authored per-mille fog positions + * so close to 1000 that the s32 rounding pulled the fog visibly in + * (Cradle's distant truss fogged out vs 1964 at Draw 2.5x; exact at + * Draw 1.0x). At Fog 1.0x the clip is the authored far: exact N64 at + * any Draw distance. Draw distance still scales prop draw distance + * (propobj.c) and the room pool within that clip. */ + f32 clipMult = portFogDistanceMultiplier(); + if (clipMult > portDrawDistanceMultiplier()) clipMult = portDrawDistanceMultiplier(); + scaledFarFog = arg0->Visibility.FarFog * clipMult; } viSetZRange(arg0->Visibility.BlendMultiplier, scaledFarFog); #else @@ -425,6 +443,12 @@ void fogLoadCurrentEnvironment(EnvironmentRecord *arg0) g_CurrentEnvironment.DifferenceFromFarIntensity = (s32) arg0->Fog.DifferenceFromFarIntensity; g_CurrentEnvironment.FarIntensity = arg0->Fog.FarIntensity; +#ifdef PORT + /* D540: remember the env + multipliers for the live re-apply below. */ + s_portFogEnv = arg0; + s_portFogDD = portDrawDistanceMultiplier(); + s_portFogFM = portFogDistanceMultiplier(); +#endif g_CurrentEnvironment.Red = arg0->Sky.Red; g_CurrentEnvironment.Green = arg0->Sky.Green; g_CurrentEnvironment.Blue = arg0->Sky.Blue; @@ -527,6 +551,7 @@ void fogLoadLevelEnvironment(s32 level_id, s32 arg1) g_PortAuthoredScaledFarFogIntensity = FLT_MAX; g_PortAuthoredScaledDifferenceFromFarFogIntensity = 0.0f; g_PortAuthoredFarFog = 10000.0f; + s_portFogEnv = NULL; /* D540: no live re-apply until this level's env loads */ #endif if (arg1) @@ -690,6 +715,17 @@ Gfx *fogSetRenderFogColor(Gfx *gdl, s32 arg1) return gdl; } +#ifdef PORT + /* D540: Draw distance / Fog distance apply live (they used to wait for + * the next level load): rebuild the fog state from the same environment + * on the game thread when either multiplier changed. */ + if (s_portFogEnv != NULL && + (portDrawDistanceMultiplier() != s_portFogDD || portFogDistanceMultiplier() != s_portFogFM)) + { + fogLoadCurrentEnvironment(s_portFogEnv); + } +#endif + // note: both branches are exactly the same. // maybe one has debug code? if (arg1) diff --git a/src/game/bondview2.c b/src/game/bondview2.c index f9202539..9a359e1d 100644 --- a/src/game/bondview2.c +++ b/src/game/bondview2.c @@ -352,15 +352,6 @@ int gameScriptedCameraActive(void) (g_CameraMode == CAMERAMODE_SWIRL) || (g_CameraMode == CAMERAMODE_FADESWIRL); } -/* D243 M-169: a monotonic epoch counter bumped by chrai.c's - * AI_TRYTeleportingChrToPad case right after the decomp call that - * legitimately re-anchors a chr's model root joint via setsuboffset() for a - * real cutscene shot-change. playerTick() polls it to re-seed the camera - * look-at filter exactly on a shot-change (M-190). Global, not per-chr: - * sufficient for this cutscene's scope. */ -static u32 g_d243TeleportEpoch = 0; -void d243NotifyTeleport(void) { g_d243TeleportEpoch++; } -u32 d243GetTeleportEpoch(void) { return g_d243TeleportEpoch; } #endif void solo_char_load(void) @@ -10629,64 +10620,16 @@ s32 playerTick(PropRecord *prop) RenderPosView *rp = g_playerPointers[index]->bodyModel->render_pos; matrix_4x4_multiply_homogeneous(currentPlayerGetViewToWorldMtxf(), (Mtxf *) rp, (Mtxf *) mtx); #ifdef PORT - /* D243 M-170 (superseded by M-190 below): the original "fix" - * unconditionally froze field_488.pos during POSEND, only - * letting a write through on the tick a shot-change teleport - * fired. That killed the shake but also killed legitimate - * camera tracking of Bond's real per-tick motion within a - * shot -- the M-189 buffer-overlap fix that landed this same - * session removed the actual cause of the shake (corrupted - * render-skeleton data), so freezing is no longer needed and - * was actively wrong (user-reported: "camera angles do not - * follow bond"). */ - /* D243 M-190: write field_488.pos every tick unconditionally - * (real tracking restored), and instead re-seed the look-at - * filter's leaky-integrator accumulator (field_3B8, read by - * bondviewUpdatePlayerCollisionPositionFields) the instant a - * legitimate shot-change teleport fires, rather than letting - * it slowly converge over ~20 ticks (M-143's original shake - * mechanism, confirmed PC-only vs N64 in M-144). Detected via - * the same d243TeleportEpoch counter M-170 already used - * (bumped by d243NotifyTeleport() in chrai.c's - * AI_TRYTeleportingChrToPad handler) -- only the response to - * an epoch change is different now: reset the filter to the - * new position instead of freezing the input to it. */ - extern u32 d243GetTeleportEpoch(void); - static u32 s_d243LastEpoch = 0; - static int s_d243HaveEpoch = 0; - u32 epoch; - int justTeleported; - epoch = d243GetTeleportEpoch(); - justTeleported = (!s_d243HaveEpoch) || (epoch != s_d243LastEpoch); - s_d243LastEpoch = epoch; - s_d243HaveEpoch = 1; + /* D243 history (M-170 freeze, M-190 teleport re-seed) removed + * 2026-10-06 (D552): the shake's real cause was the M-189 + * sizeof(Model) buffer overlap; the remaining ~20-tick filter + * convergence after a shot-change teleport is the original + * camera swivel onto Bond (Dam ending), so the write below + * and the look-at filter are the decomp's, unmodified. */ #endif g_playerPointers[index]->field_488.pos.x = mtx[12] + (mtx[4] * 7.0f); g_playerPointers[index]->field_488.pos.y = mtx[13] + (mtx[5] * 7.0f); g_playerPointers[index]->field_488.pos.z = mtx[14] + (mtx[6] * 7.0f); -#ifdef PORT - if ((g_CameraMode == CAMERAMODE_POSEND) && justTeleported) - { - f32 fx = g_playerPointers[index]->field_488.pos.x; - f32 fy = g_playerPointers[index]->field_488.pos.y; - f32 fz = g_playerPointers[index]->field_488.pos.z; - /* Re-seed so the filter's steady state (field_3C4/8/C = - * field_3B8 * FACTOR_2) already equals the new position, - * instead of a fresh ~20-tick geometric decay toward it - * (bondviewUpdatePlayerCollisionPositionFields, the - * FACTOR_1/FACTOR_2 pair). field_3B8 is the PRE-scale - * accumulator, so it must be seeded at pos/FACTOR_2, not - * pos itself -- field_3C4/8/C (what the camera actually - * reads) are set directly to the real position so it's - * correct even before the next tick's filter update. */ - g_playerPointers[index]->field_3B8.f[0] = fx / S7F081478_FACTOR_2; - g_playerPointers[index]->field_3B8.f[1] = fy / S7F081478_FACTOR_2; - g_playerPointers[index]->field_3B8.f[2] = fz / S7F081478_FACTOR_2; - g_playerPointers[index]->field_3C4 = fx; - g_playerPointers[index]->field_3C8 = fy; - g_playerPointers[index]->field_3CC = fz; - } -#endif } return ret; diff --git a/src/game/chrai.c b/src/game/chrai.c index 7128c132..c0de32fd 100644 --- a/src/game/chrai.c +++ b/src/game/chrai.c @@ -4177,20 +4177,6 @@ void ai(PropDefHeaderRecord *Entityp, PROP_TYPE EntityType) chr->chrflags = chr->chrflags | CHRFLAG_INIT; setsubroty(chr->model, FacingDirection); setsuboffset(chr->model, &pos); -#ifdef PORT - /* D243 M-169: signal the real shot-change moment - * to the X3/X4 render-pos freeze experiments -- - * see the d243NotifyTeleport() definition - * (bondview2.c) for the full rationale. This is - * the exact decomp call (setsuboffset, line - * above, unmodified) that a legitimate cutscene - * teleport already performs; the notify is a - * side-channel observer only, no logic change. */ - { - extern void d243NotifyTeleport(void); - d243NotifyTeleport(); - } -#endif chrDetectRooms(chr); if (chr->prop == g_CurrentPlayer->prop) { diff --git a/src/game/file2.h b/src/game/file2.h index 4be8d4d9..0cdcc9fb 100644 --- a/src/game/file2.h +++ b/src/game/file2.h @@ -20,7 +20,15 @@ #define OPTION_CONTROLTYPE 0x0700 #define OPTION_SCREENCINEMA 0x0800 +#ifdef PORT +/* D557 (Rule-2 sign-off: maintainer 2026-10-06): new/blank profiles start with + * Look ahead OFF on PC -- its pitch auto-centring fights mouse and right-stick + * look (PD's PC port likewise keeps it on only for N64). Existing saves keep + * their stored option bits; only BLANKSAVEDATA-initialised profiles change. */ +#define DEFAULT_OPTIONS (OPTION_AUTOAIM | OPTION_SIGHTONSCREEN | OPTION_DISPLAYAMMO) +#else #define DEFAULT_OPTIONS (OPTION_AUTOAIM | OPTION_SIGHTONSCREEN | OPTION_LOOKAHEAD | OPTION_DISPLAYAMMO) +#endif extern ChrRecord *g_CurModelChr; diff --git a/src/game/gun.c b/src/game/gun.c index 32ee1808..9659aa90 100644 --- a/src/game/gun.c +++ b/src/game/gun.c @@ -1728,7 +1728,13 @@ void gunInitProjectileFromPlayer(ObjectRecord *obj, coord3d *targetpos, Mtxf *ar u8 rooms[2]; s32 pad_rooms; u8 pad_a[0x4c]; +#ifdef AVOID_UB + s32 sp54[0x14]; /* D545: bgFindRoomsAlongSegment below writes up to 0x14 room + numbers here; on N64 the overflow landed in `pad_a` + (0x4c = 19 more s32), on PC it could hit live locals */ +#else s32 sp54; +#endif s32 sp50; s32 pad_sp; @@ -1788,7 +1794,11 @@ void gunInitProjectileFromPlayer(ObjectRecord *obj, coord3d *targetpos, Mtxf *ar rooms[0] = bondviewGetCurrentPlayersRoom(); rooms[1] = 0xff; +#ifdef AVOID_UB + bgFindRoomsAlongSegment(bondviewGetCurrentPlayersPosition3(), &pos, rooms, obj->projectile->unkCC, sp54, &sp50, 0x14); +#else bgFindRoomsAlongSegment(bondviewGetCurrentPlayersPosition3(), &pos, rooms, obj->projectile->unkCC, &sp54, &sp50, 0x14); +#endif } } @@ -1807,7 +1817,13 @@ void generate_player_thrown_grenade(s32 hand) Mtxf spA0_a; struct WeaponObjRecord *wor; s32 new_prop_type; +#ifdef AVOID_UB + struct coord3d sp94; /* D549: bullet_path_from_screen_center writes a coord3d here; + on N64 the 8-byte overflow hit a dead stack slot, on PC it + can hit live locals (wor/new_prop_type/...) */ +#else s32 sp94; // sp148 +#endif struct coord3d base_speed_vec; // sp136 struct PropRecord* player_prop; // sp132 struct coord3d *bondprevpos; // sp128 @@ -1898,7 +1914,13 @@ void generate_player_thrown_knife(s32 hand) Mtxf spA0_a; s32 padding; s32 new_prop_type; +#ifdef AVOID_UB + struct coord3d sp94; /* D549: bullet_path_from_screen_center writes a coord3d here; + on N64 the 8-byte overflow hit a dead stack slot, on PC it + can hit live locals (wor/new_prop_type/...) */ +#else s32 sp94; +#endif struct coord3d base_speed_vec; Mtxf sp40_f; struct PropRecord* player_prop; @@ -1988,7 +2010,13 @@ void generate_player_thrown_object(s32 hand) Mtxf spA0_a; struct WeaponObjRecord *wor; s32 new_prop_type; +#ifdef AVOID_UB + struct coord3d sp94; /* D549: bullet_path_from_screen_center writes a coord3d here; + on N64 the 8-byte overflow hit a dead stack slot, on PC it + can hit live locals (wor/new_prop_type/...) */ +#else s32 sp94; // sp148 +#endif struct coord3d base_speed_vec; // sp136 struct PropRecord* player_prop; // sp132 struct coord3d *bondprevpos; // sp128 diff --git a/tools_pc/capture_p7.sh b/tools_pc/capture_p7.sh index 17222bd5..e5b4df24 100755 --- a/tools_pc/capture_p7.sh +++ b/tools_pc/capture_p7.sh @@ -1,20 +1,41 @@ #!/bin/bash # P7 golden capture on Linux (mirrors tools_pc/verify.sh's golden recipe). # -# Usage: capture_p7.sh [-b BUILD_DIR] : [: ...] +# Usage: capture_p7.sh [-b BUILD_DIR] [-p PLATFORM] [-j N] : [...] # BUILD_DIR build dir holding ge007.x86_64, relative to the repo root # (default: build-linux). The repo root is the parent of the # directory this script lives in, so the script can be invoked # from anywhere; no machine-specific path is baked in. -# Writes only inside the repo: /data/ge007.ini + ge007.eep (the -# pinned 640x480 ini and the canonical save, re-pinned before every level; -# the local save is only snapshotted for the exit restore), -# /ppm (the game's dump dir, removed after each level), and -# /tools_pc/golden//linux/. -# Per-level run logs go to /cap_.log: scratch, never tracked. +# PLATFORM golden set to write: tools_pc/golden/// +# (default: linux; `deck` = Steam Deck set, same exe/recipe). +# -j N run up to N levels concurrently (default 1); result lines +# print in the original level order. Each game quits via +# GE_QUITFRAME (the only kill is `timeout`'s own-child watchdog). +# PIXEL RESULTS ASSUME EACH INSTANCE HOLDS 60 fps (D117: the +# intro flyby is wall-clock paced): use -j 2-3 only on a strong +# GPU with nothing else loading it (no local LLM loaded); use +# -j 1 on weak boxes (Steam Deck, Intel HD 3000, ...). +# Isolation: every level runs with CWD = a fresh temp work dir holding its +# OWN data/ (copy of ge007..z64 + pccg-/ + pcmodels-/ + +# the canonical save tools_pc/golden/ge007.eep + the pinned 640x480 ini) and +# its own ppm/ (the game resolves data/ from its CWD first). The repo's +# data/ is only READ, never modified; the work dir is removed after each +# level. ROMID comes from /CMakeCache.txt, else the single +# data/ge007.*.z64 present, else ntsc-final (override: GE_ROMID). +# Writes only: /tools_pc/golden/// and the per-level +# run logs /cap_.log (scratch, never tracked). +# Windows: run from Git Bash/MSYS2 with -b build-pc -p win (no DISPLAY needed; +# the .exe resolves from /ge007.x86_64). +# Runs on a stripped copy of the repo on the device (Steam Deck Desktop +# Mode: DISPLAY=:0 bash, python3 + PIL, no compiler): put the prebuilt binary +# at /ge007.x86_64 and data/ + tools_pc/{verify.sh,golden/ge007.eep} +# in the copy. # DISPLAY must be inherited from the caller -- the capture needs a real GL # context, and there is deliberately no default display/XAUTHORITY here. # +# GE_FAKE_DECK=0 on every run: on a Steam Deck the D283 preset would force +# 1280x800 fullscreen over the pinned 640x480 ini (harmless elsewhere). +# # Recipe: the per-level windows/scripts/seed are READ OUT OF tools_pc/verify.sh # (golden_dump_for / golden_script_for / GOLDEN_SEED) so this tool and the gate # cannot drift apart -- see tools_pc/golden/README.md for what the windows are @@ -28,37 +49,43 @@ # playtest save is exactly this class of drift. (History: the P7 round of # 2026-10-04 ran with a cleared eep; that set was re-captured 2026-10-05, D525.) ROOT="$(cd "$(dirname "$0")/.." && pwd)" -BUILD_DIR=build-linux -if [ "${1:-}" = -b ]; then - [ -n "${2:-}" ] || { echo "error: -b needs a BUILD_DIR argument"; exit 2; } - BUILD_DIR="$2"; shift 2 -fi +BUILD_DIR=build-linux; PLATFORM=linux; JOBS=1 +while getopts "b:p:j:" opt; do + case "$opt" in + b) BUILD_DIR="$OPTARG" ;; + p) PLATFORM="$OPTARG" ;; + j) JOBS="$OPTARG" ;; + *) echo "usage: capture_p7.sh [-b BUILD_DIR] [-p PLATFORM] [-j N] : ..."; exit 2 ;; + esac +done +shift $((OPTIND - 1)) +case "$JOBS" in ''|*[!0-9]*|0) echo "error: -j needs a positive integer (got '$JOBS')"; exit 2 ;; esac +case "$PLATFORM" in ''|*[!A-Za-z0-9_-]*) echo "error: bad PLATFORM '$PLATFORM'"; exit 2 ;; esac [ "$#" -gt 0 ] || { echo "error: no : specs given"; exit 2; } EXE="$ROOT/$BUILD_DIR/ge007.x86_64" [ -x "$EXE" ] || { echo "error: no executable at $EXE (pass -b BUILD_DIR)"; exit 2; } -[ -n "${DISPLAY:-}" ] || { echo "error: DISPLAY is unset; the capture needs a real GL context (no default provided)"; exit 2; } +case "$(uname -s)" in + MINGW*|MSYS*|CYGWIN*) ;; # Windows (Git Bash / MSYS2): the desktop is the GL context; no DISPLAY + *) [ -n "${DISPLAY:-}" ] || { echo "error: DISPLAY is unset; the capture needs a real GL context (no default provided)"; exit 2; } ;; +esac +PY=""; for c in python3 python; do "$c" -c "import PIL" >/dev/null 2>&1 && { PY=$c; break; }; done +[ -n "$PY" ] || { echo "error: no python with Pillow (PIL) on PATH (needed to write the PNGs)"; exit 2; } VSH="$ROOT/tools_pc/verify.sh" [ -f "$VSH" ] || { echo "error: no tools_pc/verify.sh under $ROOT (the recipe lives there)"; exit 2; } CANON_EEP="$ROOT/tools_pc/golden/ge007.eep" [ -f "$CANON_EEP" ] || { echo "error: canonical golden save missing: $CANON_EEP (D529)"; exit 2; } -cd "$ROOT" # the loop's dump/golden paths are root-relative (ppm/, tools_pc/golden/) -EEP="$ROOT/data/ge007.eep" -EEP_ORIG=$(mktemp); EEP_HAD=0 -[ -f "$EEP" ] && { EEP_HAD=1; cp "$EEP" "$EEP_ORIG"; } -cp "$CANON_EEP" "$EEP" -INI="$ROOT/data/ge007.ini" -INI_ORIG="" -[ -f "$INI" ] && { INI_ORIG=$(mktemp); cp "$INI" "$INI_ORIG"; } -cleanup() { - [ -n "$INI_ORIG" ] && cp "$INI_ORIG" "$INI" 2>/dev/null - if [ "$EEP_HAD" = 1 ]; then cp "$EEP_ORIG" "$EEP" 2>/dev/null; else rm -f "$EEP" 2>/dev/null; fi - rm -f "$INI_ORIG" "$EEP_ORIG" 2>/dev/null -} -# D534: restore on EXIT only; INT/TERM just exit (which fires EXIT), so an -# interrupted capture cannot continue past the restore with the snapshot gone. -trap cleanup EXIT -trap 'exit 130' INT -trap 'exit 143' TERM +cd "$ROOT" # golden paths are root-relative (tools_pc/golden/) + +ROMID="${GE_ROMID:-$(sed -n 's/^ROMID:[A-Za-z]*=//p' "$ROOT/$BUILD_DIR/CMakeCache.txt" 2>/dev/null | head -n1)}" +if [ -z "$ROMID" ]; then + roms=$(ls "$ROOT"/data/ge007.*.z64 2>/dev/null) + if [ "$(printf '%s\n' "$roms" | grep -c .)" = 1 ]; then + ROMID=$(basename "$roms" .z64); ROMID="${ROMID#ge007.}" + else ROMID=ntsc-final; fi +fi +for need in "ge007.$ROMID.z64" "pccg-$ROMID" "pcmodels-$ROMID"; do + [ -e "$ROOT/data/$need" ] || { echo "error: $ROOT/data/$need missing (needed for the per-run work dir; set GE_ROMID to pick another region)"; exit 2; } +done # recipe comes from the tracked gate tool, not from this file eval "$(grep -m1 '^GOLDEN_DUMP=' "$VSH")" @@ -66,38 +93,72 @@ eval "$(grep -m1 '^golden_dump_for()' "$VSH")" eval "$(grep -m1 '^golden_script_for()' "$VSH")" eval "$(grep -m1 '^GOLDEN_SEED=' "$VSH")" -for spec in "$@"; do - name="${spec%%:*}"; num="${spec##*:}" - [ "$name" != "$num" ] || { echo "error: bad spec $spec"; exit 2; } +TMPROOT=$(mktemp -d) +trap 'kill $(jobs -rp) 2>/dev/null; rm -rf "$TMPROOT"' EXIT +trap 'exit 130' INT +trap 'exit 143' TERM + +# capture_one NAME NUM -> prints the result line on stdout +capture_one() { + local name="$1" num="$2" dump script lo hi step last qf w t0 t1 rc n f stem dump=$(golden_dump_for "$name") script=$(golden_script_for "$name") IFS='-:' read -r lo hi step <<< "$dump" last=$(( lo + step * ((hi - lo) / step) )) qf=$(( last + 2 )) - printf '[Window]\nWidth = 640\nHeight = 480\n' > "$INI" - # D531: re-pin the CANONICAL save before every level (the frame depends on - # the save's CONTENT, D529). The D524-era re-pin restored $EEP_ORIG (the - # local pre-capture save, or an empty file when none existed), so the - # pre-loop canonical install below was silently overwritten from level 1: - # the 2026-10-05 box re-round ran under the box's local save, not the - # canonical one. EEP_ORIG is kept only for the exit restore. - cp "$CANON_EEP" "$EEP" - rm -f ge007.crash.log - rm -rf ppm + w="$TMPROOT/$name"; rm -rf "$w"; mkdir -p "$w/data" + # D531/D529: every level gets the CANONICAL save (the frame depends on its + # content) and the pinned defaults-only 640x480 ini -- in its OWN data/. + cp "$ROOT/data/ge007.$ROMID.z64" "$w/data/" && + cp -r "$ROOT/data/pccg-$ROMID" "$ROOT/data/pcmodels-$ROMID" "$w/data/" && + cp "$CANON_EEP" "$w/data/ge007.eep" && + printf '[Window]\nWidth = 640\nHeight = 480\n' > "$w/data/ge007.ini" || + { echo "== $name rc=setup-failed frames=0 secs=0 window=$dump"; return; } t0=$(date +%s) - ( export GE_PCDUMP="$dump" GE_QUITFRAME="$qf" GE_INPUTSCRIPT="$script" \ - GE_RSEED="$GOLDEN_SEED"; timeout 300 "$EXE" -level_"$num" ) > "cap_$name.log" 2>&1 + ( cd "$w" && export GE_PCDUMP="$dump" GE_QUITFRAME="$qf" GE_INPUTSCRIPT="$script" \ + GE_RSEED="$GOLDEN_SEED" GE_FAKE_DECK=0; timeout 300 "$EXE" -level_"$num" ) > "$ROOT/cap_$name.log" 2>&1 rc=$? n=0 - if [ -d ppm ]; then - mkdir -p "tools_pc/golden/$name/linux" - for f in ppm/*.ppm; do + if [ -d "$w/ppm" ]; then + mkdir -p "$ROOT/tools_pc/golden/$name/$PLATFORM" + for f in "$w"/ppm/*.ppm; do + [ -f "$f" ] || continue stem=$(basename "$f" .ppm) - python3 -c "from PIL import Image; Image.open('$f').save('tools_pc/golden/$name/linux/$stem.png')" + "$PY" -c "import sys; from PIL import Image; Image.open(sys.argv[1]).save(sys.argv[2])" \ + "$f" "$ROOT/tools_pc/golden/$name/$PLATFORM/$stem.png" n=$((n+1)) done - rm -rf ppm fi + rm -rf "$w" t1=$(date +%s) echo "== $name rc=$rc frames=$n secs=$((t1-t0)) window=$dump" +} + +# validate every spec up front, then run up to JOBS at once; results are +# replayed in the original order +i=0 +for spec in "$@"; do + name="${spec%%:*}"; num="${spec##*:}" + [ "$name" != "$num" ] || { echo "error: bad spec $spec"; exit 2; } done +nxt=0 +flush_done() { + local blocking="${1:-0}" + while [ "$nxt" -lt "$i" ] || { [ "$blocking" = 1 ] && [ "$nxt" -lt "$total" ]; }; do + if [ ! -f "$TMPROOT/res.$nxt" ]; then + [ "$blocking" = 1 ] && { sleep 0.2; continue; } + return + fi + cat "$TMPROOT/res.$nxt"; nxt=$((nxt + 1)) + done +} +total=$# +for spec in "$@"; do + name="${spec%%:*}"; num="${spec##*:}" + while [ "$(jobs -rp | wc -l)" -ge "$JOBS" ]; do sleep 0.2; flush_done; done + ( capture_one "$name" "$num" > "$TMPROOT/res.$i.tmp"; mv "$TMPROOT/res.$i.tmp" "$TMPROOT/res.$i" ) & + i=$((i + 1)) + flush_done +done +flush_done 1 +wait diff --git a/tools_pc/docs_budget.py b/tools_pc/docs_budget.py index c778ea23..4f79a3d9 100644 --- a/tools_pc/docs_budget.py +++ b/tools_pc/docs_budget.py @@ -27,7 +27,7 @@ BUDGETS = { "docs/HANDOFF.md": 3000, "docs/ROADMAP.md": 12000, "docs/porting-notes.md": 60000, - "docs/dev/findings.md": 600000, + "docs/dev/findings.md": 650000, "docs/dev/findings-index.csv": 40000, } diff --git a/tools_pc/golden/README.md b/tools_pc/golden/README.md index b21d0793..8caf258c 100644 --- a/tools_pc/golden/README.md +++ b/tools_pc/golden/README.md @@ -3,11 +3,13 @@ `//frame_NNNNNN.png`: reference `GE_PCDUMP` captures for `tools_pc/verify.sh`, one folder per solo level (all 21, `cuba` = the ending) and per platform (`win`, and `linux` — 21 levels x 3 frames on each, captured -2026-10-05 at the same stems). +2026-10-05 at the same stems; `deck` = the Steam Deck set, captured with +`capture_p7.sh -p deck`, per-level pixel limits = the linux ones for now). **Recipe** (2026-10-02 re-base; `verify.sh` applies all of it): - `data/ge007.ini` pinned to ONLY `[Window] Width = 640, Height = 480` (every - other option at its compiled-in default; verify.sh backs yours up). + other option at its compiled-in default). Each run gets its own copy in a + temp work dir (see Isolation below); your `data/ge007.ini` is never touched. - `GE_RSEED=0x0123456789abcdef` (pinned PRNG). - `GE_INPUTSCRIPT="20:START"` (skips the intro flyby; the scripted pad is the only input). Window per level, exactly as `verify.sh`'s `golden_dump_for` @@ -26,14 +28,27 @@ and per platform (`win`, and `linux` — 21 levels x 3 frames on each, captured each of the four candidate saves, 21.557% with none) was only ever measured on Archives, and the 2026-10-05 A/B refutes it (facility 177.47 scene-level on the 2026-10-05 playtest save, 0.0-0.25 on every other candidate on disk). `verify.sh` - and `capture_p7.sh` install the canonical eep the same way they pin the ini - (snapshot the local file if any, install the canonical one, restore-or-remove on - exit), so the gate is reproducible on a fresh clone and in CI. A local playtest + and `capture_p7.sh` install the canonical eep into each run's own work dir + (the repo's `data/` is never modified), so the gate is reproducible on a fresh clone and in CI. A local playtest save is **never** the gate's input. The linux 63-frame set was re-captured on the box under the canonical eep 2026-10-05 (D531; the 2026-10-05 re-round, D530, turned out to have run under the box-LOCAL save — the `capture_p7.sh` per-level re-pin bug, fixed in D531) and is valid. - Every run ends via `GE_QUITFRAME` (orderly quit; never a hard kill, D344). +- **Isolation + parallelism:** `verify.sh` and `capture_p7.sh` run every level with + CWD = a fresh temp work dir holding its own `data/` (ROM, `pccg-/`, + `pcmodels-/`, the canonical save, the pinned ini) and its own `ppm/`; the + repo's `data/` is only read (the old pin/restore of `data/ge007.ini`/`.eep` is + gone). That makes levels safe to run concurrently: + `tools_pc/verify.sh sweep -j 3` / `tools_pc/capture_p7.sh -j 2 ...`. Pixel + results assume each instance holds 60 fps (D117): `-j 2-3` only on a strong GPU + with nothing else loading it (no local LLM loaded), `-j 1` on weak boxes. + Measured 2026-10-05 with a busy GPU (an LLM resident at ~100%): Surface 1's + frame 900 failed at `-j 2` and `-j 3` and passed at `-j 1`. +- Windows set: from Git Bash, `bash tools_pc/capture_p7.sh -b build-pc -p win -j 3 : ...` + (no DISPLAY on Windows; `-j 1` if the GPU is loaded). +- Deck set: on the device (Desktop Mode, `DISPLAY=:0`, prebuilt binary at + `/ge007.x86_64`): `bash tools_pc/capture_p7.sh -b -p deck : ...`. **Gate** (two tiers): the structural framediff (cell means, coverage, phash) catches gross breakage; then a per-pixel `--exact --tol 2` check @@ -143,3 +158,69 @@ capture with the recipe above and replace `//*.png`. ssh shell) so `verify.sh`'s fail-closed probe keeps the pixcount/framediff skip even though the game's readback works; the cross-platform numbers are computed on Windows against the merged linux PNGs. See D525. +- **2026-10-05 (isolated + parallel capture, `deck` platform):** `verify.sh` and + `capture_p7.sh` now run each level in its own temp work dir (own `data/` copy, + pinned ini, canonical save, `ppm/`) instead of pinning/restoring the repo's + `data/`; added `-j N` (concurrent levels, verdicts in level order) and a `deck` + platform (`verify.sh --platform deck`, `capture_p7.sh -p deck`). +- **2026-10-05 (D540 re-base #2, win only):** final fog design (far clip scales + by min(Fog, Draw distance); the game's fog math untouched, so fogged levels + render the N64's own fog at defaults). Re-captured whole levels: aztec, cradle, + dam, egypt, jungle, statue, surface1, streets (24 frames, win). Side-by-sides: + fog only (Cradle's distant truss is a faint ghost, Frigate's sea unchanged); + props, characters, gun and HUD present, no overlays. Pass-A vs pass-B + (over tol 2): <=0.30% (aztec 0.19, egypt 0.26, statue 0.30), cradle/dam/jungle/ + surface1 <=0.003%; streets 0.75-2.1% (timer digits jitter; promoted from the + pass whose state the confirming runs reproduce). Confirming pass 21/21 PASS + (silo failed once with worst_cell 127 and passed on the single re-run; streets + failed twice before its re-promotion, then passed twice). The `/linux/` + set is stale on the re-captured levels until the box re-captures. +- **2026-10-05 (D540, linux re-base + first `deck` set):** same final fog code, + captured concurrently on the two boxes. **linux** (X220, `-j 1`, rebuilt from + the working tree): pass A against the existing set; 13 levels already within + limit and were left; re-captured aztec, cradle, egypt, statue, surface1, dam, + jungle (A/B 0.00-0.45%, dam 0.000%, jungle 1.7% under its 3% limit; dam/jungle + differ from the old set by the fog change, 8.4% / 37.9%). streets/silo A/B run + 0.3-3.0% on the box (left as captured earlier; they match their goldens). + **deck** (Steam Deck, Desktop Mode, the X220-built binary + bundled SDL2, + `capture_p7.sh -p deck -j 2`): all 21 levels, A/B <= 0.04% except jungle + 1.65% and statue 0.44%; `-j 3` failed its validation on the Deck (archives + 4.96%, frigate 7-11%), `-j 2` agreed (<= 0.031%), 286 s per 21-level pass. + Both tools now set `GE_FAKE_DECK=0` on every run: on a Deck the D283 preset + otherwise forces 1280x800 fullscreen over the pinned ini. Also on 2026-10-05, + before the final fog design, a first D540 win re-base (17 levels) was + superseded by re-base #2 above; the levels it alone touched pass re-base #2's + confirming pass. **Run sweeps with the GPU otherwise idle** (no local LLM + loaded): 60 fps pacing is part of the gate (D117). +- **2026-10-06 (v0.5.0 re-base, linux + deck):** D543 (CPU near-plane clip + + per-vertex N64 fog) and D546 (Draw/LOD 2.0x default) re-captured on both + boxes concurrently from one X220-built binary (tree `21c5b5c5`), canonical + eep md5-checked on each box. Two full passes each (X220 `-j 1` 551 s/pass, + Deck `-j 2` 286 s/pass, every run rc 0). Pass A vs B over tol 2: deck 63/63 + within limit; linux 60/63 — Dam drifted 5.2-6.0% between A and B (Bond's + settle position after the wall-clock-paced flyby, D117), so two Dam-only + runs C/D were taken: C vs D <= 0.022% and C vs the previous golden <= 0.023%, + so C was promoted (A and B were the outliers). Change vs the previous set is + concentrated where fog/draw distance bites: Statue ~26%, Surface 2 20-27%, + Streets 19-20%, Surface 1 ~13%, Depot ~10%, Cradle ~5%; indoor levels + <= 0.4%. Cradle and Frigate compared by eye against 1964/GEPD stills: match. + **win (same day, same tree):** `capture_p7.sh` now runs on Windows too + (Git Bash; `-b build-pc -p win`, no DISPLAY; picks the python that has + Pillow). Two passes at `-j 3` on the dev box (190 s each, every run rc 0, + a local LLM resident on the GPU): 62/63 A/B-stable; Streets 1400 jittered + 1.5% (timer digits), two Streets-only `-j 1` runs C/D sat within 0.81% of + pass B, so B was promoted for Streets, A elsewhere. Deltas vs the previous + win set mirror linux/deck (Statue ~26%, Surface 2 ~27%, Streets ~20%, + Surface 1 ~13%, Depot ~10%, Cradle ~5%). Cross-platform spread on the new + sets, over tol 2, non-Cuba: win-linux 0.406-4.553%, win-deck 0.188-3.264%, + linux-deck 0.281-3.828% (informational, not a parity gate). +- **2026-10-06 (D553, near-clip fog recomputed):** Windows sweep after the + change flagged surface1/depot/cradle; re-captured surface1/depot/statue/ + cradle on all three platforms, two passes each. Only **cradle** really + moved (~4.9% over tol 2, A/B <= 0.12% on every platform; the near walkway + is very slightly lighter, still in line with the 1964 still) and was + re-based x3. depot/surface1 "regressions" were `-j 3` pacing flakes with a + local LLM on the GPU: captured alone, depot is pixel-identical to its + golden and surface1/statue are within 0.5% -- those sets were left as-is. + The Deck must be in Desktop Mode for captures (Game Mode = Wayland/ + gamescope: every run times out with 0 frames). diff --git a/tools_pc/golden/archives/deck/frame_000900.png b/tools_pc/golden/archives/deck/frame_000900.png new file mode 100644 index 00000000..fe4b5be8 Binary files /dev/null and b/tools_pc/golden/archives/deck/frame_000900.png differ diff --git a/tools_pc/golden/archives/deck/frame_001200.png b/tools_pc/golden/archives/deck/frame_001200.png new file mode 100644 index 00000000..5028fd9e Binary files /dev/null and b/tools_pc/golden/archives/deck/frame_001200.png differ diff --git a/tools_pc/golden/archives/deck/frame_001500.png b/tools_pc/golden/archives/deck/frame_001500.png new file mode 100644 index 00000000..4ad3a5a5 Binary files /dev/null and b/tools_pc/golden/archives/deck/frame_001500.png differ diff --git a/tools_pc/golden/archives/linux/frame_000900.png b/tools_pc/golden/archives/linux/frame_000900.png index a076d333..d6191b17 100644 Binary files a/tools_pc/golden/archives/linux/frame_000900.png and b/tools_pc/golden/archives/linux/frame_000900.png differ diff --git a/tools_pc/golden/archives/linux/frame_001200.png b/tools_pc/golden/archives/linux/frame_001200.png index 6db04eb8..67384b74 100644 Binary files a/tools_pc/golden/archives/linux/frame_001200.png and b/tools_pc/golden/archives/linux/frame_001200.png differ diff --git a/tools_pc/golden/archives/linux/frame_001500.png b/tools_pc/golden/archives/linux/frame_001500.png index 256c0dba..d967c643 100644 Binary files a/tools_pc/golden/archives/linux/frame_001500.png and b/tools_pc/golden/archives/linux/frame_001500.png differ diff --git a/tools_pc/golden/archives/win/frame_000900.png b/tools_pc/golden/archives/win/frame_000900.png index 14e7f075..c4b8c8ad 100644 Binary files a/tools_pc/golden/archives/win/frame_000900.png and b/tools_pc/golden/archives/win/frame_000900.png differ diff --git a/tools_pc/golden/archives/win/frame_001200.png b/tools_pc/golden/archives/win/frame_001200.png index 9826a46b..d0de3b42 100644 Binary files a/tools_pc/golden/archives/win/frame_001200.png and b/tools_pc/golden/archives/win/frame_001200.png differ diff --git a/tools_pc/golden/archives/win/frame_001500.png b/tools_pc/golden/archives/win/frame_001500.png index e05e96ae..0cc88fd5 100644 Binary files a/tools_pc/golden/archives/win/frame_001500.png and b/tools_pc/golden/archives/win/frame_001500.png differ diff --git a/tools_pc/golden/aztec/deck/frame_000900.png b/tools_pc/golden/aztec/deck/frame_000900.png new file mode 100644 index 00000000..2cd4816a Binary files /dev/null and b/tools_pc/golden/aztec/deck/frame_000900.png differ diff --git a/tools_pc/golden/aztec/deck/frame_001200.png b/tools_pc/golden/aztec/deck/frame_001200.png new file mode 100644 index 00000000..e8ad716c Binary files /dev/null and b/tools_pc/golden/aztec/deck/frame_001200.png differ diff --git a/tools_pc/golden/aztec/deck/frame_001500.png b/tools_pc/golden/aztec/deck/frame_001500.png new file mode 100644 index 00000000..c0f71821 Binary files /dev/null and b/tools_pc/golden/aztec/deck/frame_001500.png differ diff --git a/tools_pc/golden/aztec/linux/frame_000900.png b/tools_pc/golden/aztec/linux/frame_000900.png index 99dcfa36..774daf6f 100644 Binary files a/tools_pc/golden/aztec/linux/frame_000900.png and b/tools_pc/golden/aztec/linux/frame_000900.png differ diff --git a/tools_pc/golden/aztec/linux/frame_001200.png b/tools_pc/golden/aztec/linux/frame_001200.png index 3ce51307..3816f61d 100644 Binary files a/tools_pc/golden/aztec/linux/frame_001200.png and b/tools_pc/golden/aztec/linux/frame_001200.png differ diff --git a/tools_pc/golden/aztec/linux/frame_001500.png b/tools_pc/golden/aztec/linux/frame_001500.png index aa1f206b..29146902 100644 Binary files a/tools_pc/golden/aztec/linux/frame_001500.png and b/tools_pc/golden/aztec/linux/frame_001500.png differ diff --git a/tools_pc/golden/aztec/win/frame_000900.png b/tools_pc/golden/aztec/win/frame_000900.png index c05e209a..3577ce0f 100644 Binary files a/tools_pc/golden/aztec/win/frame_000900.png and b/tools_pc/golden/aztec/win/frame_000900.png differ diff --git a/tools_pc/golden/aztec/win/frame_001200.png b/tools_pc/golden/aztec/win/frame_001200.png index c8148ce9..f3955137 100644 Binary files a/tools_pc/golden/aztec/win/frame_001200.png and b/tools_pc/golden/aztec/win/frame_001200.png differ diff --git a/tools_pc/golden/aztec/win/frame_001500.png b/tools_pc/golden/aztec/win/frame_001500.png index 7875234e..0db736a5 100644 Binary files a/tools_pc/golden/aztec/win/frame_001500.png and b/tools_pc/golden/aztec/win/frame_001500.png differ diff --git a/tools_pc/golden/bunker1/deck/frame_000900.png b/tools_pc/golden/bunker1/deck/frame_000900.png new file mode 100644 index 00000000..560c7cf2 Binary files /dev/null and b/tools_pc/golden/bunker1/deck/frame_000900.png differ diff --git a/tools_pc/golden/bunker1/deck/frame_001200.png b/tools_pc/golden/bunker1/deck/frame_001200.png new file mode 100644 index 00000000..18ba1a37 Binary files /dev/null and b/tools_pc/golden/bunker1/deck/frame_001200.png differ diff --git a/tools_pc/golden/bunker1/deck/frame_001500.png b/tools_pc/golden/bunker1/deck/frame_001500.png new file mode 100644 index 00000000..ed387267 Binary files /dev/null and b/tools_pc/golden/bunker1/deck/frame_001500.png differ diff --git a/tools_pc/golden/bunker1/linux/frame_000900.png b/tools_pc/golden/bunker1/linux/frame_000900.png index 360cabfa..39be63c9 100644 Binary files a/tools_pc/golden/bunker1/linux/frame_000900.png and b/tools_pc/golden/bunker1/linux/frame_000900.png differ diff --git a/tools_pc/golden/bunker1/linux/frame_001200.png b/tools_pc/golden/bunker1/linux/frame_001200.png index e5cf3f41..fba85095 100644 Binary files a/tools_pc/golden/bunker1/linux/frame_001200.png and b/tools_pc/golden/bunker1/linux/frame_001200.png differ diff --git a/tools_pc/golden/bunker1/linux/frame_001500.png b/tools_pc/golden/bunker1/linux/frame_001500.png index 8426a615..d9fd6cd7 100644 Binary files a/tools_pc/golden/bunker1/linux/frame_001500.png and b/tools_pc/golden/bunker1/linux/frame_001500.png differ diff --git a/tools_pc/golden/bunker1/win/frame_000900.png b/tools_pc/golden/bunker1/win/frame_000900.png index b3f79614..c83cda58 100644 Binary files a/tools_pc/golden/bunker1/win/frame_000900.png and b/tools_pc/golden/bunker1/win/frame_000900.png differ diff --git a/tools_pc/golden/bunker1/win/frame_001200.png b/tools_pc/golden/bunker1/win/frame_001200.png index 0a3fd2db..74198c7c 100644 Binary files a/tools_pc/golden/bunker1/win/frame_001200.png and b/tools_pc/golden/bunker1/win/frame_001200.png differ diff --git a/tools_pc/golden/bunker1/win/frame_001500.png b/tools_pc/golden/bunker1/win/frame_001500.png index 42b4b786..9057f234 100644 Binary files a/tools_pc/golden/bunker1/win/frame_001500.png and b/tools_pc/golden/bunker1/win/frame_001500.png differ diff --git a/tools_pc/golden/bunker2/deck/frame_000900.png b/tools_pc/golden/bunker2/deck/frame_000900.png new file mode 100644 index 00000000..6f0a16f5 Binary files /dev/null and b/tools_pc/golden/bunker2/deck/frame_000900.png differ diff --git a/tools_pc/golden/bunker2/deck/frame_001200.png b/tools_pc/golden/bunker2/deck/frame_001200.png new file mode 100644 index 00000000..fa0b2206 Binary files /dev/null and b/tools_pc/golden/bunker2/deck/frame_001200.png differ diff --git a/tools_pc/golden/bunker2/deck/frame_001500.png b/tools_pc/golden/bunker2/deck/frame_001500.png new file mode 100644 index 00000000..ad32e6dd Binary files /dev/null and b/tools_pc/golden/bunker2/deck/frame_001500.png differ diff --git a/tools_pc/golden/bunker2/linux/frame_000900.png b/tools_pc/golden/bunker2/linux/frame_000900.png index 2ef51266..60acee0b 100644 Binary files a/tools_pc/golden/bunker2/linux/frame_000900.png and b/tools_pc/golden/bunker2/linux/frame_000900.png differ diff --git a/tools_pc/golden/bunker2/linux/frame_001200.png b/tools_pc/golden/bunker2/linux/frame_001200.png index 56ed7c2f..6fe07737 100644 Binary files a/tools_pc/golden/bunker2/linux/frame_001200.png and b/tools_pc/golden/bunker2/linux/frame_001200.png differ diff --git a/tools_pc/golden/bunker2/linux/frame_001500.png b/tools_pc/golden/bunker2/linux/frame_001500.png index 2304acac..5bc31dd9 100644 Binary files a/tools_pc/golden/bunker2/linux/frame_001500.png and b/tools_pc/golden/bunker2/linux/frame_001500.png differ diff --git a/tools_pc/golden/bunker2/win/frame_000900.png b/tools_pc/golden/bunker2/win/frame_000900.png index 587aa839..deef5a6d 100644 Binary files a/tools_pc/golden/bunker2/win/frame_000900.png and b/tools_pc/golden/bunker2/win/frame_000900.png differ diff --git a/tools_pc/golden/bunker2/win/frame_001200.png b/tools_pc/golden/bunker2/win/frame_001200.png index 45c94466..636600eb 100644 Binary files a/tools_pc/golden/bunker2/win/frame_001200.png and b/tools_pc/golden/bunker2/win/frame_001200.png differ diff --git a/tools_pc/golden/bunker2/win/frame_001500.png b/tools_pc/golden/bunker2/win/frame_001500.png index 063514ec..134859b5 100644 Binary files a/tools_pc/golden/bunker2/win/frame_001500.png and b/tools_pc/golden/bunker2/win/frame_001500.png differ diff --git a/tools_pc/golden/caverns/deck/frame_000900.png b/tools_pc/golden/caverns/deck/frame_000900.png new file mode 100644 index 00000000..07463a44 Binary files /dev/null and b/tools_pc/golden/caverns/deck/frame_000900.png differ diff --git a/tools_pc/golden/caverns/deck/frame_001200.png b/tools_pc/golden/caverns/deck/frame_001200.png new file mode 100644 index 00000000..f2cfb8f5 Binary files /dev/null and b/tools_pc/golden/caverns/deck/frame_001200.png differ diff --git a/tools_pc/golden/caverns/deck/frame_001500.png b/tools_pc/golden/caverns/deck/frame_001500.png new file mode 100644 index 00000000..52427afc Binary files /dev/null and b/tools_pc/golden/caverns/deck/frame_001500.png differ diff --git a/tools_pc/golden/caverns/linux/frame_000900.png b/tools_pc/golden/caverns/linux/frame_000900.png index edfd2290..6891efbf 100644 Binary files a/tools_pc/golden/caverns/linux/frame_000900.png and b/tools_pc/golden/caverns/linux/frame_000900.png differ diff --git a/tools_pc/golden/caverns/linux/frame_001200.png b/tools_pc/golden/caverns/linux/frame_001200.png index ab7d1113..a73da465 100644 Binary files a/tools_pc/golden/caverns/linux/frame_001200.png and b/tools_pc/golden/caverns/linux/frame_001200.png differ diff --git a/tools_pc/golden/caverns/linux/frame_001500.png b/tools_pc/golden/caverns/linux/frame_001500.png index ce319d0b..64051025 100644 Binary files a/tools_pc/golden/caverns/linux/frame_001500.png and b/tools_pc/golden/caverns/linux/frame_001500.png differ diff --git a/tools_pc/golden/caverns/win/frame_000900.png b/tools_pc/golden/caverns/win/frame_000900.png index 08d84559..b9493454 100644 Binary files a/tools_pc/golden/caverns/win/frame_000900.png and b/tools_pc/golden/caverns/win/frame_000900.png differ diff --git a/tools_pc/golden/caverns/win/frame_001200.png b/tools_pc/golden/caverns/win/frame_001200.png index f45bd1e7..0d11d1db 100644 Binary files a/tools_pc/golden/caverns/win/frame_001200.png and b/tools_pc/golden/caverns/win/frame_001200.png differ diff --git a/tools_pc/golden/caverns/win/frame_001500.png b/tools_pc/golden/caverns/win/frame_001500.png index 1d0c93f7..a257e9c4 100644 Binary files a/tools_pc/golden/caverns/win/frame_001500.png and b/tools_pc/golden/caverns/win/frame_001500.png differ diff --git a/tools_pc/golden/control/deck/frame_000900.png b/tools_pc/golden/control/deck/frame_000900.png new file mode 100644 index 00000000..9b461016 Binary files /dev/null and b/tools_pc/golden/control/deck/frame_000900.png differ diff --git a/tools_pc/golden/control/deck/frame_001200.png b/tools_pc/golden/control/deck/frame_001200.png new file mode 100644 index 00000000..cf28c9b8 Binary files /dev/null and b/tools_pc/golden/control/deck/frame_001200.png differ diff --git a/tools_pc/golden/control/deck/frame_001500.png b/tools_pc/golden/control/deck/frame_001500.png new file mode 100644 index 00000000..519d9a3e Binary files /dev/null and b/tools_pc/golden/control/deck/frame_001500.png differ diff --git a/tools_pc/golden/control/linux/frame_000900.png b/tools_pc/golden/control/linux/frame_000900.png index 087451d1..84f4c57c 100644 Binary files a/tools_pc/golden/control/linux/frame_000900.png and b/tools_pc/golden/control/linux/frame_000900.png differ diff --git a/tools_pc/golden/control/linux/frame_001200.png b/tools_pc/golden/control/linux/frame_001200.png index 19de9180..2c6c68e1 100644 Binary files a/tools_pc/golden/control/linux/frame_001200.png and b/tools_pc/golden/control/linux/frame_001200.png differ diff --git a/tools_pc/golden/control/linux/frame_001500.png b/tools_pc/golden/control/linux/frame_001500.png index 7662bd20..726b9c8f 100644 Binary files a/tools_pc/golden/control/linux/frame_001500.png and b/tools_pc/golden/control/linux/frame_001500.png differ diff --git a/tools_pc/golden/control/win/frame_000900.png b/tools_pc/golden/control/win/frame_000900.png index ec1eb171..071284dd 100644 Binary files a/tools_pc/golden/control/win/frame_000900.png and b/tools_pc/golden/control/win/frame_000900.png differ diff --git a/tools_pc/golden/control/win/frame_001200.png b/tools_pc/golden/control/win/frame_001200.png index 76e5c388..d9b87ee8 100644 Binary files a/tools_pc/golden/control/win/frame_001200.png and b/tools_pc/golden/control/win/frame_001200.png differ diff --git a/tools_pc/golden/control/win/frame_001500.png b/tools_pc/golden/control/win/frame_001500.png index 937e1b69..cc4c542d 100644 Binary files a/tools_pc/golden/control/win/frame_001500.png and b/tools_pc/golden/control/win/frame_001500.png differ diff --git a/tools_pc/golden/cradle/deck/frame_001000.png b/tools_pc/golden/cradle/deck/frame_001000.png new file mode 100644 index 00000000..9810b09a Binary files /dev/null and b/tools_pc/golden/cradle/deck/frame_001000.png differ diff --git a/tools_pc/golden/cradle/deck/frame_001200.png b/tools_pc/golden/cradle/deck/frame_001200.png new file mode 100644 index 00000000..9df69998 Binary files /dev/null and b/tools_pc/golden/cradle/deck/frame_001200.png differ diff --git a/tools_pc/golden/cradle/deck/frame_001400.png b/tools_pc/golden/cradle/deck/frame_001400.png new file mode 100644 index 00000000..3df70ee1 Binary files /dev/null and b/tools_pc/golden/cradle/deck/frame_001400.png differ diff --git a/tools_pc/golden/cradle/linux/frame_001000.png b/tools_pc/golden/cradle/linux/frame_001000.png index 5b52e902..a76eeb71 100644 Binary files a/tools_pc/golden/cradle/linux/frame_001000.png and b/tools_pc/golden/cradle/linux/frame_001000.png differ diff --git a/tools_pc/golden/cradle/linux/frame_001200.png b/tools_pc/golden/cradle/linux/frame_001200.png index 40eb65a2..d312a932 100644 Binary files a/tools_pc/golden/cradle/linux/frame_001200.png and b/tools_pc/golden/cradle/linux/frame_001200.png differ diff --git a/tools_pc/golden/cradle/linux/frame_001400.png b/tools_pc/golden/cradle/linux/frame_001400.png index 7f0a73c8..c352b641 100644 Binary files a/tools_pc/golden/cradle/linux/frame_001400.png and b/tools_pc/golden/cradle/linux/frame_001400.png differ diff --git a/tools_pc/golden/cradle/win/frame_001000.png b/tools_pc/golden/cradle/win/frame_001000.png index eba7522f..ef85aa1e 100644 Binary files a/tools_pc/golden/cradle/win/frame_001000.png and b/tools_pc/golden/cradle/win/frame_001000.png differ diff --git a/tools_pc/golden/cradle/win/frame_001200.png b/tools_pc/golden/cradle/win/frame_001200.png index cdb575ee..3333425d 100644 Binary files a/tools_pc/golden/cradle/win/frame_001200.png and b/tools_pc/golden/cradle/win/frame_001200.png differ diff --git a/tools_pc/golden/cradle/win/frame_001400.png b/tools_pc/golden/cradle/win/frame_001400.png index ce8fea8b..6f4ac447 100644 Binary files a/tools_pc/golden/cradle/win/frame_001400.png and b/tools_pc/golden/cradle/win/frame_001400.png differ diff --git a/tools_pc/golden/cuba/deck/frame_000300.png b/tools_pc/golden/cuba/deck/frame_000300.png new file mode 100644 index 00000000..fc1d0bd4 Binary files /dev/null and b/tools_pc/golden/cuba/deck/frame_000300.png differ diff --git a/tools_pc/golden/cuba/deck/frame_000600.png b/tools_pc/golden/cuba/deck/frame_000600.png new file mode 100644 index 00000000..02936be9 Binary files /dev/null and b/tools_pc/golden/cuba/deck/frame_000600.png differ diff --git a/tools_pc/golden/cuba/deck/frame_000900.png b/tools_pc/golden/cuba/deck/frame_000900.png new file mode 100644 index 00000000..63efa1d2 Binary files /dev/null and b/tools_pc/golden/cuba/deck/frame_000900.png differ diff --git a/tools_pc/golden/cuba/linux/frame_000300.png b/tools_pc/golden/cuba/linux/frame_000300.png index ab4695be..79106aba 100644 Binary files a/tools_pc/golden/cuba/linux/frame_000300.png and b/tools_pc/golden/cuba/linux/frame_000300.png differ diff --git a/tools_pc/golden/cuba/linux/frame_000600.png b/tools_pc/golden/cuba/linux/frame_000600.png index 36df6c51..8adce9b4 100644 Binary files a/tools_pc/golden/cuba/linux/frame_000600.png and b/tools_pc/golden/cuba/linux/frame_000600.png differ diff --git a/tools_pc/golden/cuba/linux/frame_000900.png b/tools_pc/golden/cuba/linux/frame_000900.png index fbab7016..813741d4 100644 Binary files a/tools_pc/golden/cuba/linux/frame_000900.png and b/tools_pc/golden/cuba/linux/frame_000900.png differ diff --git a/tools_pc/golden/cuba/win/frame_000300.png b/tools_pc/golden/cuba/win/frame_000300.png index 8a848bcd..6ed68bd9 100644 Binary files a/tools_pc/golden/cuba/win/frame_000300.png and b/tools_pc/golden/cuba/win/frame_000300.png differ diff --git a/tools_pc/golden/cuba/win/frame_000600.png b/tools_pc/golden/cuba/win/frame_000600.png index b3081237..515a7ca4 100644 Binary files a/tools_pc/golden/cuba/win/frame_000600.png and b/tools_pc/golden/cuba/win/frame_000600.png differ diff --git a/tools_pc/golden/cuba/win/frame_000900.png b/tools_pc/golden/cuba/win/frame_000900.png index f8673fa5..0fbd00bc 100644 Binary files a/tools_pc/golden/cuba/win/frame_000900.png and b/tools_pc/golden/cuba/win/frame_000900.png differ diff --git a/tools_pc/golden/dam/deck/frame_001500.png b/tools_pc/golden/dam/deck/frame_001500.png new file mode 100644 index 00000000..0e6be62c Binary files /dev/null and b/tools_pc/golden/dam/deck/frame_001500.png differ diff --git a/tools_pc/golden/dam/deck/frame_001600.png b/tools_pc/golden/dam/deck/frame_001600.png new file mode 100644 index 00000000..586831c7 Binary files /dev/null and b/tools_pc/golden/dam/deck/frame_001600.png differ diff --git a/tools_pc/golden/dam/deck/frame_001700.png b/tools_pc/golden/dam/deck/frame_001700.png new file mode 100644 index 00000000..5d4a5599 Binary files /dev/null and b/tools_pc/golden/dam/deck/frame_001700.png differ diff --git a/tools_pc/golden/dam/linux/frame_001500.png b/tools_pc/golden/dam/linux/frame_001500.png index 5a5e7f92..ccecf237 100644 Binary files a/tools_pc/golden/dam/linux/frame_001500.png and b/tools_pc/golden/dam/linux/frame_001500.png differ diff --git a/tools_pc/golden/dam/linux/frame_001600.png b/tools_pc/golden/dam/linux/frame_001600.png index a803d404..44f124c8 100644 Binary files a/tools_pc/golden/dam/linux/frame_001600.png and b/tools_pc/golden/dam/linux/frame_001600.png differ diff --git a/tools_pc/golden/dam/linux/frame_001700.png b/tools_pc/golden/dam/linux/frame_001700.png index 2b88c9c2..7e09a336 100644 Binary files a/tools_pc/golden/dam/linux/frame_001700.png and b/tools_pc/golden/dam/linux/frame_001700.png differ diff --git a/tools_pc/golden/dam/win/frame_001500.png b/tools_pc/golden/dam/win/frame_001500.png index a2072847..cae59cdc 100644 Binary files a/tools_pc/golden/dam/win/frame_001500.png and b/tools_pc/golden/dam/win/frame_001500.png differ diff --git a/tools_pc/golden/dam/win/frame_001600.png b/tools_pc/golden/dam/win/frame_001600.png index 3068d62d..9ed7cb5b 100644 Binary files a/tools_pc/golden/dam/win/frame_001600.png and b/tools_pc/golden/dam/win/frame_001600.png differ diff --git a/tools_pc/golden/dam/win/frame_001700.png b/tools_pc/golden/dam/win/frame_001700.png index 7afcef32..e23ce38f 100644 Binary files a/tools_pc/golden/dam/win/frame_001700.png and b/tools_pc/golden/dam/win/frame_001700.png differ diff --git a/tools_pc/golden/depot/deck/frame_001000.png b/tools_pc/golden/depot/deck/frame_001000.png new file mode 100644 index 00000000..9c90a93c Binary files /dev/null and b/tools_pc/golden/depot/deck/frame_001000.png differ diff --git a/tools_pc/golden/depot/deck/frame_001200.png b/tools_pc/golden/depot/deck/frame_001200.png new file mode 100644 index 00000000..4d089d94 Binary files /dev/null and b/tools_pc/golden/depot/deck/frame_001200.png differ diff --git a/tools_pc/golden/depot/deck/frame_001400.png b/tools_pc/golden/depot/deck/frame_001400.png new file mode 100644 index 00000000..4b40c190 Binary files /dev/null and b/tools_pc/golden/depot/deck/frame_001400.png differ diff --git a/tools_pc/golden/depot/linux/frame_001000.png b/tools_pc/golden/depot/linux/frame_001000.png index 8123ed33..42e7b54e 100644 Binary files a/tools_pc/golden/depot/linux/frame_001000.png and b/tools_pc/golden/depot/linux/frame_001000.png differ diff --git a/tools_pc/golden/depot/linux/frame_001200.png b/tools_pc/golden/depot/linux/frame_001200.png index 7b9d548f..e73a196f 100644 Binary files a/tools_pc/golden/depot/linux/frame_001200.png and b/tools_pc/golden/depot/linux/frame_001200.png differ diff --git a/tools_pc/golden/depot/linux/frame_001400.png b/tools_pc/golden/depot/linux/frame_001400.png index 3dab71d8..2e0025f9 100644 Binary files a/tools_pc/golden/depot/linux/frame_001400.png and b/tools_pc/golden/depot/linux/frame_001400.png differ diff --git a/tools_pc/golden/depot/win/frame_001000.png b/tools_pc/golden/depot/win/frame_001000.png index 5fd0f7e8..0823b9a4 100644 Binary files a/tools_pc/golden/depot/win/frame_001000.png and b/tools_pc/golden/depot/win/frame_001000.png differ diff --git a/tools_pc/golden/depot/win/frame_001200.png b/tools_pc/golden/depot/win/frame_001200.png index 71fbfb9e..de28a183 100644 Binary files a/tools_pc/golden/depot/win/frame_001200.png and b/tools_pc/golden/depot/win/frame_001200.png differ diff --git a/tools_pc/golden/depot/win/frame_001400.png b/tools_pc/golden/depot/win/frame_001400.png index c6ec03e6..24cac04e 100644 Binary files a/tools_pc/golden/depot/win/frame_001400.png and b/tools_pc/golden/depot/win/frame_001400.png differ diff --git a/tools_pc/golden/egypt/deck/frame_000900.png b/tools_pc/golden/egypt/deck/frame_000900.png new file mode 100644 index 00000000..0b13d8eb Binary files /dev/null and b/tools_pc/golden/egypt/deck/frame_000900.png differ diff --git a/tools_pc/golden/egypt/deck/frame_001200.png b/tools_pc/golden/egypt/deck/frame_001200.png new file mode 100644 index 00000000..5fa2f8a6 Binary files /dev/null and b/tools_pc/golden/egypt/deck/frame_001200.png differ diff --git a/tools_pc/golden/egypt/deck/frame_001500.png b/tools_pc/golden/egypt/deck/frame_001500.png new file mode 100644 index 00000000..f48ef1f3 Binary files /dev/null and b/tools_pc/golden/egypt/deck/frame_001500.png differ diff --git a/tools_pc/golden/egypt/linux/frame_000900.png b/tools_pc/golden/egypt/linux/frame_000900.png index 76da144a..180bf81e 100644 Binary files a/tools_pc/golden/egypt/linux/frame_000900.png and b/tools_pc/golden/egypt/linux/frame_000900.png differ diff --git a/tools_pc/golden/egypt/linux/frame_001200.png b/tools_pc/golden/egypt/linux/frame_001200.png index 758ccd61..fe9b5c27 100644 Binary files a/tools_pc/golden/egypt/linux/frame_001200.png and b/tools_pc/golden/egypt/linux/frame_001200.png differ diff --git a/tools_pc/golden/egypt/linux/frame_001500.png b/tools_pc/golden/egypt/linux/frame_001500.png index 0dfa06d5..4c68a28d 100644 Binary files a/tools_pc/golden/egypt/linux/frame_001500.png and b/tools_pc/golden/egypt/linux/frame_001500.png differ diff --git a/tools_pc/golden/egypt/win/frame_000900.png b/tools_pc/golden/egypt/win/frame_000900.png index 92b699a6..1b38c5d4 100644 Binary files a/tools_pc/golden/egypt/win/frame_000900.png and b/tools_pc/golden/egypt/win/frame_000900.png differ diff --git a/tools_pc/golden/egypt/win/frame_001200.png b/tools_pc/golden/egypt/win/frame_001200.png index 7c9ffa43..9d188434 100644 Binary files a/tools_pc/golden/egypt/win/frame_001200.png and b/tools_pc/golden/egypt/win/frame_001200.png differ diff --git a/tools_pc/golden/egypt/win/frame_001500.png b/tools_pc/golden/egypt/win/frame_001500.png index 8c5a518e..893d6810 100644 Binary files a/tools_pc/golden/egypt/win/frame_001500.png and b/tools_pc/golden/egypt/win/frame_001500.png differ diff --git a/tools_pc/golden/facility/deck/frame_000900.png b/tools_pc/golden/facility/deck/frame_000900.png new file mode 100644 index 00000000..4ab2ec97 Binary files /dev/null and b/tools_pc/golden/facility/deck/frame_000900.png differ diff --git a/tools_pc/golden/facility/deck/frame_001200.png b/tools_pc/golden/facility/deck/frame_001200.png new file mode 100644 index 00000000..9b613520 Binary files /dev/null and b/tools_pc/golden/facility/deck/frame_001200.png differ diff --git a/tools_pc/golden/facility/deck/frame_001500.png b/tools_pc/golden/facility/deck/frame_001500.png new file mode 100644 index 00000000..c3582794 Binary files /dev/null and b/tools_pc/golden/facility/deck/frame_001500.png differ diff --git a/tools_pc/golden/facility/linux/frame_000900.png b/tools_pc/golden/facility/linux/frame_000900.png index 428478e9..792d0052 100644 Binary files a/tools_pc/golden/facility/linux/frame_000900.png and b/tools_pc/golden/facility/linux/frame_000900.png differ diff --git a/tools_pc/golden/facility/linux/frame_001200.png b/tools_pc/golden/facility/linux/frame_001200.png index 3403a0e1..5dcf8e8f 100644 Binary files a/tools_pc/golden/facility/linux/frame_001200.png and b/tools_pc/golden/facility/linux/frame_001200.png differ diff --git a/tools_pc/golden/facility/linux/frame_001500.png b/tools_pc/golden/facility/linux/frame_001500.png index bd6b12bd..9114ef39 100644 Binary files a/tools_pc/golden/facility/linux/frame_001500.png and b/tools_pc/golden/facility/linux/frame_001500.png differ diff --git a/tools_pc/golden/facility/win/frame_000900.png b/tools_pc/golden/facility/win/frame_000900.png index 56ac2410..ae870cab 100644 Binary files a/tools_pc/golden/facility/win/frame_000900.png and b/tools_pc/golden/facility/win/frame_000900.png differ diff --git a/tools_pc/golden/facility/win/frame_001200.png b/tools_pc/golden/facility/win/frame_001200.png index bf1b82e2..56bf4daa 100644 Binary files a/tools_pc/golden/facility/win/frame_001200.png and b/tools_pc/golden/facility/win/frame_001200.png differ diff --git a/tools_pc/golden/facility/win/frame_001500.png b/tools_pc/golden/facility/win/frame_001500.png index 8b5a090a..c47d51f7 100644 Binary files a/tools_pc/golden/facility/win/frame_001500.png and b/tools_pc/golden/facility/win/frame_001500.png differ diff --git a/tools_pc/golden/frigate/deck/frame_001000.png b/tools_pc/golden/frigate/deck/frame_001000.png new file mode 100644 index 00000000..d8aad5ef Binary files /dev/null and b/tools_pc/golden/frigate/deck/frame_001000.png differ diff --git a/tools_pc/golden/frigate/deck/frame_001200.png b/tools_pc/golden/frigate/deck/frame_001200.png new file mode 100644 index 00000000..8b0eba8a Binary files /dev/null and b/tools_pc/golden/frigate/deck/frame_001200.png differ diff --git a/tools_pc/golden/frigate/deck/frame_001400.png b/tools_pc/golden/frigate/deck/frame_001400.png new file mode 100644 index 00000000..9101511a Binary files /dev/null and b/tools_pc/golden/frigate/deck/frame_001400.png differ diff --git a/tools_pc/golden/frigate/linux/frame_001000.png b/tools_pc/golden/frigate/linux/frame_001000.png index fde527ec..d1425ef6 100644 Binary files a/tools_pc/golden/frigate/linux/frame_001000.png and b/tools_pc/golden/frigate/linux/frame_001000.png differ diff --git a/tools_pc/golden/frigate/linux/frame_001200.png b/tools_pc/golden/frigate/linux/frame_001200.png index 44f5df5a..6bdd5083 100644 Binary files a/tools_pc/golden/frigate/linux/frame_001200.png and b/tools_pc/golden/frigate/linux/frame_001200.png differ diff --git a/tools_pc/golden/frigate/linux/frame_001400.png b/tools_pc/golden/frigate/linux/frame_001400.png index ed1cf037..b302f0a8 100644 Binary files a/tools_pc/golden/frigate/linux/frame_001400.png and b/tools_pc/golden/frigate/linux/frame_001400.png differ diff --git a/tools_pc/golden/frigate/win/frame_001000.png b/tools_pc/golden/frigate/win/frame_001000.png index ee430321..13a1ac9e 100644 Binary files a/tools_pc/golden/frigate/win/frame_001000.png and b/tools_pc/golden/frigate/win/frame_001000.png differ diff --git a/tools_pc/golden/frigate/win/frame_001200.png b/tools_pc/golden/frigate/win/frame_001200.png index a862eee3..6ecc54f2 100644 Binary files a/tools_pc/golden/frigate/win/frame_001200.png and b/tools_pc/golden/frigate/win/frame_001200.png differ diff --git a/tools_pc/golden/frigate/win/frame_001400.png b/tools_pc/golden/frigate/win/frame_001400.png index 85f9a25d..16125478 100644 Binary files a/tools_pc/golden/frigate/win/frame_001400.png and b/tools_pc/golden/frigate/win/frame_001400.png differ diff --git a/tools_pc/golden/jungle/deck/frame_000900.png b/tools_pc/golden/jungle/deck/frame_000900.png new file mode 100644 index 00000000..13633f25 Binary files /dev/null and b/tools_pc/golden/jungle/deck/frame_000900.png differ diff --git a/tools_pc/golden/jungle/deck/frame_001200.png b/tools_pc/golden/jungle/deck/frame_001200.png new file mode 100644 index 00000000..18b46083 Binary files /dev/null and b/tools_pc/golden/jungle/deck/frame_001200.png differ diff --git a/tools_pc/golden/jungle/deck/frame_001500.png b/tools_pc/golden/jungle/deck/frame_001500.png new file mode 100644 index 00000000..59c03daf Binary files /dev/null and b/tools_pc/golden/jungle/deck/frame_001500.png differ diff --git a/tools_pc/golden/jungle/linux/frame_000900.png b/tools_pc/golden/jungle/linux/frame_000900.png index d67d70f5..71ca23d6 100644 Binary files a/tools_pc/golden/jungle/linux/frame_000900.png and b/tools_pc/golden/jungle/linux/frame_000900.png differ diff --git a/tools_pc/golden/jungle/linux/frame_001200.png b/tools_pc/golden/jungle/linux/frame_001200.png index 1b730ced..b7184c70 100644 Binary files a/tools_pc/golden/jungle/linux/frame_001200.png and b/tools_pc/golden/jungle/linux/frame_001200.png differ diff --git a/tools_pc/golden/jungle/linux/frame_001500.png b/tools_pc/golden/jungle/linux/frame_001500.png index c1f34778..e7953397 100644 Binary files a/tools_pc/golden/jungle/linux/frame_001500.png and b/tools_pc/golden/jungle/linux/frame_001500.png differ diff --git a/tools_pc/golden/jungle/win/frame_000900.png b/tools_pc/golden/jungle/win/frame_000900.png index 08525e18..15b3001a 100644 Binary files a/tools_pc/golden/jungle/win/frame_000900.png and b/tools_pc/golden/jungle/win/frame_000900.png differ diff --git a/tools_pc/golden/jungle/win/frame_001200.png b/tools_pc/golden/jungle/win/frame_001200.png index 2bd14660..b168a58c 100644 Binary files a/tools_pc/golden/jungle/win/frame_001200.png and b/tools_pc/golden/jungle/win/frame_001200.png differ diff --git a/tools_pc/golden/jungle/win/frame_001500.png b/tools_pc/golden/jungle/win/frame_001500.png index 8c5b818b..11d9c91d 100644 Binary files a/tools_pc/golden/jungle/win/frame_001500.png and b/tools_pc/golden/jungle/win/frame_001500.png differ diff --git a/tools_pc/golden/runway/deck/frame_000900.png b/tools_pc/golden/runway/deck/frame_000900.png new file mode 100644 index 00000000..168782d7 Binary files /dev/null and b/tools_pc/golden/runway/deck/frame_000900.png differ diff --git a/tools_pc/golden/runway/deck/frame_001200.png b/tools_pc/golden/runway/deck/frame_001200.png new file mode 100644 index 00000000..ff19bc50 Binary files /dev/null and b/tools_pc/golden/runway/deck/frame_001200.png differ diff --git a/tools_pc/golden/runway/deck/frame_001500.png b/tools_pc/golden/runway/deck/frame_001500.png new file mode 100644 index 00000000..f8624921 Binary files /dev/null and b/tools_pc/golden/runway/deck/frame_001500.png differ diff --git a/tools_pc/golden/runway/linux/frame_000900.png b/tools_pc/golden/runway/linux/frame_000900.png index a4d7e68d..b5815fdb 100644 Binary files a/tools_pc/golden/runway/linux/frame_000900.png and b/tools_pc/golden/runway/linux/frame_000900.png differ diff --git a/tools_pc/golden/runway/linux/frame_001200.png b/tools_pc/golden/runway/linux/frame_001200.png index 7a586f8e..850f7851 100644 Binary files a/tools_pc/golden/runway/linux/frame_001200.png and b/tools_pc/golden/runway/linux/frame_001200.png differ diff --git a/tools_pc/golden/runway/linux/frame_001500.png b/tools_pc/golden/runway/linux/frame_001500.png index 8aae50e1..c06a3a94 100644 Binary files a/tools_pc/golden/runway/linux/frame_001500.png and b/tools_pc/golden/runway/linux/frame_001500.png differ diff --git a/tools_pc/golden/runway/win/frame_000900.png b/tools_pc/golden/runway/win/frame_000900.png index cea1873d..dd3dd62d 100644 Binary files a/tools_pc/golden/runway/win/frame_000900.png and b/tools_pc/golden/runway/win/frame_000900.png differ diff --git a/tools_pc/golden/runway/win/frame_001200.png b/tools_pc/golden/runway/win/frame_001200.png index 370b2fbd..63e4e7d2 100644 Binary files a/tools_pc/golden/runway/win/frame_001200.png and b/tools_pc/golden/runway/win/frame_001200.png differ diff --git a/tools_pc/golden/runway/win/frame_001500.png b/tools_pc/golden/runway/win/frame_001500.png index 02e339c1..c94f55f7 100644 Binary files a/tools_pc/golden/runway/win/frame_001500.png and b/tools_pc/golden/runway/win/frame_001500.png differ diff --git a/tools_pc/golden/silo/deck/frame_000900.png b/tools_pc/golden/silo/deck/frame_000900.png new file mode 100644 index 00000000..4ecbdc62 Binary files /dev/null and b/tools_pc/golden/silo/deck/frame_000900.png differ diff --git a/tools_pc/golden/silo/deck/frame_001200.png b/tools_pc/golden/silo/deck/frame_001200.png new file mode 100644 index 00000000..fc36d055 Binary files /dev/null and b/tools_pc/golden/silo/deck/frame_001200.png differ diff --git a/tools_pc/golden/silo/deck/frame_001500.png b/tools_pc/golden/silo/deck/frame_001500.png new file mode 100644 index 00000000..382e6511 Binary files /dev/null and b/tools_pc/golden/silo/deck/frame_001500.png differ diff --git a/tools_pc/golden/silo/linux/frame_000900.png b/tools_pc/golden/silo/linux/frame_000900.png index 883b6187..901e71b2 100644 Binary files a/tools_pc/golden/silo/linux/frame_000900.png and b/tools_pc/golden/silo/linux/frame_000900.png differ diff --git a/tools_pc/golden/silo/linux/frame_001200.png b/tools_pc/golden/silo/linux/frame_001200.png index 3dfa9735..0847f5fd 100644 Binary files a/tools_pc/golden/silo/linux/frame_001200.png and b/tools_pc/golden/silo/linux/frame_001200.png differ diff --git a/tools_pc/golden/silo/linux/frame_001500.png b/tools_pc/golden/silo/linux/frame_001500.png index 2d51e07a..7a12f69b 100644 Binary files a/tools_pc/golden/silo/linux/frame_001500.png and b/tools_pc/golden/silo/linux/frame_001500.png differ diff --git a/tools_pc/golden/silo/win/frame_000900.png b/tools_pc/golden/silo/win/frame_000900.png index 8df7c692..71f50bba 100644 Binary files a/tools_pc/golden/silo/win/frame_000900.png and b/tools_pc/golden/silo/win/frame_000900.png differ diff --git a/tools_pc/golden/silo/win/frame_001200.png b/tools_pc/golden/silo/win/frame_001200.png index 1ed859ba..902371ab 100644 Binary files a/tools_pc/golden/silo/win/frame_001200.png and b/tools_pc/golden/silo/win/frame_001200.png differ diff --git a/tools_pc/golden/silo/win/frame_001500.png b/tools_pc/golden/silo/win/frame_001500.png index 896edb49..c33241ae 100644 Binary files a/tools_pc/golden/silo/win/frame_001500.png and b/tools_pc/golden/silo/win/frame_001500.png differ diff --git a/tools_pc/golden/statue/deck/frame_000900.png b/tools_pc/golden/statue/deck/frame_000900.png new file mode 100644 index 00000000..9bf73695 Binary files /dev/null and b/tools_pc/golden/statue/deck/frame_000900.png differ diff --git a/tools_pc/golden/statue/deck/frame_001200.png b/tools_pc/golden/statue/deck/frame_001200.png new file mode 100644 index 00000000..473f11f1 Binary files /dev/null and b/tools_pc/golden/statue/deck/frame_001200.png differ diff --git a/tools_pc/golden/statue/deck/frame_001500.png b/tools_pc/golden/statue/deck/frame_001500.png new file mode 100644 index 00000000..a74e0992 Binary files /dev/null and b/tools_pc/golden/statue/deck/frame_001500.png differ diff --git a/tools_pc/golden/statue/linux/frame_000900.png b/tools_pc/golden/statue/linux/frame_000900.png index 11562e93..3edfd076 100644 Binary files a/tools_pc/golden/statue/linux/frame_000900.png and b/tools_pc/golden/statue/linux/frame_000900.png differ diff --git a/tools_pc/golden/statue/linux/frame_001200.png b/tools_pc/golden/statue/linux/frame_001200.png index 29fe2b73..b196e29e 100644 Binary files a/tools_pc/golden/statue/linux/frame_001200.png and b/tools_pc/golden/statue/linux/frame_001200.png differ diff --git a/tools_pc/golden/statue/linux/frame_001500.png b/tools_pc/golden/statue/linux/frame_001500.png index 83b09089..87cde952 100644 Binary files a/tools_pc/golden/statue/linux/frame_001500.png and b/tools_pc/golden/statue/linux/frame_001500.png differ diff --git a/tools_pc/golden/statue/win/frame_000900.png b/tools_pc/golden/statue/win/frame_000900.png index c7b34497..04b1ced6 100644 Binary files a/tools_pc/golden/statue/win/frame_000900.png and b/tools_pc/golden/statue/win/frame_000900.png differ diff --git a/tools_pc/golden/statue/win/frame_001200.png b/tools_pc/golden/statue/win/frame_001200.png index d3318f40..e0901a5c 100644 Binary files a/tools_pc/golden/statue/win/frame_001200.png and b/tools_pc/golden/statue/win/frame_001200.png differ diff --git a/tools_pc/golden/statue/win/frame_001500.png b/tools_pc/golden/statue/win/frame_001500.png index 689437d6..282fbe5f 100644 Binary files a/tools_pc/golden/statue/win/frame_001500.png and b/tools_pc/golden/statue/win/frame_001500.png differ diff --git a/tools_pc/golden/streets/deck/frame_001000.png b/tools_pc/golden/streets/deck/frame_001000.png new file mode 100644 index 00000000..b6893122 Binary files /dev/null and b/tools_pc/golden/streets/deck/frame_001000.png differ diff --git a/tools_pc/golden/streets/deck/frame_001200.png b/tools_pc/golden/streets/deck/frame_001200.png new file mode 100644 index 00000000..267b88ac Binary files /dev/null and b/tools_pc/golden/streets/deck/frame_001200.png differ diff --git a/tools_pc/golden/streets/deck/frame_001400.png b/tools_pc/golden/streets/deck/frame_001400.png new file mode 100644 index 00000000..424f5dc9 Binary files /dev/null and b/tools_pc/golden/streets/deck/frame_001400.png differ diff --git a/tools_pc/golden/streets/linux/frame_001000.png b/tools_pc/golden/streets/linux/frame_001000.png index 9ca907e5..a13d11a4 100644 Binary files a/tools_pc/golden/streets/linux/frame_001000.png and b/tools_pc/golden/streets/linux/frame_001000.png differ diff --git a/tools_pc/golden/streets/linux/frame_001200.png b/tools_pc/golden/streets/linux/frame_001200.png index ab6d85cb..ac5383bc 100644 Binary files a/tools_pc/golden/streets/linux/frame_001200.png and b/tools_pc/golden/streets/linux/frame_001200.png differ diff --git a/tools_pc/golden/streets/linux/frame_001400.png b/tools_pc/golden/streets/linux/frame_001400.png index 3c06bd47..fd2d6cd4 100644 Binary files a/tools_pc/golden/streets/linux/frame_001400.png and b/tools_pc/golden/streets/linux/frame_001400.png differ diff --git a/tools_pc/golden/streets/win/frame_001000.png b/tools_pc/golden/streets/win/frame_001000.png index 869cdceb..969f1d31 100644 Binary files a/tools_pc/golden/streets/win/frame_001000.png and b/tools_pc/golden/streets/win/frame_001000.png differ diff --git a/tools_pc/golden/streets/win/frame_001200.png b/tools_pc/golden/streets/win/frame_001200.png index ee549e78..6a9c3ae8 100644 Binary files a/tools_pc/golden/streets/win/frame_001200.png and b/tools_pc/golden/streets/win/frame_001200.png differ diff --git a/tools_pc/golden/streets/win/frame_001400.png b/tools_pc/golden/streets/win/frame_001400.png index 5fc972ff..66c69494 100644 Binary files a/tools_pc/golden/streets/win/frame_001400.png and b/tools_pc/golden/streets/win/frame_001400.png differ diff --git a/tools_pc/golden/surface1/deck/frame_000900.png b/tools_pc/golden/surface1/deck/frame_000900.png new file mode 100644 index 00000000..f10e5ca0 Binary files /dev/null and b/tools_pc/golden/surface1/deck/frame_000900.png differ diff --git a/tools_pc/golden/surface1/deck/frame_001200.png b/tools_pc/golden/surface1/deck/frame_001200.png new file mode 100644 index 00000000..ecf55af4 Binary files /dev/null and b/tools_pc/golden/surface1/deck/frame_001200.png differ diff --git a/tools_pc/golden/surface1/deck/frame_001500.png b/tools_pc/golden/surface1/deck/frame_001500.png new file mode 100644 index 00000000..13a37eb2 Binary files /dev/null and b/tools_pc/golden/surface1/deck/frame_001500.png differ diff --git a/tools_pc/golden/surface1/linux/frame_000900.png b/tools_pc/golden/surface1/linux/frame_000900.png index 23a93f9f..61785a2e 100644 Binary files a/tools_pc/golden/surface1/linux/frame_000900.png and b/tools_pc/golden/surface1/linux/frame_000900.png differ diff --git a/tools_pc/golden/surface1/linux/frame_001200.png b/tools_pc/golden/surface1/linux/frame_001200.png index 406c79a8..422c502e 100644 Binary files a/tools_pc/golden/surface1/linux/frame_001200.png and b/tools_pc/golden/surface1/linux/frame_001200.png differ diff --git a/tools_pc/golden/surface1/linux/frame_001500.png b/tools_pc/golden/surface1/linux/frame_001500.png index 1b379ea2..907348a6 100644 Binary files a/tools_pc/golden/surface1/linux/frame_001500.png and b/tools_pc/golden/surface1/linux/frame_001500.png differ diff --git a/tools_pc/golden/surface1/win/frame_000900.png b/tools_pc/golden/surface1/win/frame_000900.png index 498a283c..c897978e 100644 Binary files a/tools_pc/golden/surface1/win/frame_000900.png and b/tools_pc/golden/surface1/win/frame_000900.png differ diff --git a/tools_pc/golden/surface1/win/frame_001200.png b/tools_pc/golden/surface1/win/frame_001200.png index f1350736..be556074 100644 Binary files a/tools_pc/golden/surface1/win/frame_001200.png and b/tools_pc/golden/surface1/win/frame_001200.png differ diff --git a/tools_pc/golden/surface1/win/frame_001500.png b/tools_pc/golden/surface1/win/frame_001500.png index 22e5ba56..795f7997 100644 Binary files a/tools_pc/golden/surface1/win/frame_001500.png and b/tools_pc/golden/surface1/win/frame_001500.png differ diff --git a/tools_pc/golden/surface2/deck/frame_001000.png b/tools_pc/golden/surface2/deck/frame_001000.png new file mode 100644 index 00000000..b4634747 Binary files /dev/null and b/tools_pc/golden/surface2/deck/frame_001000.png differ diff --git a/tools_pc/golden/surface2/deck/frame_001200.png b/tools_pc/golden/surface2/deck/frame_001200.png new file mode 100644 index 00000000..30506dbc Binary files /dev/null and b/tools_pc/golden/surface2/deck/frame_001200.png differ diff --git a/tools_pc/golden/surface2/deck/frame_001400.png b/tools_pc/golden/surface2/deck/frame_001400.png new file mode 100644 index 00000000..203c7944 Binary files /dev/null and b/tools_pc/golden/surface2/deck/frame_001400.png differ diff --git a/tools_pc/golden/surface2/linux/frame_001000.png b/tools_pc/golden/surface2/linux/frame_001000.png index 29ee77bf..c5deb8fc 100644 Binary files a/tools_pc/golden/surface2/linux/frame_001000.png and b/tools_pc/golden/surface2/linux/frame_001000.png differ diff --git a/tools_pc/golden/surface2/linux/frame_001200.png b/tools_pc/golden/surface2/linux/frame_001200.png index 71cce9c1..d2c7fe2e 100644 Binary files a/tools_pc/golden/surface2/linux/frame_001200.png and b/tools_pc/golden/surface2/linux/frame_001200.png differ diff --git a/tools_pc/golden/surface2/linux/frame_001400.png b/tools_pc/golden/surface2/linux/frame_001400.png index 1af105ae..149be53a 100644 Binary files a/tools_pc/golden/surface2/linux/frame_001400.png and b/tools_pc/golden/surface2/linux/frame_001400.png differ diff --git a/tools_pc/golden/surface2/win/frame_001000.png b/tools_pc/golden/surface2/win/frame_001000.png index 9719a990..c846013a 100644 Binary files a/tools_pc/golden/surface2/win/frame_001000.png and b/tools_pc/golden/surface2/win/frame_001000.png differ diff --git a/tools_pc/golden/surface2/win/frame_001200.png b/tools_pc/golden/surface2/win/frame_001200.png index 61886f0f..16689e0b 100644 Binary files a/tools_pc/golden/surface2/win/frame_001200.png and b/tools_pc/golden/surface2/win/frame_001200.png differ diff --git a/tools_pc/golden/surface2/win/frame_001400.png b/tools_pc/golden/surface2/win/frame_001400.png index 619fe6ad..476945ee 100644 Binary files a/tools_pc/golden/surface2/win/frame_001400.png and b/tools_pc/golden/surface2/win/frame_001400.png differ diff --git a/tools_pc/golden/train/deck/frame_000900.png b/tools_pc/golden/train/deck/frame_000900.png new file mode 100644 index 00000000..6891bb1e Binary files /dev/null and b/tools_pc/golden/train/deck/frame_000900.png differ diff --git a/tools_pc/golden/train/deck/frame_001200.png b/tools_pc/golden/train/deck/frame_001200.png new file mode 100644 index 00000000..b95a7024 Binary files /dev/null and b/tools_pc/golden/train/deck/frame_001200.png differ diff --git a/tools_pc/golden/train/deck/frame_001500.png b/tools_pc/golden/train/deck/frame_001500.png new file mode 100644 index 00000000..13cace9f Binary files /dev/null and b/tools_pc/golden/train/deck/frame_001500.png differ diff --git a/tools_pc/golden/train/linux/frame_000900.png b/tools_pc/golden/train/linux/frame_000900.png index 069de860..f4e7ef93 100644 Binary files a/tools_pc/golden/train/linux/frame_000900.png and b/tools_pc/golden/train/linux/frame_000900.png differ diff --git a/tools_pc/golden/train/linux/frame_001200.png b/tools_pc/golden/train/linux/frame_001200.png index 2e30acfe..766dd3a4 100644 Binary files a/tools_pc/golden/train/linux/frame_001200.png and b/tools_pc/golden/train/linux/frame_001200.png differ diff --git a/tools_pc/golden/train/linux/frame_001500.png b/tools_pc/golden/train/linux/frame_001500.png index 319b1e41..67e94bba 100644 Binary files a/tools_pc/golden/train/linux/frame_001500.png and b/tools_pc/golden/train/linux/frame_001500.png differ diff --git a/tools_pc/golden/train/win/frame_000900.png b/tools_pc/golden/train/win/frame_000900.png index f8c73f7b..a65a2a83 100644 Binary files a/tools_pc/golden/train/win/frame_000900.png and b/tools_pc/golden/train/win/frame_000900.png differ diff --git a/tools_pc/golden/train/win/frame_001200.png b/tools_pc/golden/train/win/frame_001200.png index 460912d5..165848b7 100644 Binary files a/tools_pc/golden/train/win/frame_001200.png and b/tools_pc/golden/train/win/frame_001200.png differ diff --git a/tools_pc/golden/train/win/frame_001500.png b/tools_pc/golden/train/win/frame_001500.png index 638b7f08..248bff10 100644 Binary files a/tools_pc/golden/train/win/frame_001500.png and b/tools_pc/golden/train/win/frame_001500.png differ diff --git a/tools_pc/make_icon.py b/tools_pc/make_icon.py new file mode 100644 index 00000000..a1412ad7 --- /dev/null +++ b/tools_pc/make_icon.py @@ -0,0 +1,213 @@ +#!/usr/bin/env python3 +"""Generate the project's own icon (original art). + +The mark is an ORIGINAL generic version of the GE default aim crosshair: +a circle with four lines (N/S/E/W) crossing the circle's edge and +extending well past it, open at the centre. Proportions are measured from +the game's own default crosshair (thin ring, long bars, open centre). +Site gold (#e8a13a) on near-black (#050607). +A circle-plus-ticks reticle is a generic functional shape (no logo art, +wordmark or screenshot copied, cf. tree-hygiene rule 4: repo imagery +must not be derivative game content). + +Outputs (all regenerated byte-identically; deterministic): + docs/img/icon/ge007-icon.png 512x512 favicon / README icon + cmake/icon/ge007.ico multi-size Windows exe icon (16..256) + port/fast3d/ge007_icon.h 32x32 RGBA array for the SDL window icon + (SDL_SetWindowIcon, see gfx_sdl2.cpp) + +Usage: + python tools_pc/make_icon.py # write all three + python tools_pc/make_icon.py --check # exit 1 if any output is missing, + # or the mark is not exactly centred + +Requires Pillow (>=10.1, for load_default(size)). +""" +from __future__ import annotations + +import io +import struct +import sys +from pathlib import Path + +try: + from PIL import Image, ImageDraw +except ImportError: + sys.exit("Pillow is required: pip install Pillow") + +ROOT = Path(__file__).resolve().parent.parent + +OUT_ICON = ROOT / "docs" / "img" / "icon" / "ge007-icon.png" +OUT_ICO = ROOT / "cmake" / "icon" / "ge007.ico" +OUT_HEADER = ROOT / "port" / "fast3d" / "ge007_icon.h" + +# --- palette ----------------------------------------------------------------- +BG = (5, 6, 7) # site near-black +GOLD = (232, 161, 58) # site gold (#e8a13a) + + +def _crosshair(size: int, small: bool = False) -> Image.Image: + """The generic GE default crosshair: a circle with N/S/E/W lines + crossing its edge and extending well past it, open centre, in site gold + (#e8a13a); proportions measured from the game's own default crosshair + (thin ring, long bars, open centre). Rendered at 2x for AA, then + downsized. small: a chunkier variant (thicker strokes, wider gap) so + the mark survives 16px in the .ico small slots (and the 32px window + icon).""" + S = size * 2 + img = Image.new("RGBA", (S, S), BG + (255,)) + dr = ImageDraw.Draw(img, "RGBA") + # Exact centre = the line between the two middle supersample pixels + # (continuous coordinate S/2; pixel i covers [i, i+1)). Every shape is + # drawn symmetric about it with inclusive PIL coordinates, so each size + # is pixel-exactly mirror-symmetric (checked by _symmetric()). + c = S // 2 + + def rr(v: float) -> int: + return int(round(S * v / 512)) + + def even(v: float) -> int: + return max(2, 2 * int(round(S * v / 512 / 2))) + + def disc(r: int) -> list: + return [c - r, c - r, c + r - 1, c + r - 1] + + # faint gold glow behind the reticle (kept very subtle: flat + calm) + glow = Image.new("RGBA", (S, S), (0, 0, 0, 0)) + gd = ImageDraw.Draw(glow) + gr = rr(190 if small else 200) + for i in range(24, 0, -1): + g = gr * i // 24 + if g > 0: + gd.ellipse(disc(g), fill=GOLD + (4,)) + img.alpha_composite(glow) + + # the reticle: a circle + four bars (N/S/E/W) crossing the circle's + # edge and extending well past it, with an open centre -- proportions + # measured off the game's own default crosshair (thin ring, long bars; + # bar reach ~1.45x the ring's outer radius, centre gap ~0.30x). + # Small sizes get the chunky variant (thicker strokes, wider gap so + # it stays open at 16px). + circle_r = rr(140 if small else 150) # outer radius of the ring + ring_w = rr(34 if small else 16) # ring stroke width + bar_w = even(30 if small else 14) # bar stroke width (even: symmetric) + reach = rr(205 if small else 218) + gap = rr(52 if small else 45) + hw = bar_w // 2 + + dr.ellipse(disc(circle_r), outline=GOLD + (255,), width=ring_w) + for a, b in ((c - reach, c - gap - 1), (c + gap, c + reach - 1)): + dr.rectangle([c - hw, a, c + hw - 1, b], fill=GOLD + (255,)) # vertical + dr.rectangle([a, c - hw, b, c + hw - 1], fill=GOLD + (255,)) # horizontal + + return img.resize((size, size), Image.LANCZOS) + + + +def _symmetric(img: Image.Image) -> bool: + """Pixel-exact left-right and top-bottom mirror symmetry: the mark is + exactly centred at this size. (Not the diagonal: LANCZOS resamples the + two axes in a fixed order, so a transpose differs by rounding only.)""" + return (img.tobytes() == img.transpose(Image.FLIP_LEFT_RIGHT).tobytes() + and img.tobytes() == img.transpose(Image.FLIP_TOP_BOTTOM).tobytes()) + + +def _asymmetric_sizes() -> list: + sizes = [(512, False), (16, True), (24, True), (32, True), + (48, False), (64, False), (128, False), (256, False)] + return [f"{s}{' (small)' if sm else ''}" for s, sm in sizes + if not _symmetric(_crosshair(s, small=sm))] + +def _ico_png(size: int, small: bool) -> bytes: + buf = io.BytesIO() + _crosshair(size, small=small).save(buf, format="PNG") + return buf.getvalue() + + +def make_ico() -> None: + _crosshair(512).save(OUT_ICON) + # Multi-size .ico with PNG-compressed entries (Vista+ style; what the + # 256 slot always is). Small sizes get the chunky variant so the mark + # survives 16px; a single 512-master downsample washes out there. + entries = [(_ico_png(s, small=True), s) for s in (16, 24, 32)] + entries += [(_ico_png(s, small=False), s) for s in (48, 64, 128, 256)] + n = len(entries) + offset = 6 + 16 * n + dirs, data = b"", b"" + for png, s in entries: + wh = 0 if s == 256 else s # 0 means 256 in the ICO format + dirs += struct.pack(" None: + """Emit the 32x32 RGBA array for the SDL window icon (gfx_sdl2.cpp). + Byte order is plain R,G,B,A per pixel (portable; the C++ side packs + it against explicit masks). + + NOTE: this output is C, and CMake compiles it inside gfx_sdl2.cpp's + include — it is NOT independently syntax-checked by this script. Keep + it trivially valid (see the row-brace comment below; a flat cell list + without row braces is ill-formed for a 3-D array).""" + img = _crosshair(32, small=True).convert("RGBA") + px = img.load() + rows = [] + for y in range(32): + row = [] + for x in range(32): + r, g, b, a = px[x, y] + row.append(f"{{{r:3d},{g:3d},{b:3d},{a:3d}}}") + # Row braces are REQUIRED: without them all 1024 cells sit at the + # top level of the [32][32][4] initializer (1024 > first dim 32 -> + # "too many initializers"). + rows.append(" {" + ",".join(row) + "}") + OUT_HEADER.write_text( + "/* Generated by tools_pc/make_icon.py -- do not edit by hand.\n" + " * The project mark (a generic version of the GE default aim\n" + " * crosshair) as a 32x32 RGBA array, row-major, plain R,G,B,A\n" + " * byte order per pixel; used by gfx_sdl2.cpp for\n" + " * SDL_SetWindowIcon. Regenerate: python tools_pc/make_icon.py */\n" + "#ifndef GE007_ICON_H\n" + "#define GE007_ICON_H\n" + "\n" + "#include \n" + "\n" + "#define GE007_ICON_SIZE 32\n" + "static const uint8_t ge007_icon_rgba[GE007_ICON_SIZE]" + "[GE007_ICON_SIZE][4] = {\n" + + ",\n".join(rows) + "\n};\n" + "#endif /* GE007_ICON_H */\n" + ) + + +def main() -> int: + args = sys.argv[1:] + check = "--check" in args + for p in (OUT_ICON.parent, OUT_ICO.parent, OUT_HEADER.parent): + p.mkdir(parents=True, exist_ok=True) + bad = _asymmetric_sizes() + if bad: + print("icon is not exactly centred (mirror-asymmetric) at: " + ", ".join(bad)) + return 1 + if not check: + make_ico() + make_header() + print(f"wrote {OUT_ICON.relative_to(ROOT)}") + print(f"wrote {OUT_ICO.relative_to(ROOT)}") + print(f"wrote {OUT_HEADER.relative_to(ROOT)}") + return 0 + stale = [p for p in (OUT_ICON, OUT_ICO, OUT_HEADER) if not p.exists()] + if stale: + for p in stale: + print(f"stale/missing: {p.relative_to(ROOT)}") + return 1 + print("ok (all icon outputs present and mirror-symmetric at every size; " + "regenerate with: python tools_pc/make_icon.py)") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools_pc/verify.sh b/tools_pc/verify.sh index 4109cea6..bcbd30a6 100644 --- a/tools_pc/verify.sh +++ b/tools_pc/verify.sh @@ -19,7 +19,22 @@ # a directory of PPMs/PNGs pulled off that box) # # Options: -# --platform win|linux default: autodetect via `uname -s` +# --platform win|linux|deck +# default: autodetect via `uname -s`. `deck` (Steam +# Deck) behaves like linux (build-linux/ge007.x86_64) +# but its goldens live in tools_pc/golden//deck/ +# (per-level pixel limits = the linux ones for now). +# -j N sweep only: run up to N levels concurrently (default +# 1). Every level runs in its OWN temp work dir (own +# data/ copy + ini + canonical save + ppm/), so +# concurrent runs cannot clobber each other and the +# repo's data/ is only ever READ (never pinned or +# modified). Verdicts print in level order. Each game +# quits via GE_QUITFRAME (never hard-killed). PIXEL +# RESULTS ASSUME EACH INSTANCE HOLDS 60 fps (D117: the +# intro flyby is wall-clock paced): use -j 2-3 on a +# strong GPU with nothing else loading it (no local +# LLM loaded); use -j 1 on weak boxes. # --dump lo-hi:step GE_PCDUMP window (default: 900-1500:300, the golden # capture window for every level and both modes) # --script "..." GE_INPUTSCRIPT (default "20:START": skip the intro @@ -72,7 +87,7 @@ cd "$(dirname "$0")/.." ROOT="$(pwd)" # --- arg parsing ------------------------------------------------------------- -MODE=""; LEVEL_ARG=""; PLATFORM=""; DUMP=""; SCRIPT="20:START"; JSON=0; AGAINST="" +MODE=""; LEVEL_ARG=""; PLATFORM=""; DUMP=""; SCRIPT="20:START"; JSON=0; AGAINST=""; JOBS=1 GOLDEN_DUMP="900-1500:300" USER_DUMP=0; USER_SCRIPT=0 # Per-level golden recipe overrides: Cuba is the ending cutscene + credits (it @@ -108,25 +123,28 @@ POSARGS=() while [ $# -gt 0 ]; do case "$1" in --platform) PLATFORM="$2"; shift 2 ;; + -j) JOBS="$2"; shift 2 ;; + -j[0-9]*) JOBS="${1#-j}"; shift ;; --dump) DUMP="$2"; USER_DUMP=1; shift 2 ;; --script) SCRIPT="$2"; USER_SCRIPT=1; shift 2 ;; --against) AGAINST="$2"; shift 2 ;; --json) JSON=1; shift ;; - -h|--help) sed -n '2,34p' "$0"; exit 0 ;; + -h|--help) sed -n '2,49p' "$0"; exit 0 ;; *) POSARGS+=("$1"); shift ;; esac done set -- "${POSARGS[@]:-}" +case "$JOBS" in ''|*[!0-9]*|0) echo "error: -j needs a positive integer (got '$JOBS')" >&2; exit 2 ;; esac case "${1:-}" in sweep) MODE=sweep; shift; SWEEP_SUBSET="${*:-}" ;; parity) MODE=parity; LEVEL_ARG="${2:-}"; shift 2 2>/dev/null || true ;; - -h) sed -n '2,34p' "$0"; exit 0 ;; + -h) sed -n '2,49p' "$0"; exit 0 ;; "") # D529 never-do 4: a no-arg run must NEVER look like a green verdict. It # prints the usage and exits NON-zero (2026-10-05: a no-arg pair was # cited as a "double-green" 21/21 because it exited 0). - sed -n '2,34p' "$0" + sed -n '2,49p' "$0" echo "error: no mode given -- a sweep is 'verify.sh sweep', one level is 'verify.sh ' (never cite a no-arg run as a gate result; D529)" >&2 exit 1 ;; *) MODE=single; LEVEL_ARG="$1" ;; @@ -139,6 +157,7 @@ if [ -z "$PLATFORM" ]; then esac fi +case "$PLATFORM" in win|linux|deck) ;; *) echo "error: --platform must be win, linux or deck (got '$PLATFORM')" >&2; exit 2 ;; esac if [ "$PLATFORM" = win ]; then BUILD_DIR=build-pc EXE="$ROOT/build-pc/ge007.x86_64.exe" @@ -156,7 +175,7 @@ fi # unrecognised -> stay conservative (skip), i.e. an unknown box behaves # exactly as before. LINUX_SOFTWARE_GL=1 -if [ "$PLATFORM" = linux ]; then +if [ "$PLATFORM" = linux ] || [ "$PLATFORM" = deck ]; then # Fail-closed: only a CLEAN probe may enable the pixel gate. glxinfo absent, # a non-zero exit, no "OpenGL renderer string:" line, an unknown sentinel, or # a software renderer all leave LINUX_SOFTWARE_GL=1 (today's skip, same NOTE). @@ -227,91 +246,50 @@ golden_dir_for() { echo "" } -# --- ini pin: golden frames are captured at 640x480 with every other option -# at its compiled-in default (a personal ini -- FovScale, DrawDistance, -# MSAA... -- changes the frame and would fail the golden); back up + restore -INI="$ROOT/data/ge007.ini" -INI_BAK="" -INI_CREATED=0 -pin_ini_640x480() { - # Idempotent (sweep mode calls this once per level, and the game rewrites - # ge007.ini after every run): snapshot the USER's ini on the FIRST call only, - # then re-pin. Snapshotting on every call used to back up the previous level's - # game-written ini, so a sweep left the user's personal settings replaced on - # disk by compiled-in defaults, and leaked one temp file per level. - # D532: fail CLOSED (the old `2>/dev/null || true` call sites ran the gate - # under an unpinned ini when the write failed -- the frame depends on a - # defaults-only 640x480 ini, same as the save pin, so an unpinned ini is a - # spurious-verdict source, not a skipped nicety). Matches pin_eep's - # fail-closed style. - if [ -z "$INI_BAK" ] && [ "$INI_CREATED" = 0 ]; then - if [ -f "$INI" ]; then - INI_BAK=$(mktemp) - cp "$INI" "$INI_BAK" || - { echo "error: cannot snapshot $INI (D532) -- the gate must not run with the user's ini unpinned" >&2; exit 2; } - else INI_CREATED=1; fi - fi - printf '[Window]\nWidth = 640\nHeight = 480\n' > "$INI" || - { echo "error: cannot write the pinned ini $INI (D532) -- the gate must not run with the ini unpinned" >&2; exit 2; } +# --- per-run isolation (replaces the old ini/eep pin + restore of /data): +# every level runs with CWD = a fresh temp work dir holding its OWN data/ +# (the game resolves data/ from its CWD first, port/src/system.c) and its own +# ppm/ -- the repo's data/ is only READ, never modified, so concurrent runs +# (-j N) cannot clobber each other and an interrupted run cannot leave the +# maintainer's ge007.ini / ge007.eep swapped. Golden frames are captured at +# 640x480 with every other option at its compiled-in default (a personal ini +# changes the frame), and the frame depends on the save's CONTENT (D529), so +# each work dir gets the pinned defaults-only ini + the canonical save +# (tools_pc/golden/ge007.eep, in-tree so a fresh clone or CI can run it). +CANON_EEP="$ROOT/tools_pc/golden/ge007.eep" +[ -f "$CANON_EEP" ] || { echo "error: canonical golden save missing: $CANON_EEP (D529)" >&2; exit 2; } +ROMID="${GE_ROMID:-$(sed -n 's/^ROMID:[A-Za-z]*=//p' "$BUILD_DIR/CMakeCache.txt" 2>/dev/null | head -n1)}" +ROMID="${ROMID:-ntsc-final}" +for need in "ge007.$ROMID.z64" "pccg-$ROMID" "pcmodels-$ROMID"; do + [ -e "$ROOT/data/$need" ] || { echo "error: $ROOT/data/$need missing (needed to build the per-run work dir; set GE_ROMID to pick another region)" >&2; exit 2; } +done +# stage_workdir DIR -> populate DIR/data (the game creates DIR/ppm itself) +stage_workdir() { + local d="$1" + mkdir -p "$d/data" && + cp "$ROOT/data/ge007.$ROMID.z64" "$d/data/" && + cp -r "$ROOT/data/pccg-$ROMID" "$ROOT/data/pcmodels-$ROMID" "$d/data/" && + cp "$CANON_EEP" "$d/data/ge007.eep" && + printf '[Window]\nWidth = 640\nHeight = 480\n' > "$d/data/ge007.ini" } -restore_ini() { - if [ -n "$INI_BAK" ]; then cp "$INI_BAK" "$INI" && rm -f "$INI_BAK" - elif [ "$INI_CREATED" = 1 ]; then rm -f "$INI"; fi - INI_BAK=""; INI_CREATED=0 -} -# D534: restore on EXIT only; INT/TERM just exit (which fires EXIT). A combined -# `EXIT INT TERM` trap restored on Ctrl-C and then let the sweep CONTINUE -- -# the next level's pin re-installed the canonical save with the snapshot -# already deleted, so the user's own ge007.eep was left replaced on disk. -trap 'restore_ini; restore_eep; kill_ours' EXIT +# EXIT only (D534): INT/TERM just exit, which fires EXIT. +CUR_CAPDIR="" +trap 'kill_ours; kill $(jobs -rp) 2>/dev/null; [ -n "${SWEEP_TMP:-}" ] && rm -rf "$SWEEP_TMP"' EXIT trap 'exit 130' INT trap 'exit 143' TERM -# --- save pin (D523, SUPERSEDED by D529): the goldens were captured with a -# save file PRESENT. D529 (2026-10-05 A/B) proved the frame depends on the -# save's CONTENT, not just its presence -- D523's "presence, not which -# save" claim had only been measured on Archives (worst_cell 0.175625 on -# each of the four candidate saves, 21.557% with none), and the 2026-10-05 -# sweep (facility 177.47 scene-level on the local playtest save vs 0.0-0.25 -# on every other candidate on disk) refutes it. The gate now INSTALLS THE -# CANONICAL SAVE (tools_pc/golden/ge007.eep, 2 KB, in-tree so a fresh -# clone or CI can run it) the same way it pins the ini: snapshot the local -# file if any, install the canonical one, restore (or remove) on exit. -EEP_PIN="$ROOT/data/ge007.eep" -CANON_EEP="$ROOT/tools_pc/golden/ge007.eep" -EEP_PIN_BAK="" -EEP_PIN_HAD=0 -pin_eep() { - [ -f "$CANON_EEP" ] || { echo "error: canonical golden save missing: $CANON_EEP (D529)" >&2; exit 2; } - # Same idempotence rule as pin_ini_640x480: keep the FIRST snapshot (the save - # as it was before the sweep), not the previous level's written save. - if [ -z "$EEP_PIN_BAK" ]; then - EEP_PIN_BAK=$(mktemp) - if [ -f "$EEP_PIN" ]; then EEP_PIN_HAD=1; cp "$EEP_PIN" "$EEP_PIN_BAK"; fi - fi - # Every level starts from the SAME save state: the runs write the save, and - # the frame depends on its CONTENT (D529), so feed each run the canonical - # copy rather than a local playtest save or level N-1's write. - cp "$CANON_EEP" "$EEP_PIN" -} -restore_eep() { - [ -n "$EEP_PIN_BAK" ] || return 0 - if [ "$EEP_PIN_HAD" = 1 ]; then cp "$EEP_PIN_BAK" "$EEP_PIN"; else rm -f "$EEP_PIN"; fi - rm -f "$EEP_PIN_BAK" - EEP_PIN_BAK=""; EEP_PIN_HAD=0 -} - # --- run one level -> sets globals: R_STATUS R_FRAMES R_SYM R_BRIEF ------- run_one() { local name="$1" num="$2" dump="$3" watchdog="${4:-90}" local capdir; capdir=$(mktemp -d) + CUR_CAPDIR="$capdir" + stage_workdir "$capdir" || + { echo "error: cannot stage the work dir $capdir (data copy failed)" >&2; rm -rf "$capdir"; exit 2; } [ "$USER_DUMP" = 1 ] || dump=$(golden_dump_for "$name") local script="$SCRIPT" [ "$USER_SCRIPT" = 1 ] || script=$(golden_script_for "$name") R_STATUS=""; R_FRAMES=0; R_SYM=""; R_BRIEF="" - rm -f "$ROOT/ge007.crash.log" - # Quit right after the last requested GE_PCDUMP frame via GE_QUITFRAME (an # orderly D344 quit), instead of idling out the watchdog; the watchdog stays # the safety net for a level that never gets there (hang / flake). @@ -323,10 +301,11 @@ run_one() { quitframe=$(( lastframe + 2 )) fi - ( cd "$ROOT" && export GE_PCDUMP="$dump" + ( cd "$capdir" && export GE_PCDUMP="$dump" [ -n "$quitframe" ] && export GE_QUITFRAME="$quitframe" [ -n "$script" ] && export GE_INPUTSCRIPT="$script" export GE_RSEED="$GOLDEN_SEED" + export GE_FAKE_DECK=0 # the D283 Deck preset forces 1280x800 fullscreen over the pinned ini exec "$EXE" "-level_$num" ) >"$capdir/run.log" 2>&1 & local gamepid=$! GAMEPID=$gamepid @@ -342,20 +321,20 @@ run_one() { fi wait "$gamepid" 2>/dev/null GAMEPID="" - mv "$ROOT"/ppm "$capdir/ppm" 2>/dev/null || mkdir -p "$capdir/ppm" + mkdir -p "$capdir/ppm" R_FRAMES=$(ls "$capdir/ppm"/*.ppm 2>/dev/null | wc -l) - if [ -f "$ROOT/ge007.crash.log" ]; then + if [ -f "$capdir/ge007.crash.log" ]; then R_STATUS=CRASH - cp "$ROOT/ge007.crash.log" "$capdir/crash.log" + cp "$capdir/ge007.crash.log" "$capdir/crash.log" # Step 9 (R5): crash_brief.py owns address extraction + symbolication # (both platforms -- the old inline `0x1[0-9a-fA-F]{8,}` grep here # assumed the Windows 0x140000000 image base and would never match a # Linux no-PIE 0x20000000-based address) and prints a dispatch-ready # brief with known-parked-signature and porting-notes/findings hits. R_BRIEF=$(python "$ROOT/tools_pc/crash_brief.py" \ - --crash-log "$capdir/crash.log" --exe "$EXE" --platform "$PLATFORM" \ + --crash-log "$capdir/crash.log" --exe "$EXE" --platform "${PLATFORM/deck/linux}" \ --level "$name" --json 2>&1) R_SYM=$(echo "$R_BRIEF" | python -c " import json,sys @@ -398,7 +377,7 @@ verify_level() { local status="$R_STATUS" frames="$R_FRAMES" sym="$R_SYM" worst="" note="" if [ "$status" = PASS ]; then - if [ "$PLATFORM" = linux ] && [ "$LINUX_SOFTWARE_GL" = 1 ]; then + if { [ "$PLATFORM" = linux ] || [ "$PLATFORM" = deck ]; } && [ "$LINUX_SOFTWARE_GL" = 1 ]; then note="GE_PCDUMP reads black on llvmpipe/WSLg — pixcount/framediff skipped, crash-detect only" else local last; last=$(ls "$CAPDIR"/ppm/*.ppm 2>/dev/null | tail -1) @@ -476,8 +455,6 @@ case "$MODE" in [ -z "$resolved" ] && { echo "unknown level '$LEVEL_ARG'" >&2; exit 2; } name="${resolved%%:*}"; num="${resolved##*:}" [ "${resolved%%:*}" = num ] && name=$(name_for_num "$num") - pin_ini_640x480 - pin_eep verify_level "$name" "$num" "${DUMP:-$GOLDEN_DUMP}" ;; @@ -487,26 +464,55 @@ case "$MODE" in subset="${SWEEP_LEVELS:-${SWEEP_SUBSET:-}}" entries=("${LEVELS[@]}") [ -n "$subset" ] && read -ra entries <<< "$subset" + # -j N: up to N levels at once, each in its own work dir (see above). + # Per-level stdout/stderr/rc are buffered in $SWEEP_TMP and replayed in the + # ORIGINAL level order, so the output is identical to a sequential sweep. + SWEEP_TMP=$(mktemp -d) + wd=60; [ "$JOBS" -gt 1 ] && wd=90 # slower under contention; still only a fallback + nxt=0 + flush_done() { # emit every finished level that is next in order + local blocking="${1:-0}" rc nm j + while [ "$nxt" -lt "${#entries[@]}" ]; do + if [ ! -f "$SWEEP_TMP/$nxt.rc" ]; then + [ "$blocking" = 1 ] && { sleep 0.2; continue; } + return + fi + rc=$(cat "$SWEEP_TMP/$nxt.rc") + [ -s "$SWEEP_TMP/$nxt.err" ] && cat "$SWEEP_TMP/$nxt.err" >&2 + [ "$rc" -ne 0 ] && RC=1 + if [ -s "$SWEEP_TMP/$nxt.out" ]; then + # Pre-existing bug (found alongside D244's diagnostics): `head -1` + # silently discarded emit_verdict's --json line on every sweep run; + # keep the verdict line (head -1) and pull the JSON line separately. + head -1 "$SWEEP_TMP/$nxt.out" + if [ "$JSON" = 1 ]; then + j=$(grep -m1 '^{' "$SWEEP_TMP/$nxt.out") + [ -n "$j" ] && RESULTS_JSON+=("$j") + fi + else + nm="${entries[$nxt]%%:*}" + echo "$nm: NO-FRAMES frames=0 (setup failed -- see stderr)" + RC=1 + fi + nxt=$((nxt + 1)) + done + } + idx=0 for e in "${entries[@]}"; do n="${e%%:*}"; num="${e##*:}" - pin_ini_640x480 - pin_eep - out=$(verify_level "$n" "$num" "${DUMP:-$GOLDEN_DUMP}" 60) - rc=$? - [ "$rc" -ne 0 ] && RC=1 - # Pre-existing bug (found alongside D244's diagnostics): `head -1` here - # silently discarded emit_verdict's --json line (its 2nd line of - # output, starting with `{`) on every sweep run -- RESULTS_JSON above - # was declared for exactly this and never actually populated, so - # `--json` sweeps have never produced a usable verdict array; the - # workflow's "Extract JSON verdict" step (scans for the last `[{` in - # sweep.log) always got an empty `[]`. - echo "$out" | head -1 - if [ "$JSON" = 1 ]; then - j=$(echo "$out" | grep -m1 '^{') - [ -n "$j" ] && RESULTS_JSON+=("$j") - fi + while [ "$(jobs -rp | wc -l)" -ge "$JOBS" ]; do sleep 0.2; flush_done; done + ( + trap 'kill_ours; [ -n "$CUR_CAPDIR" ] && rm -rf "$CUR_CAPDIR"; exit 143' TERM + RCV=2 # an `exit` inside run_one (setup failure) still writes an rc + trap 'echo "$RCV" > "$SWEEP_TMP/$idx.rc"' EXIT + verify_level "$n" "$num" "${DUMP:-$GOLDEN_DUMP}" "$wd" >"$SWEEP_TMP/$idx.out" 2>"$SWEEP_TMP/$idx.err" + RCV=$? + ) & + idx=$((idx + 1)) + flush_done done + flush_done 1 + wait echo "SWEEP DONE" if [ "$JSON" = 1 ] && [ "${#RESULTS_JSON[@]}" -gt 0 ]; then { printf '[' @@ -526,8 +532,6 @@ case "$MODE" in resolved=$(resolve_level "$LEVEL_ARG") [ -z "$resolved" ] && { echo "unknown level '$LEVEL_ARG'" >&2; exit 2; } name="${resolved%%:*}"; num="${resolved##*:}" - pin_ini_640x480 - pin_eep run_one "$name" "$num" "${DUMP:-$GOLDEN_DUMP}" 90 if [ "$R_STATUS" != PASS ]; then emit_verdict "$name" "$R_STATUS" "$R_FRAMES" "" "$R_SYM"