diff --git a/build-pc-cmd.bat b/build-pc-cmd.bat new file mode 100644 index 00000000..0030873f --- /dev/null +++ b/build-pc-cmd.bat @@ -0,0 +1,17 @@ +@echo off +REM D356-session fix: build the PC port from a plain cmd environment. +REM +REM Background: the pi agent shell (git-bash/MSYS) silently drops nearly all +REM inherited env vars at the boundary into msys-2.0.dll programs (sh/gcc) -- +REM they see only ~7 MSYS-standard vars, so gcc has no TEMP/TMP and fails +REM with "Cannot create temporary file in C:\Windows\: Permission denied" +REM (silent exit 1 under ninja). cmd.exe carries the full Windows env, so we +REM build through it with the MSYS2 toolchain prepended to PATH (needed for +REM cc1's sibling DLLs) and a guaranteed-writable temp dir. +setlocal +set "PATH=C:\msys64\mingw64\bin;C:\msys64\usr\bin;%PATH%" +set "TEMP=C:\Users\james\AppData\Local\Temp" +set "TMP=%TEMP%" +cd /d C:\Users\james\Source\Repos\gh-fullhist +C:\msys64\mingw64\bin\cmake.exe --build build-pc -j %* +endlocal & exit /b %errorlevel% diff --git a/docs/dev/D357-SETTINGS-VALUES-PLAN.md b/docs/dev/D357-SETTINGS-VALUES-PLAN.md new file mode 100644 index 00000000..62a2e7c7 --- /dev/null +++ b/docs/dev/D357-SETTINGS-VALUES-PLAN.md @@ -0,0 +1,139 @@ +# D357 — Settings thresholds & values: align display units/defaults with the PD port + Turok PC + +Status: **PROPOSAL — awaiting sign-off** (2026-09-27). Baselines: the PD port +checkout (`C:\Users\james\Source\Repos\pd_port`, `port/src/optionsmenu.c` + +`input.c`/`video.c` config registrations), the Turok PC (Nightdive) list from +the maintainer's notes (`docs/dev/notes/OPTIONS-MENU-PLAN.md` §6 — *taken from +user notes, not checked independently*), and the N64 watch (ground truth for +the `Bond.*` rows only). Triggered by maintainer report: the current values +are "a bit confusing". + +Companion change landed with this doc's review: the dim per-file row tag is +now **"(profile)"** (was "(save)"; user request, both UIs). + +## 1. Problem + +The D356 regroup fixed the *layout*; the *values* still mix three unit +families with no consistent rule: + +- **raw internal units shown as-is**: `Input.MouseSensitivity` (1–500, 100 = + 1× — the "100" is a % of a 1.0 multiplier, not a count), + `Input.PadDeadzone` (raw 0–30000, `dispDiv=300` → "23%" but the step of 500 + raw is 1.67 display-%, so the slider lands on 23, 25, 26…), + `Bond.Music`/`Bond.FX` (0–32767, `dispDiv=328`, step 128 = 0.39 display-%); +- **N64-era vocabulary on PC rows**: texture filter "Nearest / Bilinear / + **3-Point**" (the N64 watch term for trilinear), "FOV scale %" instead of + degrees; +- **defaults that don't read as defaults**: MSAA shows "4x" fine, but + `FpsCap` 0–1000 with step 1 invites 117 FPS; `Video.LodDistance` default + 150% vs `DrawDistance` 150% with a 25-step grid (100/125/150/175…). + +The PD port is the house style to copy: per-stick sensitivity as a **±10 +multiplier (default 1.0, negatives invert)** (`input.c:1537`), mouse speed +**-30..30 (default 2.5)**, deadzone **raw 0–32767 per stick+axis**, rumble +**0–1**, MSAA as a **2x/4x/8x/16x dropdown**, frame cap as **"N FPS"**, +volumes as 0–100 %. Turok's (user's list) conventions: single mouse +sensitivity knob, per-section resets (landed, D356), master/sound/music +volume trio (master still M3-backlog). + +## 2. Principles + +1. **Display, don't rescale the world.** Every change is port-only (row + table, `dispDiv`/step/`unit` fields, value-text, `kResetDefaults`): no + `src/game` change, and config *storage units* are kept wherever a change + would migrate existing ini files (principle 3). +2. **Human units in the value column**: `%` on a 0–100 grid, `×` multipliers + with 1.0 as the default, `N FPS`, `Off/2x/4x/8x`, `Unlimited`. A slider + step must map to a **whole display unit** (the deadzone 1.67%-step bug is + the canonical example to fix). +3. **Ini stability**: renaming a config key's storage unit is a migration. + Where the plan needs it, register a *new* key (e.g. + `Input.PadDeadzonePct`) and keep the old key as a read-once legacy alias — + the D333 `Input.AimMode`→`Input.AimStyle` alias pattern (`input.c:1962`). + Each migration gets a findings note. +4. **`kResetDefaults` stays in lockstep** with the config C initializers + (D356 Gate E): every default change in this plan updates the table in + `optionsoverlay.c` in the same commit; the `GE_WSPROBE_RESET` probe + re-verifies. +5. **Defaults = the config C initializers**, never a new menu-only default. + +## 3. Row-by-row audit & proposal + +Current state from `optionsoverlay.c` `rows[]` + the `configRegister*` +ranges (2026-09-27). "Display" = what the value column shows today. + +### INPUT + +| Row | Now (default · range · step · display) | PD port | Turok (notes) | Proposal | +|---|---|---|---|---| +| Mouse sensitivity | 100 · 1–500 · 5 · raw `100` | `MouseSpeedX/Y` float −30..30, def 2.5 (sign = invert) | single knob | **Display as a × multiplier**: keep storage (100 = 1.0×), value text `%.1f×` (v/100); range 10–300 (0.1–3.0×), step 10 (0.1×). Label stays "Mouse sensitivity". | +| Invert look (mouse) | Off/On | folded into the −/× sign of MouseSpeed | — | **Keep On/Off** (PD's negative-multiplier style is an alternative; a separate invert is simpler next to the × slider). Decision: confirm. | +| Aim style / Aim range | N64/Centred (PC) · PC/N64 | — (GE-only, D337/D338) | — | Keep as-is (GE-specific; hiddenIfOn already sane). | +| Invert look (controller) | Off/On | sign of stick scale | — | Keep. | +| Stick deadzone | 7000 raw · 0–30000 · 500 · ÷300 → `23%` (1.67%-steps) | raw 0–32767 per stick+axis, `DEFAULT_DEADZONE` | MISSING | **New key `Input.PadDeadzonePct` 0–100, step 1, default 23** (7000/30000×100); `input.c` scales back to raw at apply; old key becomes a legacy alias (principle 3). Display `N%` on a whole-% grid. | +| Trigger threshold | 23 · 1–99 · 1 · `%` | — (GE extra) | — | Keep. | + +### GAMEPLAY + +| Row | Now | Baseline | Proposal | +|---|---|---|---| +| Auto-aim / Sight / Look ahead / Ammo (profile) | Off/On (N64 watch 0/1) | N64 watch = ground truth | Keep; tag now reads **(profile)** (this doc's companion change). | +| Skip intro / No hit flash / All unlocked | Off/On | Turok: screen-shake/hit-flash family | Keep. | +| HUD scale | 100 · 75–150 · 5 · `%` | — | Keep (whole-% grid already; range cap D226). | + +### GRAPHICS + +| Row | Now | Baseline | Proposal | +|---|---|---|---| +| Anti-aliasing | 1/2/4/8, def 4, shows `None/2x/4x/8x`, restart | PD dropdown 2x/4x/8x/**16x**, def 4x | **Add 16x to the sequence** (`kMsaaSeq`, range clamp 1–16) — SDL/GL supports it; PD has it. Decision: perf cost on mid GPUs (restart already flagged). | +| Texture filter | Nearest / Bilinear / **3-Point** | PC convention: Nearest/Linear/Bilinear | **Rename the enum names to `Nearest / Linear / Bilinear`** (3-point *is* trilinear; the N64 term is the last one left on a PC row). Storage 0/1/2 unchanged. | +| Anisotropic filtering | 4 · 1–16 · 1 · `x` | Turok 1–16 | Keep; **dim/disable when filter = Nearest** (anisotropy only applies to filtered sampling) — `hiddenIfOff`-style guard, UI-only. | +| FOV scale | 100 · 50–150 · 5 · `%` | Turok 3: 60–120° slider | **Display in degrees**: the 100% baseline is the N64 vertical FOV scaled to the port's widescreen math — value text `N°` (v/100 × baseline, 1 decimal); storage unchanged. Decision: degrees vs % (Turok uses degrees; % is the existing mental model — pick one, recommend degrees). | +| Native widescreen / Widescreen auto FOV / Crop overscan | Off/On | — | Keep. | +| Draw distance | 150 · 100–400 · 25 · `%` (hidden under auto-FOV) | Turok: draw-distance row | Keep the % grid (25% steps read fine); **step 25 → display already whole-%**. No change. | +| LOD distance | 150 · 25–400 · 25 · `%` | — | Keep; note default 150 vs DrawDistance 150 is intentional parity. | + +### AUDIO + +| Row | Now | Baseline | Proposal | +|---|---|---|---| +| Music volume (profile) | 32767 · 0–32767 · 128 · ÷328 → 0.39%-steps | Turok: 0–100% | **step 128 → 328** (exactly 1 display-%; 32767/328 = 99.9 ≈ "100%"). Storage unchanged (N64 `VOLUME_MAX` semantics). | +| FX volume (profile) | same | same | same. | +| *(master volume)* | — | Turok trio | M3-backlog, out of scope (shape the section for it, D356 note). | + +### VIDEO + +| Row | Now | Baseline | Proposal | +|---|---|---|---| +| Fullscreen | Off/On | Turok: window mode (borderless missing) | Keep; borderless = separate backlog item. | +| Resolution | action row (presets) | both | Keep. | +| VSync | On/Off | both | Keep. | +| Frame rate cap | 60 · 0–1000 · 1 · `Uncapped/N FPS` | Turok: "up to 120 FPS" / uncapped | **Preset grid**: 0/30/60/120/144 (step = next preset, wraps to Uncapped); value text already `N FPS`/`Uncapped`. Decision: preset grid vs free 1-step (recommend presets; 117 FPS has no purpose). | +| Show FPS | Off/On | both | Keep. | + +## 4. Terminology + +- Per-file row tag: **"(save)" → "(profile)"** (landed with this doc's + review; `frontoptions.c` + `optionsoverlay.c`, both draw paths + comments). +- Open question for the maintainer: the front screen's top row still reads + **"Save file: File N"** — rename to "Profile: File N" for consistency? + (Not done; the save *file* is literally what's being chosen.) + +## 5. Gates + +1. Port-only per principle 1; the deadzone key migration (principle 3) is + the only config-surface change and follows the D333 alias pattern. +2. `kResetDefaults` updated in the same commit as every default/range + change; `GE_WSPROBE_RESET` (front + `-level_33`) re-run, all sections + `failures=0`. +3. Build via `build-pc-cmd.bat` (the pi-shell MSYS env gap, D356 note). +4. Visual check owed (maintainer): front options screen + F10 in all five + sections; the overlap fix from the same session (the level-1 save-file + row no longer paints under the first content row). + +## 6. Out of scope + +- Key rebinding (Phase 4), master volume (M3), output-device selection, + borderless window, per-axis controller sensitivity (PD-style four-knob — + GE's one-knob design is D238/D304). +- Any `src/game` change; the N64 watch keeps its own values and wording. diff --git a/port/src/frontoptions.c b/port/src/frontoptions.c index d1b48f64..6573e70e 100644 --- a/port/src/frontoptions.c +++ b/port/src/frontoptions.c @@ -330,8 +330,10 @@ void frontOptionsMenuInterface(void) * D352 chooser; folders only, "none" retired, plan §5.5). */ playSfx(DOOR_LOCK_SFX); watchSettingsChooseFile(1); - } else if (s_hl >= 0 && s_hl < s_rowN) { - int i = s_rowIdx[s_hl]; + } else if (s_hl >= 1 && s_hl <= s_rowN) { + /* s_hl is item-numbered (0 = the save-file row); content row k + * is item k+1, so the row index is s_hl - 1. */ + int i = s_rowIdx[s_hl - 1]; playSfx(DOOR_LOCK_SFX); if (optionsRowIsSlider(i) && cursor_h_pos >= BAR_X0 - 4 && cursor_h_pos <= BAR_X1 + 4) { @@ -384,7 +386,7 @@ void frontOptionsMenuInterface(void) if (s_hl == 0) { watchSettingsChooseFile(dir); /* save-file row */ } else { - int i = s_rowIdx[s_hl]; + int i = s_rowIdx[s_hl - 1]; if (optionsRowIsReset(i)) optionsRowActivateReset(i); else @@ -399,7 +401,7 @@ void frontOptionsMenuInterface(void) /* D356: reset arm state -- the screen's selected rows[] index (s_hl 0 is * the save row, not a rows[] entry); moving on disarms, goBack clears. */ - optionsResetMaintain(s_level == 1 && s_hl > 0 ? s_rowIdx[s_hl] : -1); + optionsResetMaintain(s_level == 1 && s_hl > 0 ? s_rowIdx[s_hl - 1] : -1); /* The dossier: tabs + a blank CLASSIFIED page, as the cheat screen. */ disable_all_switches(walletinst[0]); @@ -518,19 +520,22 @@ Gfx *frontOptionsMenuDraw(Gfx *DL) for (int k = 0; k < s_rowN; k++) { int i = s_rowIdx[k]; - int y = rowY(k); + /* Content row k is item k+1 (item 0 is the save-file row), so + * it sits one line below the save row -- rowY() already shifts + * level-1 items down by one. */ + int y = rowY(k + 1); char label[48]; snprintf(label, sizeof(label), "%s\n", optionsRowLabel(i)); - if (k == s_hl) { + if (k + 1 == s_hl) { DL = microcode_constructor_related_to_menus(DL, ROW_X - 2, y - 1, ROW_X + measureW(label) + 5, y + 0xE, HILITE); } DL = ink(DL, ROW_X, y, label, INK); - /* D356: per-file rows in the mixed-scope section carry a dim - * "(save)" tag (the section title already says which file). */ + /* D357: per-file rows in the mixed-scope section carry a dim + * "(profile)" tag (the section title already says which file). */ if (optionsRowIsSaveScoped(i)) { - DL = ink(DL, ROW_X + measureW(label) + 4, y, "(save)\n", INK_DIM); + DL = ink(DL, ROW_X + measureW(label) + 4, y, "(profile)\n", INK_DIM); } if (optionsRowIsSlider(i)) { diff --git a/port/src/optionsoverlay.c b/port/src/optionsoverlay.c index 3fdfdf4e..fcfbe4e6 100644 --- a/port/src/optionsoverlay.c +++ b/port/src/optionsoverlay.c @@ -146,7 +146,7 @@ struct Row { int dispDiv; /* D356: 1 = this row's value lives in the selected save file (per-file - * watch rows). Carries the dim "(save)" tag on the only section that + * watch rows). Carries the dim "(profile)" tag on the only section that * mixes scopes (GAMEPLAY); on headers the flag is recomputed at init to * mean "this section contains per-file rows" (drives the "(File N)" * title annotation in both UIs). */ @@ -158,7 +158,7 @@ static struct Row rows[] = { * GAMEPLAY, GRAPHICS, AUDIO, VIDEO; docs/dev/D356-SETTINGS-REGROUP-PLAN.md). * The D353 "BOND FILE" section -- the one named after WHERE a value is * saved -- is gone: its surviving rows split into GAMEPLAY (the per-file - * watch toggles, dim "(save)" tagged since the section mixes scopes) and + * watch toggles, dim "(profile)" tagged since the section mixes scopes) and * AUDIO (the per-file volume sliders, homogeneous so title annotation * only), and the "Edit file" chooser row is retired in favour of the * single top save-file row on the front options screen (frontoptions.c). @@ -198,7 +198,7 @@ static struct Row rows[] = { * Bond.AimControl collides with Input.AimMode (two rows fighting over * the same N64-vs-PC aim model). * The four surviving per-file toggles move here from the D353 BOND FILE - * section and carry .saveScoped (dim "(save)" tag, the only mixed-scope + * section and carry .saveScoped (dim "(profile)" tag, the only mixed-scope * section). */ /* { .key="Bond.Look", .label="Look up/down (watch; stacks)", .kind=ROW_TOGGLE, .names=kReverse, .found=1, .uiMax=1, .cfgMax=1 }, */ @@ -1436,11 +1436,11 @@ Gfx *optionsOverlayEmit(void) } gdl = drawText(gdl, OV_LABEL_X, rowY, (char *)r->label, r->found ? col : 0x808080ff); - /* D356: per-file rows in the mixed-scope section carry a dim - * "(save)" tag (the section title already says which file). */ + /* D357: per-file rows in the mixed-scope section carry a dim + * "(profile)" tag (the section title already says which file). */ if (r->saveScoped && r->kind != ROW_HEADER) { gdl = drawText(gdl, OV_LABEL_X + measureText(r->label) + 5, rowY, - "(save)", 0x909090ff); + "(profile)", 0x909090ff); } if (!r->found) { gdl = drawTextR(gdl, right, rowY, "(n/a)", 0x808080ff); @@ -1520,7 +1520,7 @@ int optionsRowIsBondChooser(int i) /* D356: content rows carry the literal per-file flag from the table; header * rows were recomputed at init to "this section contains per-file rows". - * Both drive UI decoration (the "(save)" tag, the "(File N)" annotation). + * Both drive UI decoration (the "(profile)" tag, the "(File N)" annotation). * The F10 overlay and the front options screen are the only consumers. */ int optionsRowIsSaveScoped(int i) {