feat(input): add a second gamepad binding in Controls #1990

Merged
RoksTK merged 1 commit from feat/gamepad-alternate-bindings into main 2026-11-11 22:00:39 +00:00
RoksTK commented 2026-11-10 22:52:10 +00:00 (Migrated from github.com)

I wanted to roll with either the right face button or R4 on my Steam Controller. Hookshot currently lets you assign one gamepad button per action.

This adds Input 1 and Input 2 under Gamepad in the F1 Controls menu. Either input can trigger the action. Existing bindings load into Input 1, and Input 2 starts empty. The menu can also capture rear paddles such as R4 on supported controllers.

Testing

  • On Windows with a Steam Controller, the right face button and R4 both worked for Roll in the v2.0.14 test build and the test build with the newer Controls menu. Both builds saved the bindings.
  • Rebased on upstream main at bebb0c6 (review round 1). The binding, prompt font, menu click, and touchpad tests pass.
  • The full Windows suite had 319 passes, 21 skips, and five failures. All five failures also reproduced on unmodified main with the same tools.

Note

gpu/vk/vk_api.h is now a host dependency on Windows too (it was Linux-only), so a change to it also rebuilds the host. The host reads HOS_PAD_N from it.

Screenshots

Current Controls layout at 1280 × 720

The updated Controls menu showing B in Input 1 and R4 in Input 2

Steam Controller test on v2.0.14

The Steam Controller test showing B and R4 assigned to Roll

AI disclosure

OpenAI Codex assisted with implementation and automated testing. Claude (Anthropic) assisted with the review changes.

I wanted to roll with either the right face button or R4 on my Steam Controller. Hookshot currently lets you assign one gamepad button per action. This adds **Input 1** and **Input 2** under **Gamepad** in the F1 Controls menu. Either input can trigger the action. Existing bindings load into Input 1, and Input 2 starts empty. The menu can also capture rear paddles such as R4 on supported controllers. ## Testing - On Windows with a Steam Controller, the right face button and R4 both worked for Roll in the v2.0.14 test build and the test build with the newer Controls menu. Both builds saved the bindings. - Rebased on upstream `main` at `bebb0c6` (review round 1). The binding, prompt font, menu click, and touchpad tests pass. - The full Windows suite had 319 passes, 21 skips, and five failures. All five failures also reproduced on unmodified `main` with the same tools. ## Note `gpu/vk/vk_api.h` is now a host dependency on Windows too (it was Linux-only), so a change to it also rebuilds the host. The host reads `HOS_PAD_N` from it. ## Screenshots Current Controls layout at 1280 × 720 ![The updated Controls menu showing B in Input 1 and R4 in Input 2](https://raw.githubusercontent.com/RoksTK/Hookshot/041ec2dd07399f3eccc2b5276cb7f8bcb55e1c2d/pr-images/gamepad-alternate-bindings/controls-main.png) Steam Controller test on v2.0.14 ![The Steam Controller test showing B and R4 assigned to Roll](https://raw.githubusercontent.com/RoksTK/Hookshot/041ec2dd07399f3eccc2b5276cb7f8bcb55e1c2d/pr-images/gamepad-alternate-bindings/controls-steam-controller.png) ## AI disclosure OpenAI Codex assisted with implementation and automated testing. Claude (Anthropic) assisted with the review changes.
Joey-Magnus (Migrated from github.com) requested changes 2026-11-11 20:06:23 +00:00
Joey-Magnus (Migrated from github.com) left a comment

Nice feature, and the core is solid. The binding table moved from Interlocked* to an SRW lock, and that works on Linux through the host/posix/windows.h shim. The ini format stays readable both ways: an older build's atoi("1,17") still reads slot 0. The paddles keep SDL3's order (right/left paddle 1, then 2), so R4/L4/R5/L5 are the right names. The tests are extended rather than weakened (test_bound_keys, test_menu_hit, test_touchpad_menu). The one red CI job (test_release_scripts on Ubuntu) is unrelated to this change.

The problem is main. This branch is 69 commits behind and conflicts in gpu/vk/vk_api.h, host/prompts.c and tests/test_bound_keys.py. One of those conflicts needs more than picking a side:

