Repository navigation
feat(mods): a Mods tab, merged resource size tables, ExeFS patches and Atmosphere cheats - #2478
Draft
hckrman101 wants to merge 4 commits into
Draft
hckrman101 wants to merge 4 commits into
hckrman101 wants to merge 4 commits into
Conversation
…d Atmosphere cheats Mods that each grow files (HD texture packs) could not be combined: only one resource size table was used. Hookshot now merges every enabled mod's table into the game's at start (the button prompts' entries kept) and raises the entry of every file a mod serves to fit its size (host/mods.c, host/restbl.c, host/zs.c). NotRexed#2270's merge of the prompts' entries into the winning mod's table stays as the fallback when no merged table can be made. The Insert menu gets a Mods tab (shown only when there are mods): each mod on or off and its place in the list, kept in hookshot_mods.txt and applied at the next start (the restart question names them), and the cheats of the enabled mods, switched at once. ExeFS patches (IPS, IPS32, IPSwitch .pchtxt) are applied to main's image when the game is built (nx2/exefs_patches.py, nx2/image.py), so the code is translated from the patched instructions; a write into the padding after the code (a code cave) makes it code. exefs_patches.txt in the game folder lists what a build applied; the Mods tab says when a patch waits for a build. Atmosphere cheat-code files (separate from the Debug tab's built-in cheats) run in a cheat VM after dmnt's (host/cheats.c), every presented frame, on guest memory the game was given; cheats that write the game's code are listed as not supported. The list, order and switches, the size table's merge, the patch formats and the cheat VM follow valentderah/totk-pc-recomp (MIT), whose notice is kept in host/restbl.h and nx2/exefs_patches.py. Co-authored-by: Valiantsin Dzerakh <valentderah@gmail.com> Co-authored-by: conongnang <173552257+conongnang@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The control bridge's bridge_fill names each MenuTab and checks the count: with the Mods tab added before Debug, its table needs "mods" there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s shown, and the tabs after it
With the Mods tab the banner's tabs came close to the build's text at its left, which shrank and then went away
(at Large it was drawn at its size or not at all; at Small and Medium not under 10 points). The banner now starts with
a small header that is always there: Hookshot's icon (assets/hookshot.png, the start-up screen's) and the build at 13
points, down from 15. The tabs are a little smaller (21 points, from 23) and stay in the middle while that leaves the
header room, else start after it, with the Q and E keys at their ends as before.
When the header and every tab still do not fit (Small at 1280x720 with all six tabs and a long source build's
version), the header gives up "Hookshot " first (the icon says it), then the git suffix past the release
("v2.1.0-rc.3..."), then a size step, and only then do the tabs get smaller. tests/test_menu_size.py checks the order
and the shortened version.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Nov 11, 2026
hckrman101
added a commit
to hckrman101/Hookshot
that referenced
this pull request
Nov 11, 2026
Contributor
|
I'm prototyping the optional indexed RomFS image from #583 (tracked in #1007; no PR yet). At I'll handle image-backed base reads/enumeration in that package and revalidate against your real mod loader. Mod ordering, switches and RESTBL merging remain in this PR. Whichever lands second will need that integration; the existing folder path should keep working. No loading-speed gain is established yet. |
This was referenced Nov 12, 2026
Draft
hckrman101
pushed a commit
that referenced
this pull request
Nov 12, 2026
) **What this is for:** contributors rebuilding from source should not spend another minute drawing the same button prompts. **What is measured:** on an Intel Core i9-13900K running Arch Linux, the finish stage at main `c544828f` took 67.707 and 66.898 seconds; two unchanged runs took 0.603 and 0.651 seconds, and the final head took 0.616 seconds, clearing the run-time files as well. Other work was running throughout. **Not measured:** Windows, a packaged Builder update, a complete compile/lift, or gameplay performance. Refs #2636. ## Problem `nx2/make_game.py` removes the game folder's `prompts` directory and runs `prompt_assets.py` on every build when fontTools is available. Most source edits do not change anything those assets depend on. This avoids drawing unchanged assets; it does not make the drawing itself faster. #2636's A1 plans to measure the finish stage, but its A1–A10 items do not already propose this prompt cache. It complements #2674 and #2675, which address unnecessary retranslation rather than prompt generation. ## What changed - **`nx2/make_game.py`:** source-checkout builds request incremental prompt generation. Packaged builders still regenerate every time, using the finish stage's existing launcher distinction. `RX_RELEASE` cannot distinguish them: make_game sets it for source builds too. Missing fontTools still removes the replacement prompts, keeping the existing fallback to the game's icons. - **`nx2/prompt_assets.py`:** fingerprint the inputs before generating, draw into a fresh sibling directory, and record the successful outputs only if a second fingerprint still matches. Reuse requires both matching inputs and every recorded output's size and SHA-256. **A failed generation still removes the prompts, as before**, so the game shows its own icons. An interrupted builder can leave the previous complete set without a stamp; the next build regenerates it. The drawing, encoding and asset formats are unchanged. - **`tests/test_prompt_skip.py`:** game-free fixtures run the actual finish-stage prompt call and production cache/publication functions, with small authored files and a stand-in for expensive drawing. - **`docs/development.md`:** explain reuse, its limits, and `HOOKSHOT_FORCE_PROMPTS=1`. The force switch follows `env_on`: unset, empty, `0`, `false`, `off` and `no` are off, ignoring case and surrounding spaces. ### What prevents a wrong skip The input record covers: - The four prompt source modules, including controller, keyboard and binding tables. - The game's icon font and optional bold label font, compression dictionary, common texture/layout archives, and matching resource-size table names and contents. - The repository's controller and keyboard fonts, and added, changed or removed PNG overrides. - Python/platform; fontTools, Pillow, ASTC encoder, texture decoder, NumPy, zstandard and archspec availability and versions; Pillow's native library versions and the selected ASTC backend. - Generation parameters and relevant library switches, including `SOURCE_DATE_EPOCH`, `FREETYPE_PROPERTIES` and the encoder's custom CPU definitions. Languages and saved bindings are not build inputs here: the generator uses its source tables, and the runtime applies the selected controls. The output record includes files **and directories**: the keyboard directory itself is a runtime marker. Legitimately absent optional outputs stay absent; a missing, truncated or same-size-corrupted recorded file forces regeneration. The runtime's own compressed copies and control-specific keys are excluded before hashing, not mistaken for generator output. **The files the game derives at run time are still cleared by every build**, including an unchanged hit: their own keys do not cover changes to `host/prompts.c`. Only the known runtime filenames are removed; if one cannot be cleared, the build regenerates instead of keeping on doubt. An unreadable input cannot authorize reuse. The log gives the reason, even if a forced regeneration subsequently fails. A source edited during generation leaves no stamp, addressing the stale-input certification problem discussed in [#2674's review](#2674 (comment)). ## How it was tested 2026-11-12; Arch Linux, kernel `7.2.15-1-omarchy`, x86-64, Intel Core i9-13900K, 32 logical CPUs, Python 3.12.16. Base `c544828ff`; final head `2ec89de8`. No game or GPU program was launched for these checks. ### Finish-stage measurements These execute the real finish-stage statements through completion, including binary/image copies, the unchanged shader-list check, pipeline-list work and prompt generation, in a disposable copy of an existing built game folder. Compilation, translation and shader preparation are not included. They are **not whole rebuild times**. All optional prompt packages were present: fontTools 4.68.0, Pillow 13.0.0, astc-encoder-py 0.1.12, texture2ddecoder 1.0.6, archspec 0.2.7, NumPy 2.5.4 and zstandard 0.25.0. | Run | Source | Finish time | 1-minute load before → after | Top other work | |---|---|---:|---:|---| | Before 1 | `c544828f` | 67.707 s | 7.13 → 7.40 | Existing game 316 → 315% CPU; file indexing 66 → 79%; relay check also active | | Before 2 | `c544828f` | 66.898 s | 7.06 → 8.24 | Existing game 315 → 318%; terminal process about 41% | | First generation with cache | `63d5c965` | 66.400 s | 8.24 → 8.19 | Existing game 318 → 320%; terminal process about 42% | | Unchanged 1 | `63d5c965` | 0.603 s | 8.19 → 8.19 | Existing game 320%; terminal process about 42% | | Unchanged 2 | `63d5c965` | 0.651 s | 8.19 → 8.19 | Existing game 320%; terminal process about 42% | | Changed table source | `681208dd` | 63.147 s | 6.99 → 6.95 | Existing game 320 → 321%; terminal process about 42% | | Regenerate, final head | `2ec89de8` | 65.007 s | 5.77 → 5.86 | Existing game 317 → 316%; terminal process about 43% | | Unchanged, final head, with run-time files present | `2ec89de8` | 0.616 s | 5.79 → 5.79 | Existing game 316%; terminal process about 43% | Other work was running; none was a quiet measurement. CPU percentages are lifetime averages from snapshots, not interval measurements. The first generation is included to show that this is a skip, not a faster drawing algorithm. The unchanged runs print: ```text prompts: unchanged, kept ``` Adding a harmless comment to the button-table source forces a fresh generation: ```text prompts: regenerated (table changed) ``` The comment was removed afterwards. A timestamp-only touch does not regenerate: the key checks content. An additional run on the final head verifies an unchanged hit with all six runtime-derived files present: all six are removed, while the 16 generated originals and the stamp retain their sizes and SHA-256 values. The focused fixture also compares the retained originals byte for byte. The final timing rows above include that cleanup. **Asset parity:** all 16 regenerated files, 274,704,680 bytes, compare byte for byte with a full generation by main `c544828f`. Only the new stamp is excluded. Both sides used the same `SOURCE_DATE_EPOCH` because fontTools otherwise writes the current time into the OpenType header. This comparison setting is not introduced into normal builds. A development cache hit retains the earlier font timestamp; packaged builds retain their old full-generation behavior. ### Game-free checks - **23 focused tests pass** on the final change. They cover unchanged reuse, every input kind, package versions/presence, switch/parameter changes, missing/truncated/same-size-corrupted output, empty directories, clearing runtime derivatives while retaining byte-identical originals, regeneration when runtime cleanup fails, interruption, mid-generation edits, unreadable input, force-switch parsing, incomplete output, publication failure, and the packaged/no-fontTools paths. A non-zero generator result removes prompts for both source and packaged builds; interruption before that result leaves the old set unstamped. - The same unchanged-build test against main's finish stage fails behaviorally: `2 != 1: unchanged finish must not draw the prompts twice`. - Removing each of **18 guards/key components** in memory makes its corresponding behavior test fail on the final change. Three later corrections also fail against the earlier version of this patch: returned failure retained old prompts in both build paths, runtime files survived a hit, and an uncleared runtime file did not force regeneration. The unreadable-input diagnostic has a separate before-fails/after-passes proof. - The whole suite one commit earlier, at `f20ea58b` (before a failed generation was made to remove the prompts again and the run-time files to be cleared), `python -I -B tests/run_all.py --jobs 8`: ```text 472 passed, 0 failed, 20 skipped in 133 s (linux, 8 at once) ``` Load was 7.37 → 19.48. The first full run, at 32 checks at once, was **471 passed, 1 failed, 20 skipped**: a clang++ child compiling `texture_pass.cpp` for `nx2/test_depth_sample_types.py` died with SIGKILL. That fixture and its input trees are unchanged from main. No kernel OOM line was found, so the cause is not established. The complete run with eight workers passes, including that check. The failed run is retained, not counted as a pass. On the final head `2ec89de8` the focused checks, the baseline failure and all guard mutations were rerun, and the whole suite once more with eight workers: **471 passed, 1 failed, 20 skipped in 136 s** (load 11.14 → 22.62). The one failure is `tests/test_release_scripts.py`, `test_nightly_two_runs_at_once_end_at_the_newer_commit`, the intermittent race reported in #2626; this change touches none of its files. - No core dumps were recorded. Nothing generated from the game is in this change. ## Not tested - Windows filesystem behavior or a Windows build. - The packaged Windows Builder or Linux AppImage build/update end to end. Their always-regenerate selection is covered by a game-free test only. - A full compile/lift or a new gameplay run, other CPUs, or gameplay/frame-rate benefits. No screenshots: the regenerated asset bytes match, and no visual change is intended. ## Risks The input list must grow when the generator gains new file dependencies or outputs outside the prompt directory. A regression flags new local module imports, but it does not replace reviewing new file reads. Generation temporarily needs room for both old and new assets. Publication uses completed directories and rollback on a rename error; interruption cannot create a stamped half-set, though a process killed between the two directory renames may require regeneration on the next build. ## Overlaps The open-PR scan found 47 PRs touching one of these files. The actual conflict hunks were read: - **#2151:** adjacent item-button generation conflicts with the finish-stage prompt call/error branch. Preserve both steps and account for added prompt dependencies. - **#2261:** substantive overlap. It adds `--wheel-icons`, another input folder and output outside `prompts`. Keep that work outside this skip or extend the input/output proof and staging; accepting both hunks mechanically is not enough. - #1872, #1958, #2304, #2317, #2430, #2465, #2474, #2478, #2608, #2673, #2675 and #2686 share code files but merged cleanly in the scan. That is not proof that any newly added generator dependency is covered. Assisted-by: GPT-6 Astra Co-Authored-By: GPT-6 Astra <noreply@openai.com> Co-authored-by: GPT-6 Astra <noreply@openai.com>
hckrman101
pushed a commit
that referenced
this pull request
Nov 12, 2026
**What this is for:** keep controller-button prompts working when a RomFS mod includes resource size tables for more than one game version, following [hckrman101's review of #2270](#2270 (review)). **What is measured:** on Linux, GCC and Clang both pass 12 file-routing cases and the existing merge checks, including seven malformed-table refusals; both new open-order cases fail against main `fce53453`. **Not measured:** a new game run, Windows file enumeration, or any change to a normal single-table seed's gameplay. ## Problem The reviewer noted that: > `build()` picks the mod table with `hos_mods_find`, which returns the first match in hash order, while `hos_mods_romfs` passes the path actually opened. `host/prompts.c::sizes()` merges once and remembers the source file. If the font is requested first, the first matching mod table can belong to another game version: the real table is subsequently served unmerged alongside the larger font. In the opposite order, the real table is merged, but the font's check asks about the other table and disables the replacement font. The other review nit was about CI visibility, not another implementation change. At the 2026-11-11 17:13 CST check, #2270's check list still contained only two successful Conventional Commit title checks. That is not evidence that its GitHub regression suite ran. The current `.github/workflows/source-regressions.yml` already runs all discovered checks for pull requests to main; this PR needs those checks approved and green before merge. ## What changed - `host/prompts.c`: discover the resource-table name in the game's own RomFS, using the same first-by-name rule as `nx2/prompt_assets.py`, before checking a mod's table for the font. This also works when the font is opened first. - `host/mods.c`: replace the prefix/suffix hash-order search with an exact, case/slash-normalized lookup of that name. It uses the same winning indexed entry as the normal RomFS open path. - `tests/test_mods_size_table.py`: add a second, wholly made-up table with different entries, exercise both table-first and font-first requests, and check both the returned files and merged bytes. `--source <host-directory>` runs the same fixtures against unmodified main. - `readme.md`: explain which version's table is used. The merge algorithm, maximum-per-entry rule, malformed-table fallback, last-sorted-mod precedence and a mod's own font priority are unchanged. No game file or data made from the game is added. ## How to try it No switch or rebuild of a mod is needed. A mod containing two differently versioned size-table files should keep its matching table's entries and the controller font, irrespective of which file is requested first. The game-free reproduction is: ```sh python -I -B tests/test_mods_size_table.py --cc gcc python -I -B tests/test_mods_size_table.py --cc clang # In this checkout, against another checkout's unmodified host sources: python -I -B tests/test_mods_size_table.py --cc gcc --source /path/to/main/host ``` ## How it was tested 2026-11-11; Arch Linux, kernel `7.2.15-1-omarchy`, Core i9-13900K; GCC 16.2.1 and Clang 23.1.3; deps Python 3.12 with `-I -B`. Base `fce53453`, branch `fd017d3f`. The machine was heavily loaded (load average about 287 during the focused checks). No GPU was used. Final branch: `bd4b1ec3`, one commit on current main `ef81aee7`. The rebase from `fce53453` was clean and its range-diff is `=`. Both compiler proofs, including the two failing open orders without the fix, were rerun after rebase; the relevant base host-source blobs are identical. The full-suite totals below are the recorded `fce53453`-based run, not a second full suite on the newer base. The focused runs after the rebase had load snapshots of 71.63 to 75.04. Both compilers print: ```text ok: merge_restbl: the larger entry of each file, entries of one side kept, sorted; 7 unmergeable tables refused ok: 12 cases served as they should: no mod, a mod with a table, two table versions in both open orders, one that grows a file without, two mods, a table that cannot be merged, no table of ours, a mod's own font, compressed and size-less frames ``` Against the base, **both compilers fail both new cases**, rather than a source-text or compilation check: | Request order | Main's returned font | Main's returned actual table | Branch | |---|---|---|---| | Table, then font | The game's font; replacement disabled | Merged with the correct table | Correct merged table and controller font | | Font, then table | Controller font | The unmerged mod table; the other version seeded the merge | Correct merged table and controller font | The normal single-table cases still compare all merged bytes to the independent expected table. No new game run was made: the ordinary seed's merge and resource sizes are unchanged, and the changed source-selection case is fully made up in the fixture. This does not claim a new gameplay result. The whole `tests/run_all.py` suite ran once, in the idle scheduling class: ```text 449 passed, 1 failed, 20 skipped in 288 s (linux, 32 at once) ``` Load was 111.79 at its start and 92.91 at its end. The only failure is the unrelated main regression `test_env_on.py`: `HOS_FPS_LOG (host/oot_high_fps.c:15)` still uses `getenv(...) != NULL` after #2470. That source and the guard test are unchanged here. `test_updater_asset_digest.py` **passed** via this machine's fallback; it is not counted as a failure. `coredumpctl` reported `No coredumps found.` The same guard was then run against the untouched `fce53453` checkout with GCC and Clang; both fail on that exact `HOS_FPS_LOG` line. This is the only remaining suite failure for B, not a mod-table regression. Before pushing: one commit on current main, the commit identity and trailers as the project asks, and no game file or build output among the four changed files. Rerun on the pushed head by me: the test with gcc and with clang passes, and against main's `host/` sources it fails with both. ## Not tested - Windows runtime enumeration and a Windows game build. - A real mod containing multiple versions' tables, long play, or another game update's layout. - A game RomFS with ambiguous multiple applicable tables. Selection intentionally matches the prompt-assets builder; this does not invent a new version-selection policy. - GitHub CI for this not-yet-opened follow-up; the local results above do not stand in for it. ## Risks The font's initial preparation now enumerates the game's resource directory once per preparation rather than taking the first matching mod entry. It relies on `hos_romfs_dir` being the game's RomFS. Unsupported/ambiguous base layouts need a separate version-selection decision; ordinary supported single-table layouts and the merge's bounds are unchanged. ## Overlaps The final standalone overlap scan found **46 open PRs** touching these files; the actual conflict hunks were read: - [#2151](#2151): its punctuation helper is inserted beside `game_table`; retain both helpers. - [#2317](#2317): the adjacent font-output comment changes to a cache path, plus the fixture's `string.h` include. Keep its path changes and this exact-table selection. - [#2478](#2478), draft: substantive overlap. Its all-mod-table merger changes the old lookup's fallback semantics, the README and fixture sources. The exact-key selection must be carried into that resolver, including its merged-table/fallback behavior; do not resolve this by blindly keeping one side. - [#2561](#2561): the README's large player/developer sections are replaced by documentation links. Move this table-selection explanation into its new mod documentation instead of retaining the old README section wholesale. - #2304, #2430, #2435 and #2583 touch the same code files but merged cleanly in the scan. No separate claim issue: this is the review follow-up to #2270. Assisted-by: GPT-6 Astra Co-Authored-By: GPT-6 Astra <noreply@openai.com> Co-authored-by: GPT-6 Astra <noreply@openai.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Branch: feat/mods-cheats, head f654ef2 (5d9a877, then a fix, a merge of main 940eb9e, and the menu header)
Mods that each make files bigger, such as HD texture packs, can't be combined today: the game reads one resource size table, so only one mod's grown entries count. ExeFS patches (IPS, pchtxt) and Atmosphère cheat files, which many Switch mods ship, aren't loaded at all. This PR merges the size tables, adds a Mods tab to the Insert menu, applies ExeFS patches when the game is built, and runs cheat files.
What changes
modcache/rsizetable.zsand logged.hookshot_mods.txtand apply at the next start; closing the menu asks to restart ("the mods").mods/<name>/exefs/*.ipsand*.pchtxtfiles (IPS, IPS32, IPSwitch) are applied to main's image before the lift relocates and translates it, as rtld would see the module. The code is then compiled from the patched instructions, and image.bin holds the patched data.@nsobid)..text(a code cave) becomes code the lift translates.exefs_patches.txtinto the game folder. The Mods tab uses it to say when a mod's patches wait for the next build (a change takes a rebuild, since the code is compiled ahead of time).mods/<name>/cheats/E0F3CCFAF50065AE.txtin Atmosphère/dmnt format. They are separate from the Debug tab's built-in cheats (Add opt-in Ocarina of Time developer debug tools #785 tools,oot_cheat_items.h), which this PR doesn't touch.hookshot_mods.txt.{Master}codes are always on.assets/hookshot.png) and the build, a little smaller than before (13 points, from 15). The tabs are a little smaller too (21 points, from 23). They stay centred while that leaves the header room; otherwise they start after it, with the Q/E keys at their ends as before. Where it is still too tight (Small at 1280x720 with a long source build's version), the header drops "Hookshot " first (the icon says it), then the git suffix after the release (v2.1.0-rc.3...), then a size step. Only after that do the tabs shrink. With no mods (five tabs) the same rules apply.Credit
runtime/mods.c,romfs.c,restbl.c,cheats.candtools/exefs_patches.py. That project is MIT, Copyright (c) 2026 Valiantsin Dzerakh; its notice is kept inhost/restbl.handnx2/exefs_patches.py, and host/PROVENANCE.md names the files.Verified
On an RTX 3090 (Linux), headless runs with copies of saves. The test mods were made locally from the user's own files and never shared.
Mods that grow the same file load together.
Title.bntx(+64 KiB) and the HUD'sCommon.bntx(+64 KiB), with its own table.Title.bntx(+256 KiB), with its own table.resource size table: 2 tables of mods merged (3 entries), 2 entries raised for mod files ... served from modcache. The title texture is served from B (lower in the list) and the HUD from A.ExeFS patches.
.text. In game, R3 switches to first person (screenshots: before / after).mov x19, x0re-encoded asadd x19, x0, #0in the camera view update): its bytes are in image.bin.1075 bytes of main patched by 2 filesandmain's code reaches 0x3c46f8c now (a code cave); the Mods tab shows both mods' patches as built in.Mods tab. In game, with screenshots: the list, a mod moved up (hookshot_mods.txt written; "Restart needed: the mods"), and a cheat switched on (log
cheats: ... 1 on).Game-free checks, 399 of 399 passing on Linux:
test_mods_sizes(three mods over a synthetic RomFS and tables: the merged entries, the served files, the counts, hookshot_mods.txt across two starts, ExeFS status, no mods = nothing written).test_exefs_patches(IPS/IPS32/pchtxt, build ids, the mods' order, code/data/linker tables on a synthetic main, caves, the list).test_cheat_vm(stores, buttons, pointer chains, loops, register conditions, out-of-guest reads, code writes refused).test_mods_menu(the tab's lines and keys against a fake host, hidden without mods or with an older host).test_mods_size_table,test_fs_links,test_present_trace,test_input_background.Menu header, in the game (RTX 3090, Linux, headless; the game's own frames). Small, Medium and Large at 1280x720, 1600x900, 1920x1080 and 2560x1440, with six tabs and the version
v2.1.0-rc.3-135-g32bb5f4-dirty; plus five tabs (no mods) at Small 1280x720 and Medium 1920x1080. Before: at Medium and Large the version was hidden, and at Large the tab keys reached the bar's ends. After: the icon and the version show at every size. Small and Medium keep the whole "Hookshot v2.1.0-rc.3-135-g32bb5f4-dirty"; Large, at every resolution, shows "v2.1.0-rc.3..." beside the icon. Nothing overlaps, and the Q/E keys stay inside the bar. The full suite (tests/run_all.py) passes after the merge and the header: 412 passed, 0 failed, 12 skipped.tests/test_menu_size.pychecks the header's order of shortening and the shortened version.Not tested
test_cheat_vm.atmosphere/exefs_patches/<name>/layout and opcode C4/0xF extensions are not supported.Screenshots (game output only): pr-shots/a-third-person.png, a-first-person.png, mods-tab.png, mods-tab-moving.png, mods-tab-moved.png, mods-tab-cheat-on.png (in ~/.cache/hookshot-work/port-mods/pr-shots/).
Menu header screenshots (the game's own frames)
Before, 6 tabs, Large, 1920x1080 (the version is hidden):
After, Medium and Large at 1920x1080, Small at 1280x720, and Medium with no mods: