WIP: feat(mods): a Mods tab, merged resource size tables, ExeFS patches and Atmosphere cheats (HumanBean's draft from Hookshot, with the lift fix) #134

Draft
snaildev99 wants to merge 5 commits from snaildev99/Longshot:feat/mods-exefs-patches into main
Contributor

Why

Mods that change the game's code (ExeFS patches: .ips, .pchtxt) do nothing on Longshot, two mods that each ship a resource size table cannot load together, and there is no place in the game to see or order the mods. HumanBean's draft for all of this was on Hookshot (https://github.com/NotRexed/Hookshot/pull/2478) and never came over; he asked for it to be brought here.

What changed

HumanBean's two commits are ported as they were, under his name; the changes of the port and of the review follow them.

  • mods (HumanBean): host/mods.c reads the mods' order and switches from longshot_mods.txt, serves one size table merged from every enabled mod's (host/restbl.c, host/zs.c, written to modcache/rsizetable.zs), and gives the menu its list (hos_mod_info, hos_mod_edit, hos_mods_changed).
  • ExeFS patches (HumanBean): nx2/exefs_patches.py reads mods/<name>/exefs/*.ips and *.pchtxt when the game is built, nx2/image.py writes them into main before it is relocated and lifted (a write into the padding after the code makes it code), nx2/make_game.py puts them in the lift's key and writes exefs_patches.txt.
  • cheats (HumanBean): host/cheats.c, Atmosphere cheats of the mods that are on, data cheats switched in the menu.
  • menu (HumanBean): the Mods tab in gpu/vk/vk_menu.cpp (draw_mods, mods_tick), shown when there are mods; MkwVkHost gains five functions (gpu/vk/vk_api.h, host/rx_backend.c).
  • menu (snaildev99): the compact bar, which main gained after the draft, draws the Mods tab's full page, dimmed as the full menu, when the Mods tab is the current one (draw_tune_bar).

What the port changed in HumanBean's commits (7 files conflicted with main):

  • gpu/vk/vk_api.h, host/rx_backend.c: the five functions come after main's screen_size; MKWVK_API_VERSION is 22 (the draft's was 18, main is at 21), and can_mods() asks for 22.
  • gpu/vk/vk_menu.cpp: the Mods block sits before draw_options; tab_titles (at file scope on main) gains "Mods"; the row handling of menu_tick is main's, inside the draft's else for the tabs that are not Mods.
  • host/mods.c: main's hos_mods_file (an exact path) with the draft's rule for the size table in it.
  • nx2/build.py: zs.c joins ZSTD_HOST. tests/test_mods_size_table.py: zs.c beside the others.
  • README.md: the draft's Mods section with Longshot's names; main's note on tables for several game versions kept. The Mods tab's file is longshot_mods.txt.
  • tests/test_mods_menu.py: version 22 and the new order of the host's functions.

What the review changed (the last commit):

  • host/mods.c hos_mods_file: no table for prompts.c when the merged one is served.
  • host/mods.c mod_text, exefs_status: a mod that is off says "Its ExeFS patches are not applied while it is off", in the tab and in the log, instead of asking for a build.
  • host/mods.c: "Longshot" in the Mods tab's texts and in longshot_mods.txt's header.
  • host/mods.c fit_sizes, game_case: a mod file whose path is in another case than the game's takes the game's entry. The game's case is looked up only when the mod's own case misses the table, so a mod whose paths match costs no extra reads at the start.
  • gpu/vk/vk_menu.cpp menu_tick: a Restart or Quit waiting for its second press is cleared on the Mods tab.
  • nx2/exefs_patches.py classify: bytes into a relocated word are left out as before, the file's note in exefs_patches.txt now says how many, and the comment says what happens to DT_RELR words (tests/test_exefs_patches.py, the README).

Flags and settings

Flag What it does Default now Recommendation Helps on Doesn't help on Untested on
Mods tab (Insert menu) lists the mods, each On or Off, in an order that can be changed; their cheats shown when there are mods on every system - Linux
longshot_mods.txt (game folder) the Mods tab's order and switches none: the folders' names' order, all on on every system - Linux
mods/<name>/exefs/*.ips, *.pchtxt code patches, applied when the game is built applied at the next build on every system - pchtxt files, Linux
mods/<name>/cheats/<build id>.txt Atmosphere cheats data cheats off until switched on every system - every system: no cheat file was tried

Measurements

main: 39647e1a (2026-11-13)
this PR: feat/mods-exefs-patches @ ca32fd3b
Hardware: RTX 3090, Windows 11
Scene: a child save in Kokiri Forest and Hyrule Field, 2048x1152, 60 fps
Method: one start and about twelve minutes of play by the mod's author, with the mod of the build lines below as the only mod; then a second start; the numbers are longshot_log.txt lines. The merge's time is from the two starts of this PR's build (the first right after the build, the second with the files read before). main has no merge to time against it, and without a mods folder hos_mods_init returns before any of it, so there is no cost when no mods are installed (read in the code, not timed)
How to reproduce: a mod with ExeFS patches in mods, nx2/make_game.py into that game folder, start the game

main @ 39647e1a this PR @ ca32fd3b
A mod with ExeFS code patches its exefs folder is left out built in: the lines below
The size table's merge at the start none: main has no such step 111 ms and 56 ms in two starts, median 84 ms (1 mod's table merged, 90 entries; 15 entries raised for mod files)
The mod in play its code patches do nothing its menu page, picking characters, the swap in the field, its cloth and voice hooks: all work
The Mods tab none lists the mod; moving in it and switching tabs behave
Frame rate in play 60.0 fps in each 5 s line of the log

The build and the run are of 3c23346a, the same changes before the rebases onto 39647e1a (main's commits since touch release and CI files only; the docs commit came after), Windows 11, llvm-mingw:

exefs: code        OoT Character Select/exefs/E0F3CCFAF50065AE4EA01EB5AEB13B1A45DA3348.ips (4935 bytes)
exefs: main's code reaches 0x3c47000 now (a code cave)
exefs: 4935 bytes of main patched by 1 files
functions with code below their entry, started at it by a goto: 28
compiled in 5s, 0 failed
the build took 0:02:17, compiled in 0:00:11 (the last one 0:15:18)

(The second build, after the review's changes: mods.c and vk_menu.cpp compiled again, no warning in either. The first, of the port itself, took 15 min 18 s.)

The run's log:

mods: OoT Character Select: 534 RomFS files (16 replace the game's, 518 added), ExeFS patches
mods: OoT Character Select: ExeFS E0F3CCFAF50065AE4EA01EB5AEB13B1A45DA3348.ips: built into the game
mods: resource size table: 1 tables of mods merged (90 entries), 15 entries raised for mod files (0 added files estimated), served from modcache, 111 ms
(second start) mods: resource size table: 1 tables of mods merged (90 entries), 15 entries raised for mod files (0 added files estimated), served from modcache, 56 ms
mods: 1 mods from <game folder>\mods: 551 RomFS entries
mods: System/Resource/ResourceSizeTable.Product.111.Nin_Ounce_NVN2.rsizetable.zs <- <game folder>\modcache/rsizetable.zs (merged)
vi: 9492 frames presented (60.0 fps over the last 5.0 s, interval 1)

Testing

Tested:

  • python -I -B tests/run_all.py on Windows 11: 462 passed, 70 skipped, 10 failed. The same 10 fail on main on this PC: test_builder_details_hitbox, test_builder_dlss_update, test_portable_archive, test_portable_builder_home, test_portable_builder_startup (no .NET SDK here), test_fpx_sticky, test_spawn_emit (they build the translator; this toolchain copy has no C++ standard headers), test_check_third_party, test_release, test_setup_runtime.
  • A Windows build with a mod that has six ExeFS code patches in one file (https://github.com/snaildev99/OoT-Character-Select): the lines above.
  • In the game (Windows 11, RTX 3090, a child save), played by the mod's author: the Insert menu's Mods tab lists the mod, and moving in it and switching tabs both ways behave as on the other tabs; the mod's own page opens from the game's menu, a character is picked and is the player in the field at once (young Zelda with her cloth, which goes through one of the code patches, and Saria), and both speak with their own voices (another of the patches). No crash, 60 fps.

Not tested:

  • Switching a mod Off and On and the restart question, moving a mod up or down, and the mouse on the tab's lines. Only looking, moving the selection and changing tabs were tried.
  • The compact bar with the Mods tab current.
  • Linux.
  • A pchtxt mod, a cheat file, two mods that each ship a size table, a mod whose paths differ in case from the game's, a patch that writes a relocated word.

Needs other testers:

  • Linux: the Mods tab with a RomFS mod, and a build with a mod that has ExeFS patches, expecting the same as on Windows (T-3AW7)

Screenshots

this PR @ 3c23346a (the build of the run above), Windows 11, RTX 3090, from the mod's author:

this PR @ 3c23346a: the mod's Characters page in the game's own menu, King Zora picked, in a window whose title bar says Longshot

The mod's Characters page in the game's own menu with King Zora picked, the game in a window (its title bar says Longshot). The page's fourth tab, the picked character's model and its view all come from the mod's ExeFS code patches, so this is those patches at work in a Longshot build. On main the patches are left out: nothing to show beside it.

this PR @ 3c23346a: the Insert menu's Mods tab, the mod listed and On

The Insert menu (F1) of the same run: the Mods tab, with the mod listed and On.

Risk and rollback

The Mods tab adds a tab to the Insert menu's strip only when there are mods. With mods, the size table the game gets is the merged one, where before it was the last mod's own. A build with ExeFS patches lifts patched code: without such patches the lift and its key are as before. Reverting the commits is enough.

Reviewer notes

  • The lift fix a code-cave routine needs is on main (#131), so its commit is no longer here.
  • The compact bar on the Mods tab: I draw the full Mods page (dimmed) from draw_tune_bar, because tests/test_menu_tune.py asks for if (tune_mode) draw_tune_bar(k); as it is. The other way would be to leave the Mods tab out of the strip while the bar is up. Say if you prefer that.
  • DT_RELR words: I took the second of your two ways (leave them out and say so). Applying them needs the addends read again after apply(); I can do that here or as a follow-up.
  • The draft's third commit on that branch (the banner's header) is not here: another topic.

Issues

See #42

Checklist

For everyone

  • One topic.
  • The title is a Conventional Commit (type(scope): description).
  • Rebased on current main.
  • python -I -B tests/run_all.py run and passing (any failure that also fails on main is named above).
  • Tested in the game, or what wasn't is named under Testing.
  • Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests).
  • Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported.
  • No game files, dumps, build output or generated files.
  • third_party/ untouched (a new folder with a PROVENANCE.md instead).
  • The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled.

If applicable

  • AI assistance recorded with Assisted-by: / Co-Authored-By: trailers.
  • Flags noted, and the switch reference updated (docs/development.md, the README).
  • Screenshots for a change that shows on screen.
  • MKWVK_API_VERSION bumped (gpu/vk/vk_api.h's MkwVkHost or MkwVkRenderer changed).
  • A game-free test added for the fix, failing on main and passing here.
  • A check under tests/ that reads changed source updated in this PR, not weakened.
  • Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths.
  • Docs updated.

🤖 Generated with Claude Code

## Why Mods that change the game's code (ExeFS patches: `.ips`, `.pchtxt`) do nothing on Longshot, two mods that each ship a resource size table cannot load together, and there is no place in the game to see or order the mods. HumanBean's draft for all of this was on Hookshot (https://github.com/NotRexed/Hookshot/pull/2478) and never came over; he asked for it to be brought here. ## What changed HumanBean's two commits are ported as they were, under his name; the changes of the port and of the review follow them. - mods (HumanBean): `host/mods.c` reads the mods' order and switches from `longshot_mods.txt`, serves one size table merged from every enabled mod's (`host/restbl.c`, `host/zs.c`, written to `modcache/rsizetable.zs`), and gives the menu its list (`hos_mod_info`, `hos_mod_edit`, `hos_mods_changed`). - ExeFS patches (HumanBean): `nx2/exefs_patches.py` reads `mods/<name>/exefs/*.ips` and `*.pchtxt` when the game is built, `nx2/image.py` writes them into main before it is relocated and lifted (a write into the padding after the code makes it code), `nx2/make_game.py` puts them in the lift's key and writes `exefs_patches.txt`. - cheats (HumanBean): `host/cheats.c`, Atmosphere cheats of the mods that are on, data cheats switched in the menu. - menu (HumanBean): the **Mods** tab in `gpu/vk/vk_menu.cpp` (`draw_mods`, `mods_tick`), shown when there are mods; `MkwVkHost` gains five functions (`gpu/vk/vk_api.h`, `host/rx_backend.c`). - menu (snaildev99): the compact bar, which main gained after the draft, draws the Mods tab's full page, dimmed as the full menu, when the Mods tab is the current one (`draw_tune_bar`). What the port changed in HumanBean's commits (7 files conflicted with main): - `gpu/vk/vk_api.h`, `host/rx_backend.c`: the five functions come after main's `screen_size`; `MKWVK_API_VERSION` is 22 (the draft's was 18, main is at 21), and `can_mods()` asks for 22. - `gpu/vk/vk_menu.cpp`: the Mods block sits before `draw_options`; `tab_titles` (at file scope on main) gains "Mods"; the row handling of `menu_tick` is main's, inside the draft's `else` for the tabs that are not Mods. - `host/mods.c`: main's `hos_mods_file` (an exact path) with the draft's rule for the size table in it. - `nx2/build.py`: `zs.c` joins `ZSTD_HOST`. `tests/test_mods_size_table.py`: `zs.c` beside the others. - `README.md`: the draft's Mods section with Longshot's names; main's note on tables for several game versions kept. The Mods tab's file is `longshot_mods.txt`. - `tests/test_mods_menu.py`: version 22 and the new order of the host's functions. What the review changed (the last commit): - `host/mods.c` `hos_mods_file`: no table for prompts.c when the merged one is served. - `host/mods.c` `mod_text`, `exefs_status`: a mod that is off says "Its ExeFS patches are not applied while it is off", in the tab and in the log, instead of asking for a build. - `host/mods.c`: "Longshot" in the Mods tab's texts and in `longshot_mods.txt`'s header. - `host/mods.c` `fit_sizes`, `game_case`: a mod file whose path is in another case than the game's takes the game's entry. The game's case is looked up only when the mod's own case misses the table, so a mod whose paths match costs no extra reads at the start. - `gpu/vk/vk_menu.cpp` `menu_tick`: a Restart or Quit waiting for its second press is cleared on the Mods tab. - `nx2/exefs_patches.py` `classify`: bytes into a relocated word are left out as before, the file's note in `exefs_patches.txt` now says how many, and the comment says what happens to DT_RELR words (`tests/test_exefs_patches.py`, the README). ## Flags and settings | Flag | What it does | Default now | Recommendation | Helps on | Doesn't help on | Untested on | |---|---|---|---|---|---|---| | Mods tab (Insert menu) | lists the mods, each On or Off, in an order that can be changed; their cheats | shown when there are mods | on | every system | - | Linux | | `longshot_mods.txt` (game folder) | the Mods tab's order and switches | none: the folders' names' order, all on | on | every system | - | Linux | | `mods/<name>/exefs/*.ips`, `*.pchtxt` | code patches, applied when the game is built | applied at the next build | on | every system | - | pchtxt files, Linux | | `mods/<name>/cheats/<build id>.txt` | Atmosphere cheats | data cheats off until switched | on | every system | - | every system: no cheat file was tried | ## Measurements main: 39647e1a (2026-11-13) this PR: feat/mods-exefs-patches @ ca32fd3b Hardware: RTX 3090, Windows 11 Scene: a child save in Kokiri Forest and Hyrule Field, 2048x1152, 60 fps Method: one start and about twelve minutes of play by the mod's author, with the mod of the build lines below as the only mod; then a second start; the numbers are `longshot_log.txt` lines. The merge's time is from the two starts of this PR's build (the first right after the build, the second with the files read before). main has no merge to time against it, and without a `mods` folder `hos_mods_init` returns before any of it, so there is no cost when no mods are installed (read in the code, not timed) How to reproduce: a mod with ExeFS patches in `mods`, `nx2/make_game.py` into that game folder, start the game | | main @ 39647e1a | this PR @ ca32fd3b | |---|---|---| | A mod with ExeFS code patches | its `exefs` folder is left out | built in: the lines below | | The size table's merge at the start | none: main has no such step | 111 ms and 56 ms in two starts, median 84 ms (1 mod's table merged, 90 entries; 15 entries raised for mod files) | | The mod in play | its code patches do nothing | its menu page, picking characters, the swap in the field, its cloth and voice hooks: all work | | The Mods tab | none | lists the mod; moving in it and switching tabs behave | | Frame rate in play | | 60.0 fps in each 5 s line of the log | The build and the run are of 3c23346a, the same changes before the rebases onto 39647e1a (main's commits since touch release and CI files only; the docs commit came after), Windows 11, llvm-mingw: ``` exefs: code OoT Character Select/exefs/E0F3CCFAF50065AE4EA01EB5AEB13B1A45DA3348.ips (4935 bytes) exefs: main's code reaches 0x3c47000 now (a code cave) exefs: 4935 bytes of main patched by 1 files functions with code below their entry, started at it by a goto: 28 compiled in 5s, 0 failed the build took 0:02:17, compiled in 0:00:11 (the last one 0:15:18) ``` (The second build, after the review's changes: `mods.c` and `vk_menu.cpp` compiled again, no warning in either. The first, of the port itself, took 15 min 18 s.) The run's log: ``` mods: OoT Character Select: 534 RomFS files (16 replace the game's, 518 added), ExeFS patches mods: OoT Character Select: ExeFS E0F3CCFAF50065AE4EA01EB5AEB13B1A45DA3348.ips: built into the game mods: resource size table: 1 tables of mods merged (90 entries), 15 entries raised for mod files (0 added files estimated), served from modcache, 111 ms (second start) mods: resource size table: 1 tables of mods merged (90 entries), 15 entries raised for mod files (0 added files estimated), served from modcache, 56 ms mods: 1 mods from <game folder>\mods: 551 RomFS entries mods: System/Resource/ResourceSizeTable.Product.111.Nin_Ounce_NVN2.rsizetable.zs <- <game folder>\modcache/rsizetable.zs (merged) vi: 9492 frames presented (60.0 fps over the last 5.0 s, interval 1) ``` ## Testing **Tested:** - `python -I -B tests/run_all.py` on Windows 11: 462 passed, 70 skipped, 10 failed. The same 10 fail on main on this PC: `test_builder_details_hitbox`, `test_builder_dlss_update`, `test_portable_archive`, `test_portable_builder_home`, `test_portable_builder_startup` (no .NET SDK here), `test_fpx_sticky`, `test_spawn_emit` (they build the translator; this toolchain copy has no C++ standard headers), `test_check_third_party`, `test_release`, `test_setup_runtime`. - A Windows build with a mod that has six ExeFS code patches in one file (https://github.com/snaildev99/OoT-Character-Select): the lines above. - In the game (Windows 11, RTX 3090, a child save), played by the mod's author: the Insert menu's Mods tab lists the mod, and moving in it and switching tabs both ways behave as on the other tabs; the mod's own page opens from the game's menu, a character is picked and is the player in the field at once (young Zelda with her cloth, which goes through one of the code patches, and Saria), and both speak with their own voices (another of the patches). No crash, 60 fps. **Not tested:** - Switching a mod Off and On and the restart question, moving a mod up or down, and the mouse on the tab's lines. Only looking, moving the selection and changing tabs were tried. - The compact bar with the Mods tab current. - Linux. - A pchtxt mod, a cheat file, two mods that each ship a size table, a mod whose paths differ in case from the game's, a patch that writes a relocated word. **Needs other testers:** - Linux: the Mods tab with a RomFS mod, and a build with a mod that has ExeFS patches, expecting the same as on Windows (T-3AW7) ## Screenshots this PR @ 3c23346a (the build of the run above), Windows 11, RTX 3090, from the mod's author: ![this PR @ 3c23346a: the mod's Characters page in the game's own menu, King Zora picked, in a window whose title bar says Longshot](https://git.magicbean.dev/attachments/8155b8ed-38c5-4647-a19b-ddf20785c95d) The mod's Characters page in the game's own menu with King Zora picked, the game in a window (its title bar says Longshot). The page's fourth tab, the picked character's model and its view all come from the mod's ExeFS code patches, so this is those patches at work in a Longshot build. On main the patches are left out: nothing to show beside it. ![this PR @ 3c23346a: the Insert menu's Mods tab, the mod listed and On](https://git.magicbean.dev/attachments/69cd4070-b7a0-4f4a-9171-9d2a34ce7e66) The Insert menu (F1) of the same run: the Mods tab, with the mod listed and On. ## Risk and rollback The Mods tab adds a tab to the Insert menu's strip only when there are mods. With mods, the size table the game gets is the merged one, where before it was the last mod's own. A build with ExeFS patches lifts patched code: without such patches the lift and its key are as before. Reverting the commits is enough. ## Reviewer notes - The lift fix a code-cave routine needs is on main (#131), so its commit is no longer here. - The compact bar on the Mods tab: I draw the full Mods page (dimmed) from `draw_tune_bar`, because `tests/test_menu_tune.py` asks for `if (tune_mode) draw_tune_bar(k);` as it is. The other way would be to leave the Mods tab out of the strip while the bar is up. Say if you prefer that. - DT_RELR words: I took the second of your two ways (leave them out and say so). Applying them needs the addends read again after `apply()`; I can do that here or as a follow-up. - The draft's third commit on that branch (the banner's header) is not here: another topic. ## Issues See #42 ## Checklist ### For everyone - [x] One topic. - [x] The title is a Conventional Commit (`type(scope): description`). - [x] Rebased on current `main`. - [x] `python -I -B tests/run_all.py` run and passing (any failure that also fails on `main` is named above). - [x] Tested in the game, or what wasn't is named under Testing. - [x] Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests). - [x] Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported. - [x] No game files, dumps, build output or generated files. - [x] `third_party/` untouched (a new folder with a `PROVENANCE.md` instead). - [x] The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled. ### If applicable - [x] AI assistance recorded with `Assisted-by:` / `Co-Authored-By:` trailers. - [x] Flags noted, and the switch reference updated (docs/development.md, the README). - [x] Screenshots for a change that shows on screen. - [x] `MKWVK_API_VERSION` bumped (`gpu/vk/vk_api.h`'s `MkwVkHost` or `MkwVkRenderer` changed). - [x] A game-free test added for the fix, failing on `main` and passing here. - [x] A check under `tests/` that reads changed source updated in this PR, not weakened. - [ ] Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths. - [x] Docs updated. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
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). #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 <[email protected]>
Co-authored-by: conongnang <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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 <[email protected]>
a64-kit's emit_function writes a function's instructions in address order, so
the C function began at its lowest instruction. A function is its entry and
everything reachable from it without crossing another entry: code it branches
to at a lower address, which is no entry of its own, came first. 26 functions
of build e0f3ccfa are like that (25 in libcurl, which has one unwind entry for
all of its code, and the return address of a setjmp call), and so is a mod's
patch routine that goes on in spare code below its entry.

nx2/lift_fixes.py wraps emit_function: such a function starts with a goto to
its entry. tests/test_lift_entry_below.py checks it without the game.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
fix(menu): the compact bar leaves the Mods tab its full page
Some checks failed
PR description / Description follows the template (pull_request_target) Successful in 9s
CI / Conventional Commit title (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been cancelled
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been cancelled
CI / Workflow lint (actionlint) (pull_request) Has been cancelled
Third-party parts / As their notes say (pull_request) Has been cancelled
Third-party parts / The same as upstream (pull_request) Has been cancelled
1faee42e07
The compact bar (added to main after the Mods tab was written) draws the
selected row of the current tab. The Mods tab has no such rows: its lines come
from the host. With the bar asked for on the Mods tab the full page is drawn.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
HumanBean left a comment

Code review only: I read the diff and the code around it, and did not build or run anything.

The port is careful. The API bump to 22 with can_mods() checking it, the posix shims that zs.c and mods.c now need, the exefs patches going into the lift key through the environment, and the checks that read source being updated all look right. Some things need to change before this can merge:

  • The size table when the merged one is served: hos_mods_file still gives prompts.c the last mod's own table, which goes against its own comment and the description (inline).
  • Mods that are off are counted as waiting for a build: an off mod's ExeFS files are never in exefs_patches.txt, so the Mods tab tells the player to rebuild to apply patches that the build will always skip (inline).
  • The player sees Hookshot's name in Longshot: in the Mods tab text and in the header of longshot_mods.txt (inline).
  • Rebase on main: main already has #131 (cf0dbb34). The lift commit here (e20dc232) is the same change and becomes empty after a rebase. The "Rebased on current main" box is ticked, but the PR still sits on 1e5fcc20, and the Reviewer notes about merging 131 first are out of date.

There are also smaller points inline: RELR words in exefs_patches.py, the case of mod paths in the size table keys, and the Restart/Quit confirmation on the Mods tab. The size table is now merged at every start with mods (decoding the game's table and reading the header of every mod .zs), so please put that startup time in the Measurements along with the in-game run.

Code review only: I read the diff and the code around it, and did not build or run anything. The port is careful. The API bump to 22 with `can_mods()` checking it, the posix shims that `zs.c` and `mods.c` now need, the exefs patches going into the lift key through the environment, and the checks that read source being updated all look right. Some things need to change before this can merge: - **The size table when the merged one is served:** `hos_mods_file` still gives prompts.c the last mod's own table, which goes against its own comment and the description (inline). - **Mods that are off are counted as waiting for a build:** an off mod's ExeFS files are never in `exefs_patches.txt`, so the Mods tab tells the player to rebuild to apply patches that the build will always skip (inline). - **The player sees Hookshot's name in Longshot:** in the Mods tab text and in the header of `longshot_mods.txt` (inline). - **Rebase on main:** main already has https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/131 (cf0dbb34). The lift commit here (e20dc232) is the same change and becomes empty after a rebase. The "Rebased on current main" box is ticked, but the PR still sits on 1e5fcc20, and the Reviewer notes about merging 131 first are out of date. There are also smaller points inline: RELR words in exefs_patches.py, the case of mod paths in the size table keys, and the Restart/Quit confirmation on the Mods tab. The size table is now merged at every start with mods (decoding the game's table and reading the header of every mod `.zs`), so please put that startup time in the Measurements along with the in-game run.
@ -5105,1 +5247,3 @@
changed = true;
Row& r = rows[tab_row_to_global(cur_tab, selected)];
const bool confirm_selected = &r == &rows[kQuitRow] || &r == &rows[kRestartRow];
if (pending && (pending != &r || !mine)) pending = nullptr, changed = true;
Owner

Nit: now that this sits in the else branch for the tabs that aren't Mods, a pending Restart/Quit confirmation is no longer cleared when the player goes to the Mods tab. It stays armed for when they come back to that row. Clearing pending whenever cur_tab == TAB_MODS would keep main's behaviour.

Nit: now that this sits in the `else` branch for the tabs that aren't Mods, a pending Restart/Quit confirmation is no longer cleared when the player goes to the Mods tab. It stays armed for when they come back to that row. Clearing `pending` whenever `cur_tab == TAB_MODS` would keep main's behaviour.
host/mods.c Outdated
@ -167,3 +286,3 @@
return;
}
qsort(mods, (size_t)nmods, sizeof *mods, mod_cmp);
fputs("# Hookshot's mods: their order and switches, as the Mods tab sets them (Insert menu). One line a mod, top of the\n"
Owner

This header goes into the player's longshot_mods.txt: it should say Longshot's mods, not Hookshot's.

This header goes into the player's `longshot_mods.txt`: it should say Longshot's mods, not Hookshot's.
host/mods.c Outdated
@ -180,0 +350,4 @@
while (line && line != built && line[-1] != '\n') line = strstr(line + 1, want);
const char *kind = line ? line + strlen(want) : "";
if (!line) {
m->exefs_waiting++;
Owner

exefs_status also runs over mods that are off, but write_list only writes the files of mods that are on. So every ExeFS file of an off mod lands here as exefs_waiting, and mod_text tells the player to "build the game again (Hookshot Builder) to apply" patches that the build will skip. Count waiting files only for enabled mods. For an off mod, either say nothing or say that its patches are not applied while it is off.

`exefs_status` also runs over mods that are off, but `write_list` only writes the files of mods that are on. So every ExeFS file of an off mod lands here as `exefs_waiting`, and `mod_text` tells the player to "build the game again (Hookshot Builder) to apply" patches that the build will skip. Count waiting files only for enabled mods. For an off mod, either say nothing or say that its patches are not applied while it is off.
host/mods.c Outdated
@ -180,0 +454,4 @@
const Entry *e = &table[i];
if (!e->key || e->is_dir) continue;
char key[MAX_PATH], game[MAX_PATH], host[MAX_PATH];
restbl_key(e->rel, key, sizeof key);
Owner

Suggestion: the key uses the mod file's own path case (e->rel comes from fd.cFileName). RESTBL keys are CRC32s of the game's case-sensitive paths, so a mod that ships ui/... where the game has UI/... gets an entry under the wrong hash. On Windows, game_file still finds the game file, so restbl_get(base, key) misses and this continues as if the file were streamed. Taking the case from the game's own path (or from the shipped table's key) would make the raise work for those mods.

Suggestion: the key uses the mod file's own path case (`e->rel` comes from `fd.cFileName`). RESTBL keys are CRC32s of the game's case-sensitive paths, so a mod that ships `ui/...` where the game has `UI/...` gets an entry under the wrong hash. On Windows, `game_file` still finds the game file, so `restbl_get(base, key)` misses and this `continue`s as if the file were streamed. Taking the case from the game's own path (or from the shipped table's key) would make the raise work for those mods.
host/mods.c Outdated
@ -202,2 +609,4 @@
snprintf(key, sizeof key, "%s", rel);
lower_slash(key);
if (sizes_key[0] && !strcmp(key, sizes_key)) {
if (!sizes_fallback[0]) return 0;
Owner

This returns the last mod's own table even when sizes_served (the merged table) is what gets served. The comment above and the PR description both say "none when the merged one is served". prompts.c build() then checks and merges into that mod table. If prompts can't merge it (a frame without a content size, or a table not in the game's format), build() returns 0 and the player gets the game's own icons in texts, even though the served merged table already has the prompts' entries. Suggested fix: if (sizes_served[0] || !sizes_fallback[0]) return 0;.

This returns the last mod's own table even when `sizes_served` (the merged table) is what gets served. The comment above and the PR description both say "none when the merged one is served". prompts.c `build()` then checks and merges into that mod table. If prompts can't merge it (a frame without a content size, or a table not in the game's format), `build()` returns 0 and the player gets the game's own icons in texts, even though the served merged table already has the prompts' entries. Suggested fix: `if (sizes_served[0] || !sizes_fallback[0]) return 0;`.
host/mods.c Outdated
@ -238,0 +669,4 @@
k = 0, d = m->note;
if (m->overridden) ADD("%d of its files are taken by a mod lower in the list. ", m->overridden);
if (m->exefs_waiting)
ADD("%d ExeFS patch file%s %s not in the game yet: build the game again (Hookshot Builder) to apply %s. ", m->exefs_waiting,
Owner

The player sees this in Longshot's Mods tab: "Hookshot Builder" should be "Longshot Builder". The same goes for "Nothing Hookshot uses." a few lines up (668).

The player sees this in Longshot's Mods tab: "Hookshot Builder" should be "Longshot Builder". The same goes for "Nothing Hookshot uses." a few lines up (668).
@ -0,0 +237,4 @@
if word is not None and self.bound[word] == "JUMP_SLOT":
return "unsupported", "it writes a function pointer the dynamic linker binds at main+%X" % word, {}
if word is not None:
continue # (an explicit-addend relocation's word: the dynamic linker writes over it, as on the console. A DT_RELR
Owner

nso2.py turns DT_RELR words into RELATIVE records whose addend is read from the file before the patch, so they are in self.bound and their patch bytes are dropped here. On the console, the RELR addend is the (patched) word, so the patch takes effect there. The comment says the opposite of what happens. Either apply RELR words, which needs the addend recomputed after apply(), or fix the comment and treat them like the explicit-addend ones, with a note in exefs_patches.txt saying so.

nso2.py turns DT_RELR words into `RELATIVE` records whose `addend` is read from the file before the patch, so they are in `self.bound` and their patch bytes are dropped here. On the console, the RELR addend is the (patched) word, so the patch takes effect there. The comment says the opposite of what happens. Either apply RELR words, which needs the addend recomputed after `apply()`, or fix the comment and treat them like the explicit-addend ones, with a note in exefs_patches.txt saying so.
Owner

You cooking 🔥

You cooking 🔥
snaildev99 force-pushed feat/mods-exefs-patches from 1faee42e07
Some checks failed
PR description / Description follows the template (pull_request_target) Successful in 9s
CI / Conventional Commit title (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been cancelled
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been cancelled
CI / Workflow lint (actionlint) (pull_request) Has been cancelled
Third-party parts / As their notes say (pull_request) Has been cancelled
Third-party parts / The same as upstream (pull_request) Has been cancelled
to 841d80ed5d
Some checks failed
PR description / Description follows the template (pull_request_target) Successful in 7s
CI / Conventional Commit title (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been cancelled
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been cancelled
CI / Workflow lint (actionlint) (pull_request) Has been cancelled
Third-party parts / As their notes say (pull_request) Has been cancelled
Third-party parts / The same as upstream (pull_request) Has been cancelled
2026-11-13 06:42:47 +00:00
Compare
Author
Contributor

Thanks for the review. Rebased onto main @ 0eb3a3c6 (the lift commit dropped out, as #131 is in), and the last commit (841d80ed) takes each point:

  • The size table when the merged one is served: hos_mods_file now returns 0 then (sizes_served[0] || !sizes_fallback[0]), as you suggested. I checked that fit_sizes starts the merged table from prompts/rsizetable.zs, so the prompts' entries are in what is served.
  • Mods that are off: their ExeFS files are no longer told as waiting. The tab says "Its ExeFS patches are not applied while it is off", and the log's line for such a file had the same mistake and says "not applied: its mod is off" now.
  • Hookshot's name: the two texts of the Mods tab and the header of longshot_mods.txt.
  • Restart/Quit on the Mods tab: pending is cleared there.
  • The case of mod paths: game_case gives the path as the game's folders spell it, and fit_sizes asks for it only when the mod's own case misses the table, so a mod whose paths match the game's does no extra reads at the start. Windows only: where the file system tells the cases apart the path is left as it is.
  • RELR words: the comment now says what happens, and a file whose bytes into relocated words were left out says how many in its note in exefs_patches.txt (and the README says so). Applying them instead needs the addends read again after apply(): say if you want that here.

Built on Windows after these changes (mods.c and vk_menu.cpp compile without a warning; the build's lines are in the description). The full run_all is 462 passed with the same 10 failures as main on this PC. Not run in the game yet: that is next, with the merge's start-up time from the log.

Thanks for the review. Rebased onto main @ 0eb3a3c6 (the lift commit dropped out, as #131 is in), and the last commit (841d80ed) takes each point: - **The size table when the merged one is served:** `hos_mods_file` now returns 0 then (`sizes_served[0] || !sizes_fallback[0]`), as you suggested. I checked that `fit_sizes` starts the merged table from `prompts/rsizetable.zs`, so the prompts' entries are in what is served. - **Mods that are off:** their ExeFS files are no longer told as waiting. The tab says "Its ExeFS patches are not applied while it is off", and the log's line for such a file had the same mistake and says "not applied: its mod is off" now. - **Hookshot's name:** the two texts of the Mods tab and the header of `longshot_mods.txt`. - **Restart/Quit on the Mods tab:** `pending` is cleared there. - **The case of mod paths:** `game_case` gives the path as the game's folders spell it, and `fit_sizes` asks for it only when the mod's own case misses the table, so a mod whose paths match the game's does no extra reads at the start. Windows only: where the file system tells the cases apart the path is left as it is. - **RELR words:** the comment now says what happens, and a file whose bytes into relocated words were left out says how many in its note in `exefs_patches.txt` (and the README says so). Applying them instead needs the addends read again after `apply()`: say if you want that here. Built on Windows after these changes (`mods.c` and `vk_menu.cpp` compile without a warning; the build's lines are in the description). The full run_all is 462 passed with the same 10 failures as main on this PC. Not run in the game yet: that is next, with the merge's start-up time from the log.
snaildev99 force-pushed feat/mods-exefs-patches from 841d80ed5d
Some checks failed
PR description / Description follows the template (pull_request_target) Successful in 7s
CI / Conventional Commit title (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been cancelled
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been cancelled
CI / Workflow lint (actionlint) (pull_request) Has been cancelled
Third-party parts / As their notes say (pull_request) Has been cancelled
Third-party parts / The same as upstream (pull_request) Has been cancelled
to ca32fd3b1a
Some checks are pending
CI / Conventional Commit title (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Blocked by required conditions
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Blocked by required conditions
CI / Workflow lint (actionlint) (pull_request) Blocked by required conditions
Third-party parts / As their notes say (pull_request) Blocked by required conditions
Third-party parts / The same as upstream (pull_request) Blocked by required conditions
PR description / Description follows the template (pull_request_target) Successful in 9s
2026-11-13 06:48:17 +00:00
Compare
Author
Contributor

Run in the game (Windows 11, RTX 3090, a child save, the build of the review's changes): the Mods tab lists the mod and behaves; the mod's code patches all work (its menu page, picking a character, the swap in the field, the cloth and the voice hooks); the size table's merge took 111 ms at the start (1 table merged, 90 entries; 15 raised); 60 fps. The log's lines are in the description. Also: a line on longshot_mods.txt in docs/development.md, rebased onto main @ 39647e1a, and a test request for Linux (T-3AW7). Still to come: a picture of the Mods tab, and what the description lists under Not tested (switching and moving mods on the tab, a cheat file, a pchtxt mod).

Run in the game (Windows 11, RTX 3090, a child save, the build of the review's changes): the Mods tab lists the mod and behaves; the mod's code patches all work (its menu page, picking a character, the swap in the field, the cloth and the voice hooks); the size table's merge took 111 ms at the start (1 table merged, 90 entries; 15 raised); 60 fps. The log's lines are in the description. Also: a line on longshot_mods.txt in docs/development.md, rebased onto main @ 39647e1a, and a test request for Linux (T-3AW7). Still to come: a picture of the Mods tab, and what the description lists under Not tested (switching and moving mods on the tab, a cheat file, a pchtxt mod).
Author
Contributor

A picture from the run is in the description's Screenshots section.

A picture from the run is in the description's Screenshots section.
Author
Contributor

Two pictures from the run are in the description now: the mod's page in the game's menu (its code patches at work), and the Insert menu's Mods tab with the mod listed and On.

Two pictures from the run are in the description now: the mod's page in the game's menu (its code patches at work), and the Insert menu's Mods tab with the mod listed and On.
Author
Contributor

A second start of the same build: the size table's merge took 56 ms (111 ms on the first start, right after the build). Both lines are in the description. What keeps this WIP: the Linux build and run (T-3AW7 is open for it), which I cannot do here.

A second start of the same build: the size table's merge took 56 ms (111 ms on the first start, right after the build). Both lines are in the description. What keeps this WIP: the Linux build and run (T-3AW7 is open for it), which I cannot do here.
Author
Contributor

A correction for whoever takes T-3AW7: for its second step, do not use the mod's public v0.1.2 zip. Its patch file enters one routine with bl and comes back with a plain b, which the console and the emulators take as a return and the lift cannot (the rest of the calling function runs twice): it crashes at the first character picked. That is a fault of the mod, fixed in its source (one byte, the bl is a b) and in the build I ran here; a v0.1.3 of the mod with it is on its way, and I will link it here. The first step (the Mods tab with any RomFS mod) is unaffected.

A correction for whoever takes T-3AW7: for its second step, do not use the mod's public v0.1.2 zip. Its patch file enters one routine with `bl` and comes back with a plain `b`, which the console and the emulators take as a return and the lift cannot (the rest of the calling function runs twice): it crashes at the first character picked. That is a fault of the mod, fixed in its source (one byte, the `bl` is a `b`) and in the build I ran here; a v0.1.3 of the mod with it is on its way, and I will link it here. The first step (the Mods tab with any RomFS mod) is unaffected.
Author
Contributor

A mod package for testing this, for T-3AW7 and anyone who wants to see ExeFS patches at work:

https://github.com/snaildev99/OoT-Character-Select/releases/tag/v0.1.3 (OoT-Character-Select-v0.1.3-lite.zip, 171 MB, SHA-256 ff60cd9bd4f7c5ca1cdbfbb2242fd8fd2dfa08e221e856dd1c2c65d64dd2d5e1)

It is the mod of the measurements above: one mod folder with RomFS files, a resource size table and one .ips file of six code patches, for the game's 1.1.1 build.

  1. Copy the folder OoT Character Select from the zip's mods folder into the game folder's mods.
  2. Build from this branch into that game folder (nx2/make_game.py). Expected in the build's output: exefs: code OoT Character Select/exefs/E0F3CCFAF50065AE4EA01EB5AEB13B1A45DA3348.ips (4935 bytes) and exefs: main's code reaches 0x3c47000 now (a code cave).
  3. Start the game. Expected in longshot_log.txt: ExeFS ... built into the game and the mods: resource size table: 1 tables of mods merged line; the Insert menu shows the Mods tab with the mod On.
  4. In a save: open the − menu, go left from Key Items to the Characters page, pick a character with A, close the menu. The character is the player in the field.

Take v0.1.3, not v0.1.2: the older package has a routine that is entered with bl and goes back with a branch, which the lift cannot run (the function's tail runs twice), and it crashes at the first pick. That was the mod's fault and is fixed in v0.1.3; it is the one byte between the two packages.

Still open here: the checklist's Linux box, which is why this stays WIP. Nothing changed on the branch since ca32fd3b.

A mod package for testing this, for T-3AW7 and anyone who wants to see ExeFS patches at work: https://github.com/snaildev99/OoT-Character-Select/releases/tag/v0.1.3 (`OoT-Character-Select-v0.1.3-lite.zip`, 171 MB, SHA-256 `ff60cd9bd4f7c5ca1cdbfbb2242fd8fd2dfa08e221e856dd1c2c65d64dd2d5e1`) It is the mod of the measurements above: one mod folder with RomFS files, a resource size table and one `.ips` file of six code patches, for the game's 1.1.1 build. 1. Copy the folder `OoT Character Select` from the zip's `mods` folder into the game folder's `mods`. 2. Build from this branch into that game folder (`nx2/make_game.py`). Expected in the build's output: `exefs: code OoT Character Select/exefs/E0F3CCFAF50065AE4EA01EB5AEB13B1A45DA3348.ips (4935 bytes)` and `exefs: main's code reaches 0x3c47000 now (a code cave)`. 3. Start the game. Expected in `longshot_log.txt`: `ExeFS ... built into the game` and the `mods: resource size table: 1 tables of mods merged` line; the Insert menu shows the Mods tab with the mod On. 4. In a save: open the − menu, go left from Key Items to the Characters page, pick a character with A, close the menu. The character is the player in the field. Take v0.1.3, not v0.1.2: the older package has a routine that is entered with `bl` and goes back with a branch, which the lift cannot run (the function's tail runs twice), and it crashes at the first pick. That was the mod's fault and is fixed in v0.1.3; it is the one byte between the two packages. Still open here: the checklist's Linux box, which is why this stays WIP. Nothing changed on the branch since ca32fd3b.
Some checks are pending
CI / Conventional Commit title (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Blocked by required conditions
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Blocked by required conditions
CI / Workflow lint (actionlint) (pull_request) Blocked by required conditions
Third-party parts / As their notes say (pull_request) Blocked by required conditions
Third-party parts / The same as upstream (pull_request) Blocked by required conditions
PR description / Description follows the template (pull_request_target) Successful in 9s
This pull request is marked as a work in progress.
Some workflows are waiting to be reviewed.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u 'feat/mods-exefs-patches:snaildev99-feat/mods-exefs-patches'
git switch 'snaildev99-feat/mods-exefs-patches'
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
MagicBeanDev/Longshot!134
No description provided.