blocking, gpu/vk/vk_api.h:47 (HOS_PAD_N 17 → 21) against main's host/prompts.c: since #2366, the live button prompts give each Switch button × pad input a code point of its own, at BIND_BASE + (face * HOS_SW_N + b) * HOS_PAD_N + pad (text_code on main). nx2/prompt_assets.py builds the font with a stride of len(PADS) = 17. Once this PR raises HOS_PAD_N, the game points texts at code points the font doesn't have at those places. I merged this branch into main locally, kept main's prompts.c, and set the API version to 18: tests/test_prompt_font_bind.py then fails with face 0: text code U+E041 became 61471, not 61467. When you rebase, keep the prompts' stride at the 17 inputs that have artwork. For example, give it its own constant (HOS_PAD_RTRIGGER + 1) in text_code and anywhere else prompts.c uses HOS_PAD_N for code points. Also make a paddle bound in slot 0 fall back to switch_pad[b], as you did in cmap4_bind (host/prompts.c:124 here). The alternative is to add the paddles to PADS in prompt_assets.py and draw artwork for them, but that's a bigger change.

blocking, gpu/vk/vk_api.h:18: main is at MKWVK_API_VERSION 17 now, so this needs to become 18, not 15.

question, host/nn_hid.c:608-613 (hos_bind): the old swap kept every button bound ("none left without"). With a second slot, binding an input that is already some button's only input into an empty Input 2 moves it there, because old is -1. That button is then left with no gamepad input. For example, put Y's face button into A's Input 2, and Y becomes None. The menu does show it, so this may be what you want, but it's easy to do by accident on the face buttons. Should a slot-1 bind skip inputs that are another button's slot 0, or show a short note in the help box when it takes one?

minor, nx2/build.py:843: making vk_api.h a host dependency on Windows too is right, now that the host reads HOS_PAD_N. It's worth one sentence in the description, since it changes rebuild behaviour beyond this feature.

Nice feature, and the core is solid. The binding table moved from `Interlocked*` to an SRW lock, and that works on Linux through the `host/posix/windows.h` shim. The ini format stays readable both ways: an older build's `atoi("1,17")` still reads slot 0. The paddles keep SDL3's order (right/left paddle 1, then 2), so R4/L4/R5/L5 are the right names. The tests are extended rather than weakened (`test_bound_keys`, `test_menu_hit`, `test_touchpad_menu`). The one red CI job (`test_release_scripts` on Ubuntu) is unrelated to this change. The problem is main. This branch is 69 commits behind and conflicts in `gpu/vk/vk_api.h`, `host/prompts.c` and `tests/test_bound_keys.py`. One of those conflicts needs more than picking a side: **blocking**, `gpu/vk/vk_api.h:47` (`HOS_PAD_N` 17 → 21) against main's `host/prompts.c`: since #2366, the live button prompts give each Switch button × pad input a code point of its own, at `BIND_BASE + (face * HOS_SW_N + b) * HOS_PAD_N + pad` (`text_code` on main). `nx2/prompt_assets.py` builds the font with a stride of `len(PADS)` = 17. Once this PR raises `HOS_PAD_N`, the game points texts at code points the font doesn't have at those places. I merged this branch into main locally, kept main's `prompts.c`, and set the API version to 18: `tests/test_prompt_font_bind.py` then fails with `face 0: text code U+E041 became 61471, not 61467`. When you rebase, keep the prompts' stride at the 17 inputs that have artwork. For example, give it its own constant (`HOS_PAD_RTRIGGER + 1`) in `text_code` and anywhere else `prompts.c` uses `HOS_PAD_N` for code points. Also make a paddle bound in slot 0 fall back to `switch_pad[b]`, as you did in `cmap4_bind` (`host/prompts.c:124` here). The alternative is to add the paddles to `PADS` in `prompt_assets.py` and draw artwork for them, but that's a bigger change. **blocking**, `gpu/vk/vk_api.h:18`: main is at `MKWVK_API_VERSION 17` now, so this needs to become 18, not 15. **question**, `host/nn_hid.c:608-613` (`hos_bind`): the old swap kept every button bound ("none left without"). With a second slot, binding an input that is already some button's only input into an empty Input 2 moves it there, because `old` is -1. That button is then left with no gamepad input. For example, put Y's face button into A's Input 2, and Y becomes None. The menu does show it, so this may be what you want, but it's easy to do by accident on the face buttons. Should a slot-1 bind skip inputs that are another button's slot 0, or show a short note in the help box when it takes one? **minor**, `nx2/build.py:843`: making `vk_api.h` a host dependency on Windows too is right, now that the host reads `HOS_PAD_N`. It's worth one sentence in the description, since it changes rebuild behaviour beyond this feature.
RoksTK commented 2026-11-11 20:39:13 +00:00 (Migrated from github.com)

