forked from Ports/goldeneye-pc-port
D401: Rumble Pak -> real gamepad haptics (re-promotes D224/M-87)
Port layer only (zero src/ edits), per docs/dev/RUMBLE-PLAN.md: - port/src/input.c: inputRumbleSupported/inputRumble -> SDL_GameControllerRumble; detection via SDL_GameControllerHasRumble (SDL >= 2.0.18) else SDL_JoystickIsHaptic + known-type Windows fallback; zeroed on pad close. Single global gRumbleScale (dflt 0.5) registered as Input.RumbleScale. - port/src/libultra.c: the two M-87 blockers resolved - g_contStatus[i].status now sets CONT_CARD_ON for connected rumble-capable pads (so joyRumblePakInit arms) and osPfsInit returns PFS_ERR_DEVICE for them (so osMotorInit is attempted); osMotorInit/Start/Stop drive the haptics API. - port/src/optionsoverlay.c: one global 'Rumble strength' ROW_SLIDER (Input.RumbleScale, 0.05 detents, %.2f) in the INPUT section + kResetDefaults entry. The four per-player Input.PlayerN.RumbleScale keys were dropped (user decision 2026-09-28); stale keys in an existing ini log four 'unknown key' LOG_NOTE lines and self-clean on next configSave. - Real-pad haptics confirmed working by the user (2026-09-28). Build env: build-pc.sh self-heal guard revision - re-exec now writes a self-contained .ps1 (msys->native argv conversion mangles $-bearing -Command strings; file write is conversion-free), builds PATH explicitly, picks TMP from writable candidates (GetTempPath honours the broken inherited TMP), self-logs to build-pc/ge007-native-reexec-*.log, and treats a null LASTEXITCODE as 'did not execute' (PS 5.1 piped native commands error non-terminating). OPEN: the guard still fails inside build-pc.sh while the identical standalone .ps1 invocation works - AGENTS.md documents the workaround. docs: RUMBLE-PLAN.md (new, tracked), findings.md D401 entry + index rows, QOL-INVENTORY D224 -> DONE (D401), findings-index.csv regenerated.
This commit is contained in:
1 parent
a548fc4de6
commit
471eecd6d3
10 files changed
+702
-13
No files matched your search
@@ -39,6 +39,53 @@ Needs CMake + SDL2 + zlib + OpenGL, and must run from the MSYS2 MINGW64 shell
|
||||
(see `build-pc.sh` header and `docs/building.md`). ROM goes in `./data/`
|
||||
(not distributed); assets must be extracted from it first (`docs/building.md`).
|
||||
|
||||
**Windows build environment (three recurring failure modes — diagnose in this order):**
|
||||
|
||||
1. **`Cannot create temporary file in C:\Windows\: Permission denied`** at the
|
||||
link step. The PE toolchain (ninja → cmd → gcc/ld) needs a writable
|
||||
TMP/TEMP; the msys→native env conversion drops it in non-login shells
|
||||
(agent harnesses; the `C:/msys64` tree here was built for `D:/M/msys64`,
|
||||
so its conversion is unreliable). `build-pc.sh` now **self-heals**: it
|
||||
probes a native child's TMP (via a file — piped `cmd.exe` stdout is
|
||||
unreliable under msys console emulation) and, if broken, re-runs
|
||||
cmake+build under PowerShell with `TMP`/`TEMP` set natively (the
|
||||
native→native boundary passes env through intact; only msys→native is
|
||||
broken). If you run cmake/ninja *by hand* from a broken shell: run them
|
||||
from cmd/PowerShell with `C:\msys64\mingw64\bin` on PATH.
|
||||
**Guard revision (2026-09-28):** the re-exec writes a self-contained
|
||||
`.ps1` to a temp file and runs `powershell -File` with plain path/word
|
||||
args only (the msys→native argv conversion mangles `$`-bearing
|
||||
`-Command` strings; a file write is conversion-free). Inside: PATH is
|
||||
built explicitly (never trust the inherited value); TMP is chosen from
|
||||
writable candidates (`%LocalAppData%\Temp` first —
|
||||
`[System.IO.Path]::GetTempPath()` honours the inherited *broken* TMP,
|
||||
observed as `C:\Windows\`); every native step self-logs to
|
||||
`build-pc/ge007-native-reexec-{diag,cmake,build}.log`; a null
|
||||
`$LASTEXITCODE` ("did not execute") is a distinct failure (exit 2/3) —
|
||||
in PS 5.1 a piped native command errors out non-terminating and never
|
||||
sets it, so an unguarded `if ($LASTEXITCODE -ne 0)` passes vacuously.
|
||||
In `.ps1`, `-DROMID=$Var` is a literal (bare tokens don't expand) — it
|
||||
must be `"-DROMID=$Var"`. Launch powershell plainly — `env -i` before it
|
||||
breaks the nested cmake launch (verified 2026-09-28).
|
||||
**OPEN (next session):** the guard still fails *inside* `build-pc.sh`
|
||||
(cmake "did not execute": the `.ps1` runs, but `$LASTEXITCODE` stays
|
||||
empty) while the *identical* standalone invocation succeeds (cmake runs
|
||||
and the build completes — that is how the D401 builds were made). First
|
||||
test: diff the heredoc-generated `.ps1` against a hand-extracted copy
|
||||
(suspect: heredoc line-ending/whitespace corruption).
|
||||
**Working workaround:** extract the heredoc `.ps1` and run
|
||||
`powershell -NoProfile -ExecutionPolicy Bypass -File <ps1> "C:\msys64\usr\bin" "C:\msys64\mingw64\bin" C:/msys64 <repo-win-path> build-pc <romid>`.
|
||||
2. **`cannot open output file ge007.x86_64.exe: Permission denied`.** A
|
||||
**running** `ge007.x86_64.exe` locks the output file (Windows rule; you
|
||||
can't relink over a live PE). Check with `Get-Process | Where-Object {
|
||||
$_.ProcessName -like '*ge007*' }` and close the game before rebuilding.
|
||||
An agent must not kill the user's game process to make a link succeed.
|
||||
3. **`cc1.exe: ... libmpfr-6.dll: cannot open shared object file`.** The
|
||||
calling PATH lacks `C:\msys64\mingw64\bin` (the gcc driver finds cc1 via
|
||||
its own directory, but the child needs the mingw DLL dir on PATH). In an
|
||||
msys shell: `export PATH="/c/msys64/mingw64/bin:/c/msys64/usr/bin:$PATH"`
|
||||
before building.
|
||||
|
||||
## Verification ritual (after any build-affecting change)
|
||||
|
||||
1. **Undefined symbols.** Every symbol referenced by the compiled set (see `CMakeLists.txt`: `SRC_GAME`, `SRC_ENGINE`, `SRC_LIBAUDIO`, `SRC_LIBULTRARE_AUDIO`, `SRC_LIBULTRARE_DATA`, `SRC_GU`, `SRC_PORT*`) must be defined exactly once in the compiled set or in `port/`. Symbols that live in EXCLUDED files (`libultra/io/*`, `libultrare/io/*` except `vitbl.c`, `libultra/os/*`, `libultrare/os/*`, `sched.c`, `rmon.c`, `vi.c`, `src/*.s`) must be provided by `port/src/libultra.c`, `gesched.c`, `n64stubs.c`, `random.c`, or `ucode.c`.
|
||||
|
||||
+127
-1
@@ -14,7 +14,7 @@
|
||||
# Example (Linux):
|
||||
# sudo apt install cmake libsdl2-dev zlib1g-dev libgl1-mesa-dev
|
||||
# Example (macOS):
|
||||
# brew install cmake sdl2 zlib
|
||||
# brew install cmake libsdl2-dev
|
||||
# Example (Windows/MSYS2):
|
||||
# pacman -S mingw-w64-x86_64-toolchain mingw-w64-x86_64-SDL2 \
|
||||
# mingw-w64-x86_64-zlib mingw-w64-x86_64-cmake
|
||||
@@ -24,6 +24,132 @@ set -euo pipefail
|
||||
ROMID="${1:-ntsc-final}"
|
||||
BUILD_DIR="${BUILD_DIR:-build-pc}"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Windows toolchain env guard (see AGENTS.md "Build" — the recurring
|
||||
# "Cannot create temporary file in C:\Windows\: Permission denied" link
|
||||
# failure).
|
||||
#
|
||||
# The PE toolchain (ninja -> cmd -> gcc/ld) creates temp files under the
|
||||
# child's TMP/TEMP. A full MSYS2 MINGW64 login shell arranges that for
|
||||
# native children; other shells (agent harnesses, non-login shells) do not
|
||||
# -- the msys->native env conversion silently drops/breaks TMP and the
|
||||
# linker falls back to C:\Windows\. Probe what a native child actually
|
||||
# sees (via a file: msys console emulation makes piped cmd.exe stdout
|
||||
# unreliable), and if broken, re-run the cmake+build steps under
|
||||
# PowerShell. Crucially the temp vars are set INSIDE PowerShell: the broken
|
||||
# boundary is msys->native, while native->native (powershell -> cmake ->
|
||||
# ninja) passes env vars through intact.
|
||||
# GE_PC_BUILD_VIA_NATIVE=1 prevents a re-exec loop.
|
||||
# ---------------------------------------------------------------------------
|
||||
if [ -n "${MSYSTEM:-}" ] && [ -z "${GE_PC_BUILD_VIA_NATIVE:-}" ]; then
|
||||
_probe="$(mktemp -d "${TMPDIR:-/tmp}/gebuildprobe.XXXXXX")"
|
||||
_probe_win="$(cygpath -w "${_probe}" 2>/dev/null || echo 'C:\\msys64\\tmp')"
|
||||
/c/Windows/system32/cmd.exe //c "if exist \"%TMP%\" (echo ok > \"${_probe_win}/ok.txt\") else (echo bad > \"${_probe_win}/bad.txt\")" >/dev/null 2>&1 || true
|
||||
if [ ! -s "${_probe}/ok.txt" ]; then
|
||||
command -v powershell >/dev/null 2>&1 || {
|
||||
echo "ERROR: native TMP is unusable from this shell and powershell.exe was not found for the fallback." >&2
|
||||
echo "Manual fix: run cmake+ninja from cmd/PowerShell with C:\\msys64\\mingw64\\bin on PATH, or" >&2
|
||||
echo "set the user env vars (setx TMP \"C:\\msys64\\tmp\"; setx TEMP \"C:\\msys64\\tmp\") and re-open the shell." >&2
|
||||
rm -rf "${_probe}"; exit 1
|
||||
}
|
||||
echo "==> native TMP unusable from this shell; re-running cmake+build via PowerShell"
|
||||
export GE_PC_BUILD_VIA_NATIVE=1
|
||||
_here="$(pwd -W 2>/dev/null || cygpath -w "$(pwd)")"
|
||||
# Resolve the Windows-side toolchain paths from what msys found (no
|
||||
# hardcoded install dir: C:\msys64 on this box, D:\M\msys64
|
||||
# elsewhere). usr/bin first: mingw gcc.exe needs msys-2.0.dll from
|
||||
# there.
|
||||
_cmake_root="$(dirname "$(command -v cmake)")" # .../mingw64/bin
|
||||
_msys_root="$(cd "${_cmake_root}/../.." && pwd -W 2>/dev/null || cygpath -w "$(cd "${_cmake_root}/../.." && pwd)")" # msys install root (.. twice: bin -> mingw64 -> root)
|
||||
_bin_w="$(cygpath -w "${_cmake_root}")"
|
||||
_usrbin_w="$(cygpath -w "${_msys_root}/usr/bin")"
|
||||
# The PowerShell script is written to a .ps1 FILE, not passed via
|
||||
# -Command: the msys->native argv conversion mangles $-bearing
|
||||
# strings (a '\$LASTEXITCODE' check arrived as an empty token and a
|
||||
# failed cmake would have exited 0 silently). A file write is
|
||||
# conversion-free; only plain path/word arguments cross the boundary.
|
||||
_ps1="$(mktemp "${TMPDIR:-/tmp}/ge007-rerun.XXXXXX.ps1")"
|
||||
cat > "${_ps1}" <<'PS1'
|
||||
param([string]$UsrBin, [string]$Bin, [string]$MsysRoot, [string]$Here, [string]$BuildDir, [string]$RomId)
|
||||
# Self-logging: a headless shell loses this process's console output, so
|
||||
# every fact that matters goes to diag/log files in the build dir.
|
||||
$diag = Join-Path $BuildDir "ge007-native-reexec-diag.log"
|
||||
$cmakeLog = Join-Path $BuildDir "ge007-native-reexec-cmake.log"
|
||||
$buildLog = Join-Path $BuildDir "ge007-native-reexec-build.log"
|
||||
# The inherited native PATH may be mangled/dropped by the broken msys->native
|
||||
# env conversion, so build it explicitly: the toolchain only needs the two
|
||||
# msys bin dirs plus the usual system dirs. (usr/bin first: mingw gcc/cmake
|
||||
# need msys-2.0.dll from there.)
|
||||
$env:PATH = "$UsrBin;$Bin;C:\Windows\System32;C:\Windows;C:\Windows\System32\Wbem"
|
||||
# Writable temp for the native toolchain. [System.IO.Path]::GetTempPath()
|
||||
# CANNOT be used here: it honours the inherited (broken) TMP/TEMP env vars
|
||||
# and returned C:\Windows\. Pick the first writable candidate instead.
|
||||
$tmpdir = $null
|
||||
foreach ($cand in @((Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'Temp'), (Join-Path $MsysRoot 'tmp'))) {
|
||||
try {
|
||||
if (-not (Test-Path $cand)) { New-Item -ItemType Directory -Force -Path $cand -ErrorAction Stop | Out-Null }
|
||||
$probe = Join-Path $cand 'ge007-tmp-write-test.tmp'
|
||||
'x' | Out-File -FilePath $probe -ErrorAction Stop
|
||||
Remove-Item $probe -ErrorAction SilentlyContinue
|
||||
$tmpdir = $cand
|
||||
break
|
||||
} catch { }
|
||||
}
|
||||
if ($null -eq $tmpdir) { "no writable temp dir found" | Out-File $diag -Encoding ascii; Write-Host 'no writable temp dir for the native toolchain'; exit 4 }
|
||||
$env:TMP = $tmpdir
|
||||
$env:TEMP = $tmpdir
|
||||
"start UsrBin=[$UsrBin] Bin=[$Bin] MsysRoot=[$MsysRoot] Here=[$Here] BuildDir=[$BuildDir] RomId=[$RomId] tmp=[$tmpdir]" | Out-File $diag -Encoding ascii
|
||||
$exe = "$Bin\cmake.exe"
|
||||
"exe=[$exe] exists=[$(Test-Path -LiteralPath $exe)]" | Out-File $diag -Append -Encoding ascii
|
||||
Set-Location $Here
|
||||
"cwd=[$([System.IO.Directory]::GetCurrentDirectory())]" | Out-File $diag -Append -Encoding ascii
|
||||
try {
|
||||
# -DROMID must be a DOUBLE-QUOTED string: a bare `-DROMID=$RomId` token
|
||||
# does NOT expand the variable in PowerShell (cmake got the literal
|
||||
# "$RomId" and failed with "Unknown ROMID").
|
||||
$out = & $exe -S . -B $BuildDir "-DROMID=$RomId" 2>&1
|
||||
"cmake ran LASTEXITCODE=[$LASTEXITCODE]" | Out-File $diag -Append -Encoding ascii
|
||||
$out | Out-File -FilePath $cmakeLog -Encoding ascii
|
||||
} catch {
|
||||
"cmake TERMINAL ERROR: $($_.Exception.Message)" | Out-File $diag -Append -Encoding ascii
|
||||
}
|
||||
if ($null -eq $LASTEXITCODE) { Write-Host "cmake.exe did not execute (diag: $diag)"; exit 2 }
|
||||
if ($LASTEXITCODE -ne 0) { Write-Host "cmake re-configure failed (rc=$LASTEXITCODE; log: $cmakeLog)"; exit $LASTEXITCODE }
|
||||
Write-Host "cmake reconfigured (TMP=$($env:TMP))"
|
||||
try {
|
||||
$out = & $exe --build $BuildDir -j 2>&1
|
||||
"build ran LASTEXITCODE=[$LASTEXITCODE]" | Out-File $diag -Append -Encoding ascii
|
||||
$out | Out-File -FilePath $buildLog -Encoding ascii
|
||||
} catch {
|
||||
"build TERMINAL ERROR: $($_.Exception.Message)" | Out-File $diag -Append -Encoding ascii
|
||||
}
|
||||
if ($null -eq $LASTEXITCODE) { Write-Host "cmake --build did not execute (diag: $diag)"; exit 3 }
|
||||
if ($LASTEXITCODE -ne 0) { Write-Host "build failed (rc=$LASTEXITCODE; log: $buildLog)"; exit $LASTEXITCODE }
|
||||
"done" | Out-File $diag -Append -Encoding ascii
|
||||
Write-Host "cmake + build ok (native re-exec, TMP=$($env:TMP))"
|
||||
PS1
|
||||
_ps1_win="$(cygpath -w "${_ps1}")"
|
||||
rc=0
|
||||
# NOTE: do NOT launch powershell via `env -i` (tried 2026-09-28): a
|
||||
# native child started with a scrubbed msys env cannot launch
|
||||
# cmake.exe (CreateProcess fails silently; LASTEXITCODE stays null).
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "${_ps1_win}" "${_usrbin_w}" "${_bin_w}" "${_msys_root}" "${_here}" "${BUILD_DIR}" "${ROMID}" || rc=$?
|
||||
rm -f "${_ps1}"
|
||||
if [ "${rc}" -ne 0 ]; then
|
||||
# Surface the .ps1's own diagnostics (console output is lost in a
|
||||
# headless shell) plus whatever the failing step wrote.
|
||||
for _lg in "${BUILD_DIR}/ge007-native-reexec-diag.log" "${BUILD_DIR}/ge007-native-reexec-cmake.log" "${BUILD_DIR}/ge007-native-reexec-build.log"; do
|
||||
if [ -s "${_lg}" ]; then
|
||||
echo "---- ${_lg} (last 40 lines) ----" >&2
|
||||
tail -40 "${_lg}" >&2
|
||||
fi
|
||||
done
|
||||
fi
|
||||
exit "${rc}"
|
||||
fi
|
||||
rm -rf "${_probe}"
|
||||
fi
|
||||
|
||||
echo "==> Configuring PC port (ROMID=${ROMID})"
|
||||
cmake -S . -B "${BUILD_DIR}" -DROMID="${ROMID}"
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ ScreenShakeIntensity. Debug: FrameDump, InputLog.
|
||||
| **DisplayFPS** _(DONE M-83, D213)_ | config `Video.DisplayFPS` | P | S | Top-right readout, config-only; ~0.5 s sample window. |
|
||||
| **Anisotropic filtering** _(DONE M-83, D212)_ | `Video.Anisotropy` (1–16, dflt 4) | P | S | fast3d already had the GL hook + a hardcoded 4×; M-83 exposed it as config. MipmapFilter / TextureFilter2D still not surfaced. |
|
||||
| Per-pad tuning: per-stick deadzones, stick scale, rumble scale, device index, swap sticks, C-button mapping | `input.c` padsCfg block | P | M | Medium. Matters for real-controller users; PD pattern is a config section per pad. **M-87 survey addendum:** PD does *incremental* hot-plug (`SDL_CONTROLLERDEVICEADDED/REMOVED` opens/closes individual pads into the first free slot, remembers `deviceIndex=-1` on removal so a replugged pad returns to its configured seat, `input.c:473-491`); GE's port instead does a full rescan-close-and-reopen-everything (`inputRescanPads()`, `input.c:983-1000`) and the game itself latches the controller mask at boot, so a pad added later merges into controller 0 for play. PD's incremental-with-remembered-seat approach is the natural implementation shape for this row and sidesteps the boot-latch merge — fold into the design when this row gets built, not a separate item. |
|
||||
| **Rumble → D224 (M-87). DEPRIORITIZED (M-87, user call) — gamepad-only, this release targets mouse/keyboard as primary.** GE's port fully no-ops the N64 Rumble Pak (`port/src/libultra.c:1220-1223` `osMotorInit/Start/Stop` all stubbed "no accessories on the PC") — GE genuinely has a rumble subsystem (`src/motor.c`, `src/joy.c`) that never reaches any output. **PD's PC port wires this straight through to real gamepad rumble**: `inputRumbleSupported()`/`inputRumble()` call `SDL_GameControllerRumble()` when the connected pad reports haptics, `Input.PadN.RumbleScale` config + an options-menu slider, `osMemSize`-style "pretend the accessory is there" pattern applied to a real one instead of faked. | P | M | Concrete PD-port precedent, straightforward port-layer wiring, ready whenever gamepad support is prioritized (no `src/` game-logic touch — GE's `motor.c` calls already exist, just need `osMotorStart/Stop` in `libultra.c` to route to a real `SDL_GameControllerRumble` call the way PD's do). |
|
||||
| **Rumble** _(DONE D401 — D224 / M-87, re-promoted 2026-09-27)_ | `port/src/input.c` + `port/src/libultra.c` + `port/src/optionsoverlay.c` | P | M | D401 implements the PD precedent per `docs/dev/RUMBLE-PLAN.md`: `inputRumbleSupported()`/`inputRumble()` → `SDL_GameControllerRumble()` when the pad reports haptics (SDL ≥ 2.0.18 `HasRumble`, else `SDL_JoystickIsHaptic` + known-type Windows fallback), per-pad haptics detection (`SDL_GameControllerHasRumble` / `SDL_JoystickIsHaptic` + known-type Windows fallback), a single global `Input.RumbleScale` (0..1, dflt 0.5) config + one `Rumble strength` slider row; `libultra.c` sets `CONT_CARD_ON` in `g_contStatus` for rumble-capable pads and routes `osPfsInit` → `PFS_ERR_DEVICE`, `osMotorInit/Start/Stop` → the haptics API — the two M-87 addendum blockers that kept `joyRumblePakInit` from arming. Zero `src/` edits. **Real-pad haptics confirmed working by the user (2026-09-28).** Plan item 3 landed the same day: the original four per-player rows were replaced (user decision) with one global `Rumble strength` slider in the F10 overlay + front options screen (`optionsoverlay.c`, shared `rows[]`), 0.05 detents, `kResetDefaults` 0.5; the four `Input.PlayerN.RumbleScale` ini keys were dropped (stale keys self-clean on the next save). A disabled-when-unsupported row state is deferred with the per-pad tuning row (needs a generic row-disabled mechanism). Unverified: `RumbleScale=0` silence check (trivial early-out). |
|
||||
| FakeGamepads / FirstGamepadNum / UseHIDAPI | `input.c` | P | S–M | Niche (local MP on PC); defer until MP is actually played. |
|
||||
| Window polish: DefaultFullscreen/Maximize, CenterWindow, AllowHiDpi, ExclusiveFullscreen | `video.c` vid* block | P | S | HiDpi + center are the useful ones on modern Windows. |
|
||||
| VSync adaptive (−1..10) | `Video.VSync` range | P | S | Note for FPS plan — GE's 0/1 toggle is fine until then. |
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
# Rumble Pak → real gamepad haptics (port plan)
|
||||
|
||||
Status: **IMPLEMENTED — finding D401 (2026-09-27/28).** Items 1+2+3 all
|
||||
landed (port layer only; zero `src/` edits), re-promoting the
|
||||
DEPRIORITIZED finding **D224** (M-87) to DONE. **Real-pad haptics confirmed
|
||||
working by the user (2026-09-28).** Menu shape: **one global `Rumble
|
||||
strength` slider** (`Input.RumbleScale`, user decision 2026-09-28 — the
|
||||
four per-player rows were dropped). Only the `RumbleScale=0` silence check
|
||||
remains unverified (trivial early-out path).
|
||||
|
||||
Scope: wire the N64 Rumble Pak path (already present in `src/`) through to
|
||||
`SDL_GameControllerRumble()` on the PC. **Port-layer only — zero `src/game` or
|
||||
`src/` logic edits**, so rule 2 / ABI-exception do not apply.
|
||||
|
||||
Reference: the PD PC port (local checkout `../pd_port`, `port/src/input.c`
|
||||
+ `port/src/libultra.c`) implements the identical feature for the same Rare
|
||||
engine family. Copy its shape; the only friction is that **GE's `src/joy.c`
|
||||
calls the classic libultra names** (`osMotorInit/Start/Stop`, `osPfsInit/
|
||||
osPfsIsPlug`) while PD's engine renamed them (`osMotorProbe`,
|
||||
`__osMotorAccess`, `osPfsInitPak`). We translate names, not logic.
|
||||
|
||||
---
|
||||
|
||||
## Ground truth (current state)
|
||||
|
||||
Game side — **already correct, untouched:**
|
||||
- `src/joy.c` full Rumble-Pak state machine: `joyRumblePakInit` (:178),
|
||||
`joyRumblePakTick` (:298), `joyRumblePakStart`/`Stop`. Call sites (verified
|
||||
2026-09-27 for D401) live in `gunfire.c:3284` (start) / `:3288` (stop),
|
||||
`bondview2.c:9722` (start) / `:9726` (stop, incl. 2P second pad),
|
||||
`lv.c:1688` (stop at level start), plus `sched.c:272` (stop on level
|
||||
teardown).
|
||||
- `src/motor.c` is **EXCLUDED** from the PC build (N64 SI-DMA driver), replaced
|
||||
by no-op stubs. CMakeLists.txt:525 documents this.
|
||||
|
||||
Port side — **all no-oped today, in `port/src/libultra.c`:**
|
||||
- `osPfsInit` (:1413) → `return PFS_ERR_NOPACK;`
|
||||
- `osPfsIsPlug` (:1415) → `*pattern = 0; return 0;`
|
||||
- `osMotorInit` (:1417) → `return -1;`
|
||||
- `osMotorStart` (:1419) → `return -1;`
|
||||
- `osMotorStop` (:1420) → `return -1;`
|
||||
- `g_contStatus[i].status = 0;` (:1102) — `CONT_CARD_ON` **never set**.
|
||||
|
||||
Gamepad side — **exists, no change needed:**
|
||||
- `port/src/input.c` owns `static SDL_GameController *pads[MAX_PADS]`
|
||||
(MAX_PADS = 4, aligned with `MAXCONTROLLERS`), open/close, hotplug,
|
||||
`inputComputePad()`. This is the exact object `SDL_GameControllerRumble()`
|
||||
needs.
|
||||
|
||||
## The two upstream blockers (D224 M-87 addendum — MUST land together)
|
||||
|
||||
`joyRumblePakInit` (src/joy.c:178) only drives the motor if BOTH pass:
|
||||
1. `g_ContStatus[index].status & CONT_CARD_ON` → today always 0, so init
|
||||
never even runs.
|
||||
2. `osPfsInit(...)` returns `PFS_ERR_ID_FATAL` **or** `PFS_ERR_DEVICE` →
|
||||
today returns `PFS_ERR_NOPACK`, so `osMotorInit` is never attempted.
|
||||
|
||||
If the SDL wiring is done without (1)+(2), the state machine parks at
|
||||
`RUMBLEPAKINITSTATE_NOT_READY` forever and nothing rumbles despite "looking
|
||||
wired." Both fixes are in the same file we already touch, so no extra cost.
|
||||
|
||||
---
|
||||
|
||||
## Work items
|
||||
|
||||
### 1. `port/src/input.c` — detection + API
|
||||
|
||||
Add per-pad fields (GE uses flat statics, not PD's `padsCfg[]` array):
|
||||
```c
|
||||
static int padRumbleOn[MAX_PADS]; // set where pads[i] is opened (line ~582)
|
||||
static float padRumbleScale[MAX_PADS] = { 0.5f, 0.5f, 0.5f, 0.5f };
|
||||
```
|
||||
On pad open (in the `SDL_GameControllerOpen` path, line ~575-589), mirror PD:
|
||||
```c
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 18)
|
||||
padRumbleOn[i] = SDL_GameControllerHasRumble(pads[i]);
|
||||
#else
|
||||
padRumbleOn[i] = SDL_JoystickIsHaptic(SDL_GameControllerGetJoystick(pads[i]));
|
||||
if (!padRumbleOn[i]) {
|
||||
const SDL_GameControllerType t = SDL_GameControllerGetType(pads[i]);
|
||||
padRumbleOn[i] = t && (t != SDL_CONTROLLER_TYPE_VIRTUAL); // Win no-haptic fallback
|
||||
}
|
||||
#endif
|
||||
```
|
||||
On pad close, `padRumbleOn[i] = 0;`. GE has no `inputCloseController`
|
||||
(PD-only) — the close sites are `inputDestroy()` and the close loop in
|
||||
`inputRescanPads()`, both of which zero it (`inputOpenPads` re-detects on
|
||||
open anyway).
|
||||
|
||||
New public fns (declare in `port/include/input.h`, define in input.c):
|
||||
```c
|
||||
int inputRumbleSupported(int idx); // 0<=idx<MAX_PADS ? padRumbleOn[idx] : 0
|
||||
void inputRumble(int idx, float strength, float time); // scaled SDL_GameControllerRumble
|
||||
float inputRumbleGetScale(int idx);
|
||||
void inputRumbleSetScale(int idx, float v);
|
||||
```
|
||||
`inputRumble` body (from PD, verbatim logic):
|
||||
```c
|
||||
if (idx < 0 || idx >= MAX_PADS || !pads[idx]) return;
|
||||
if (padRumbleScale[idx] <= 0.f) return;
|
||||
if (padRumbleOn[idx]) {
|
||||
strength *= padRumbleScale[idx];
|
||||
if (strength <= 0.f) { strength = 0; time = 0; }
|
||||
else { strength *= 65535.f; time *= 1000.f; }
|
||||
SDL_GameControllerRumble(pads[idx], (Uint16)strength, (Uint16)strength, (Uint32)time);
|
||||
}
|
||||
```
|
||||
In init, add the two PS4/5 hints (PD input.c:696-697) so BT pads report
|
||||
haptics:
|
||||
```c
|
||||
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, "1");
|
||||
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, "1");
|
||||
```
|
||||
Register per-pad config (in the existing `configRegister*` block, ~line 2457+):
|
||||
```c
|
||||
char key[48];
|
||||
for (int c = 0; c < MAX_PADS; ++c) {
|
||||
sprintf(key, "Input.Player%d.RumbleScale", c + 1);
|
||||
configRegisterFloat(key, &padRumbleScale[c], 0.f, 1.f);
|
||||
}
|
||||
```
|
||||
Note: **`strFmt` is NOT available in `input.c`** (0 uses, no decl in
|
||||
`port/include`) — use a local `sprintf` buffer as above. `configRegisterFloat`
|
||||
is declared in `port/include/config.h` (already included by input.c:80); it's
|
||||
used elsewhere (video.c) so the symbol is already in the link set.
|
||||
|
||||
### 2. `port/src/libultra.c` — rewire the shims
|
||||
|
||||
`contSnapshotFromKeyboard` (:1096): set the status bit for connected,
|
||||
rumble-capable pads (need a per-channel capability check → call
|
||||
`inputRumbleSupported(i)`; it's declared in input.h so include it):
|
||||
```c
|
||||
g_contStatus[i].type = connected ? CONT_TYPE_NORMAL : 0;
|
||||
g_contStatus[i].status = (connected && inputRumbleSupported(i)) ? CONT_CARD_ON : 0;
|
||||
```
|
||||
Replace the 5 accessory stubs (:1413-1420) with real routing (PD libultra.c:235-351):
|
||||
```c
|
||||
s32 osPfsInit(OSMesgQueue *q, OSPfs *pfs, int channel) {
|
||||
(void)q;
|
||||
// "pretend the Rumble Pak is present" when the real pad can rumble:
|
||||
// returning PFS_ERR_DEVICE is what joyRumblePakInit (src/joy.c:186-190)
|
||||
// needs to proceed to osMotorInit.
|
||||
return inputRumbleSupported(channel) ? PFS_ERR_DEVICE : PFS_ERR_NOPACK;
|
||||
}
|
||||
s32 osPfsIsPlug(OSMesgQueue *q, u8 *pattern) {
|
||||
(void)q;
|
||||
if (pattern) {
|
||||
*pattern = 0;
|
||||
for (int i = 0; i < MAXCONTROLLERS; ++i)
|
||||
if (inputRumbleSupported(i)) *pattern |= (u8)(1 << i);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
s32 osMotorInit(OSMesgQueue *mq, OSPfs *pfs, int channel) {
|
||||
if (pfs && inputRumbleSupported(channel)) {
|
||||
pfs->queue = mq;
|
||||
pfs->channel = channel;
|
||||
pfs->activebank = 0xff; // PD's osMotorProbe stores this too
|
||||
// NOTE: PFS_MOTOR_INITIALIZED is NOT defined in GE's headers, and
|
||||
// joy.c never reads pfs->status — it only checks (osMotorInit()==0)
|
||||
// to set RUMBLEPAKINITSTATE_READY (src/joy.c:190). Skip the status store.
|
||||
return 0;
|
||||
}
|
||||
(void)mq; (void)channel;
|
||||
return PFS_ERR_NOPACK;
|
||||
}
|
||||
s32 osMotorStart(OSPfs *pfs) {
|
||||
if (!pfs) return PFS_ERR_NOPACK;
|
||||
// N64 motor has no duration; the game's joyRumblePakTimer60 (joy.c) turns it
|
||||
// off. Issue one generous window, PD-style ("hope the timer stops it"):
|
||||
inputRumble(pfs->channel, 1.0f, 5.0f);
|
||||
return 0;
|
||||
}
|
||||
s32 osMotorStop(OSPfs *pfs) {
|
||||
if (!pfs) return PFS_ERR_NOPACK;
|
||||
inputRumble(pfs->channel, 0.0f, 0.0f); // zero-duration / zero-strength = stop
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
**JPN note (2026-09-27):** `_HW_VERSION_1` defines `MAXCONTROLLERS` as 6
|
||||
while `MAX_PADS` is 4 — the `MAXCONTROLLERS`-bounded loops above are safe
|
||||
because `inputRumbleSupported()` bounds-checks against `MAX_PADS` and reports
|
||||
unsupported (silent) for `i >= 4`. `osPfsIsPlug` has **no GE game-side call
|
||||
sites** — its rewiring is completeness/hygiene only; the load-bearing stubs
|
||||
are `osPfsInit`/`osMotor*`.
|
||||
|
||||
Headers confirmed: `OSPfs` (`include/PR/os.h:282`) has `queue` + `channel`; the
|
||||
error macros `PFS_ERR_DEVICE` (:642) and `PFS_ERR_ID_FATAL` (:641) and
|
||||
`PFS_ERR_NOPACK` (:631) are all defined there. **`PFS_MOTOR_INITIALIZED` is NOT
|
||||
defined** anywhere in GE's headers and `joy.c` never reads `pfs->status`, so it
|
||||
is not needed. `input.h` is already included by libultra.c (line 50), so
|
||||
`inputRumbleSupported` is directly callable there.
|
||||
|
||||
### 3. (DONE 2026-09-28) options-menu slider — single global
|
||||
|
||||
Landed in `port/src/optionsoverlay.c` (not PD's `optionsmenu.c` — GE's F10
|
||||
overlay + front options screen share `rows[]`, walked by `frontoptions.c`
|
||||
via the `optionsRow*` accessors): ONE `ROW_SLIDER` row `Rumble strength`
|
||||
on `Input.RumbleScale` (0.05 detents, generic `CONFIG_OPT_FLOAT` display
|
||||
`%.2f`), in the INPUT section after the controller rows, plus a
|
||||
`kResetDefaults` entry (0.5, the `gRumbleScale` C initializer) so INPUT's
|
||||
"Reset to defaults" covers it.
|
||||
|
||||
**Decision (user, 2026-09-28):** the original four-per-player-row design
|
||||
(`Rumble (Player 1..4)` on `Input.Player%d.RumbleScale`) was coded and
|
||||
reviewed, then replaced with the single global row **before its first
|
||||
compile**: the scale applies to every rumble-capable pad, and the four
|
||||
`Input.PlayerN.RumbleScale` ini keys are dropped — stale keys in an
|
||||
existing ini log four harmless `unknown key` `LOG_NOTE` lines at config
|
||||
load and self-clean on the next `configSave` (which rewrites the whole
|
||||
file with the registered keys). Per-player rumble tuning stays a G-class
|
||||
stretch goal (QOL-INVENTORY) — and a per-pad override-on-top-of-global
|
||||
design would be a no-op for everyone anyway, because `configSave`
|
||||
rewrites all registered keys and the "user explicitly set this?" signal
|
||||
is burned as soon as any key exists at the 0.5 default. The
|
||||
row-disabled-when-unsupported state is deferred with the per-pad tuning
|
||||
row (the overlay has no generic row-disabled mechanism); the row is inert
|
||||
(no-op) on non-rumble setups.
|
||||
|
||||
**Original plan (for reference):** PD wires the per-pad scale into the
|
||||
options menu (`optionsmenu.c:533-540`): `inputRumbleGetScale(player)`,
|
||||
`inputRumbleSetScale(player, v/10)`, disabled when
|
||||
`!inputRumbleSupported(player)`. In GE this maps onto
|
||||
`port/src/optionsoverlay.c` / the per-pad tuning row (QOL-INVENTORY.md).
|
||||
Config file value alone is enough for the feature; the UI is the polish.
|
||||
|
||||
---
|
||||
|
||||
## Verification ritual (AGENTS.md)
|
||||
|
||||
- [x] `./build-pc.sh ntsc-final` compiles clean (2026-09-27). New
|
||||
`inputRumble*` defined once in input.c, declared in input.h; no new
|
||||
undefined/duplicate symbols (full `/linkcheck` sweep pending at D401
|
||||
close).
|
||||
- [x] `CONT_CARD_ON` + `PFS_ERR_DEVICE` land (the two-blocker addendum);
|
||||
the state machine's READY path is provable by code read — the runtime
|
||||
proof is folded into the real-pad test below.
|
||||
- [x] **By-hand, real pad:** user confirmed the rumble works on a real
|
||||
controller (2026-09-28) — fire a weapon (`gunfire.c:3284` recoil),
|
||||
explosion rumble, 2P second pad (`bondview2.c:9722/9726`). Non-rumble
|
||||
pad and mouse/keyboard-only stay silent by construction (`pads[idx]`
|
||||
NULL / `padRumbleOn` 0 ⇒ `inputRumble` no-op).
|
||||
- [ ] `Input.RumbleScale=0` fully silences (unverified — the trivial
|
||||
`gRumbleScale <= 0` early-out in `inputRumble`); max value loud
|
||||
(confirmed implicitly by the working playtest at scale 0.5).
|
||||
|
||||
## Finding log
|
||||
|
||||
Done (2026-09-27): **D401** added to `docs/dev/findings.md` (§F index +
|
||||
full entry) documenting the D224 implementation + the two-blocker
|
||||
resolution; the `QOL-INVENTORY.md` M-87/D224 row re-promoted
|
||||
DEPRIORITIZED → DONE (D401); `docs/dev/findings-index.csv` regenerated.
|
||||
|
||||
---
|
||||
|
||||
## Effort
|
||||
|
||||
- Items 1+2: **~1–2 h** coding (mostly a rename + copy from PD) + build/linkcheck.
|
||||
- Hands-on pad testing: **~30 min–1 h** (the only genuinely open-ended part —
|
||||
feels-right tuning).
|
||||
- Item 3 (UI slider): **~1 h**, landed 2026-09-28 as the single global
|
||||
`Rumble strength` row.
|
||||
|
||||
**Total for working core: half a day max, <2 h of pure code.**
|
||||
@@ -31,7 +31,7 @@ D192,CLOSED,Front-end mission/level-select grid pointer still can't reach the ou
|
||||
D221,FIXED,SELECT FILE mouse hit-box offset —,"FIXED (M-91) — explicit {min,max} coord2d pairs under #ifdef PORT; human mouse-playtest confirmed."
|
||||
D222,FIXED,High Video.FovScale breaks culling —,"FIXED, awaiting visual verification (M-121)."
|
||||
D223,FIXED,QoL ask: mouse-wheel weapon cycling should be directional like a normal PC shooter —,"FIXED (M-105, port/src/input.c) —…"
|
||||
D224,?,"QoL gap (M-87, from a PD-PC-port survey):","DEPRIORITIZED (M-87, user call)."
|
||||
D224,PARTIAL,"QoL gap (M-87, from a PD-PC-port survey):","LANDED via D401 (2026-09-27, re-promoted from DEPRIORITIZED)."
|
||||
D225,FIXED,"Dev-tooling QoL (M-87, user ask):","FIXED (2026-09-18) — GE_DEBUG_UNLOCKALL=1 short-circuits fileGetIsCheatUnlocked() (src/game/file2.c, #ifdef PORT, cached getenv) to always return TRUE; default off, no save mutation."
|
||||
D226,FIXED,"QoL ask: player-adjustable HUD scale for the ammo counter, bottom-left pickup/item status text, and top-of-screen dialogue/subtitle text.","FIXED (2026-09-24): Game.HudScale 75–150% (user-verified, capped at 150) via fast3d G_HUDSCALE_EXT rect scaling about per-element anchors; dialogue/bottom messages width-capped."
|
||||
D227,FIXED,Sky renders as two independently-moving halves —,"FIXED (M-100b, PR #43) —"
|
||||
@@ -277,3 +277,4 @@ D397,RESOLVED,SELECT FILE background + intro gun-barrel show an interlaced/comb
|
||||
D398,FIXED,PC Options label: D348's procedural slider glyph replaced with an existing mainfolderimages texture (2026-09-27).,"SUPERSEDED by D400 (icon removed, text-only label; purple-hover bug moot; the D400 textMeasure arg-swap bug was found and fixed there)."
|
||||
D399,PARTIAL,File-select bottom row (SELECT FILE text + Copy/Erase + PC Options dot/label) shifted 15px left as a block (user sign-off 2026-09-27).,"LANDED (port-only, D343 class):"
|
||||
D400,PARTIAL,"File-select ""PC Options"" label:","LANDED (port-only, frontoptions.c):"
|
||||
D401,PARTIAL,Rumble Pak → real gamepad haptics (re-promotes D224/M-87):,"LANDED (port-only, input.c/input.h/libultra.c + optionsoverlay.c per docs/dev/RUMBLE-PLAN.md; zero src/ edits)."
|
||||
|
+97
-1
@@ -415,7 +415,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
|
||||
| D221 | **SELECT FILE mouse hit-box offset — compiler stack-layout dependency, not coordinate mismatch (M-87 QA / M-91 fix).** — full `## D221` entry at file tail | **FIXED (M-91)** — explicit `{min,max}` `coord2d` pairs under `#ifdef PORT`; human mouse-playtest confirmed. Distinct from D192 (mode-0 clamp range, still open). |
|
||||
| D222 | **High `Video.FovScale` breaks culling — NPCs and some world objects stop rendering near the screen edges (M-87, user QA).** — full `## D222` entry at file tail | **FIXED, awaiting visual verification (M-121).** Root cause traced one step further than the original write-up… |
|
||||
| D223 | **QoL ask: mouse-wheel weapon cycling should be directional like a normal PC shooter — wheel up = next weapon, wheel down = previous — instead of both directions…** — full `## D223` entry at file tail | **FIXED (M-105, `port/src/input.c`) —… |
|
||||
| D224 | **QoL gap (M-87, from a PD-PC-port survey): the N64 Rumble Pak is fully dropped instead of modernized.** — full `## D224` entry at file tail | **DEPRIORITIZED (M-87, user call).** Gamepad-only —… |
|
||||
| D224 | **QoL gap (M-87, from a PD-PC-port survey): the N64 Rumble Pak is fully dropped instead of modernized.** — full `## D224` entry at file tail | **LANDED via D401 (2026-09-27, re-promoted from DEPRIORITIZED).** Rumble Pak now drives real SDL gamepad haptics (`SDL_GameControllerRumble`) via the port-layer `inputRumble*` API + `CONT_CARD_ON`/`PFS_ERR_DEVICE` gating; single global `Input.RumbleScale` (0..1) config + one `Rumble strength` slider row. Real-pad playtest owed. Gamepad-only —… |
|
||||
| D225 | **Dev-tooling QoL (M-87, user ask): a debug-build/env toggle that unlocks all missions + all cheats, for testing.** — full `## D225` entry at file tail | **FIXED (2026-09-18)** — `GE_DEBUG_UNLOCKALL=1` short-circuits `fileGetIsCheatUnlocked()` (`src/game/file2.c`, `#ifdef PORT`, cached getenv) to always return TRUE; default off, no save mutation. Build-verified; headless boot with `GE_STARTMENU=7` (mission select) crash-free. |
|
||||
| D226 | **QoL ask: player-adjustable HUD scale for the ammo counter, bottom-left pickup/item status text, and top-of-screen dialogue/subtitle text.** — full `## D226` entry at file tail | **FIXED (2026-09-24)**: Game.HudScale 75–150% (user-verified, capped at 150) via fast3d G_HUDSCALE_EXT rect scaling about per-element anchors; dialogue/bottom messages width-capped. |
|
||||
| D227 | **Sky renders as two independently-moving halves — "two sky processes" (M-93 QA; root cause M-100b: unit error, `unk20`/`unk24` are S10.5 not texels).** — full `## D227` entries (M-94→M-100b) at file tail | **FIXED (M-100b, PR #43)** — also fixed "scrolls too fast" (M-96) + over-tiling; human play-test confirmed. Full history in the sections. |
|
||||
@@ -685,6 +685,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
|
||||
| D398 | **PC Options label: D348's procedural slider glyph replaced with an existing mainfolderimages texture (2026-09-27).** Full `## D398` entry at file tail. | SUPERSEDED by D400 (icon removed, text-only label; purple-hover bug moot; the D400 `textMeasure` arg-swap bug was found and fixed there). |
|
||||
| D399 | **File-select bottom row (SELECT FILE text + Copy/Erase + PC Options dot/label) shifted 15px left as a block (user sign-off 2026-09-27).** Full `## D399` entry at file tail. | LANDED (port-only, D343 class): `selectpos.f[0]` 110→95 (new `#ifdef PORT` in front.c), Copy 200/222→185/207, Erase 282/304→267/289 (D343 PORT block), `LABEL_X` 358→343 (`frontoptions.c`); hit bounds recompute from the shifted positions. Clean build, PCDUMP frame 195 no overlaps; by-eye check owed. |
|
||||
| D400 | **File-select "PC Options" label: icon removed, text-only; fixed a swapped textMeasure() arg bug and a 7px vertical misalignment vs Copy/Erase (user sign-off 2026-09-27).** Full `## D400` entry at file tail. | LANDED (port-only, `frontoptions.c`): D398's dot icon dropped on user instruction ("most straightforward design"); label now a plain `textRender` word, GAP 28 after Erase. Two measurement bugs fixed: (1) `textMeasure(textheight, textwidth)` args passed swapped -> label 35px above the bar, hot band transposed; (2) bare-label height measures 0 (vs 14 for newline-terminated Copy/Erase) -> 7px lower tops; fixed by restoring the `kLabelNL` height measure. PCDUMP frame 195: tops aligned 279.8 vs 279.3, 31px clearance from Erase; by-eye check owed. |
|
||||
| D401 | **Rumble Pak → real gamepad haptics (re-promotes D224/M-87): `CONT_CARD_ON` gate + `osPfs*`/`osMotor*` shims rewired to `SDL_GameControllerRumble`, single global `Rumble strength` slider on `Input.RumbleScale` (2026-09-27/28).** Full `## D401` entry at file tail. | LANDED (port-only, `input.c`/`input.h`/`libultra.c` + `optionsoverlay.c` per `docs/dev/RUMBLE-PLAN.md`; zero `src/` edits). Both M-87 addendum blockers: `g_contStatus[i].status` now sets `CONT_CARD_ON` for connected rumble-capable pads (so `joyRumblePakInit` arms) and `osPfsInit` returns `PFS_ERR_DEVICE` for them (so `osMotorInit` is attempted). New `inputRumble*` API + global `Input.RumbleScale` (0..1, dflt 0.5; the four per-player `Input.PlayerN.RumbleScale` keys were dropped in the final menu-shape decision); `osMotorStart/Stop` drive it. **Real-pad haptics confirmed working by user (2026-09-28)**; plan item 3 (one global `Rumble strength` slider in the F10 overlay + front options screen, reset-table entry) landed the same day. `RumbleScale=0` silence check still unverified (trivial early-out path). |
|
||||
|
||||
|
||||
Phase 2 replaced the Phase-1 demo loop with the real `mainproc()` on real OS
|
||||
@@ -10772,6 +10773,13 @@ D243 (cutscene shake/duplication, its own CONSOLIDATED NEXT STEPS block appears
|
||||
|
||||
## D224 — QoL gap (M-87, from a PD-PC-port survey): the N64 Rumble Pak is fully dropped instead of modernized.
|
||||
|
||||
**DONE via D401 (2026-09-27).** Re-promoted from DEPRIORITIZED; implemented per
|
||||
`docs/dev/RUMBLE-PLAN.md` exactly as the M-87 addendum prescribed — both
|
||||
upstream blockers (`CONT_CARD_ON` gate + `osPfsInit` → `PFS_ERR_DEVICE`) landed
|
||||
with the `SDL_GameControllerRumble` wiring, plus `Input.PlayerN.RumbleScale`
|
||||
config (options-menu slider deferred). Real-pad playtest + `RumbleScale=0`
|
||||
silence check owed. See `## D401` (file tail) for the full record.
|
||||
|
||||
**QoL gap (M-87, from a PD-PC-port survey): the N64 Rumble Pak is fully dropped instead of modernized.** GE genuinely has a rumble subsystem (`src/motor.c`, `src/joy.c`) but the port's `osMotorInit/Start/Stop` (`port/src/libultra.c:1220-1223`) are all hard stubs — "no accessories on the PC," `osMotorStart`/`osMotorStop` return `-1` unconditionally, `osPfsFindFile`-family returns `PFS_ERR_NOPACK`. So every in-game rumble event (weapon recoil, explosions, etc.) computes and calls into a dead path. **PD's PC port wires the equivalent straight through to real gamepad rumble:** `inputRumbleSupported(idx)` checks `SDL_GameControllerHasRumble`/`SDL_JoystickIsHaptic` (with a Windows fallback: some pads report no haptics but rumble anyway), `inputRumble(idx, strength, time)` calls `SDL_GameControllerRumble()` scaled by a per-pad `RumbleScale` config value (options-menu slider, `Input.PadN.RumbleScale`), and `osPfsFindFile` reports a "Rumble Pak" present exactly when a real pad supports rumble (`libultra.c` — same "pretend the N64 accessory is there" pattern GE's port already uses for `osMemSize`/Expansion Pak, just applied to a real capability instead of faked). Cross-ref `QOL-INVENTORY.md` (folded into the existing "per-pad tuning" row).
|
||||
|
||||
**Status (from §F table):** **DEPRIORITIZED (M-87, user call).** Gamepad-only — irrelevant to mouse/keyboard, which is the primary target for this next release; no keyboard/mouse play is affected by NOT doing this. Root cause + fix still valid whenever gamepad support becomes a priority (no `src/` game-logic touch: GE's `motor.c`/`joy.c` call sites already exist and already call the `osMotor*` shims; the fix is entirely in `port/src/libultra.c` + `input.c` — add `inputRumbleSupported`/`inputRumble` per the PD shape, wire `osMotorStart`/`osMotorStop` to them, add `Input.PadN.RumbleScale` config). **M-87 PD-legacy-survey addendum (`docs/dev/notes/PD-LEGACY-SURVEY.md` candidate 1, high confidence) — the D224 fix as originally stated is NOT sufficient by itself.** Two more upstream blockers sit before the stubbed `osMotorInit/Start/Stop` ever gets reached: (1) `port/src/libultra.c:1096-1098` sets `g_contStatus[i].status = 0` for every pad — `CONT_CARD_ON` is never set, and `src/joy.c:184` gates the *entire* rumble-pak init path on that bit, so `joyRumblePakInit()` never even calls `osPfsInit`. (2) `src/joy.c:186-190` only attempts `osMotorInit` if `osPfsInit`'s return is `PFS_ERR_ID_FATAL` or `PFS_ERR_DEVICE` — but the port's stub (`libultra.c:1214`) returns `PFS_ERR_NOPACK`, so motor init is never attempted even once (1) is fixed. PD's exact trick (`libultra.c:213-227,349-351`): set `CONT_CARD_ON` for connected pads, and make `osPfsInitPak()` return `PFS_ERR_DEVICE` specifically when the pad supports rumble. **Whenever D224 gets implemented, both of these must land alongside the `SDL_GameControllerRumble` wiring** or the game's state machine parks at `RUMBLEPAKINITSTATE_NOT_READY` forever and nothing audibly changes despite looking wired up correctly.
|
||||
@@ -15293,3 +15301,91 @@ right margin (canvas 440). **Owed: by-eye check via `scratch\playtest_v040.bat`
|
||||
(idle legibility, gold hover not purple, click opens the F10 overlay, vertical
|
||||
alignment with Copy/Erase).** Cross-ref D348 (glyph lineage), D398
|
||||
(superseded icon approach), D399 (bottom-row 15px shift).
|
||||
|
||||
## D401 — Rumble Pak → real gamepad haptics: `CONT_CARD_ON` gate + `osPfs*`/`osMotor*` shims rewired to `SDL_GameControllerRumble`, global `Input.RumbleScale` slider (2026-09-27/28).
|
||||
|
||||
Implements **D224 (M-87)** per `docs/dev/RUMBLE-PLAN.md` — the N64 Rumble
|
||||
Pak state machine (`src/joy.c` `joyRumblePakInit`/`joyRumblePakTick`/Start/Stop,
|
||||
call sites `gunfire.c:3284/3288`, `bondview2.c:9722/9726`, `lv.c:1688`,
|
||||
`sched.c:272`) now drives real SDL gamepad haptics. **Port layer only: zero
|
||||
`src/` edits**, so rule 2 / the ABI exception are not implicated.
|
||||
|
||||
**The two M-87 addendum blockers** (both `port/src/libultra.c`):
|
||||
|
||||
1. `contSnapshotFromKeyboard` left `g_contStatus[i].status = 0`, so
|
||||
`joyRumblePakInit` (`src/joy.c:178` — requires `status & CONT_CARD_ON`) never
|
||||
ran and `joyRumblePak` was never populated. Now
|
||||
`status = (connected && inputRumbleSupported(i)) ? CONT_CARD_ON : 0` —
|
||||
gated on real capability so non-rumble pads never enter the rumble path
|
||||
(cleaner than PD's unconditional bit; `joyRumblePakInit` is the real gate).
|
||||
`type` stays `CONT_TYPE_NORMAL` (includes `CONT_JOYPORT`; PD's
|
||||
`CONT_ABSOLUTE` would have broken the init gate).
|
||||
2. `osPfsInit` stubbed `PFS_ERR_NOPACK`, so `osMotorInit` was never
|
||||
attempted (`src/joy.c:186-190` proceeds only on `PFS_ERR_ID_FATAL` or
|
||||
`PFS_ERR_DEVICE`). Now returns `PFS_ERR_DEVICE` ("Rumble Pak present, not a
|
||||
Memory Pak") for rumble-capable channels, `PFS_ERR_NOPACK` otherwise.
|
||||
|
||||
**`port/src/input.c`** (flat statics, GE style — not PD's `padsCfg[]`):
|
||||
`padRumbleOn[MAX_PADS]` + a single global `gRumbleScale` (default 0.5) —
|
||||
the per-pad `padRumbleScale[]` array was superseded 2026-09-28 by the
|
||||
global slider (see "Menu row" below). Detection in
|
||||
`inputOpenPads`: `SDL_GameControllerHasRumble` on SDL ≥ 2.0.18, else
|
||||
`SDL_JoystickIsHaptic` with the known-type Windows no-haptic fallback; zeroed
|
||||
on close in `inputDestroy`/`inputRescanPads`. PS4/PS5 HIDAPI rumble hints in
|
||||
`inputInit`. New API: `inputRumbleSupported(idx)` (bounds-checked against
|
||||
`MAX_PADS` — JPN `_HW_VERSION_1` has `MAXCONTROLLERS == 6`, so out-of-range
|
||||
channels report unsupported and stay silent), `inputRumble(idx, strength,
|
||||
time)` (no-op on `gRumbleScale <= 0`; scales strength, `*65535` / `*1000`
|
||||
units, `SDL_GameControllerRumble`; `(0,0)` stops), global
|
||||
`inputRumbleGetScale(void)`/`inputRumbleSetScale(f32)`. Config: a single
|
||||
`Input.RumbleScale` (range 0..1, default 0.5) registered in
|
||||
`inputConfigInit`; the four per-player `Input.Player%d.RumbleScale` keys
|
||||
were dropped in the same-day menu-shape decision (stale keys in an
|
||||
existing ini log four `unknown key` `LOG_NOTE` lines and self-clean on the
|
||||
next `configSave`). `keyboard`/mouse-only play stays silent: no pad open
|
||||
⇒ `pads[idx]` NULL ⇒ `inputRumble` no-ops.
|
||||
|
||||
**`port/src/libultra.c`** shims: `osPfsInit` as above; `osPfsIsPlug` sets its
|
||||
pattern bits for the capable channels over `MAXCONTROLLERS` (completeness
|
||||
only — **no GE game call sites**); `osMotorInit` stores `pfs->queue`,
|
||||
`pfs->channel`, `pfs->activebank = 0xff` (PD `osMotorProbe` shape) and returns
|
||||
0 for capable channels, `PFS_ERR_NOPACK` otherwise (no `PFS_MOTOR_INITIALIZED`
|
||||
store — not defined in GE headers and `joy.c` never reads `pfs->status`);
|
||||
`osMotorStart` → `inputRumble(channel, 1.0f, 5.0f)` (one generous window;
|
||||
the game's `joyRumblePakTimer60` normally stops it, PD-style), `osMotorStop` →
|
||||
`inputRumble(channel, 0.0f, 0.0f)`; both return 0 for non-NULL `pfs`.
|
||||
|
||||
**Verification.** `./build-pc.sh ntsc-final` clean (no new undefined/duplicate
|
||||
symbols; `inputRumble*` defined once in input.c, declared in input.h).
|
||||
**Real-pad haptics confirmed working by the user (2026-09-28)** — the gun-
|
||||
impact rumble path is live on a real controller. The `RumbleScale=0`
|
||||
silence check remains unverified (it is the trivial `gRumbleScale <= 0`
|
||||
early-out in `inputRumble`). Same-session environment fix: the
|
||||
recurring `Cannot create temporary file in C:\Windows\: Permission denied`
|
||||
link failure is now self-healed by `build-pc.sh` (native-TMP probe →
|
||||
cmake+ninja re-run under PowerShell with `TMP`/`TEMP` set natively; see the
|
||||
AGENTS.md "Build" note), and `cannot open output file ... Permission denied`
|
||||
is documented as the running-`.exe`-lock case (close the game before
|
||||
rebuilding).
|
||||
|
||||
**Menu row (plan item 3, 2026-09-28).** The deferred options-menu slider
|
||||
landed the day after the user confirmed the haptics, as a **single global
|
||||
row** (user decision — "making the rumble menu just 1 player"):
|
||||
`port/src/optionsoverlay.c` gains one `ROW_SLIDER` row — `Rumble strength`
|
||||
on `Input.RumbleScale` (0.05 detents; the generic `CONFIG_OPT_FLOAT`
|
||||
value/display path, shown `%.2f`) — placed in the INPUT section after the
|
||||
other controller rows, with a `kResetDefaults` entry (0.5, the
|
||||
`gRumbleScale` C initializer) so INPUT's "Reset to defaults" covers it.
|
||||
The originally-coded four per-player rows (`Rumble (Player 1..4)` on
|
||||
`Input.Player%d.RumbleScale`) were replaced before first compile; the
|
||||
four ini keys are dropped rather than kept as hidden overrides (stale ones
|
||||
self-clean on the next save, and a per-pad override would be a no-op since
|
||||
`configSave` rewrites all registered keys — see `RUMBLE-PLAN.md` item 3).
|
||||
Both the F10 overlay and the front options screen walk the shared `rows[]`
|
||||
(`frontoptions.c` via the `optionsRow*` accessors), so the row appears in
|
||||
both. A "row disabled when no pad is rumble-capable" state is NOT
|
||||
implemented — the overlay has no generic row-disabled mechanism; the row is
|
||||
inert (no-op) on unsupported/absent pads, and the disabled state is deferred
|
||||
with the per-pad tuning row (`QOL-INVENTORY.md`), which will need the same
|
||||
mechanism for deadzone/swap rows.
|
||||
|
||||
@@ -88,6 +88,17 @@ int inputMenuPointerLive(void);
|
||||
/* Re-enumerate gamepads after a hotplug (SDL_CONTROLLERDEVICEADDED/REMOVED). */
|
||||
void inputRescanPads(void);
|
||||
|
||||
/* D401: Rumble Pak -> real gamepad haptics. The osMotor* shims in
|
||||
* libultra.c (game's src/joy.c rumble state machine) route here.
|
||||
* idx is the N64 controller channel (0..3; out-of-range is a no-op).
|
||||
* strength is 0..1, time seconds; 0/0 stops an active rumble. The scale
|
||||
* is the single global Input.RumbleScale config value (0 = silent),
|
||||
* applied to every pad. */
|
||||
int inputRumbleSupported(int idx);
|
||||
void inputRumble(int idx, f32 strength, f32 time);
|
||||
f32 inputRumbleGetScale(void);
|
||||
void inputRumbleSetScale(f32 v);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
+85
-2
@@ -70,6 +70,7 @@
|
||||
*/
|
||||
|
||||
#include "port_math.h" /* real system math decls; see header for why */
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
@@ -291,6 +292,13 @@ static int padBPrev[MAX_PADS]; /* B/Y edges; track through menus too */
|
||||
static int padYPrev[MAX_PADS];
|
||||
static int padSelectPrev = 0; /* Select (BACK) edge: overlay toggle */
|
||||
|
||||
/* D401: per-pad haptics (N64 Rumble Pak -> SDL_GameControllerRumble).
|
||||
* padRumbleOn is detected when the pad opens (inputOpenPads); gRumbleScale
|
||||
* is the single global Input.RumbleScale config value (0 = silent,
|
||||
* 1 = full), applied to every pad. */
|
||||
static int padRumbleOn[MAX_PADS];
|
||||
static float gRumbleScale = 0.5f;
|
||||
|
||||
static int mouseEnabled = 1;
|
||||
static int mouseGrabbed = 1; /* released while the window is unfocused */
|
||||
|
||||
@@ -579,11 +587,27 @@ static void inputOpenPads(void)
|
||||
if (pads[i]) {
|
||||
continue;
|
||||
}
|
||||
padRumbleOn[i] = 0; /* D401: (re)detected below when the pad opens */
|
||||
pads[i] = SDL_GameControllerOpen(i);
|
||||
if (pads[i]) {
|
||||
connectedMask |= (1 << i);
|
||||
sysLogPrintf(LOG_NOTE, "input: opened gamepad %d '%s' as controller %d",
|
||||
i, SDL_GameControllerName(pads[i]), i);
|
||||
/* D401: haptics detection (PD pattern, pd_port input.c:320-324).
|
||||
* SDL_GameControllerHasRumble() landed in SDL 2.0.18 even though
|
||||
* SDL_GameControllerRumble() is from 2.0.9. */
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 18)
|
||||
padRumbleOn[i] = SDL_GameControllerHasRumble(pads[i]);
|
||||
#else
|
||||
padRumbleOn[i] = SDL_JoystickIsHaptic(SDL_GameControllerGetJoystick(pads[i]));
|
||||
if (!padRumbleOn[i]) {
|
||||
/* At least on Windows some controllers report no haptics but
|
||||
* rumble will still function: assume it's supported for a
|
||||
* known controller type. */
|
||||
const SDL_GameControllerType ctype = SDL_GameControllerGetType(pads[i]);
|
||||
padRumbleOn[i] = ctype && (ctype != SDL_CONTROLLER_TYPE_VIRTUAL);
|
||||
}
|
||||
#endif
|
||||
sysLogPrintf(LOG_NOTE, "input: opened gamepad %d '%s' as controller %d (rumble %s)",
|
||||
i, SDL_GameControllerName(pads[i]), i, padRumbleOn[i] ? "yes" : "no");
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < MAX_PADS; ++i) {
|
||||
@@ -812,6 +836,11 @@ int inputInit(void)
|
||||
}
|
||||
}
|
||||
|
||||
/* D401: make BT PS4/PS5 pads report haptics (PD input.c:696-697). Must
|
||||
* be set before the pads below are opened. */
|
||||
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, "1");
|
||||
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, "1");
|
||||
|
||||
for (int i = 0; i < MAX_PADS; ++i) {
|
||||
pads[i] = NULL;
|
||||
}
|
||||
@@ -854,6 +883,7 @@ void inputDestroy(void)
|
||||
SDL_GameControllerClose(pads[i]);
|
||||
pads[i] = NULL;
|
||||
}
|
||||
padRumbleOn[i] = 0; /* D401: closed */
|
||||
}
|
||||
if (SDL_WasInit(SDL_INIT_GAMECONTROLLER)) {
|
||||
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
|
||||
@@ -2181,12 +2211,59 @@ void inputRescanPads(void)
|
||||
SDL_GameControllerClose(pads[i]);
|
||||
pads[i] = NULL;
|
||||
}
|
||||
padRumbleOn[i] = 0; /* D401: re-detected by inputOpenPads below */
|
||||
}
|
||||
inputOpenPads();
|
||||
sysLogPrintf(LOG_NOTE, "input: rescanned pads (mask=0x%x, %d controller(s))",
|
||||
connectedMask, numControllers);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------
|
||||
* D401: Rumble Pak -> real gamepad haptics.
|
||||
*
|
||||
* The game's Rumble-Pak state machine (src/joy.c joyRumblePakInit/Tick;
|
||||
* events from gunfire.c / bondview2.c / lv.c / sched.c) drives the
|
||||
* osMotor* shims in port/src/libultra.c, which route here. `strength`
|
||||
* is 0..1 and `time` is seconds; a 0-strength / 0-duration call stops
|
||||
* an active rumble. The global Input.RumbleScale config value (0 = fully
|
||||
* silent) scales the strength for every pad. idx is the N64 controller
|
||||
* channel 0..3 (MAXCONTROLLERS may be 6 on JPN; out-of-range is a no-op).
|
||||
* ------------------------------------------------------------------------ */
|
||||
int inputRumbleSupported(int idx)
|
||||
{
|
||||
if (idx < 0 || idx >= MAX_PADS) return 0;
|
||||
return padRumbleOn[idx];
|
||||
}
|
||||
|
||||
void inputRumble(int idx, f32 strength, f32 time)
|
||||
{
|
||||
if (idx < 0 || idx >= MAX_PADS || !pads[idx]) return;
|
||||
if (gRumbleScale <= 0.f) return;
|
||||
if (padRumbleOn[idx]) {
|
||||
strength *= gRumbleScale;
|
||||
if (strength <= 0.f) {
|
||||
strength = 0.f;
|
||||
time = 0.f;
|
||||
} else {
|
||||
strength *= 65535.f;
|
||||
time *= 1000.f;
|
||||
}
|
||||
SDL_GameControllerRumble(pads[idx], (Uint16)strength, (Uint16)strength, (Uint32)time);
|
||||
}
|
||||
}
|
||||
|
||||
f32 inputRumbleGetScale(void)
|
||||
{
|
||||
return gRumbleScale;
|
||||
}
|
||||
|
||||
void inputRumbleSetScale(f32 v)
|
||||
{
|
||||
if (v < 0.f) v = 0.f;
|
||||
if (v > 1.f) v = 1.f;
|
||||
gRumbleScale = v;
|
||||
}
|
||||
|
||||
int inputConnectedMask(void)
|
||||
{
|
||||
return connectedMask;
|
||||
@@ -2495,6 +2572,12 @@ PD_CONSTRUCTOR static void inputConfigInit(void)
|
||||
configRegisterInt("Input.PadDeadzone", &padDeadzone, 0, 30000);
|
||||
configRegisterInt("Input.PadTriggerPct", &padTriggerPct, 1, 99);
|
||||
configRegisterInt("Input.PadLookInvertY", &padLookInvertY, 0, 1);
|
||||
/* D401: global rumble scale (0 = silent .. 1 = full), applied to every
|
||||
* pad in inputRumble(). One setting for all pads (user call, 2026-09-28):
|
||||
* the original per-pad Input.PlayerN.RumbleScale keys were dropped --
|
||||
* stale ones in an existing ini log a one-time "unknown key" note and
|
||||
* self-clean on the next configSave. */
|
||||
configRegisterFloat("Input.RumbleScale", &gRumbleScale, 0.f, 1.f);
|
||||
/* D380: single PC layout. Old Input.Layout is ignored and removed on
|
||||
* configSave; BindingsVersion marks the one-time stale-default migration. */
|
||||
configRegisterInt("Input.BindingsVersion", &bindsVersion, 0, 3);
|
||||
|
||||
+57
-7
@@ -1099,7 +1099,10 @@ static void contSnapshotFromKeyboard(void)
|
||||
u16 button = connected ? (u16)inputComputePad(i, &sx, &sy) : 0;
|
||||
|
||||
g_contStatus[i].type = connected ? CONT_TYPE_NORMAL : 0;
|
||||
g_contStatus[i].status = 0;
|
||||
/* D401: present a Rumble Pak (CONT_CARD_ON) whenever the connected
|
||||
* pad can actually rumble. joyRumblePakInit (src/joy.c:184) gates
|
||||
* the whole rumble-pak path on this bit. */
|
||||
g_contStatus[i].status = (connected && inputRumbleSupported(i)) ? CONT_CARD_ON : 0;
|
||||
g_contStatus[i].errno = connected ? 0 : CONT_NO_RESPONSE_ERROR;
|
||||
|
||||
g_contPad[i].button = button;
|
||||
@@ -1409,15 +1412,62 @@ s32 osEepromLongRead(OSMesgQueue *mq, u8 addr, u8 *buf, int nbytes)
|
||||
s32 osEepromLongWrite(OSMesgQueue *mq, u8 addr, u8 *buf, int nbytes)
|
||||
{ (void)mq; return geEepromRW(addr, buf, nbytes, 1); }
|
||||
|
||||
/* Memory Pak (PFS) + Rumble Pak (motor): no accessories on the PC. */
|
||||
/* Memory Pak (PFS) + Rumble Pak (motor): no Memory Pak on the PC (saves are
|
||||
* file-backed EEPROM), but the Rumble Pak path is routed to real gamepad
|
||||
* haptics (D401): a pad that can rumble presents itself as a "card is on"
|
||||
* accessory, and the motor calls become SDL_GameControllerRumble via
|
||||
* inputRumble (port/src/input.c). */
|
||||
s32 osPfsInit(OSMesgQueue *queue, OSPfs *pfs, int channel)
|
||||
{ (void)queue; (void)pfs; (void)channel; return PFS_ERR_NOPACK; }
|
||||
{
|
||||
(void)queue; (void)pfs;
|
||||
/* PFS_ERR_DEVICE ("wrong device type") = a Rumble Pak is present, not a
|
||||
* Memory Pak: exactly what joyRumblePakInit (src/joy.c:186-190) needs
|
||||
* to proceed to osMotorInit. PD's osPfsInitPak does the same. */
|
||||
return inputRumbleSupported(channel) ? PFS_ERR_DEVICE : PFS_ERR_NOPACK;
|
||||
}
|
||||
s32 osPfsIsPlug(OSMesgQueue *queue, u8 *pattern)
|
||||
{ (void)queue; if (pattern) *pattern = 0; return 0; }
|
||||
{
|
||||
(void)queue;
|
||||
if (pattern) {
|
||||
*pattern = 0;
|
||||
/* JPN: MAXCONTROLLERS can be 6 > MAX_PADS; inputRumbleSupported()
|
||||
* bounds-checks, so the extra channels just never light up. */
|
||||
for (int i = 0; i < MAXCONTROLLERS; ++i)
|
||||
if (inputRumbleSupported(i)) *pattern |= (u8)(1 << i);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
s32 osMotorInit(OSMesgQueue *mq, OSPfs *pfs, int channel)
|
||||
{ (void)mq; (void)pfs; (void)channel; return -1; }
|
||||
s32 osMotorStart(OSPfs *pfs) { (void)pfs; return -1; }
|
||||
s32 osMotorStop(OSPfs *pfs) { (void)pfs; return -1; }
|
||||
{
|
||||
if (pfs && inputRumbleSupported(channel)) {
|
||||
pfs->queue = mq;
|
||||
pfs->channel = channel;
|
||||
pfs->activebank = 0xff;
|
||||
/* NOTE: PFS_MOTOR_INITIALIZED is not defined in GE's headers and
|
||||
* joy.c never reads pfs->status -- it only tests osMotorInit()==0
|
||||
* (src/joy.c:190) to mark the pad RUMBLEPAKINITSTATE_READY, so no
|
||||
* status store is needed (PD's osMotorProbe writes one). */
|
||||
return 0;
|
||||
}
|
||||
(void)mq; (void)channel;
|
||||
return PFS_ERR_NOPACK;
|
||||
}
|
||||
s32 osMotorStart(OSPfs *pfs)
|
||||
{
|
||||
if (!pfs) return PFS_ERR_NOPACK;
|
||||
/* The N64 motor has no duration; the game's joyRumblePakTimer60 (src/joy.c)
|
||||
* arms a per-event countdown and turns the motor off via osMotorStop.
|
||||
* Issue one generous 5 s window, PD-style ("hope the timer stops it");
|
||||
* inputRumble no-ops internally for non-rumble pads. */
|
||||
inputRumble(pfs->channel, 1.0f, 5.0f);
|
||||
return 0;
|
||||
}
|
||||
s32 osMotorStop(OSPfs *pfs)
|
||||
{
|
||||
if (!pfs) return PFS_ERR_NOPACK;
|
||||
inputRumble(pfs->channel, 0.0f, 0.0f); /* zero strength/duration = stop */
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
/* SP (RSP) — runs the software RSP inline, then posts the done messages */
|
||||
|
||||
@@ -205,6 +205,16 @@ static struct Row rows[] = {
|
||||
{ .key="Input.PadLookInvertY", .label="Invert look (controller)", .kind=ROW_TOGGLE, .step=1, .names=kOnOff },
|
||||
{ .key="Input.PadDeadzone", .label="Stick deadzone", .kind=ROW_SLIDER, .step=500 },
|
||||
{ .key="Input.PadTriggerPct", .label="Trigger threshold", .kind=ROW_SLIDER, .step=1 },
|
||||
/* D401 item 3 (docs/dev/RUMBLE-PLAN.md): Rumble Pak strength (D224
|
||||
* re-promotion). One global Input.RumbleScale for all pads (user call,
|
||||
* 2026-09-28 -- the original four per-player rows were dropped). The
|
||||
* value lives in input.c's gRumbleScale (0 = silent, 1 = full, dflt
|
||||
* 0.5), applied in inputRumble(); 0.05 detents give 20 steps, the
|
||||
* generic float value path displays them as %.2f. Inert (inputRumble
|
||||
* no-ops) when no rumble-capable pad is connected -- a generic
|
||||
* row-disabled state for that case is deferred with the per-pad tuning
|
||||
* row (QOL-INVENTORY). */
|
||||
{ .key="Input.RumbleScale", .label="Rumble strength", .kind=ROW_SLIDER, .step=0.05 },
|
||||
/* v0.4.0 M3 (modern options wave, D371): the GEPD mouse-injector
|
||||
* key-layout preset (docs/dev/notes/GEPORT-REFERENCE-DEEPDIVE.md
|
||||
* section 7.1) + the crouch bind's fire mode. Turok standard: these are
|
||||
@@ -1252,6 +1262,7 @@ static const struct { const char *key; double def; } kResetDefaults[] = {
|
||||
{ "Input.PadLookInvertY", 0 }, /* = 0 */
|
||||
{ "Input.PadDeadzone", 7000 },/* = STICK_DEADZONE (7000) */
|
||||
{ "Input.PadTriggerPct", 23 }, /* = 23 */
|
||||
{ "Input.RumbleScale", 0.5 }, /* = gRumbleScale (0.5f), D401 */
|
||||
{ "Input.CrouchMode", 0 }, /* = 0 (hold) */
|
||||
/* GRAPHICS (port/src/video.c initializers) */
|
||||
{ "Video.MSAA", 2 }, /* = 2 */
|
||||
|
||||
Reference in new issue
Block a user