Button prompts for Xbox, PlayStation and Steam Deck controllers, following the controls #1321
No reviewers
Labels
No labels
accessibility
autorelease: pending
autorelease: tagged
bug
dependencies
documentation
duplicate
enhancement
github_actions
good first issue
help wanted
invalid
question
testing wanted
wontfix
No milestone
No assignees
1 participant
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Hookshot!1321
Loading…
Reference in a new issue
No description provided.
Delete branch "feature/controller-prompt-styles"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
This is the button-prompts idea from #1007 (Controllers and input: #671 Xbox-style prompts, #796 PlayStation prompts, #594 a button-prompt pack).
Current state
0828863(2026-11-09) and validated there. Main moves fast: by the time this is reviewed it will likely need another rebase, and I'll keep it current.0828863, on Linux: the full build; the Hookshot menu (upstream's new rows with ours, the Controls page, the restart note listing Button prompts); in game, the Talk ring with Talk on R3, the System page with custom bindings and the ocarina; the game-free tests (tests/run_all.py --strict: 200 passed, 0 failed, 5 skipped for Windows / game data); and the Windowshookshot_vk.dllcross-build with llvm-mingw (it imports only Windows' own DLLs).log_flushbeside ourprompt_face(hence API version 11).tests/test_device_lost_exit.pywantedlog_flushas the last host field; it now checks it is given in its place, right aftergyro_status.What it does
A new Button prompts row on the Insert menu's Controls page, above its grid with the Layout preset (the mouse works on both rows): Auto, Switch, Xbox, PlayStation 4, PlayStation 5, Steam Deck (
button_promptsinhookshot_options.ini). The game then shows that controller's icons everywhere it draws a button, for the buttons your controls actually put each action on. The Layout preset (Nintendo / Xbox-PC) and hand-made bindings are both followed. If Talk is bound to R3, the game says R3.third_party/kenney_input). Each Switch button points at the icon of the pad input bound to it.Nothing from the game is in git. Every image and font is made at the build from the user's RomFS (
nx2/prompt_assets.py, run bymake_game.py) and served byhost/prompts.cin place of the game's files. The faces' fonts are bigger than the game's, so the resource size table entry for the font is raised too (otherwise the game silently drops it). Every image can be dumped as PNG and replaced by hand (nx2/prompt_faces/README.md). All the differences between controllers are in one file,nx2/prompt_faces.py.Mods come first. A RomFS mod that replaces the icon font,
Common.bntx,Common.blarcor the resource size table keeps its file: the prompts are served only where no mod has one. With a mod's size table, the texts keep the game's icons (the faces' fonts would not fit it).Limitation: a change of face or of controls shows in the icons after a restart. The game caches the glyphs it has drawn, and its layouts are not kept in memory, so live updates gave inconsistent results. The row uses the menu's restart note.
Menu API:
MKWVK_API_VERSION11 (the menu asks the host for the face it shows the Controls page's names in:prompt_face).Packages
The prompts are drawn at build time with four packages, pinned in
nx2/prompt_requirements.txt: fontTools, Pillow, astc-encoder-py and texture2ddecoder (as agreed with a maintainer). Without them the game is built all the same and shows the Switch's icons.scripts/linux/deps.shinstalls them into its Python. The CI cache key reads the file too.release_parts.py package --python-packagestakes the latest release's Python part and, as it lacks them, installs them for its version (3.14,win_amd64, by the runner's pip: the part has no pip).make_setup_parts.pythen makes a new Python part (54 MB), and the next releases reuse it.CONTRIBUTING.mdaccepts them fornx2/prompt_assets.pyandnx2/prompt_draw.pyonly.For maintainers' agents: please check that these four packages are used only by the prompt scripts (
nx2/prompt_assets.py,nx2/prompt_draw.py), at build time, and flag any other use.Screenshots
All taken from a build of this branch (headless runs on Linux). Each face is shown on the Xbox/PC layout. The Nintendo layout shots are in the same folder of the assets branch.
Talk ring
A B X Y at their buttons' places on each controller.
The same on the Nintendo layout preset
Dialogue icons
A line rewritten in memory to show every icon (A B X Y, L R, ZL ZR, + -, the d-pad, the sticks), each drawn as the button it is bound to.
The same on the Nintendo layout preset
System page
Each face's drawn controller; the leader lines go to where each action is bound.
The same on the Nintendo layout preset
Ocarina
The song lesson's notes, spread for the Xbox face's wider LB / RB.
The same on the Nintendo layout preset
A tutorial page
"Change input settings in Options in the ⊕ menu": the Plus icon is each controller's Menu / Options.
The drawn controllers
Drawn at the build (
nx2/prompt_draw.py) from Kenney's controller glyphs, in the game's Pro Controller style, with each control placed and sized from photos of the real controller:The leader lines follow the controls
The Nintendo preset, and a custom mapping (Target on RB, Open Map on d-pad up, Open Menu on the right stick press):
A custom binding: Talk on R3
Special Actions (PlayStation 5)
The Hookshot menu's Controls page
The Button prompts row, and the gamepad column named as the controller does:
On main
0828863: a changed Button prompts row says "(after restart)" like the other restart rows, and the menu's restart note lists it:Feedback welcome
Tested on Linux (before rebasing; see Current state for what was rerun since):
🤖 Generated with Claude Code
I was just looking for this one! How far are we from having this in a mergeable state? 🙏🏻
TY @gab9281 !
it already was, just based on 2.0.4, now rebasing
@hckrman101 this is ready for review. It is rebased on main (
0828863), with the four prompt packages pinned innx2/prompt_requirements.txtas agreed: the Linux AppImage's Python installs them, the Windows package rebuilds its Python part with them, andCONTRIBUTING.mdrestricts them to the prompt scripts. The Current state section of the description says what was tested on which rebase. The validation of this latest rebase is still running, and I'll post here when it's done.Validation of the rebase on main
0828863is done, and everything passes on Linux:tests/run_all.py --strict(200 passed, 0 failed, 5 skipped);hookshot_vk.dllcross-build.One fix was pushed (fa51d68): upstream's new
tests/test_device_lost_exit.pyrequiredlog_flushto be the last host field, and the prompts'prompt_facenow follows it. The description's Current state has the details.Thanks, this is a nice feature, and the fallbacks are careful: no fontTools means no prompts, mods win, and a mod's size table turns the font off. I read the whole diff at b78da16. CI is green. No settings-id concerns:
button_promptsis an ini key taken from the row's name and read directly byhost/prompts.c, so it doesn't use aHOS_OPT_*id. The four packages are only imported bynx2/prompt_assets.pyandnx2/prompt_draw.py.make_game.pyonly runsfind_spec("fontTools"). Linux/Windows:prompts.conly uses Win32 calls thathost/posix/windows.halready provides, andhost/*.cis globbed, so it gets built on both.One real issue (Linux AppImage):
scripts/linux/appimage.shcopiesnx2andhost, but fromthird_partyit only copies opus, zbic and a64-kit. It never copiesthird_party/kenney_input. In the AppImage,prompt_assets.pylooks forKENNEYat$T/third_party/kenney_input,TTFont(...)raises, andmake_game.pyprints "were not made" and moves on. So the shipped Linux build quietly ends up with Switch icons even though deps.sh installed the packages. Your tests were run from a checkout, so they wouldn't catch this. The fix is one line next to the opus copy:cp -a third_party/kenney_input "$T/third_party/". The Windows package is fine becauserelease_parts.pytakes every tracked file underthird_party.Smaller points (nits):
hos_prompts_face()(the menu's Auto names) callssetup(). If the game hasn't asked for a prompt file yet, that runschoose_face(), which can sleep up to 1.5 s waiting for SDL. It would run on the thread that callsmenu_tick, which is the present path. That's unlikely in practice because the font is read at boot, but having it return -1 untilsetup()has finished would rule it out.tests/test_menu_hit.cppis updated, but nothing runs it: nottests/run_all.py, not CI. That was already true on main, so it's not your fault, but the newkCtlOptionhit tests don't run anywhere either.prompt_assets.cmap4is stdlib-only, so a small test could build a cmap with it, compileprompts.c'scmap4_glyph/cmap4_bind(or a copy of them), and check that a binding moves a Switch code point to the pad icon. That is the part most likely to break silently.Could it be simpler? (optional, not a merge blocker)
The diff is about 2,300 added lines in 29 files. The biggest pieces are
prompt_assets.py(963),prompts.c(536),prompt_draw.py(161) andprompt_faces.py(109). You asked whether anything should be split. I think a split along these lines would make this much easier to review and land, and the code already supports each step's fallback:blarcand fontTools. Only fontTools is needed, and it's a pure-Python wheel. That drops Pillow, astc-encoder-py and texture2ddecoder from the first step, and most of the reason for the 54 MB Python part.make_game.pyalready gates on fontTools alone, andprompt_assets.pyalready skips textures onImportError.face_layers(thePICTURE_BUTTONSbranch whenownis None).prompt_draw.py, thedrawing/parts/_partstables inprompt_faces.py(about 50 lines of coordinates measured from photos, per controller) and the guide router inprompt_assets.py(GUIDE_*,guide_route/guide_frame/guide_layers*, about 230 lines). With nopartsfor a face,guide_layers_ofalready returns[]. That's roughly 450 lines of the most hand-tuned, hardest-to-review code, and it's the part you asked for feedback on, so it seems worth its own PR.Small dedups inside
prompts.c, whatever gets split:build,build_textures, andread_all).buildandbuild_texturescould just callread_all.write_zstd_rawrepeats the block writer inwrite_patched.prompt_faces.FACES,prompts.c faces[]together withchoose_face's numbers,tx_promptswithpad_name's 6-row table, and thevk_api.hcomment. A smallHOS_PROMPT_*enum invk_api.h, which both C files already include, would keep the C side in step. That leaves the Python list as the only other copy.None of this changes behaviour. If you'd rather keep it as one PR, the AppImage line is the only thing I'd want fixed before merge.
Nit (debug aid only): the
menu_tick()above only runs whendev.offscreen, but thismenu_recordruns wheneverRX_VK_SHOT_MENUis set. With a window it would useups[..][presents % 3]whilepresent_imageuses its ownframe_indexslot, so the two could step on each other's staging buffer. Also checkingdev.offscreenhere would keep this headless-only.@ -0,0 +1,520 @@/* (hookshot) The button prompts of the controller in use: the game's button-icon font (Font2/Scalable/External.*.bfotf,Optional: this order is repeated in
choose_face's literal numbers, intx_prompts/pad_namein vk_menu.cpp and in theprompt_facecomment in vk_api.h. AHOS_PROMPT_SWITCH..HOS_PROMPT_STEAMDECKenum in vk_api.h, which both files include, would give the C side one place to keep it.@ -0,0 +355,4 @@/* the face the game was given (faces[]), -1 none: the Insert menu names the pad inputs as that controller does. -1 toountil the game has asked for a prompt file: the menu asks from the present thread, and choose_face can wait for SDL */int hos_prompts_face(void) {Nit: the menu calls this from
pad_name(), and that runs on themenu_tick/present path. Ifsetup()hasn't run yet, this is wherechoose_face()would run, and it can wait up to 1.5 s for SDL. Returning -1 until the state is 2 or 3, without starting the setup from here, would keep the present thread from ever blocking on it.@ -0,0 +49,4 @@FACES = {"xbox": dict(font="kenney_input_xbox_series.otf", prefix="xbox_",drawing=("kenney_input_xbox_series.otf", "controller_xboxseries", [Optional: the drawn controllers (
drawing,parts, plus prompt_draw.py and the guide router in prompt_assets.py) could go to a follow-up. With them left out, the System page uses the existing relabelled Pro Controller path and the game's leader lines. See the review body for the split I'd suggest.@ -6,6 +6,7 @@# the player's PC compiles the game with them. The play profile (work/hookshot_pieces.prof) wants# LLVM 23 or newer.# python/ CPython (python-build-standalone) with lz4, numpy, capstone, unicorn and zstandard: the tools# (and nx2/prompt_requirements.txt's, which draw the button prompts)deps.sh installs the packages into the AppImage's Python, but
scripts/linux/appimage.shnever copiesthird_party/kenney_inputinto$T/third_party/. In the AppImage,prompt_assets.pyfails onTTFont(KENNEY/...)and the build falls back to Switch icons without saying why. It needscp -a third_party/kenney_input "$T/third_party/"next to the opus line in appimage.sh.@ -6,6 +6,7 @@# the player's PC compiles the game with them. The play profile (work/hookshot_pieces.prof) wants# LLVM 23 or newer.# python/ CPython (python-build-standalone) with lz4, numpy, capstone, unicorn and zstandard: the tools# (and nx2/prompt_requirements.txt's, which draw the button prompts)Good catch, thanks. Fixed in 5a235a7:
appimage.shnow copiesthird_party/kenney_inputnext to opus.tests/test_appimage_prompt_fonts.pychecks that the folderprompt_assets.pyreads from (KENNEY) is copied, and it fails without the line.@ -0,0 +355,4 @@/* the face the game was given (faces[]), -1 none: the Insert menu names the pad inputs as that controller does. -1 toountil the game has asked for a prompt file: the menu asks from the present thread, and choose_face can wait for SDL */int hos_prompts_face(void) {Done in eeae655:
hos_prompts_face()returns -1 while setup's state isn't 2 or 3, and it never starts the setup itself. So the present thread can't end up inchoose_face's wait for SDL.@ -0,0 +1,520 @@/* (hookshot) The button prompts of the controller in use: the game's button-icon font (Font2/Scalable/External.*.bfotf,Done in eeae655:
HOS_PROMPT_SWITCH..HOS_PROMPT_STEAMDECK, HOS_PROMPT_Ninvk_api.h.faces[HOS_PROMPT_N]andchoose_facein prompts.c use it, and so dotx_prompts, the row's range andpad_name's table in vk_menu.cpp. The Python list is the only other copy, andtests/test_prompt_font_bind.pychecks it againstfaces[]and the enum.@ -0,0 +49,4 @@FACES = {"xbox": dict(font="kenney_input_xbox_series.otf", prefix="xbox_",drawing=("kenney_input_xbox_series.otf", "controller_xboxseries", [Thanks for laying out the split. I'd rather keep this as one PR: it's been validated as a whole on every face and layout, and the fallbacks you listed mean each part already degrades on its own. If you'd like the drawings and the guide router looked at more closely, I'm happy to answer questions here, or to send a follow-up that simplifies them after this lands.
Done in 7c1aab3:
menu_recordhere now also needsdev.offscreen, the same as themenu_tick()above, so the menu shot stays headless-only.Thanks for the thorough review, @hckrman101. I'm keeping it as one PR. It's rebased on main (
5609af4), where the Controls page's rows now sit beside upstream's new Motion page (TAB_PAGE = TAB_MOTION + 1). Pushed:third_party/kenney_input(the blocker), withtests/test_appimage_prompt_fonts.py.HOS_PROMPT_*enum;hos_prompts_face()returns -1 until setup is done and never starts it;build/build_texturesuseread_all, and both writers share onezstd_block;build_texturesusesadd_runtoo;tests/test_prompt_font_bind.py, the game-free test you suggested. It builds a cmap withprompt_assets.cmap4, compiles prompts.c's owncmap4_glyph/cmap4_bind(taken from the source), and checks that bound Switch code points move to their pad icons, unbound ones keep theirs, and the pad icons stay. It also checks the face order across prompts.c, vk_api.h and prompt_faces.py.menu_recordonly runs whendev.offscreen.About
tests/test_menu_hit.cpp: it does run.tests/run_all.pyhastests/test_menu_hit.py(with--cxx g++), and that script compiles and runs the .cpp, including thekCtlOptionhit tests. CI runs it throughrun_all.pyas well.Validation:
tests/run_all.py --strictgives 206 passed, 0 failed, 5 skipped. I haven't rebuilt and run the game on this rebase, so CI on the new head is the check for now.this is amazing i hope it gets added
Post-merge look (this landed as
e19021bbefore I got back to it), so anything here would be a follow-up, not a blocker on this PR.I re-read the final head
3972eddagainst the merge basea34542a, and checked the files on main at the merge commit. Everything from my earlier review is addressed:scripts/linux/appimage.shon main copiesthird_party/kenney_inputinto$T/third_party/(line 54), next to opus.nx2/is copied whole, soprompt_assets.py/prompt_faces.pyare there too, anddeps.shinstallsnx2/prompt_requirements.txtinto the AppImage's Python.tests/test_appimage_prompt_fonts.pyties the copy toKENNEYin prompt_assets.py, and run_all.py picks it up by its name. On Windows the trackedthird_party/goes in through release_parts.py, so the fonts are there as well.hos_prompts_face()on the present path: fixed. It returns -1 untilsetup_stateis 2 or 3 and never startssetup(), so the menu can't end up inchoose_face's 1.5 s wait.pad_name()calls it each time and doesn't cache it, so the names switch over once the game has asked for a prompt file.RX_VK_SHOT_MENU:menu_recordnow needsdev.offscreentoo, the same as themenu_tick()above it.tests/test_prompt_font_bind.pyis registered with--cc gcc. It compiles prompts.c's owncmap4_glyph/cmap4_bindagainst a cmap fromprompt_assets.cmap4. The face-order check across faces[],HOS_PROMPT_*andFACESis a good addition. prompt_assets only imports fontTools/PIL inside functions, so the test runs without the prompt packages.HOS_PROMPT_*enum replaces the repeated literals. Keeping it as one PR is fine with me.I found nothing new in the final diff. The
MkwVkHost.prompt_facefield is added afterlog_flush, in the same order in rx_backend.c, and the API version goes to 11.One thing to keep an eye on: none of the workflows have actually run on
3972edd(every check run is still queued, and the merge commit has none), so CI hasn't confirmed the final head. The author's localrun_all.py --strict(206 passed) is the only check so far. Watching main's next CI run and the next AppImage/Windows package build would confirm it. If the AppImage build log shows "the button prompts of other controllers were not made", that's where to look first.Nice work, thanks for following up on all of it.
@ -6,6 +6,7 @@# the player's PC compiles the game with them. The play profile (work/hookshot_pieces.prof) wants# LLVM 23 or newer.# python/ CPython (python-build-standalone) with lz4, numpy, capstone, unicorn and zstandard: the tools# (and nx2/prompt_requirements.txt's, which draw the button prompts)Confirmed on main (
e19021b): appimage.sh line 54 copies it, and test_appimage_prompt_fonts.py guards it. Thanks!Hi, the button prompt in xbox is not accurate like in the pic. Also the prompt in oricania is not working. Is there anyway i can add a mod button prompt into the game? The mod works fine in emulator.

