WIP: fix: Hookshot's small player-facing fixes for v3.0.0-rc.4, restored #286

Draft
TrashPanda wants to merge 21 commits from TrashPanda/Longshot:restore/rc4-player-fixes into main
Contributor

Why

Players on v3.0.0-rc.3 still hit bugs Hookshot fixed after Longshot branched: the game closing at Windows' commit limit,
Mouse 4/5 not bindable on Linux, a control's second key lost on a rebind, the gyro and a background controller moving
Link with the menu open, a failed options save emptying the file, Linux sound not reopening, 5.1 counted wrong. These are
the small, low-risk ones, for v3.0.0-rc.4. Split out of #209.

What changed

Split out of #209 as its reviewer asked (review on #209, point 4, group c). Each commit is the merged Hookshot pull request under its original author, carried through Longshot's rename; a Restored-from: trailer names it, and the bare Hookshot numbers in the messages and the restored text are now full URLs.

Credit to the authors of the restored work: jarynclouatre, NotRexed, Jiro131, bluetoe-99, David B, Jordan Ella.

  • memory: a failed allocation at the commit limit gives back a 128 MB reserve and is tried again (vk_texture.cpp,
    vk_memory.cpp's memory_table_flush), building on Longshot's commit rule (#77)
  • input: Mouse 4/5 on Linux, a second key per control (Key 1 / Key 2), ocarina buttons and the gyro with a background
    controller or the menu open, the mouse camera while the mouse is released, the touchpad click as a binding
    (HOS_PAD_TOUCHPAD moves inside HOS_PAD_N: MKWVK_API_VERSION 21 to 22)
  • menu: a failed options save leaves the file as it was; bug-report save errors are reported, and are UTF-8 on Windows
  • audio: the Linux sound output is closed before it is reopened; Auto counts 5.1 speakers, not channels
  • game patch: the scarecrow's recall takes the note remaps (kind 11)
  • builder and setup: a direct lift clears an earlier lift's split parts; a refused dump leaves the saved game code alone;
    a stalled preparation is not promised; the setup retries a rename a scanner blocked (builder/setup/setup.c)
  • Linux: the log says why the renderer did not load

Split siblings: tools, checks and docs #284, test-request pinning #285, renderer fixes #287, new defaults and speed-ups #288.

Commits, in order:

  • 350ed8fb fix(setup): a rename blocked by a scanner is tried again, and the message carries Windows' error (Hookshot 3021, by jarynclouatre)
  • 91bd08f0 fix(memory): a failed allocation at the commit limit no longer closes the game (Hookshot issue 2941) (Hookshot 3017, by NotRexed)
  • 498c7d22 fix(menu): a control's second key survives a rebind, with a column of its own (Hookshot 2933, by jarynclouatre)
  • 50651b0b fix(input): custom ocarina note buttons count with the controller in the background (Hookshot 2944, by jarynclouatre)
  • f7d1a3ce fix(input): the native mouse camera leaves the view to the game while the mouse is released (Hookshot 2947, by jarynclouatre)
  • d1571435 fix(menu): a bug report whose report.txt could not be written out is not shown as saved (Hookshot 2948, by jarynclouatre)
  • 2af6de34 fix(input): Mouse 4 and Mouse 5 can be bound on Linux (Hookshot 2961, by jarynclouatre)
  • 240c669e fix(input): the gyro is at rest while the Insert menu is open (Hookshot 2962, by jarynclouatre)
  • cb60f53b fix(menu): a save that fails leaves the options file as it was (Hookshot 2966, by jarynclouatre)
  • 99827e14 fix(linux): the sound output is closed before it is opened again (Hookshot 2970, by jarynclouatre)
  • f84a838a fix(menu): Windows' reason for a bug report that could not be saved is UTF-8 whatever the code page (Hookshot issue 2954) (Hookshot 2978, by Jiro131)
  • 6d293860 fix(builder): a preparation the GPU driver stalled is not promised for the first start (Hookshot 2985, by jarynclouatre)
  • 45637584 feat(input): a DualShock 4 or DualSense touchpad click can be bound in Controls (Hookshot 2989, by jarynclouatre)
  • 474f52f9 fix(oot): the scarecrow's recall of the recorded song takes the note remaps too (kind 11) (Hookshot 3018, by bluetoe-99)
  • 99a45bd6 fix(build): a direct lift removes an earlier lift's split parts before it writes its own (Hookshot 2953, by jarynclouatre)
  • 89ada153 fix(builder): a refused extracted dump leaves the saved copy of the game code alone (Hookshot 2968, by jarynclouatre)
  • b0c4ba56 fix(audio): Auto counts 5.1 speakers, not channels (Hookshot 3044, by David B)
  • bad3e030 fix(linux): the log says why the renderer did not load (Hookshot 2875, by Jordan Ella)
  • 5395d066 fix(input): the controller in the background stays out of the game while the Insert menu is open (Hookshot 2963, by jarynclouatre)
  • 093cdd71 test(menu): the upscaler round trip's fixture declares logf and the stat calls save() now uses (mine)
  • 986db4c5 docs: the restored Hookshot references as full URLs (mine)

Left out of this group on purpose: the floor web above 60 fps (https://github.com/NotRexed/Hookshot/pull/2929) is
already on main as #49.

Flags and settings

Flag What it does Default now Recommendation Helps on Doesn't help on Untested on
Controls: Key 1 / Key 2 columns Each control takes two keys, each cell its own slot The defaults' two keys Default Keyboard players - Longshot builds in the game
Controls: touchpad click as an input A DualShock 4's or DualSense's touchpad click can be bound; unbound, it still opens the Insert menu Unbound (opens the menu) Default DS4, DualSense Other pads Longshot builds in the game
HOS_INPUT_BACKGROUND=1 Existing; now also covers the ocarina buttons, and stays out while the Insert menu is open Unchanged - - - -
HOS_AUDIO_CHANNELS=2 / 6 Existing; Auto now counts the speakers Windows names Auto Default Audio interfaces with many outputs - Windows in the game

Measurements

No numbers of my own: each fix was measured or shown upstream (in its commit message). Here the change is checked by
the game-free checks and a build on both platforms, main and this PR on the same PC.

main: d9648863 (2026-11-13)
this PR: restore/rc4-player-fixes @ b4c98fb5 (the runs; now 986db4c5, after the docs-only rebase onto main's 514591a3)
Hardware: RTX 4070 Ti (driver 595.99.02), Ryzen 7 9800X3D, Ubuntu 26.04.1
Scene: none (no game run)
Method: python3 -I -B tests/run_all.py --jobs 6, once on each; the builds below
How to reproduce: the same command on each commit

main @ d9648863 this PR @ b4c98fb5
run_all.py: passed / failed / skipped 513/2/18 519/2/18
failing test_lift_cache.py, test_menu_i18n.py test_lift_cache.py, test_menu_i18n.py
no-game build, Linux: files / compile failures / undefined at link 406 / 0 / 18 406 / 0 / 18 (the same symbols)
no-game build, Windows (llvm-mingw): files / compile failures / undefined at link 405 / 0 / 19 405 / 0 / 19 (the same symbols)

The numbers still hold on 514591a3: main's #274 changed a Discord link in 11 docs files and nothing these checks or builds compile; the checks that read those docs were run again on this head.

Testing

Tested: python3 -I -B tests/run_all.py --jobs 6 on Linux (Ubuntu 26.04.1, Python 3.14, Vulkan and SPIR-V headers found): this PR @ b4c98fb5: 519 passed, 2 failed, 18 skipped (test_lift_cache.py, test_menu_i18n.py); main @ d9648863 on the same PC: 513 passed, 2 failed, 18 skipped (test_lift_cache.py, test_menu_i18n.py). test_lift_cache fails on main the same way (the lift's scripts do not import here), and test_menu_i18n fails on that main too. Main then gained 514591a3 (#274, a Discord link in 11 docs files only); after the rebase onto it, the checks that read those docs passed again at 986db4c5.

Built at b4c98fb5 with no game, on Linux (system clang 21, lld) and for Windows (cross-built here with tools.lock's llvm-mingw 20261104 and its windows-x64 CPM cache, which has FFmpeg and dav1d): (before the docs-only rebase onto #274) nx2/build.py --standalone with an empty lift compiled everything in the repository that the game links, the host, its libraries and the Vulkan back end (406 files on Linux, 405 for Windows), with no failure, and its link stopped only at the symbols the game's lifted code provides (18 on Linux, 19 on Windows: the same list as main @ d9648863). RX_VK_ONLY=1 RX_RELEASE=1 linked longshot_vk.so and longshot_vk.dll, and builder/setup/setup.c linked as LongshotBuilder-Setup.exe with build-windows.yml's command. The five split branches merged together locally (the API version taken as 23) compiled and linked the same way, so they also build together.

Not tested: a run in the game, on either platform: the game build needs the game's lifted code, and I had no run
here. The setup's retry (https://github.com/NotRexed/Hookshot/pull/3021) has not run on Windows.

Needs other testers:

  • Windows: a full build of this branch with make_game.py, then a short run (title, a save, Kokiri Forest): rebind a
    control's second key, open and close the Insert menu with a controller connected, save the options; expecting no
    crash and the options file intact (T-DV9Q, claimable once a maintainer adds the testing wanted label)
  • Linux: Mouse 4/5 bound in Controls, and the sound after an output device change, expecting both to work (T-DV9Q)

Screenshots

None: no run in the game yet (the Controls page's Key 1 / Key 2 columns change on screen).

Risk and rollback

Input and menu changes are the most visible; each commit reverts on its own. MKWVK_API_VERSION goes to 22 here: the
renderer fixes (#287) take 23 and are meant to merge after this one; should they merge first, they take 22 and this one 23.

Reviewer notes

Please merge with rebase, not squash, so that each author keeps their commit. One commit of mine fixes the upscaler
round-trip check from #55: save() now uses logf and lstat, which its fixture did not declare.

Issues

See #209

Checklist

For everyone

  • One topic.
  • The title is a Conventional Commit (type(scope): description).
  • Rebased on current main.
  • python -I -B tests/run_all.py run and passing (any failure that also fails on main is named above).
  • Tested in the game, or what wasn't is named under Testing.
  • Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests).
  • Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported. (no game run: see Testing; WIP)
  • No game files, dumps, build output or generated files.
  • third_party/ untouched (a new folder with a PROVENANCE.md instead).
  • The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled.

If applicable

  • Screenshots for a change that shows on screen. (None yet: no run in the game.)
  • AI assistance recorded with Assisted-by: / Co-Authored-By: trailers.
  • Flags noted, and the switch reference updated (docs/development.md, the README).
  • MKWVK_API_VERSION bumped (gpu/vk/vk_api.h's MkwVkHost or MkwVkRenderer changed).
  • A check under tests/ that reads changed source updated in this PR, not weakened.
  • Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths. (Compiled with no game; see Testing.)
  • Docs updated.
## Why Players on v3.0.0-rc.3 still hit bugs Hookshot fixed after Longshot branched: the game closing at Windows' commit limit, Mouse 4/5 not bindable on Linux, a control's second key lost on a rebind, the gyro and a background controller moving Link with the menu open, a failed options save emptying the file, Linux sound not reopening, 5.1 counted wrong. These are the small, low-risk ones, for v3.0.0-rc.4. Split out of [#209](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/209). ## What changed Split out of [#209](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/209) as its reviewer asked (review on #209, point 4, group c). Each commit is the merged Hookshot pull request under its original author, carried through Longshot's rename; a `Restored-from:` trailer names it, and the bare Hookshot numbers in the messages and the restored text are now full URLs. Credit to the authors of the restored work: jarynclouatre, NotRexed, Jiro131, bluetoe-99, David B, Jordan Ella. - memory: a failed allocation at the commit limit gives back a 128 MB reserve and is tried again (`vk_texture.cpp`, `vk_memory.cpp`'s `memory_table_flush`), building on Longshot's commit rule (#77) - input: Mouse 4/5 on Linux, a second key per control (Key 1 / Key 2), ocarina buttons and the gyro with a background controller or the menu open, the mouse camera while the mouse is released, the touchpad click as a binding (`HOS_PAD_TOUCHPAD` moves inside `HOS_PAD_N`: `MKWVK_API_VERSION` 21 to 22) - menu: a failed options save leaves the file as it was; bug-report save errors are reported, and are UTF-8 on Windows - audio: the Linux sound output is closed before it is reopened; Auto counts 5.1 speakers, not channels - game patch: the scarecrow's recall takes the note remaps (kind 11) - builder and setup: a direct lift clears an earlier lift's split parts; a refused dump leaves the saved game code alone; a stalled preparation is not promised; the setup retries a rename a scanner blocked (`builder/setup/setup.c`) - Linux: the log says why the renderer did not load Split siblings: tools, checks and docs [#284](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/284), test-request pinning [#285](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/285), renderer fixes [#287](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/287), new defaults and speed-ups [#288](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/288). Commits, in order: - `350ed8fb` fix(setup): a rename blocked by a scanner is tried again, and the message carries Windows' error ([Hookshot 3021](https://github.com/NotRexed/Hookshot/pull/3021), by jarynclouatre) - `91bd08f0` fix(memory): a failed allocation at the commit limit no longer closes the game (Hookshot issue 2941) ([Hookshot 3017](https://github.com/NotRexed/Hookshot/pull/3017), by NotRexed) - `498c7d22` fix(menu): a control's second key survives a rebind, with a column of its own ([Hookshot 2933](https://github.com/NotRexed/Hookshot/pull/2933), by jarynclouatre) - `50651b0b` fix(input): custom ocarina note buttons count with the controller in the background ([Hookshot 2944](https://github.com/NotRexed/Hookshot/pull/2944), by jarynclouatre) - `f7d1a3ce` fix(input): the native mouse camera leaves the view to the game while the mouse is released ([Hookshot 2947](https://github.com/NotRexed/Hookshot/pull/2947), by jarynclouatre) - `d1571435` fix(menu): a bug report whose report.txt could not be written out is not shown as saved ([Hookshot 2948](https://github.com/NotRexed/Hookshot/pull/2948), by jarynclouatre) - `2af6de34` fix(input): Mouse 4 and Mouse 5 can be bound on Linux ([Hookshot 2961](https://github.com/NotRexed/Hookshot/pull/2961), by jarynclouatre) - `240c669e` fix(input): the gyro is at rest while the Insert menu is open ([Hookshot 2962](https://github.com/NotRexed/Hookshot/pull/2962), by jarynclouatre) - `cb60f53b` fix(menu): a save that fails leaves the options file as it was ([Hookshot 2966](https://github.com/NotRexed/Hookshot/pull/2966), by jarynclouatre) - `99827e14` fix(linux): the sound output is closed before it is opened again ([Hookshot 2970](https://github.com/NotRexed/Hookshot/pull/2970), by jarynclouatre) - `f84a838a` fix(menu): Windows' reason for a bug report that could not be saved is UTF-8 whatever the code page (Hookshot issue 2954) ([Hookshot 2978](https://github.com/NotRexed/Hookshot/pull/2978), by Jiro131) - `6d293860` fix(builder): a preparation the GPU driver stalled is not promised for the first start ([Hookshot 2985](https://github.com/NotRexed/Hookshot/pull/2985), by jarynclouatre) - `45637584` feat(input): a DualShock 4 or DualSense touchpad click can be bound in Controls ([Hookshot 2989](https://github.com/NotRexed/Hookshot/pull/2989), by jarynclouatre) - `474f52f9` fix(oot): the scarecrow's recall of the recorded song takes the note remaps too (kind 11) ([Hookshot 3018](https://github.com/NotRexed/Hookshot/pull/3018), by bluetoe-99) - `99a45bd6` fix(build): a direct lift removes an earlier lift's split parts before it writes its own ([Hookshot 2953](https://github.com/NotRexed/Hookshot/pull/2953), by jarynclouatre) - `89ada153` fix(builder): a refused extracted dump leaves the saved copy of the game code alone ([Hookshot 2968](https://github.com/NotRexed/Hookshot/pull/2968), by jarynclouatre) - `b0c4ba56` fix(audio): Auto counts 5.1 speakers, not channels ([Hookshot 3044](https://github.com/NotRexed/Hookshot/pull/3044), by David B) - `bad3e030` fix(linux): the log says why the renderer did not load ([Hookshot 2875](https://github.com/NotRexed/Hookshot/pull/2875), by Jordan Ella) - `5395d066` fix(input): the controller in the background stays out of the game while the Insert menu is open ([Hookshot 2963](https://github.com/NotRexed/Hookshot/pull/2963), by jarynclouatre) - `093cdd71` test(menu): the upscaler round trip's fixture declares logf and the stat calls save() now uses (mine) - `986db4c5` docs: the restored Hookshot references as full URLs (mine) Left out of this group on purpose: the floor web above 60 fps (https://github.com/NotRexed/Hookshot/pull/2929) is already on main as #49. ## Flags and settings | Flag | What it does | Default now | Recommendation | Helps on | Doesn't help on | Untested on | |---|---|---|---|---|---|---| | Controls: Key 1 / Key 2 columns | Each control takes two keys, each cell its own slot | The defaults' two keys | Default | Keyboard players | - | Longshot builds in the game | | Controls: touchpad click as an input | A DualShock 4's or DualSense's touchpad click can be bound; unbound, it still opens the Insert menu | Unbound (opens the menu) | Default | DS4, DualSense | Other pads | Longshot builds in the game | | `HOS_INPUT_BACKGROUND=1` | Existing; now also covers the ocarina buttons, and stays out while the Insert menu is open | Unchanged | - | - | - | - | | `HOS_AUDIO_CHANNELS=2` / `6` | Existing; Auto now counts the speakers Windows names | Auto | Default | Audio interfaces with many outputs | - | Windows in the game | ## Measurements No numbers of my own: each fix was measured or shown upstream (in its commit message). Here the change is checked by the game-free checks and a build on both platforms, main and this PR on the same PC. main: d9648863 (2026-11-13) this PR: restore/rc4-player-fixes @ b4c98fb5 (the runs; now 986db4c5, after the docs-only rebase onto main's 514591a3) Hardware: RTX 4070 Ti (driver 595.99.02), Ryzen 7 9800X3D, Ubuntu 26.04.1 Scene: none (no game run) Method: `python3 -I -B tests/run_all.py --jobs 6`, once on each; the builds below How to reproduce: the same command on each commit | | main @ d9648863 | this PR @ b4c98fb5 | |---|---|---| | `run_all.py`: passed / failed / skipped | 513/2/18 | 519/2/18 | | failing | test_lift_cache.py, test_menu_i18n.py | test_lift_cache.py, test_menu_i18n.py | | no-game build, Linux: files / compile failures / undefined at link | 406 / 0 / 18 | 406 / 0 / 18 (the same symbols) | | no-game build, Windows (llvm-mingw): files / compile failures / undefined at link | 405 / 0 / 19 | 405 / 0 / 19 (the same symbols) | The numbers still hold on `514591a3`: main's #274 changed a Discord link in 11 docs files and nothing these checks or builds compile; the checks that read those docs were run again on this head. ## Testing **Tested:** `python3 -I -B tests/run_all.py --jobs 6` on Linux (Ubuntu 26.04.1, Python 3.14, Vulkan and SPIR-V headers found): this PR @ `b4c98fb5`: 519 passed, 2 failed, 18 skipped (test_lift_cache.py, test_menu_i18n.py); main @ `d9648863` on the same PC: 513 passed, 2 failed, 18 skipped (test_lift_cache.py, test_menu_i18n.py). `test_lift_cache` fails on main the same way (the lift's scripts do not import here), and `test_menu_i18n` fails on that main too. Main then gained `514591a3` (#274, a Discord link in 11 docs files only); after the rebase onto it, the checks that read those docs passed again at `986db4c5`. **Built** at `b4c98fb5` with no game, on Linux (system clang 21, lld) and for Windows (cross-built here with tools.lock's llvm-mingw 20261104 and its windows-x64 CPM cache, which has FFmpeg and dav1d): (before the docs-only rebase onto #274) `nx2/build.py --standalone` with an empty lift compiled everything in the repository that the game links, the host, its libraries and the Vulkan back end (406 files on Linux, 405 for Windows), with no failure, and its link stopped only at the symbols the game's lifted code provides (18 on Linux, 19 on Windows: the same list as main @ `d9648863`). `RX_VK_ONLY=1 RX_RELEASE=1` linked `longshot_vk.so` and `longshot_vk.dll`, and `builder/setup/setup.c` linked as `LongshotBuilder-Setup.exe` with build-windows.yml's command. The five split branches merged together locally (the API version taken as 23) compiled and linked the same way, so they also build together. **Not tested:** a run in the game, on either platform: the game build needs the game's lifted code, and I had no run here. The setup's retry (https://github.com/NotRexed/Hookshot/pull/3021) has not run on Windows. **Needs other testers:** - Windows: a full build of this branch with `make_game.py`, then a short run (title, a save, Kokiri Forest): rebind a control's second key, open and close the Insert menu with a controller connected, save the options; expecting no crash and the options file intact (T-DV9Q, claimable once a maintainer adds the `testing wanted` label) - Linux: Mouse 4/5 bound in Controls, and the sound after an output device change, expecting both to work (T-DV9Q) ## Screenshots None: no run in the game yet (the Controls page's Key 1 / Key 2 columns change on screen). ## Risk and rollback Input and menu changes are the most visible; each commit reverts on its own. `MKWVK_API_VERSION` goes to 22 here: the renderer fixes ([#287](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/287)) take 23 and are meant to merge after this one; should they merge first, they take 22 and this one 23. ## Reviewer notes Please merge with **rebase**, not squash, so that each author keeps their commit. One commit of mine fixes the upscaler round-trip check from #55: `save()` now uses `logf` and `lstat`, which its fixture did not declare. ## Issues See #209 ## Checklist ### For everyone - [x] One topic. - [x] The title is a Conventional Commit (`type(scope): description`). - [x] Rebased on current `main`. - [x] `python -I -B tests/run_all.py` run and passing (any failure that also fails on `main` is named above). - [x] Tested in the game, or what wasn't is named under Testing. - [x] Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests). - [ ] Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported. (no game run: see Testing; WIP) - [x] No game files, dumps, build output or generated files. - [x] `third_party/` untouched (a new folder with a `PROVENANCE.md` instead). - [x] The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled. ### If applicable - [ ] Screenshots for a change that shows on screen. (None yet: no run in the game.) - [x] AI assistance recorded with `Assisted-by:` / `Co-Authored-By:` trailers. - [x] Flags noted, and the switch reference updated (docs/development.md, the README). - [x] `MKWVK_API_VERSION` bumped (`gpu/vk/vk_api.h`'s `MkwVkHost` or `MkwVkRenderer` changed). - [x] A check under `tests/` that reads changed source updated in this PR, not weakened. - [x] Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths. (Compiled with no game; see Testing.) - [x] Docs updated.
The Windows setup unpacks each part with tar.exe and then renames the
folder (llvm-mingw-<version>-ucrt-x86_64 to llvm-mingw). Right after
tar.exe writes hundreds of .exe and .dll files, an antivirus scan or the
search indexer can still hold one of them open, and MoveFileW on the
folder then fails with access denied (5) or a sharing violation (32).
The setup tried once and said only "Could not set up part 8 of 8
(renaming llvm-mingw-...)", with no Windows error, which matches https://github.com/NotRexed/Hookshot/pull/3011
(every run, also as administrator).

- `move_retry` in setup.c tries those two errors again every 250 ms for
up to 20 s; any other error returns at once, and GetLastError is the
last try's. It is used for the part's rename and for both moves of the
staged install into place in `publish_install` (not the backup moves).
- The rename's message now carries Windows' error, and for 5 or 32 says
another program is holding the unpacked files, often an antivirus scan,
and names the folder to allow. "Could not finish installing" carries the
error too.
- The Python setup (`setup_parts.py`) does the same on Windows with
`replace_retry` at both `os.replace` calls of `install()`; on other
systems a PermissionError is raised at once as before. Its messages
carry the error text.
- `test_portable_setup_transaction.py` cuts `move_retry` out with
`publish_install` and defines Sleep away, so its denied-move case does
not wait 20 s.

I cannot say which program holds the folder on the reporter's PC; if the
retry is not enough, the next report will carry the error number.

For https://github.com/NotRexed/Hookshot/pull/3011

## Testing

- `tests/test_setup_rename_retry.py` (new): cuts `move_retry` out of
setup.c and runs it with its own MoveFileW and Sleep, on Linux with gcc:
free, denied 3 times then free (4 calls, 3 waits), sharing violation
twice then free, path not found (1 call, no wait, error 3 kept), denied
for ever (80 calls, 79 waits, error 5). Fails on main (no `move_retry`).
- `tests/test_setup_parts_rename_retry.py` (new): `replace_retry` with
os.replace and time.sleep mocked: two PermissionErrors then free,
FileNotFoundError at once, PermissionError for ever (80 tries), not
Windows at once, and `install()` uses it at both places. Fails on main.
- `python -I -B tests/run_all.py` on Linux: 445 passed, 85 skipped, 1
failed (`test_bake_portability.py`: zstandard is not installed here).
- Not run: the Windows-only `test_portable_setup_transaction.py` and
`test_setup_runtime_paths.py` (CI runs them), and a real setup on a PC
whose scanner holds the folder.

Ported to Longshot: Longshot has no Python setup any more (#171), so setup_parts.py and its test are left out. The tool is
now moved into place by place_tool (Longshot), so move_retry is its whole-folder move, the scanner message is the
tool's, and test_setup_tool_merge.py cuts move_retry out with place_tool.

Restored-from: https://github.com/NotRexed/Hookshot/pull/3021
Fixes the crash in https://github.com/NotRexed/Hookshot/pull/2941 (v2.2.0-rc.1): the game closes every time on
the load into Hyrule Field.

**What the crash was.** I built rc.1's sources with DLSS here, using the
same llvm-mingw 20261104, the same `x86-64-v3` target and the NGX SDK
the reporter's builder had. With that build's `hookshot_vk.map`, the
reporter's backtrace reads:

```
KERNELBASE RaiseException  <- libunwind/libc++ throw
  mkwvk::memory_table_flush() +0xf2             (vk_memory.cpp)
  mkwvk::upload_command_buffer() +0x28
  flush_ub_updates +0x109 <- replay +0x3ac4 <- run_submit <- resume_queue <- schedule <- submit_job <- submit_q
  hookshot.exe (the game's lifted code)
```

8 s later the log shows `0xc00000ff` (bad function table): the exception
got as far as the game's code and couldn't be unwound.

- **Exception code** `0x20474343` is a C++ exception.
- **What threw:** the first thing `memory_table_flush` does is build its
list of page changes, so the throw is `std::bad_alloc`.
- **Why there was no memory:** the reporter has 31.9 GB of RAM with a
2.1 GB page file, so the commit limit is about 34 GB. Hookshot was at
24.3 GB of private commit as Hyrule Field loaded.

This is the same limit as https://github.com/NotRexed/Hookshot/pull/2972, reached through the renderer's own heap
rather than guest memory.

**Changes**
- **`vk_memory.cpp`:** `memory_table_flush` reserves its list in one
allocation. If that throws, it writes the changes into the mapped table
at once, as `RX_VK_TABLE_NOW=1` does, and logs it (8 times at most). The
cost of that path is that work submitted before may read the new entries
early. Before this change, the game closed.
- **`vk_texture.cpp`:** the renderer now holds 128 MB of commit as an
emergency reserve while there is room. On the first allocation that
fails, `operator new`'s handler gives the reserve back and the
allocation is tried again.
- On the next pass, https://github.com/NotRexed/Hookshot/pull/2972's commit rule sees the headroom gone and
shrinks the textures.
- Once headroom is back above the rule's reserve plus 128 MB, the
reserve is taken again.
- Waiting wouldn't help here, because the thread that runs short is the
presenting thread, the same one that trims.
- Windows only. `RX_VK_COMMIT_RESERVE_MB=0` turns it off along with the
rule.
- **`tests/test_commit_limit.py`:** covers both.

**Checked so far**
- `test_commit_limit.py` passes.
- `vk_texture.cpp` compiles syntax-only with the build's flags.
- An in-game route run on this branch is next.

The failed-allocation path itself can't be triggered here without
exhausting this PC's commit, so only the test covers it.

**Still wise for the reporter:** let Windows manage the page file. A
fixed 2 GB page file leaves about 10 GB of headroom on a 32 GB PC.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <[email protected]>
Restored-from: https://github.com/NotRexed/Hookshot/pull/3017
Fixes https://github.com/NotRexed/Hookshot/issues/2885

Some controls have two keys by default, but the Controls page showed a
control's keys as one Keyboard cell, and binding it wrote the new key to
the first slot and cleared the second. After any rebind the control kept
one key, and there was no way to give it a second one.

The host already keeps two key slots per control, so the page now shows
them as two cells, Key 1 and Key 2, next to the two gamepad inputs.
Binding or clearing a cell changes its own slot and leaves the other key
alone.

- Left and Right move through all four cells. On the keys-only rows (the
stick and camera directions) they move between the two key cells.
- A key taken from another control is still removed from it, as before.
- The options file's `key_<button>` lines are read and written as
before, so existing files load unchanged.
- The names column is narrower. Each key cell is as wide as "Right
click" needs (between a sixth and a fifth of the row), and the gamepad
cells share the rest. At Small and Medium the key names show whole; at
Large, where main already cut the gamepad names, the long ones are cut
too, and scroll when selected as before.
- "Key 1" and "Key 2" use the existing "Key %d" translations.

### Testing

- `tests/test_controls_key_slots.py` (new): cuts the page's key binding
and clearing code from `vk_menu.cpp` and runs it against a host that
records the bindings. Binding Key 2 keeps Key 1, binding Key 1 keeps Key
2, clearing one keeps the other, a keys-only row binds Key 1. It fails
on main (the second key is cleared).
- `tests/test_menu_hit.cpp`: the fourth cell's hit area, and the column
rules (`ctl_key_slot`, `ctl_column`, `ctl_step`).
- `tests/run_all.py` on Linux: 443 passed, 85 skipped, 1 failed
(`test_bake_portability.py`, no `zstandard` module on this machine; it
fails the same way on main).
- Linux build on an RTX 5060 Ti, headless with `RX_VK_SHOT_MENU=1` and
`RX_VK_MENU_SCRIPT`, 1920x1080, the menu opened before the first
picture: the page at each Menu size, before (main 4dd972a8) and after,
with Plus (Tab and Enter by default) selected in its keyboard cell.
Binding a key on the page was not done in the game; the test above
covers it.

| Menu size | Before | After |
|---|---|---|
| Small | ![before,
Small](https://raw.githubusercontent.com/jarynclouatre/Hookshot/pr-screenshots/controls-keys/m-small.webp)
| ![after,
Small](https://raw.githubusercontent.com/jarynclouatre/Hookshot/pr-screenshots/controls-keys/k-small.webp)
|
| Medium | ![before,
Medium](https://raw.githubusercontent.com/jarynclouatre/Hookshot/pr-screenshots/controls-keys/m-medium.webp)
| ![after,
Medium](https://raw.githubusercontent.com/jarynclouatre/Hookshot/pr-screenshots/controls-keys/k-medium.webp)
|
| Large | ![before,
Large](https://raw.githubusercontent.com/jarynclouatre/Hookshot/pr-screenshots/controls-keys/m-large.webp)
| ![after,
Large](https://raw.githubusercontent.com/jarynclouatre/Hookshot/pr-screenshots/controls-keys/k-large.webp)
|

Restored-from: https://github.com/NotRexed/Hookshot/pull/2933
With "controller in the background" on (`HOS_INPUT_BACKGROUND=1` or the
menu's row), the pads reach the game behind another window, but an
ocarina note bound to a custom pad button did not: `oc_pad_word`
returned the game's word unchanged whenever the window was not in front,
so the remapped notes were dropped while the rest of the pad worked.

## Change

- `oc_pad_word` gives the notes in the background when the pads count
there, by the same rule `read_input` uses (`pads_in_background` on the
switch, else the menu's row), through a small `pads_behind` helper.
- `oc_notes` takes the keyboard's note keys only with the window in
front, so a key held in another program never plays a note. The pads'
note buttons count in both cases.

## Testing

- `tests/test_ocarina_notes.py`, new case: with the pads counting in the
background, a pad's note button plays its note and the keyboard's note
key does not; without the row the word is unchanged. It fails on main
(`oc_pad_word(0)` gives no L), and passes here.
- `tests/test_input_background.py`: its fixture takes the new helper.
- Full suite on Linux: 442 passed, 85 skipped, 1 failed
(`test_bake_portability`: `zstandard` is not installed on this machine;
it fails the same on main).
- Not tested in the game.

Restored-from: https://github.com/NotRexed/Hookshot/pull/2944
With the native mouse camera, `hos_mouse_camera` skipped the mouse's
motion while the mouse was released (Esc, a menu open, or the window
losing focus), but it still wrote its last yaw and pitch back into the
camera every frame. The game's own camera work, such as swinging behind
Link, was undone until the mouse was captured again or the stick moved.

## Change

In `host/mouse_native.c`, `hos_mouse_camera` asks `hos_mouse_take`
first. With the mouse released it forgets its angles and leaves the
camera alone, so the next captured frame starts from the game's angles,
the same way it already does after the stick turns the camera.

## Testing

- `tests/test_mouse_native.py`, new case: with capture off the game's
yaw (10) is kept, and once the mouse is captured again a turn starts
from it (11). It fails on main and passes here.
- Full suite on Linux: 442 passed, 85 skipped, 1 failed
(`test_bake_portability`: `zstandard` is not installed on this machine;
it fails the same on main).
- Not tested in the game.

Restored-from: https://github.com/NotRexed/Hookshot/pull/2947
"Report a bug" in the Insert menu checked report.txt's write by
`fwrite`'s count only and dropped `fclose`'s result. On a full disk the
write only fills the stream's buffer and succeeds; the close is what
fails. The menu then showed the report as saved, and the folder held a
cut or empty report.txt.

## Change

`bug_report::write` in `gpu/vk/vk_bug_report.cpp` counts a failed
`fclose` as a failed write, so the report ends with the existing
"report.txt could not be written" reason.

## Testing

- `tests/test_bug_report.py`, new case (Linux, where `/dev/full`
exists): the driver points the report folder's report.txt at `/dev/full`
before it is written; the report is not shown as saved and the log gives
the reason. It fails on main (the report shows as saved) and passes
here.
- Full suite on Linux: 442 passed, 85 skipped, 1 failed
(`test_bake_portability`: `zstandard` is not installed on this machine;
it fails the same on main).
- Not tested on Windows.

Restored-from: https://github.com/NotRexed/Hookshot/pull/2948
On Linux, `keys_update` in `host/posix/display_sdl.c` turned three of
SDL's five mouse buttons into virtual keys. The Controls page offers
Mouse 4 and Mouse 5 (0x05 and 0x06), and `host/nn_hid.c` reads them, but
on Linux the listener never saw a side button, and a saved binding to
one (`key_zl=5,0`) never acted. Windows has them.

## Change

- `keys_update` also sets `VK_XBUTTON1` from SDL's X1 bit (8) and
`VK_XBUTTON2` from X2 (16). The bits are the same in SDL 2 and SDL 3,
and `hos_sdl_mouse_buttons` passes SDL's mask through unchanged.
- `host/posix/windows.h` defines `VK_XBUTTON1` and `VK_XBUTTON2` beside
the other mouse keys (`nn_hid.c`'s own definitions are under `#ifndef`).
- The window guard around those lines stays.

## Testing

- `tests/test_sdl_mouse_buttons.py` (new): `keys_update` and the posix
virtual keys cut from the sources, built with stand-ins for SDL. Each of
SDL's five button bits sets only its own key (5 and 6 for the side
buttons), both side buttons together set both, and nothing is set
without a window. It fails on main (keys 5 and 6 stay 0) and passes
here.
- Full suite on Linux: 2 failures, each for a tool this machine lacks
(`test_bake_portability`: `zstandard`;
`nx2/test_texture_init_barrier.py`: `clang++`); both fail the same on
main.
- No side button was pressed in the game on Linux.

Restored-from: https://github.com/NotRexed/Hookshot/pull/2961
With motion controls on, opening the Insert menu holds back a pad's
buttons and sticks (`read_input`), but `GetSixAxisSensorStates` only
asked about the window and the "Controller in background" row. Moving
the controller while in the menu still aimed in the game.

## Change

`motion_counts` in `host/nn_hid.c` takes the menu's state (`rx_option(1,
0)`) as well, and the motion counts only while the menu is closed. With
the menu open the handler gives its existing at-rest state; the switch
between live and at rest already renumbers the samples.

## Testing

- `tests/test_motion_background.py`: three new cases (menu open in
front; open behind another window with "Controller in background" on;
open with `HOS_INPUT_BACKGROUND=1`) give the pad at rest; the six cases
from before are unchanged. On main the handler never looks at the menu
(`motion_counts` has no parameter for it).
- Full suite on Linux: 2 failures, each for a tool this machine lacks
(`test_bake_portability`: `zstandard`;
`nx2/test_texture_init_barrier.py`: `clang++`); both fail the same on
main.
- No controller was moved in the game.

Restored-from: https://github.com/NotRexed/Hookshot/pull/2962
Saving the options (any row changed in the menu) opened
`hookshot_options.ini` with `"w"`, which empties the file before the
first line is written. When the write then failed, on a full disk for
example, the player was left with an empty or cut options file, and the
next start went back to the defaults.

## Change

`save()` in `gpu/vk/vk_menu.cpp` writes the options to
`hookshot_options.ini.new` beside the file, checks the writes and the
close, and only then moves the new file over the old one (`rename` on
Linux, `MoveFileExA` on Windows). A save that fails removes the part
file, leaves the options as they were, and logs one line.

- An options file that is a link keeps working as before: on Linux the
link's target is written. A link that cannot be resolved here (one to a
file not made yet, or any link on Windows) is written through in place
as before, so it stays a link.
- https://github.com/NotRexed/Hookshot/pull/2430 changes the same `fopen` so the game does not write through a
link at all. Whichever lands second can take the other's rule for links;
the part-file step does not depend on it.
- `tests/test_mesh_mode_switch.py` looked for the old
`fopen(ini_path().c_str(), "w")` call to find the end of the save; it
now looks for the new one.

## Testing

- `tests/test_menu_env_overrides.py`, new Linux cases:
- A save that cannot write a byte (`RLIMIT_FSIZE` set to 0): the file is
unchanged, no `.new` is left, one line is logged, and the next save
writes. This fails on main (the file is emptied) and passes here.
- The options file as a link, first to a missing file, then to an
existing one: it stays a link and its target holds the save.
- Full suite on Linux: 473 passed, 53 skipped, 2 failed, both the same
on main on this machine (`test_bake_portability`: `zstandard` is not
installed; `nx2/test_texture_init_barrier.py`: `clang++` is not
installed).
- The Linux renderer builds with the change.
- Not built or tested on Windows.

Restored-from: https://github.com/NotRexed/Hookshot/pull/2966
On Linux, `waveOutClose` (`host/posix/win32_posix.c`) only emptied the
queue: the SDL device that `hos_sdl_audio_open` (`host/hos_sdl.c`)
opened was never closed, and its id was dropped, so nothing could close
it. The game opens the output again when it takes no sound for 1.5 s
(`output_reopen`) and when the default device changes (`output_move`).
The first device was still open then, and where the default output takes
one open at a time the new open was refused and the sound stayed off for
the rest of the run. Elsewhere each reopen left another device and its
callback running.

## Change

- `hos_sdl_audio_open` returns what it opened (NULL when it did not
open) instead of 1 or 0, and the new `hos_sdl_audio_close` closes it:
SDL2's device (`SDL_CloseAudioDevice`), or SDL3's stream with its device
(`SDL_DestroyAudioStream`). The pull callback is not called once it
returns. An SDL3 stream that opened but did not resume is destroyed too.
- `waveOutClose` closes that device first, then empties the queue and
frees the rest.
- Windows plays through WinMM and is unchanged.

## Testing

- New `tests/test_sdl_audio_reopen.py` (Linux): builds the real
`waveOut` calls and SDL adapter over the system's SDL with
`SDL_AUDIODRIVER=dummy`, then opens the output, plays a block and closes
it, four times. On main the second open is refused; here all four open
and take their block. It ran against SDL2 2.32 (no SDL3 is installed on
this machine). It skips where neither library is installed.
- `tests/test_sdl_audio_library.py` follows the new return type.
- Full suite on Linux: 474 passed, 53 skipped, 2 failed, both the same
on main on this machine (`test_bake_portability`: `zstandard` is not
installed; `nx2/test_texture_init_barrier.py`: `clang++` is not
installed).
- No real sound device or game was run, and the SDL3 path was not run.

Restored-from: https://github.com/NotRexed/Hookshot/pull/2970
On a Windows whose system messages are not plain ASCII (French, for
one), `tests/test_bug_report.py` fails on `main` before any change: when
a report folder cannot be made, its driver gets Windows' reason in the
system's code page, not UTF-8. The game itself already gets UTF-8 there,
through its manifest; this makes the reason UTF-8 without it too.

Fixes https://github.com/NotRexed/Hookshot/issues/2954

**Cause.** `make_dir` (gpu/vk/vk_bug_report.cpp) read the reason with
`FormatMessageA`, which answers in the process's ANSI code page.
`hookshot.exe`'s manifest sets that code page to UTF-8
(`nx2/win_manifest.py`, Windows 10 1903 and newer), so the game's log
and menu got UTF-8. The test's driver is the same source built without
the manifest, and got code page 1252 on a French Windows. So would the
game on a Windows older than 1903. (https://github.com/NotRexed/Hookshot/pull/2954 first said the game was
affected on any such Windows; [its
comment](https://github.com/NotRexed/Hookshot/issues/2954#issuecomment-6838019529)
corrects that, and its score, to 0.01.)

**Fix.** `make_dir` asks for the wide text (`FormatMessageW`) and turns
it into UTF-8 itself (`WideCharToMultiByte(CP_UTF8)`), so the reason
does not depend on the manifest. The error number and the trimming are
unchanged, and with the manifest, or in English, the bytes are what they
were. Linking the manifest into the test's driver would also turn the
test green, but it would leave `make_dir` depending on the manifest,
which a Windows older than 1903 does not read.

The line of the test's step 7 on a French Windows 11 (code page 1252),
`... (Windows error 3: Le chemin d’accès spécifié est introuvable.)`,
bytes of `d’accès`:

| | without the manifest (code page 1252) | linked with the game's
manifest (code page 65001) |
|---|---|---|
| before (`FormatMessageA`) | `64 92 61 63 63 e8 73` | `64 e2 80 99 61
63 63 c3 a8 73` |
| after (`FormatMessageW`, then `WideCharToMultiByte(CP_UTF8)`) | `64 e2
80 99 61 63 63 c3 a8 73` | `64 e2 80 99 61 63 63 c3 a8 73` |

The left column is also what the test's driver says before and after.
The right column is from a small program that makes the same calls for
error 3, built with the Builder's llvm-mingw and linked with the `.res`
that `nx2/win_manifest.py` writes; the game was not built or run for it.

**Test.** `tests/test_bug_report.py`:

- `run()` reads the driver's output as bytes and decodes it as UTF-8
itself, so a line in another encoding is named in the failure instead of
ending in `subprocess`'s reader thread (`UnicodeDecodeError`, then
`'NoneType' object has no attribute 'splitlines'`).
- `wiring()` holds `make_dir` to `FormatMessageW` and
`WideCharToMultiByte(CP_UTF8, ...)`, with no `FormatMessageA`. CI's
Windows is in English, where step 7's message is ASCII either way, so
this source check is what fails there without the fix.

Without the fix, on this PC: `bug report: make_dir's reason on Windows
is not Windows' wide text turned into UTF-8`, and with that check taken
out, `bug report: the driver (plain) said a line that is not UTF-8:
b'LOG menu: bug report not written: ... (Windows error 3: Le chemin
d\x92acc\xe8s sp\xe9cifi\xe9 est introuvable.)'`.

`python -I -B tests/run_all.py` on this commit, on Windows 11 in French:
484 passed, 1 failed, 43 skipped. `main` (e0e60920) gives 483 passed, 2
failed there: the one that passes now is `tests/test_bug_report.py`, and
the one still failing, on both, is `nx2/test_nup_paths.py`, which makes
symbolic links that this account is not allowed to make (WinError 1314);
this change does not touch it.

**Not tested in the game, and nothing to see there on a current
Windows.** With the manifest the bytes are the same before and after
(the table's right column, measured with the small program, not in the
game), so a run on Windows 10 1903 or newer, this PC included, would
show the same text either way. The difference would show only on a
Windows 10 older than 1903 in a language other than English, with a
`bug-reports` folder that cannot be made; I have no such PC. (The
control bridge, which my unattended runs go through, cannot press the
Bug report row either: it sets rows that have a value, and this row is
an action.)

**Overlap.** https://github.com/NotRexed/Hookshot/pull/2948 changes `bug_report::write` in the same source file
and adds a step after step 7 in the same test. The two do not touch the
same lines, and this branch merges with https://github.com/NotRexed/Hookshot/pull/2948's head as it is now
without a conflict (`git merge-tree`).

`docs/worklist.md`: one row, score 0.01 (S3 x 0.01 x 1.0).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5.2 <[email protected]>
Restored-from: https://github.com/NotRexed/Hookshot/pull/2978
When the GPU driver stops answering during the build's shader
preparation (`prepare_watchdog`, exit status 4), the builder printed
"the shaders were not prepared now (the GPU driver stopped responding):
the game prepares them at its first start". The game does not do that.
The stopped run leaves `hookshot_prepare_skipped` beside the game, and
every start skips the preparation while it is there, so each shader is
built when it is first drawn, until a later build's preparation runs
through and removes the file.

## Change

- `nx2/make_game.py`: `not_prepared` takes the ending as an argument,
defaulting to the old one (`FIRST_START`). Exit 4 says "the game builds
each shader when it first draws it, and the next build or update tries
the preparation again". The other reasons keep their text, and the
marker's behaviour stays as it is.
- The three builders read the ending of the line instead of assuming it:
- Windows builder (`ShaderStep.Note`): the row says "Left to the game's
first start: why" as before, or "Not prepared: why. Then what happens".
- Python builder (`window.py`) and Linux builder
(`hookshot_builder.py`): the same, in each one's own wording.
- `tests/builder_workers/Program.cs` follows the rename.

## Testing

- `tests/test_prepare_at_build.py`, new cases:
- make_game: exit 4's line does not promise the first start and names
first drawn. Exit 3, a silent run and another status keep "at its first
start".
  - Both Python builders: the row text for each kind of line.
  - The new cases fail on main and pass here.
- Full suite on Linux: 473 passed, 53 skipped, 2 failed, both the same
on main on this machine (`test_bake_portability`: `zstandard` is not
installed; `nx2/test_texture_init_barrier.py`: `clang++` is not
installed).
- The C# builder and `tests/builder_workers` were not compiled: there is
no .NET SDK on this machine.
- No game ran.

Ported to Longshot: make_game.py's line and its check only. Longshot's builders are command line only (#171, #181), so
the builder windows' rows and their checks are left out.

Restored-from: https://github.com/NotRexed/Hookshot/pull/2985
Since https://github.com/NotRexed/Hookshot/pull/1846 a DualShock 4's or DualSense's touchpad click opens and
closes the Insert menu as F1 does, and it can be nothing else:
`HOS_PAD_TOUCHPAD` was its own bit past `HOS_PAD_N`, so the Controls
page cannot take it and a listening binding masks it out. https://github.com/NotRexed/Hookshot/pull/2212 asks for
it as a remappable input (the map, Minus, is the natural place for it on
a PlayStation pad). https://github.com/NotRexed/Hookshot/pull/2979 may be the same request. Home, and Select with
Start, already open the menu on these pads.

## Change

- `gpu/vk/vk_api.h`: `HOS_PAD_TOUCHPAD` is the gamepad input after the
paddles, before `HOS_PAD_N`. The saved values of the other inputs do not
move. The executable and the renderer must agree on its bit, so
`MKWVK_API_VERSION` goes to 22, as it did when the paddles were added.
- `host/nn_hid.c` is unchanged: `pad_code`, `hos_bind` and
`bound_pad_buttons` take the touchpad as they take the paddles. The
options file keeps it, and the game gets the row it is bound to.
- `gpu/vk/vk_menu.cpp`:
  - `pad_name` calls it "Touchpad".
  - A listening binding no longer masks it.
- A click presses F1 only while no row of the Controls page has the
touchpad as Input 1 or Input 2. The new `touchpad_bound` asks the host's
bindings for every row that takes a gamepad input, the ocarina notes
included.
- Defaults do not change: unbound, and after Reset to defaults, a click
opens the menu as now.
- Prompts: the touchpad has no prompt artwork. Like the paddles, it
falls past `HOS_PAD_ART_N` and shows the bound Switch button's default
icon.
- `readme.md`: a bound touchpad does its binding instead of opening the
menu.

## Testing

- `tests/test_touchpad_menu.py`:
- The poll's button block still gives the touchpad its own bit, now the
input after the paddles.
- The menu's Controls table, `touchpad_bound`, `pad_name`, the F1 line
and the listening binding's mask are cut from `vk_menu.cpp` and run with
a stand-in host. Unbound, a click is F1. Bound to Minus's Input 2, or to
an ocarina note, it is not. A listening binding takes the click as its
input, and its name is "Touchpad".
- `tests/test_bound_keys.py`: an options file with the touchpad as
Minus's input reads back, and a click gives the game Minus through
`bound_pad_buttons`.
- Both fail on main and pass here. The new C++ fixture also compiles
with llvm-mingw for Windows.
- Linux renderer built (`hookshot_vk.so`).
- Full suite on Linux: 473 passed, 53 skipped, 2 failed, both the same
on main on this machine (`test_bake_portability`: `zstandard` is not
installed; `nx2/test_texture_init_barrier.py`: `clang++` is not
installed).
- No DualShock 4 or DualSense ran it, and no game ran.

Restored-from: https://github.com/NotRexed/Hookshot/pull/2989
## Why

Add the scarecrow dialogue as an adult to covered kinds so the ocarina
button re-mappings work during this dialogue. Notes fell back to default
ocarina controls per the safety net in https://github.com/NotRexed/Hookshot/pull/1930.

## What

One line: kind 11 (the scarecrow recalling/playing the recorded song at
him) joins the note-button kinds.

Restored-from: https://github.com/NotRexed/Hookshot/pull/3018
Developers only. `nx2/lift.py --write` run again over a gen folder,
after a saved boundary in chunks.txt moved to where the earlier lift had
cut a big chunk, kept that lift's split parts (`code_NNN_M.c`) beside
the new numbered chunk holding the same functions, and the link failed
on duplicate definitions.

`chunks.write` removed an earlier lift's stale numbered chunks and hot
pieces, but left its split parts for `split_big`. After the boundary
move the new chunk is the old first part byte for byte, so
`_cut_already` (meant for a worker that finished a cut whose result was
lost) found the old parts consistent with it and kept them as this
lift's. This needs the old first part to call none of the functions that
moved out: with such a call its declarations differ from the new
chunk's, and the old parts are rejected and removed, as before. The
packaged builder clears `code_*` before a translation and is not
affected.

## Change

`write()` removes an earlier lift's split parts together with its hot
pieces, before `split_big`, so `_cut_already` only sees parts made in
this lift. `split_big` is called only from `write()`, and nothing
between the removal and `split_big` reads the parts.

## Testing

- `tests/test_chunk_split.py`, new case: two lifts through
`chunks.write` with stand-in functions, the first with one chunk that
`split_big` cuts, the second with a saved boundary at the first part's
end. Each function is defined once and none of the first lift's parts is
left. On main it fails with 333 of the 400 functions defined twice.
- `run_all.py chunk hot spawn lift`: 11 passed.
- Full suite on Linux: 2 failures, each for a tool this machine lacks
(`test_bake_portability`: `zstandard`;
`nx2/test_texture_init_barrier.py`: `clang++`); both fail the same on
main.

Restored-from: https://github.com/NotRexed/Hookshot/pull/2953
In Hookshot Builder, a game folder that was built from a supported
`.nup` can be rebuilt from an extracted dump. When that dump is of
another game version, the build is refused ("This is build X. Hookshot
supports build Y"), which is right, but `nx2/make_game.py` checked the
version only after the dump's modules had replaced the folder's saved
copy (`.hookshot_exefs`). The stamp of the earlier `.nup` stayed.
Choosing that `.nup` again then said "reading it again is skipped",
built from the refused modules and was refused too, until the folder was
deleted.

## Change

In the extracted-dump branch of `main()`:

- `check_supported_build` runs right after the dump's modules are
unpacked, before the romfs is linked in and before `.hookshot_exefs` is
touched. A refused dump leaves the folder as it was.
- An accepted dump removes the `.nup` stamp (`.hookshot_romfs`), since
the saved copies are no longer that `.nup`'s. The next build from the
`.nup` reads it again.

The `.nup` branch already checks the build before it replaces anything;
it is unchanged.

## Testing

- `tests/test_make_game_cache.py`, two new cases (the cache test's fake
`.nup` and modules, no game):
- A refused extracted dump: the folder's files are byte for byte as
before, and the earlier `.nup` then reuses its copy and reaches the lift
with the supported build.
- An accepted extracted dump: its modules are saved, the `.nup` stamp is
gone, and the earlier `.nup` is read again.
  - Both fail on main and pass here.
- Full suite on Linux: 473 passed, 53 skipped, 2 failed, both the same
on main on this machine (`test_bake_portability`: `zstandard` is not
installed; `nx2/test_texture_init_barrier.py`: `clang++` is not
installed).
- No real game build was run.

Restored-from: https://github.com/NotRexed/Hookshot/pull/2968
Fixes https://github.com/NotRexed/Hookshot/issues/2182 (one of the reports collected in https://github.com/NotRexed/Hookshot/pull/2503).

## What was wrong

With Audio output on **Auto**, Hookshot told the game 5.1 whenever the
default output device had 6 or more *channels*. An audio interface
reports all of its outputs as channels without naming them as speakers.
A Focusrite Scarlett 18i8, for example, has 8 channels and a channel
mask of `0`. The game then mixed for 5.1, and everything it puts in the
centre or behind the player went to outputs with nothing connected. That
includes voices (Saria in the ocarina cutscene), the ocarina and Link's
own sounds. Those sounds were silent while the rest of the game played
normally.

The readme already says Auto follows the device's *speakers*. The code
counted channels.

## The change

- `host/nn_audio.c`: `au_device_channels` now reads the mix format's
channel mask through a new `au_layout_channels`. A device counts as 5.1
only when its mask names a centre and a pair of speakers behind or
beside the player (5.1 back, 5.1 side, 7.1). Everything else is stereo:
mask `0`, no mask, quad, or 5.1 without a centre. The log says why, for
example: `audio: the default output has 8 channels but not 5.1 speakers
(channel mask 0): counted as stereo`. As stereo, the game's own downmix
keeps every sound.
- The Stereo and 5.1 menu choices and `HOS_AUDIO_CHANNELS` work as
before. Real 5.1 and 7.1 systems still get 5.1.
- Linux is unchanged: SDL2 reports only a channel count, not speaker
positions.
- `readme.md`: one sentence on what Auto counts.

## Testing

- New game-free check, `tests/test_audio_speaker_layout.py`. It compiles
the real `au_layout_channels` and tests it against 13 mix formats
(stereo, mono, 5.1 back and side, 7.1, 7.1 wide, interfaces with mask 0,
quad, 5.1 without a centre, formats with no mask). It also checks that
`au_device_channels` goes through the new function. It passes locally
with MSVC; `run_all.py` passes `{cxx}` on CI.
- The changed `au_device_channels` was compiled on its own and run on
the reporting PC (Windows 11, Scarlett 18i8 as the default output, RTX
3080). It reads the live mix format (8 channels, mask `0`) and returns
2.
- Not tested yet: an in-game run on v2.1.0 with this build. Before this
change, choosing **Stereo** in the menu works around the problem.

Co-authored-by: Claude Opus 5.5 <[email protected]>
Restored-from: https://github.com/NotRexed/Hookshot/pull/3044
When the Linux renderer fails to load, the log says only `(2)`. That
covers a missing file and a `hookshot_vk.so` from another build alike,
though the second is the one that needs explaining: it takes symbols
from the executable, so it fails at `dlopen`, before `mkwvk_init`'s
version check, and the game quietly falls back to WebGPU.

## Change

- **`host/posix/win32_posix.c`:** `LoadLibraryA` keeps `dlerror()`'s
text for the last failure, per thread, and `hos_load_error()` returns it
(`""` after a success). `GetLastError()` is still
`ERROR_FILE_NOT_FOUND`, as before.
- **`host/rx_backend.c`:** `load_vulkan`'s failure line prints that text
on Linux:

  ```
rx: RX_BACKEND=vulkan, but .../hookshot_vk.so did not load
(.../hookshot_vk.so: undefined symbol: mkwvk_...): WebGPU
  ```

Windows keeps the error number, which already tells a missing file (126)
from a missing function (127).

Part of the dev-loop proposal's B1 (https://github.com/NotRexed/Hookshot/pull/2636): a renderer swap during
development needs this line to say why the new one was refused.

## Testing

- **`tests/test_renderer_load_error.py` (new):**
- everywhere: the source keeps the text and logs it (it fails on
`main`);
- on Linux: builds `win32_posix.c` as it is and loads, through
`LoadLibraryA`, a `.dll` name with no library behind it, a library that
needs a symbol nothing has, then a good one.
- **The Linux part, run by hand on WSL Ubuntu (gcc), as the check does
it:**

  ```
failed|2|<tmp>/absent_vk.so: cannot open shared object file: No such
file or directory
  failed|2|<tmp>/needs.so: undefined symbol: mkwvk_from_another_build
  loaded|0|
  ```

- **Every check that reads the changed files (37), through `run_all.py`,
rebased on today's `main`, on Windows:** 34 pass;
`nx2/test_bc_storage_views.py`, `nx2/test_dll.py` and `nx2/test_leaf.py`
fail the same way on `main` here. The new check's Linux part is skipped
on Windows.
- **Not run:** the game on Linux. The change is the failure path's log
line.

See https://github.com/NotRexed/Hookshot/issues/2636 (B1).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
Co-authored-by: NotRexed <[email protected]>
Restored-from: https://github.com/NotRexed/Hookshot/pull/2875
With "Controller in background" on (or `HOS_INPUT_BACKGROUND=1`),
`read_input`'s branch for a window behind another merged the pads
straight into the game's input. The Insert menu's hold, which the branch
for the window in front applies (the pads are the menu's while it is
open, and after it closed until they are let go), was missing there. A
button pressed with the menu open behind another window, or still held
after it closed, reached the game.

## Change

The hold, the read and the merge are one helper, `pads_merge`, used by
both branches. The pads are still read while held back, so the letting
go is seen. `player_used()` is still called only in front, as before,
and the keyboard and mouse stay out behind other windows.

## Testing

- `tests/test_input_background.py` and its fixture: a new case behind
another window with the pads allowed there. With the menu open, B and
the sticks do not reach the game, though the pads are read; with the
menu closed and B still held, nothing; after B is let go and pressed
again, B and the sticks arrive. On main the first frame already carries
B. The other 17 cases pass as before.
- Full suite on Linux: 2 failures, each for a tool this machine lacks
(`test_bake_portability`: `zstandard`;
`nx2/test_texture_init_barrier.py`: `clang++`); both fail the same on
main.
- Not tested in the game.

Co-authored-by: NotRexed <[email protected]>
Restored-from: https://github.com/NotRexed/Hookshot/pull/2963
save() logs a failed write and checks for a link with lstat since
https://github.com/NotRexed/Hookshot/pull/2966; the check from #55 cut it with
logf declared after it and without <sys/stat.h>.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
docs: the restored Hookshot references as full URLs
Some checks are pending
CI / Conventional Commit title (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Blocked by required conditions
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Blocked by required conditions
CI / Workflow lint (actionlint) (pull_request) Blocked by required conditions
Third-party parts / As their notes say (pull_request) Blocked by required conditions
Third-party parts / The same as upstream (pull_request) Blocked by required conditions
PR description / Description follows the template (pull_request_target) Successful in 5s
986db4c5e7
A bare #NNNN in Longshot's tree links to Longshot's own number. The restored
text's Hookshot pull requests and issues are written as
https://github.com/NotRexed/Hookshot/pull/NNNN (or /issues/NNNN).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Some checks are pending
CI / Conventional Commit title (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Blocked by required conditions
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Blocked by required conditions
CI / Workflow lint (actionlint) (pull_request) Blocked by required conditions
Third-party parts / As their notes say (pull_request) Blocked by required conditions
Third-party parts / The same as upstream (pull_request) Blocked by required conditions
PR description / Description follows the template (pull_request_target) Successful in 5s
This pull request is marked as a work in progress.
Some workflows are waiting to be reviewed.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u 'restore/rc4-player-fixes:TrashPanda-restore/rc4-player-fixes'
git switch 'TrashPanda-restore/rc4-player-fixes'
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
MagicBeanDev/Longshot!286
No description provided.