fix(mods): merge a mod's resource size table with the button prompts' instead of switching the icons off #2270

Merged
mooki000 merged 1 commit from mods-size-table-merge into main 2026-11-11 17:07:13 +00:00
mooki000 commented 2026-11-11 12:57:53 +00:00 (Migrated from github.com)

While a mod that ships its own resource size table is loaded, the button icons in every text go back to the Switch's,
whatever controller and Button prompts setting the player has. Every randomizer seed made by
Clawshot ships one, and Clawshot lists this as a known issue ("While a seed is
loaded, button icons in text show Switch buttons instead of your controller's").

Fixes #2329.

Why it happens

The game reads one size table. The button prompts' fonts are larger than the game's, so Hookshot serves the game's table
with the font's entry raised (prompts/rsizetable.zs). A mod that makes files bigger has to ship the table with its
own entries raised (#1325), and the mod's file wins, as any mod file does. host/prompts.c then saw a mod table and
turned the prompts' font off (prompts: a mod has the resource size table: the game's own icons in texts), because the
larger font would not fit the mod's entry. The textures and layouts stayed the controller's, so the pause menu showed
the controller's pictures and Switch letters side by side.

What changed

  • host/prompts.c, host/mods.c: when the size table the game opens is a mod's file, it is served with the prompts'
    entries merged into it: each file gets the larger of the two entries, entries only one side has are kept, sorted as
    the game's (merge_restbl). The result is written once per run as raw zstd blocks to prompts/rsizetable.mod.zs, as
    the font already is, and the prompts' font is served as without a mod. Any other mod file is served as before.
  • Two mods with tables: the loader's rule stays, the table of the mod that sorts last is the base; only the prompts'
    entries are merged into it.
  • A table that cannot be merged (not the game's format, other name fields, entries out of order, a zstd frame that does
    not give its size) is served as it is, with the game's icons, as before; the log says why.
  • The log: mods: System/Resource/ResourceSizeTable.Product.111.Nin_Ounce_NVN2.rsizetable.zs <- <file> (with the button prompts' sizes merged in) and prompts: <file>: the button prompts' sizes merged into the mod's resource size table (1 entries raised or added).
  • nx2/build.py: zstd's decoder (third_party/zstd, already compiled into hookshot_vk) is compiled into the runtime
    too, from one zstd_jobs() both use. The readme's Mods section says what happens to a mod's table.

How it was tested

  • tests/test_mods_size_table.py (new): merge_restbl cut out of prompts.c on made-up tables against an independent
    model, and seven tables it must refuse; then, on Linux, mods.c and prompts.c as they are over real folders: no
    mod, a mod with a table, a mod without a table that grows a file, two mods with tables, a table that cannot be merged,
    no prompts table, a mod with its own font, and with zstandard a level-19 frame and one without its size. It fails
    on main.

  • python -I -B tests/run_all.py on Linux, before a rebase onto current main (the new test and the two tests this change edits were run again after it and pass): 361 passed, 10 skipped; the failures are the two that fail on main on
    this machine (test_updater_asset_digest.py: no .NET 8 packs offline; test_wayland_libdecor_hint.py: a clang 23
    warning) and test_bake_mimo.py, whose shader-recompiler compile failed with an empty error at load 134 and passes
    run alone (103 s).

  • In the game (RTX 4090, NVIDIA 615.78.08, Arch Linux, headless; Button prompts set to PlayStation 5, as a run
    without a controller needs; a Clawshot seed as a mod; a save in Kokiri Forest, the pause menu's Basic Controls page):

    • before (main): the DualSense picture next to Switch icons in the texts (ZL, minus, A, B, X, Y, "Select (A) Back
      (B)"), and prompts: a mod has the resource size table: the game's own icons in texts in the log;
    • after: PlayStation icons in every text (L2, the Create button, circle, cross, triangle, square), the same as the
      same build without the mod; the log says (with the button prompts' sizes merged in) after the table's line and
      prompts: ps5 icons, 16 buttons on this pad's controls. The seed's Kokiri Forest and Hyrule Field placement files
      were read from the mod and the forest loaded. The table the game wrote has all 19 entries the seed raised and the
      font's entry raised, and equals the merge computed separately in Python.

    Before and after: the Basic Controls page with a Clawshot seed loaded, Button prompts PlayStation 5

Not tested

Windows: host/prompts.c and host/mods.c compile for x86_64-w64-mingw32 with -Wall -Wextra and no warning, but
were not built into a Windows game or run there; the folder part of the test runs on Linux only (it uses host/posix's
file calls), the merge part on both. AMD and Intel: nothing here touches the GPU. Real controllers: the face was chosen
with the Button prompts setting, as a run without a controller needs.

Found while helping with Clawshot's known issue; I'll tell its maintainer about this pull request.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 [email protected]

While a mod that ships its own resource size table is loaded, the button icons in every text go back to the Switch's, whatever controller and Button prompts setting the player has. Every randomizer seed made by [Clawshot](https://github.com/hiKareeem/Clawshot) ships one, and Clawshot lists this as a known issue ("While a seed is loaded, button icons in text show Switch buttons instead of your controller's"). Fixes #2329. ## Why it happens The game reads one size table. The button prompts' fonts are larger than the game's, so Hookshot serves the game's table with the font's entry raised (`prompts/rsizetable.zs`). A mod that makes files bigger has to ship the table with its own entries raised (#1325), and the mod's file wins, as any mod file does. `host/prompts.c` then saw a mod table and turned the prompts' font off (`prompts: a mod has the resource size table: the game's own icons in texts`), because the larger font would not fit the mod's entry. The textures and layouts stayed the controller's, so the pause menu showed the controller's pictures and Switch letters side by side. ## What changed - `host/prompts.c`, `host/mods.c`: when the size table the game opens is a mod's file, it is served with the prompts' entries merged into it: each file gets the larger of the two entries, entries only one side has are kept, sorted as the game's (`merge_restbl`). The result is written once per run as raw zstd blocks to `prompts/rsizetable.mod.zs`, as the font already is, and the prompts' font is served as without a mod. Any other mod file is served as before. - Two mods with tables: the loader's rule stays, the table of the mod that sorts last is the base; only the prompts' entries are merged into it. - A table that cannot be merged (not the game's format, other name fields, entries out of order, a zstd frame that does not give its size) is served as it is, with the game's icons, as before; the log says why. - The log: `mods: System/Resource/ResourceSizeTable.Product.111.Nin_Ounce_NVN2.rsizetable.zs <- <file> (with the button prompts' sizes merged in)` and `prompts: <file>: the button prompts' sizes merged into the mod's resource size table (1 entries raised or added)`. - `nx2/build.py`: zstd's decoder (`third_party/zstd`, already compiled into `hookshot_vk`) is compiled into the runtime too, from one `zstd_jobs()` both use. The readme's Mods section says what happens to a mod's table. ## How it was tested - `tests/test_mods_size_table.py` (new): `merge_restbl` cut out of `prompts.c` on made-up tables against an independent model, and seven tables it must refuse; then, on Linux, `mods.c` and `prompts.c` as they are over real folders: no mod, a mod with a table, a mod without a table that grows a file, two mods with tables, a table that cannot be merged, no prompts table, a mod with its own font, and with `zstandard` a level-19 frame and one without its size. It fails on `main`. - `python -I -B tests/run_all.py` on Linux, before a rebase onto current `main` (the new test and the two tests this change edits were run again after it and pass): 361 passed, 10 skipped; the failures are the two that fail on `main` on this machine (`test_updater_asset_digest.py`: no .NET 8 packs offline; `test_wayland_libdecor_hint.py`: a clang 23 warning) and `test_bake_mimo.py`, whose shader-recompiler compile failed with an empty error at load 134 and passes run alone (103 s). - In the game (RTX 4090, NVIDIA 615.78.08, Arch Linux, headless; Button prompts set to PlayStation 5, as a run without a controller needs; a Clawshot seed as a mod; a save in Kokiri Forest, the pause menu's Basic Controls page): - before (`main`): the DualSense picture next to Switch icons in the texts (ZL, minus, A, B, X, Y, "Select (A) Back (B)"), and `prompts: a mod has the resource size table: the game's own icons in texts` in the log; - after: PlayStation icons in every text (L2, the Create button, circle, cross, triangle, square), the same as the same build without the mod; the log says `(with the button prompts' sizes merged in)` after the table's line and `prompts: ps5 icons, 16 buttons on this pad's controls`. The seed's Kokiri Forest and Hyrule Field placement files were read from the mod and the forest loaded. The table the game wrote has all 19 entries the seed raised and the font's entry raised, and equals the merge computed separately in Python. ![Before and after: the Basic Controls page with a Clawshot seed loaded, Button prompts PlayStation 5](https://gist.githubusercontent.com/mooki000/b86bd3f35969803fb032f71f035844f8/raw/517eb37a0eb8cbf8e2c0a617264042e1a8a2224c/hookshot-mods-size-table-before-after.jpg) ## Not tested Windows: `host/prompts.c` and `host/mods.c` compile for `x86_64-w64-mingw32` with `-Wall -Wextra` and no warning, but were not built into a Windows game or run there; the folder part of the test runs on Linux only (it uses host/posix's file calls), the merge part on both. AMD and Intel: nothing here touches the GPU. Real controllers: the face was chosen with the Button prompts setting, as a run without a controller needs. Found while helping with Clawshot's known issue; I'll tell its maintainer about this pull request. Assisted-by: Claude Opus 5.5 Co-Authored-By: Claude Opus 5.5 <[email protected]>
hckrman101 (Migrated from github.com) approved these changes 2026-11-11 16:49:30 +00:00
hckrman101 (Migrated from github.com) left a comment

Looks good to me. I read the merge and the precedence carefully.

Precedence: a mod's table still wins as a file (hos_mods_romfs keeps e->host), and the prompts' entries are only raised into it: max of the two sizes per file, entries only one side has are kept, sorted as the game's. The merge refuses anything that is not the game's format (header, name-field size, byte-length check, out-of-order parts, a name field that differs), and then the mod's table is served as it is with the game's icons, as before. The mod's version and name field are kept. Both parts are bounded (name field <= 1024, frame <= 64 MB, output buffer sized for the worst case). The ordering is right for both the CRC part and the name part. The non-table keys return 0 when a mod has the file, so a mod's own font/textures/layouts still win. Merged once per run, and the state machine with from guards the second-table case.

Nothing game-derived is added: the new test builds made-up tables, there is no binary in the diff, and the zstd change is the existing vendored decoder reused (PROVENANCE updated).

Two small nits, not blocking:

  • 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. If a mod ever carried two files ending in .rsizetable.zs (another product number besides 111), the merge could be made from the one the game never opens, and the one it opens is then served unmerged next to the larger font, which the game drops. Unlikely, but picking the key hos_mods_romfs gets for the real table name would remove it.
  • Only the Conventional Commit title check has run on this head so far; the rest of CI did not show up when I looked, so worth a glance that the full suite runs before merging. The author reports the Linux run and the in-game before/after screenshots, and says Windows was compiled for mingw but not run.

No prior comments or reviews on the PR to answer.

Looks good to me. I read the merge and the precedence carefully. Precedence: a mod's table still wins as a file (hos_mods_romfs keeps `e->host`), and the prompts' entries are only raised into it: max of the two sizes per file, entries only one side has are kept, sorted as the game's. The merge refuses anything that is not the game's format (header, name-field size, byte-length check, out-of-order parts, a name field that differs), and then the mod's table is served as it is with the game's icons, as before. The mod's version and name field are kept. Both parts are bounded (name field <= 1024, frame <= 64 MB, output buffer sized for the worst case). The ordering is right for both the CRC part and the name part. The non-table keys return 0 when a mod has the file, so a mod's own font/textures/layouts still win. Merged once per run, and the state machine with `from` guards the second-table case. Nothing game-derived is added: the new test builds made-up tables, there is no binary in the diff, and the zstd change is the existing vendored decoder reused (PROVENANCE updated). Two small nits, not blocking: - `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. If a mod ever carried two files ending in `.rsizetable.zs` (another product number besides 111), the merge could be made from the one the game never opens, and the one it opens is then served unmerged next to the larger font, which the game drops. Unlikely, but picking the key `hos_mods_romfs` gets for the real table name would remove it. - Only the Conventional Commit title check has run on this head so far; the rest of CI did not show up when I looked, so worth a glance that the full suite runs before merging. The author reports the Linux run and the in-game before/after screenshots, and says Windows was compiled for mingw but not run. No prior comments or reviews on the PR to answer.
mooki000 commented 2026-11-12 00:41:24 +00:00 (Migrated from github.com)

@hckrman101 thanks for the review here. #2592 now selects the mod table by the game's own RomFS table name instead of hash order, and made-up two-version tables prove both font-first and table-first requests with GCC and Clang (both fail on main). Your other point was CI visibility: the follow-up records the local suite separately and still needs the full GitHub regression checks approved and green, not just the title check.

@hckrman101 thanks for the review here. #2592 now selects the mod table by the game's own RomFS table name instead of hash order, and made-up two-version tables prove both font-first and table-first requests with GCC and Clang (both fail on main). Your other point was CI visibility: the follow-up records the local suite separately and still needs the full GitHub regression checks approved and green, not just the title check.
Commenting is not possible because the repository is archived.
No description provided.