Thanks for the review! All points are addressed in 49371a5.

To your question about hos_bind: good catch, I went with blocking it. A slot-1 bind no longer takes an input that is another button's only gamepad input, so binding Y's face button into A's empty Input 2 does nothing and Y keeps it. If the other button also has an Input 2, the input is taken and that button's Input 2 moves up to Input 1, so no button is ever left unbound. The Controls help box mentions this in one sentence, and test_bound_keys.py covers both cases.

The rest:

  • Rebased on main; MKWVK_API_VERSION → 18.
  • Prompt code points keep the 17-input stride via a new HOS_PAD_ART_N (= HOS_PAD_RTRIGGER + 1, asserted against PADS in test_prompt_font_bind.py). A paddle bound to Input 1 falls back to switch_pad[b].
  • The description now notes the vk_api.h host-dependency change on Windows.
Thanks for the review! All points are addressed in 49371a5. **To your question about `hos_bind`:** good catch, I went with blocking it. A slot-1 bind no longer takes an input that is another button's only gamepad input, so binding Y's face button into A's empty Input 2 does nothing and Y keeps it. If the other button also has an Input 2, the input is taken and that button's Input 2 moves up to Input 1, so no button is ever left unbound. The Controls help box mentions this in one sentence, and `test_bound_keys.py` covers both cases. **The rest:** - Rebased on main; `MKWVK_API_VERSION` → 18. - Prompt code points keep the 17-input stride via a new `HOS_PAD_ART_N` (= `HOS_PAD_RTRIGGER + 1`, asserted against `PADS` in `test_prompt_font_bind.py`). A paddle bound to Input 1 falls back to `switch_pad[b]`. - The description now notes the `vk_api.h` host-dependency change on Windows.
Joey-Magnus (Migrated from github.com) approved these changes 2026-11-11 21:46:38 +00:00
Joey-Magnus (Migrated from github.com) left a comment

Everything from my earlier review is fixed in 49371a5. MKWVK_API_VERSION is 18. The prompt code points use the 17-input stride through HOS_PAD_ART_N in text_code and cmap4_bind, with paddles falling back to switch_pad[b]. A slot-1 bind no longer takes another button's only input. I merged it into today's main: no conflict, and run_all gives 376 passed, 0 failed. test_bound_keys, test_prompt_font_bind, test_touchpad_menu and test_menu_hit all pass on their own too.

There's a collision with #2435 (Keyboard prompt face) for whichever PR lands second. #2435 still indexes prompt code points by HOS_PAD_N in text_code (BIND_BASE + (face * HOS_SW_N + b) * HOS_PAD_N + pad). Once this PR makes HOS_PAD_N 21, that has to become HOS_PAD_ART_N or the font lookups shift. #2435 also adds a HOS_PROMPT_KEYBOARD row, while pad_name here is sized [HOS_PROMPT_N + 1][HOS_PAD_RPADDLE1]. The keyboard row would then be all null pointers, so it has to keep #2435's face < HOS_PROMPT_KEYBOARD guard. CI still needs approving for this fork.

Everything from my earlier review is fixed in 49371a5. `MKWVK_API_VERSION` is 18. The prompt code points use the 17-input stride through `HOS_PAD_ART_N` in `text_code` and `cmap4_bind`, with paddles falling back to `switch_pad[b]`. A slot-1 bind no longer takes another button's only input. I merged it into today's main: no conflict, and run_all gives 376 passed, 0 failed. `test_bound_keys`, `test_prompt_font_bind`, `test_touchpad_menu` and `test_menu_hit` all pass on their own too. There's a collision with #2435 (Keyboard prompt face) for whichever PR lands second. #2435 still indexes prompt code points by `HOS_PAD_N` in `text_code` (`BIND_BASE + (face * HOS_SW_N + b) * HOS_PAD_N + pad`). Once this PR makes `HOS_PAD_N` 21, that has to become `HOS_PAD_ART_N` or the font lookups shift. #2435 also adds a `HOS_PROMPT_KEYBOARD` row, while `pad_name` here is sized `[HOS_PROMPT_N + 1][HOS_PAD_RPADDLE1]`. The keyboard row would then be all null pointers, so it has to keep #2435's `face < HOS_PROMPT_KEYBOARD` guard. CI still needs approving for this fork.
Commenting is not possible because the repository is archived.
No description provided.