Skip to content

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

Merged
hckrman101 merged 1 commit into
NotRexed:mainfrom
RoksTK:feat/gamepad-alternate-bindings
Nov 11, 2026
Merged

hckrman101 merged 1 commit into
NotRexed:mainfrom
RoksTK:feat/gamepad-alternate-bindings

Conversation

@RoksTK

@RoksTK RoksTK commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

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.

@RoksTK
RoksTK force-pushed the feat/gamepad-alternate-bindings branch from 28b50c0 to 9c08d4d Compare November 11, 2026 03:58
@RoksTK RoksTK changed the title Add a second gamepad binding in the Controls menu feat(input): add a second gamepad binding in Controls Nov 11, 2026

@Joey-Magnus Joey-Magnus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Give each gamepad action two slots in the Controls menu. Read SDL
paddles and accept either binding in play. Load old settings with an
empty alternate slot and keep the two bindings together when saving.

Update the shared input interface version and include its header in
Windows host cache keys so incremental builds rebuild both sides.

Exercise the second binding and its mouse hit target in the existing
input and menu regression tests.

Assisted-by: OpenAI Codex
Co-Authored-By: Codex <codex@openai.com>

Review (Joey-Magnus): rebased on main; MKWVK_API_VERSION 18 (main is at 17); the prompt font's code points keep their
stride of the 17 inputs with artwork (HOS_PAD_ART_N, nx2/prompt_assets.py PADS), a paddle bound to Input 1 shows its
Switch button's default icon; a button's only gamepad input is not moved into another's empty Input 2 (taken from a
button with an Input 2, that one moves up), with a note in the Controls help.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbciDXYLkJnK9V27XzPsus
@RoksTK
RoksTK force-pushed the feat/gamepad-alternate-bindings branch from 9c08d4d to 49371a5 Compare November 11, 2026 20:31
@RoksTK

RoksTK commented Nov 11, 2026

Copy link
Copy Markdown
Contributor Author

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 Joey-Magnus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@hckrman101
hckrman101 merged commit bf04924 into NotRexed:main Nov 11, 2026
LNDF added a commit to LNDF/Hookshot that referenced this pull request Nov 11, 2026
…ves in two places, API 19

- NotRexed#2282 (the Linux saves' move leaves a linked folder alone) is carried into
  host/hos_data.c: on Linux a link in the saves is made again in the data
  folder to the same folder (never renamed or copied through), a link to an
  entry that moves too follows it, and a user/ that is a link itself is used
  as it is. On Windows a link or junction keeps the saves where they are.
  tests/test_linux_saves_move.py runs its cases on hos_data.c.
- Saves in both <game>/user and the data folder (a downgrade, then the upgrade
  again): this folder's are used, nothing is merged, and
  saves-in-two-places.txt and the log say where both are and how to switch.
- MKWVK_API_VERSION 19: main's 18 is NotRexed#1990's pad inputs.
- Conflicts in nn_hid.c, docs/saves.md and docs/development.md: main's text
  with the data folder's places.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
