feat(mods): a Mods tab, merged resource size tables, ExeFS patches and Atmosphere cheats #2478

Open
hckrman101 wants to merge 4 commits from hckrman101/feat/mods-cheats into main
hckrman101 commented 2026-11-11 20:40:23 +00:00 (Migrated from github.com)

Branch: feat/mods-cheats, head f654ef2056 (5d9a877e, then a fix, a merge of main 940eb9ef, 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

  • Merged resource size table (host/mods.c, host/restbl.c, host/zs.c). At start, when mods serve files, Hookshot builds the size table the game reads:
    • It starts from the game's table, or from the button prompts' copy when they made one, so the prompts' entries stay in it.
    • Every enabled mod's own table is merged in; the larger entry wins.
    • Every file a mod serves gets its entry raised to fit its size. A file that grew is raised in proportion to the game's own allowance; a file the game doesn't have gets an estimate from Ocarina of Time's own table.
    • The tables are read with the game's zstd dictionaries when they need them. The result is written to modcache/rsizetable.zs and logged.
  • How this relates to #2270. #2270 merges the button prompts' entries into the one size table that wins (the last mod's), so a mod with its own table keeps the controller icons. This PR merges the tables of every enabled mod, and also grows entries for mod files that need it, so mods that each grow files load together. #2270's path stays as the fallback when no merged table can be made (the last mod's table with the prompts' entries merged in). This PR reuses #2270's zstd decoder build. #2270's test passes unchanged in behaviour; only its fixture's file list grew.
  • Mods tab (gpu/vk/vk_menu.cpp; MkwVkHost version 18). The tab is shown only when there are mods.
    • It lists the mods in their order, each On or Off. Left and Right switch one; Enter picks one up, Up and Down move it, and Enter puts it down. A mod lower in the list wins a file two mods have.
    • The order and switches are kept in hookshot_mods.txt and apply at the next start; closing the menu asks to restart ("the mods").
    • Without that file, the order is the folders' names, as today.
    • The help box says what each mod has: RomFS files replaced or added, files taken by a mod lower in the list, ExeFS patches built in or waiting for a build.
  • ExeFS patches (nx2/exefs_patches.py, nx2/image.py, nx2/make_game.py). mods/<name>/exefs/*.ips and *.pchtxt files (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.
    • A file counts when its build id matches (the IPS file's name, or the pchtxt's @nsobid).
    • A write into the padding after .text (a code cave) becomes code the lift translates.
    • Writes outside main, into the dynamic linker's tables, or into a lazily bound pointer make the whole file unsupported.
    • The patches are part of the lift's key; with no patches the lift and its key are as before.
    • The build writes exefs_patches.txt into 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).
  • Atmosphère cheats on the Mods tab (host/cheats.c). These are cheat-code files, mods/<name>/cheats/E0F3CCFAF50065AE.txt in Atmosphère/dmnt format. They are separate from the Debug tab's built-in cheats (#785 tools, oot_cheat_items.h), which this PR doesn't touch.
    • They run in a cheat VM after dmnt's (opcodes 0-A and C0-C3) at each presented frame, on guest memory the game was given (the image, the heap, address regions).
    • Each cheat is listed under its mod and switched at once; the switch is kept in hookshot_mods.txt. {Master} codes are always on.
    • A cheat that writes the game's code is listed as "Not supported" (the code is compiled); make it an ExeFS patch instead.
  • Menu header (gpu/vk/vk_menu.cpp). With six tabs, the build's text at the banner's left had no room: it shrank and then was hidden (at Large it was drawn at its size or not at all). The banner now starts with a small header that is always shown: Hookshot's icon (the start-up screen's 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.
  • Nothing changes without mods. No mods folder means no tab, nothing served or written, and no lift-key change. The cheat hook returns at once.

Credit

  • The list's order and switches, the size table's merge, the IPS/IPS32/pchtxt parsing and the cheat VM are ported from valentderah/totk-pc-recomp: runtime/mods.c, romfs.c, restbl.c, cheats.c and tools/exefs_patches.py. That project is MIT, Copyright (c) 2026 Valiantsin Dzerakh; its notice is kept in host/restbl.h and nx2/exefs_patches.py, and host/PROVENANCE.md names the files.
  • The cheat file loading and VM there started with conongnang.
  • The size estimates are Ocarina of Time's own, measured against the 1.1.1 table.

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.

    • Mod A grows Title.bntx (+64 KiB) and the HUD's Common.bntx (+64 KiB), with its own table.
    • Mod B grows the same Title.bntx (+256 KiB), with its own table.
    • Log: 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.
    • The HUD shows A's deliberately overwritten texture data in game, so A's grown file loaded even though the last mod's table alone doesn't fit it.
  • ExeFS patches.

    • Kabosune's First Person IPS32 (MIT): hooks at two sites plus code in the cave after .text. In game, R3 switches to first person (screenshots: before / after).
    • A harmless pchtxt of my own (mov x19, x0 re-encoded as add x19, x0, #0 in the camera view update): its bytes are in image.bin.
    • The build log says 1075 bytes of main patched by 2 files and main'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:

    • New: 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).
    • New: test_exefs_patches (IPS/IPS32/pchtxt, build ids, the mods' order, code/data/linker tables on a synthetic main, caves, the list).
    • New: test_cheat_vm (stores, buttons, pointer chains, loops, register conditions, out-of-guest reads, code writes refused).
    • New: test_mods_menu (the tab's lines and keys against a fake host, hidden without mods or with an older host).
    • Updated: #2270's 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.py checks the header's order of shortening and the shortened version.

Not tested

  • In-game effect of a data cheat. The VM ran in game with a cheat on, but the camera-FOV address I tried showed no visible change (the game likely recomputes it each frame). The VM itself is covered by test_cheat_vm.
  • The rebased build in game. The in-game runs above were on the same change before the rebase onto 2ed98c46. After the rebase, the game-free checks pass; the full rebuild (compile check) was still queued when this was written: see ~/.cache/hookshot-work/port-mods/build2.log.
  • A "before" run of main with the same mods (expected: mod A's grown HUD file doesn't fit the last mod's table).
  • Windows (the host code goes through the same Win32 calls the posix layer stands in for; not built for Windows here). AMD/Intel GPUs. The builder UI (no change there; a rebuild with new ExeFS patches is the usual Build/Update).
  • Atmosphère's atmosphere/exefs_patches/<name>/ layout and opcode C4/0xF extensions are not supported.
  • This PR and the amiibo PR both take MkwVkHost version 18; whichever lands second moves to 19.

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):

before

After, Medium and Large at 1920x1080, Small at 1280x720, and Medium with no mods:

after medium
after large
after small 720p
after no mods

Branch: feat/mods-cheats, head f654ef20560dddc30a0ab3f11440012be389cb62 (5d9a877e, then a fix, a merge of main 940eb9ef, 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 - **Merged resource size table** (host/mods.c, host/restbl.c, host/zs.c). At start, when mods serve files, Hookshot builds the size table the game reads: - It starts from the game's table, or from the button prompts' copy when they made one, so the prompts' entries stay in it. - Every enabled mod's own table is merged in; the larger entry wins. - Every file a mod serves gets its entry raised to fit its size. A file that grew is raised in proportion to the game's own allowance; a file the game doesn't have gets an estimate from Ocarina of Time's own table. - The tables are read with the game's zstd dictionaries when they need them. The result is written to `modcache/rsizetable.zs` and logged. - **How this relates to #2270.** #2270 merges the button prompts' entries into the one size table that wins (the last mod's), so a mod with its own table keeps the controller icons. This PR merges the tables of every enabled mod, and also grows entries for mod files that need it, so mods that each grow files load together. #2270's path stays as the fallback when no merged table can be made (the last mod's table with the prompts' entries merged in). This PR reuses #2270's zstd decoder build. #2270's test passes unchanged in behaviour; only its fixture's file list grew. - **Mods tab** (gpu/vk/vk_menu.cpp; MkwVkHost version 18). The tab is shown only when there are mods. - It lists the mods in their order, each On or Off. Left and Right switch one; Enter picks one up, Up and Down move it, and Enter puts it down. A mod lower in the list wins a file two mods have. - The order and switches are kept in `hookshot_mods.txt` and apply at the next start; closing the menu asks to restart ("the mods"). - Without that file, the order is the folders' names, as today. - The help box says what each mod has: RomFS files replaced or added, files taken by a mod lower in the list, ExeFS patches built in or waiting for a build. - **ExeFS patches** (nx2/exefs_patches.py, nx2/image.py, nx2/make_game.py). `mods/<name>/exefs/*.ips` and `*.pchtxt` files (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. - A file counts when its build id matches (the IPS file's name, or the pchtxt's `@nsobid`). - A write into the padding after `.text` (a code cave) becomes code the lift translates. - Writes outside main, into the dynamic linker's tables, or into a lazily bound pointer make the whole file unsupported. - The patches are part of the lift's key; with no patches the lift and its key are as before. - The build writes `exefs_patches.txt` into 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). - **Atmosphère cheats on the Mods tab** (host/cheats.c). These are cheat-code files, `mods/<name>/cheats/E0F3CCFAF50065AE.txt` in Atmosphère/dmnt format. They are separate from the Debug tab's built-in cheats (#785 tools, `oot_cheat_items.h`), which this PR doesn't touch. - They run in a cheat VM after dmnt's (opcodes 0-A and C0-C3) at each presented frame, on guest memory the game was given (the image, the heap, address regions). - Each cheat is listed under its mod and switched at once; the switch is kept in `hookshot_mods.txt`. `{Master}` codes are always on. - A cheat that writes the game's code is listed as "Not supported" (the code is compiled); make it an ExeFS patch instead. - **Menu header** (gpu/vk/vk_menu.cpp). With six tabs, the build's text at the banner's left had no room: it shrank and then was hidden (at Large it was drawn at its size or not at all). The banner now starts with a small header that is always shown: Hookshot's icon (the start-up screen's `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. - **Nothing changes without mods.** No mods folder means no tab, nothing served or written, and no lift-key change. The cheat hook returns at once. ### Credit - The list's order and switches, the size table's merge, the IPS/IPS32/pchtxt parsing and the cheat VM are ported from [valentderah/totk-pc-recomp](https://github.com/valentderah/totk-pc-recomp): `runtime/mods.c`, `romfs.c`, `restbl.c`, `cheats.c` and `tools/exefs_patches.py`. That project is MIT, Copyright (c) 2026 Valiantsin Dzerakh; its notice is kept in `host/restbl.h` and `nx2/exefs_patches.py`, and host/PROVENANCE.md names the files. - The cheat file loading and VM there started with conongnang. - The size estimates are Ocarina of Time's own, measured against the 1.1.1 table. ### 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.** - Mod A grows `Title.bntx` (+64 KiB) and the HUD's `Common.bntx` (+64 KiB), with its own table. - Mod B grows the same `Title.bntx` (+256 KiB), with its own table. - Log: `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. - The HUD shows A's deliberately overwritten texture data in game, so A's grown file loaded even though the last mod's table alone doesn't fit it. - **ExeFS patches.** - [Kabosune's First Person](https://github.com/Kabosune/oot-remake-modding) IPS32 (MIT): hooks at two sites plus code in the cave after `.text`. In game, R3 switches to first person (screenshots: before / after). - A harmless pchtxt of my own (`mov x19, x0` re-encoded as `add x19, x0, #0` in the camera view update): its bytes are in image.bin. - The build log says `1075 bytes of main patched by 2 files` and `main'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: - New: `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). - New: `test_exefs_patches` (IPS/IPS32/pchtxt, build ids, the mods' order, code/data/linker tables on a synthetic main, caves, the list). - New: `test_cheat_vm` (stores, buttons, pointer chains, loops, register conditions, out-of-guest reads, code writes refused). - New: `test_mods_menu` (the tab's lines and keys against a fake host, hidden without mods or with an older host). - Updated: #2270's `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.py` checks the header's order of shortening and the shortened version. ### Not tested - In-game effect of a data cheat. The VM ran in game with a cheat on, but the camera-FOV address I tried showed no visible change (the game likely recomputes it each frame). The VM itself is covered by `test_cheat_vm`. - The rebased build in game. The in-game runs above were on the same change before the rebase onto 2ed98c46. After the rebase, the game-free checks pass; the full rebuild (compile check) was still queued when this was written: see ~/.cache/hookshot-work/port-mods/build2.log. - A "before" run of main with the same mods (expected: mod A's grown HUD file doesn't fit the last mod's table). - Windows (the host code goes through the same Win32 calls the posix layer stands in for; not built for Windows here). AMD/Intel GPUs. The builder UI (no change there; a rebuild with new ExeFS patches is the usual Build/Update). - Atmosphère's `atmosphere/exefs_patches/<name>/` layout and opcode C4/0xF extensions are not supported. - This PR and the amiibo PR both take MkwVkHost version 18; whichever lands second moves to 19. 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): ![before](https://raw.githubusercontent.com/hckrman101/Hookshot/7d5f68c6d29039c2b775c6d91b67f62493e66bf8/mods/header-before-1920x1080-s3.png) After, Medium and Large at 1920x1080, Small at 1280x720, and Medium with no mods: ![after medium](https://raw.githubusercontent.com/hckrman101/Hookshot/7d5f68c6d29039c2b775c6d91b67f62493e66bf8/mods/header-after-1920x1080-s2.png) ![after large](https://raw.githubusercontent.com/hckrman101/Hookshot/7d5f68c6d29039c2b775c6d91b67f62493e66bf8/mods/header-after-1920x1080-s3.png) ![after small 720p](https://raw.githubusercontent.com/hckrman101/Hookshot/7d5f68c6d29039c2b775c6d91b67f62493e66bf8/mods/header-after-1280x720-s1.png) ![after no mods](https://raw.githubusercontent.com/hckrman101/Hookshot/7d5f68c6d29039c2b775c6d91b67f62493e66bf8/mods/header-after-nomods-1920x1080-s2.png)
Arkex-ai commented 2026-11-11 23:56:28 +00:00 (Migrated from github.com)

I'm prototyping the optional indexed RomFS image from #583 (tracked in #1007; no PR yet). At f654ef20, the storage boundaries to preserve are game_file, find_sizes, and the base RESTBL reads in merge_sizes: they currently inspect the loose RomFS folder directly.

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.

I'm prototyping the optional indexed RomFS image from #583 (tracked in #1007; no PR yet). At `f654ef20`, the storage boundaries to preserve are `game_file`, `find_sizes`, and the base RESTBL reads in `merge_sizes`: they currently inspect the loose RomFS folder directly. 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.
github-sync changed title from WIP: feat(mods): a Mods tab, merged resource size tables, ExeFS patches and Atmosphere cheats to feat(mods): a Mods tab, merged resource size tables, ExeFS patches and Atmosphere cheats 2026-11-12 19:01:34 +00:00
Owner

@mooki000 commented on GitHub · 2026-11-12 18:33 UTC · original

Thanks for the Mods tab work. I would like to build on your ordering and enable switches, not introduce another loader.
Your draft already separates restart-time RomFS changes from rebuild-time code patches (f654ef20, host/mods.c:1–28).
A later shared interface could let randomizers and co-op use named game events and safe item grants, while keeping your tab as the place players choose mods: I have written that up as a proposal in #2892.
First, I can offer a Linux test with a real, locally generated Clawshot seed and copied saves: loading, on/off across restart, ordering, merged size entries and controller prompts.
Clawshot already launches through HOS_MODS (755317b3, src/clawshot/pack/runtime.py:135–163).
I have not run your branch for this proposal, and that seed test would not establish ExeFS, cheat or Archipelago compatibility.
Would that test be useful on this head, or would you prefer it after your next integration with main?

> [@mooki000](https://github.com/mooki000) commented on GitHub · 2026-11-12 18:33 UTC · [original](https://github.com/NotRexed/Hookshot/pull/2478#issuecomment-6832461390) Thanks for the Mods tab work. I would like to build on your ordering and enable switches, not introduce another loader. Your draft already separates restart-time RomFS changes from rebuild-time code patches (`f654ef20`, [host/mods.c:1–28](https://git.magicbean.dev/MagicBeanDev/Hookshot/src/commit/f654ef20560dddc30a0ab3f11440012be389cb62/host/mods.c#L1-L28)). A later shared interface could let randomizers and co-op use named game events and safe item grants, while keeping your tab as the place players choose mods: I have written that up as a proposal in #2892. First, I can offer a Linux test with a real, locally generated Clawshot seed and copied saves: loading, on/off across restart, ordering, merged size entries and controller prompts. Clawshot already launches through HOS_MODS (`755317b3`, [src/clawshot/pack/runtime.py:135–163](https://github.com/hiKareeem/Clawshot/blob/755317b359baece69be8c20cda60e3d641bb56eb/src/clawshot/pack/runtime.py#L135-L163)). I have not run your branch for this proposal, and that seed test would not establish ExeFS, cheat or Archipelago compatibility. Would that test be useful on this head, or would you prefer it after your next integration with main? <!-- github-sync:c:6832461390 -->
Commenting is not possible because the repository is archived.
No description provided.