NotRexed pushed a commit that referenced this pull request Nov 12, 2026
…texts, hints and menus (#2435)

Players on the keyboard and mouse see a controller's buttons in every
prompt (the Switch's when no controller is connected), so they have to
work out which key each one means. This adds a **Keyboard** face to the
Button prompts system: the game's texts, hints and menus show the key or
mouse button each action is bound to.

Part of #1007 (keyboard button prompts, from #2028).

## What it does

- **Button prompts → Keyboard** (a seventh value). **Auto** picks it
when no controller is connected, through SDL or XInput; with one
connected, Auto picks that controller's face as before.
- Each Switch button shows the first key bound to it on the Controls
page, or the second when the first slot is clear. Mouse buttons show as
a mouse with that button lit, and keys named by words (Ctrl, Alt, Esc,
Home, PgUp, Num1...) as wider keys set in the game's own bold font.
Kenney's versions of those are too small to read at prompt size.
- The sticks' icons (Move, Change View and their direction variants)
show **WASD** (or W/A/S/D, AD, WS) and the mouse moving.
- It follows the live system from #2366: changing a key, or switching to
or from Keyboard, shows at once, like a pad rebind.
- Kept as the Switch's: the textures (ocarina notes, the letters on
black, the pause menu's controller picture) and the Talk ring's places.
The ocarina hint is spaced by the keys' icon widths.

## How

- `nx2/prompt_keys.py` (new, data only, standard library): which icon
each key and stick shows, and the code points.
- `nx2/prompt_draw.py`: the keyboard face's glyphs. They are Kenney's
Input Prompts **Keyboard & Mouse** font (CC0, the same 1.5 release as
the controller faces), in its own folder
`third_party/kenney_input_keyboard` with a `PROVENANCE.md` and the
release's `License.txt`; the AppImage copies it. The word keys and the
mice are drawn as non-crossing contours, so they fill the same under
either winding rule.
- `nx2/prompt_assets.py`: `faces_font` builds the keyboard face (the
Switch's pad icons, plus each key's icon at `KEY_BASE + key code`, past
every face's code points), and writes its `hint.bin` per key code.
- `host/prompts.c`:
- The bind arrays carry each button's first key after its pad input
(`BINDS`), so key changes go through the same live path.
  - `cmap4_bind` points a button at its key's icon on the keyboard face.
- `text_code` gives a text a code of its own for each (button, key) pair
as it first appears (`key_slot`), and points it at the key's icon where
the game keeps the font.
- Code points: the per-pair codes come from 320 codes at U+EEC0–U+EFFF,
and the sticks' codes are at U+F8C0–U+F8DF. I checked every font in the
romfs: none uses either range. The CJK fonts use most of the private use
area, including parts of U+F700–U+F7BF.
- `host/nn_hid.c`: `hos_xinput_pads()`, so Auto doesn't pick Keyboard
for an XInput-only controller.
- `gpu/vk/vk_menu.cpp`: the Keyboard value and its hint; `pad_name`
names pad inputs by place on the keyboard face.

## Testing

**In game, a build of this branch (81a5445, the same code before the
rebases onto #2448 and #1990)** on Windows 11 (native, RTX 4060),
keyboard and mouse, no controller. Driven with the control bridge
(`HOS_CONTROL`, headless, `--user` on a copy of the saves), into Hyrule
Field from a save, then the pause menu:

| Case | Result |
|---|---|
| Auto, no controller | ✅ `prompts: auto: controller "(none)":
keyboard`, `keyboard icons, 16 buttons on these keys`. The pause menu
shows Q/R on the tabs, the mouse for Rotate, E for Select, Shift for
Back; the HUD's item buttons show R and X. |
| Keyboard picked, a key rebound (A from E to F) | ✅ Select reads F. |
| Xbox picked (a pad face, unchanged by this PR) | ✅ LB/RB, Select on B
and Back on A (the pad layout in use); the ocarina hint spread for
Xbox's icons. |
| A change while the game runs (Keyboard ↔ Xbox) | ✅ with #2583: on
native Windows #2366's live path never found the font (it read it 8
bytes past its start), with or without the keyboard face. @polackangry
found it and fixed it in #2583, and with that on top of this branch,
Keyboard → Xbox → Keyboard while the game runs applies at once (`now the
xbox icons in texts` / `now the keyboard icons in texts`), the pause
menu reopened showing each face's icons. Without #2583 the row says
"(restart)" on native Windows, as for every face. |
| A pad plugged in and out, a key rebound with a hint on screen | ⚠️ Not
tested: no controller here, and both go through the live path. |

Also:
- `tests/test_prompt_font_bind.py` is extended. On the keyboard face it
checks: keys bound in the first or second slot, a key with no icon, the
sticks, and text codes for each (button, key) pair. The other faces are
unchanged. It passes, as do `prompts_live`, `appimage_prompt_fonts` and
`menu_hooks`.
- `tests/run_all.py`: the same results with and without the PR on this
machine (its other failures are missing .NET and MSVC-target C++
headers).

## Screenshots

From the build of this branch, 1280×720 (the bridge's shots: the game's
own picture).

Auto with no controller, the pause menu: Q/R tabs, the mouse for Rotate,
E for Select, Shift for Back.

![Auto, no controller: the pause menu with keyboard
prompts](https://raw.githubusercontent.com/Needicla/Hookshot/pr-2435-screenshots/screenshots/branch-auto-pause.png)

Keyboard with A rebound to F: Select reads F.

![Keyboard face, A on F: Select reads
F](https://raw.githubusercontent.com/Needicla/Hookshot/pr-2435-screenshots/screenshots/branch-keyboard-rebound-pause.png)

Xbox, for comparison (a pad face, as before this PR).

![Xbox face: LB/RB, B and
A](https://raw.githubusercontent.com/Needicla/Hookshot/pr-2435-screenshots/screenshots/branch-xbox-pause.png)

The field: the HUD's item buttons on R and X.

![Hyrule Field: the item buttons on R and
X](https://raw.githubusercontent.com/Needicla/Hookshot/pr-2435-screenshots/screenshots/branch-field-hud.png)

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

---------

Co-authored-by: Needicla <226049938+Needicla@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants