WIP: test(mods): exercise the documented RomFS file contract #93

Draft
mooki wants to merge 1 commit from mooki/Longshot:test/mods-romfs-contract into main
First-time contributor

Why

Mod authors need an example of the file route Longshot already supports, without mistaking co-op's internal hooks for a mod interface. This documents that route and runs its authored example through the real loader without a game dump. See #42.

WIP, based directly on main; independent of pull requests 51 and 68. This is documentation and test coverage, not a new loader or mod format. It does not settle the later chest-table, bridge, catalog or native-module decisions.

What changed

  • docs/mods.md: the current RomFS contract, with source symbols, discovery depth, whole-relative-path precedence, disabling, restart requirements, resource-size/prompt interaction and limitations. A JSON block describes invented text files for a game-free example; it is test input, not a manifest read by Longshot.
  • tests/test_mods_size_table.py, documented_example_checks: materializes that exact documentation example in a temporary folder and runs the existing compiled host/mods.c / host/prompts.c fixture. Six fresh processes check explicit/default roots, nested discovery, full-path order, non-conflicting files, case/slash lookup, ignored ExeFS, disabled parents and fallback after disabling every file mod. Selected file contents are checked, not just log messages.
  • README.md, Mods: corrects “folder name” to the whole relative path, says to restart after changes, and links the author guide.
  • No runtime, renderer, build, save, network, ABI or third-party code changes. Existing table-merge and file-routing assertions remain. No extra compilation is added: the example reuses the existing fixture executable.

Flags and settings

No new switches, options, menu rows or command-line flags. These existing settings are documented and exercised, not changed:

Flag What it does Default now Recommendation Helps on Doesn't help on Untested on
HOS_MODS Existing; documented and tested only: Chooses the folder containing mods Unset: mods beside the executable Off by default: keep the existing opt-in override Separate mod collections Mod format validation or save isolation Native Windows filesystem behavior
HOS_FS_OVERLAY Existing; documented only: Test overlay can override a selected mod file Unset Off by default: testing only Existing filesystem tests Checking ordinary mod precedence; leave it unset Overlay behavior was not exercised by the new example

The existing test driver's --cc and --source options are unchanged. No new setting needs a switch-reference entry.

Measurements

main: 77f605a25f (2026-11-12)
this PR: move/mods/romfs-contract @ 36539b7fa0
Suite totals: main 509 passed, 0 failed, 19 skipped; this PR 509 passed, 0 failed, 19 skipped.

Hardware: Linux 7.2.15-1-omarchy, x86_64; Python 3.12.16, GCC 16.2.1. No GPU used for the named fixture.
Scene: invented text files and resource tables, not a running game.
Method: one expanded named-driver run against this PR's source, then one against main's source. The existing --source option selects main's host directory; the documentation and new assertions remain this PR's in both runs. The whole game-free suite ran once on the exact final head. Main's full-suite receipt is the shared run on the pinned base. Times below are receipts, not a speed comparison; there were no interleaved timing pairs.
How to reproduce: python -I -B tests/test_mods_size_table.py; repeat with --source naming the host directory of a checkout at the stated main commit. Run the complete suite with python -I -B tests/run_all.py --jobs 6 --strict.

main @ 77f605a25 this PR @ 36539b7fa
Expanded driver against the selected loader source Pass: all six example starts and the existing checks Pass: all six example starts and the existing checks
Existing file/table routing cases 12 pass, including compressed and size-less frames 12 pass, including compressed and size-less frames
Whole game-free suite 509 passed, 0 failed, 19 skipped 509 passed, 0 failed, 19 skipped
Whole-suite execution time, not a performance claim 203 s 171 s

Both named runs print:

ok: merge_restbl: the larger entry of each file, entries of one side kept, sorted; 7 unmergeable tables refused
ok: documented RomFS example: HOS_MODS root, discovery, full-path order and lookup
ok: documented RomFS example: default root beside the executable
ok: documented RomFS example: disabled parent hides nested mods after restart
ok: documented RomFS example: disabled winner reveals the earlier mod after restart
ok: documented RomFS example: all file mods disabled: no replacements
ok: documented RomFS example: a single mod is not the collection root
ok: 12 cases served as they should: no mod, a mod with a table, two table versions in both open orders, one that grows a file without, two mods, a table that cannot be merged, no table of ours, a mod's own font, compressed and size-less frames

Whole-suite output, main then this PR:

509 passed, 0 failed, 19 skipped in 203 s (linux, 6 at once)
509 passed, 0 failed, 19 skipped in 171 s (linux, 6 at once)

Passing against main is expected: this records existing behavior, not a runtime bug fix. The previous updater reference-pack failure is not inherited here; main removed that updater test. The 19 skips remain explicit platform/device/dump requirements, not passes.

Testing

Tested: Linux execution of the expanded tests/test_mods_size_table.py on this branch and with main's loader source, including selected bytes from the exact documented example. Whole game-free suite on the final commit: 509 passed, 0 failed, 19 skipped. Existing source rules and table/prompt checks are retained; no C fixture source or compiler command changed.

Not tested: a game build or game run, a real Clawshot seed, progression, real controller prompts, native Windows execution, macOS filesystem behavior, or an unknown mod's safety. This driver already skips filesystem routing on Windows and macOS; a passing table-only run there would not prove the new example. No native-module, chest-table, checked-location, grant or Archipelago compatibility claim. No performance claim.

Needs other testers:

  • Windows: confirm the documented full-path precedence and disable/restart behavior with a locally generated mod and disposable copied saves, expecting the same selected replacement and the original file after all mods are disabled (request to open)

Screenshots

Nothing on screen. The example uses invented paths the game never requests, so a screenshot would not prove the contract.

Risk and rollback

The risk is misleading documentation or a fixture expectation that drifts from the loader. The example is read from the guide itself and checked through the real loader; the guide separates established behavior from proposals and warns that file mods are not a sandbox. Reverting this commit removes only docs and test coverage. It changes no installed files, saves or runtime defaults.

Reviewer notes

Start with documented_example_checks and the example table in docs/mods.md; compare host/mods.c's find_mods, mod_cmp, hos_mods_init and hos_mods_romfs. The nested example deliberately makes a folder named 00-nested win over 02-override, demonstrating that the whole relative path is compared.

Pull request 51 is the shared menu text editor, not the earlier Mods-tab proposal. Pull request 68 builds core co-op on it; coop_on_event is not a native-module ABI. This branch needs neither head. They also edit the README, but not its Mods section; this change does not touch their code.

Maintainers still need to choose the chest-table versus bridge-first direction, progression/save/conflict ownership, community recipes versus an official catalog, trusted native modules versus companions, and co-op module-first versus core-first. This test-only slice does not choose for them.

No game proof is needed to establish this documentation/test change: production behavior is unchanged. Before claiming current Longshot/Clawshot compatibility, a separate proof should run a locally generated seed for up to five minutes with a copied save, check a known changed reward and controller prompts, then restart for one minute with the mod disabled and confirm the original route. That run has not been requested or performed here and would not prove the higher tiers.

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. This documentation/test slice has one named run per source and one suite per commit, not interleaved timing pairs.
  • 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). Existing behavior only; no new switch-reference entry.
  • A check under tests/ that reads changed source updated in this PR, not weakened.
  • Docs updated.

New on this forge; not opened on GitHub.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra [email protected]

## Why Mod authors need an example of the file route Longshot already supports, without mistaking co-op's internal hooks for a mod interface. This documents that route and runs its authored example through the real loader without a game dump. See #42. **WIP, based directly on main; independent of pull requests 51 and 68.** This is documentation and test coverage, not a new loader or mod format. It does not settle the later chest-table, bridge, catalog or native-module decisions. ## What changed - `docs/mods.md`: the current RomFS contract, with source symbols, discovery depth, whole-relative-path precedence, disabling, restart requirements, resource-size/prompt interaction and limitations. A JSON block describes invented text files for a game-free example; it is test input, **not** a manifest read by Longshot. - `tests/test_mods_size_table.py`, `documented_example_checks`: materializes that exact documentation example in a temporary folder and runs the existing compiled `host/mods.c` / `host/prompts.c` fixture. Six fresh processes check explicit/default roots, nested discovery, full-path order, non-conflicting files, case/slash lookup, ignored ExeFS, disabled parents and fallback after disabling every file mod. Selected file contents are checked, not just log messages. - `README.md`, Mods: corrects “folder name” to the whole relative path, says to restart after changes, and links the author guide. - No runtime, renderer, build, save, network, ABI or third-party code changes. Existing table-merge and file-routing assertions remain. No extra compilation is added: the example reuses the existing fixture executable. ## Flags and settings No new switches, options, menu rows or command-line flags. These existing settings are documented and exercised, not changed: | Flag | What it does | Default now | Recommendation | Helps on | Doesn't help on | Untested on | |---|---|---|---|---|---|---| | `HOS_MODS` | Existing; documented and tested only: Chooses the folder containing mods | Unset: `mods` beside the executable | Off by default: keep the existing opt-in override | Separate mod collections | Mod format validation or save isolation | Native Windows filesystem behavior | | `HOS_FS_OVERLAY` | Existing; documented only: Test overlay can override a selected mod file | Unset | Off by default: testing only | Existing filesystem tests | Checking ordinary mod precedence; leave it unset | Overlay behavior was not exercised by the new example | The existing test driver's `--cc` and `--source` options are unchanged. No new setting needs a switch-reference entry. ## Measurements <!-- branch-state --> main: 77f605a25f175fbda9ad131ac6be96bbde85d9c2 (2026-11-12) this PR: move/mods/romfs-contract @ 36539b7fa0797ea9de4a26071e13ee8de0f0a5d7 Suite totals: main 509 passed, 0 failed, 19 skipped; this PR 509 passed, 0 failed, 19 skipped. <!-- /branch-state --> Hardware: Linux 7.2.15-1-omarchy, x86_64; Python 3.12.16, GCC 16.2.1. No GPU used for the named fixture. Scene: invented text files and resource tables, not a running game. Method: one expanded named-driver run against this PR's source, then one against main's source. The existing `--source` option selects main's `host` directory; the documentation and new assertions remain this PR's in both runs. The whole game-free suite ran once on the exact final head. Main's full-suite receipt is the shared run on the pinned base. Times below are receipts, not a speed comparison; there were no interleaved timing pairs. How to reproduce: `python -I -B tests/test_mods_size_table.py`; repeat with `--source` naming the `host` directory of a checkout at the stated main commit. Run the complete suite with `python -I -B tests/run_all.py --jobs 6 --strict`. | | main @ 77f605a25 | this PR @ 36539b7fa | |---|---|---| | Expanded driver against the selected loader source | Pass: all six example starts and the existing checks | Pass: all six example starts and the existing checks | | Existing file/table routing cases | 12 pass, including compressed and size-less frames | 12 pass, including compressed and size-less frames | | Whole game-free suite | 509 passed, 0 failed, 19 skipped | 509 passed, 0 failed, 19 skipped | | Whole-suite execution time, not a performance claim | 203 s | 171 s | Both named runs print: ```text ok: merge_restbl: the larger entry of each file, entries of one side kept, sorted; 7 unmergeable tables refused ok: documented RomFS example: HOS_MODS root, discovery, full-path order and lookup ok: documented RomFS example: default root beside the executable ok: documented RomFS example: disabled parent hides nested mods after restart ok: documented RomFS example: disabled winner reveals the earlier mod after restart ok: documented RomFS example: all file mods disabled: no replacements ok: documented RomFS example: a single mod is not the collection root ok: 12 cases served as they should: no mod, a mod with a table, two table versions in both open orders, one that grows a file without, two mods, a table that cannot be merged, no table of ours, a mod's own font, compressed and size-less frames ``` Whole-suite output, main then this PR: ```text 509 passed, 0 failed, 19 skipped in 203 s (linux, 6 at once) 509 passed, 0 failed, 19 skipped in 171 s (linux, 6 at once) ``` Passing against main is expected: this records existing behavior, not a runtime bug fix. The previous updater reference-pack failure is not inherited here; main removed that updater test. The 19 skips remain explicit platform/device/dump requirements, not passes. ## Testing **Tested:** Linux execution of the expanded `tests/test_mods_size_table.py` on this branch and with main's loader source, including selected bytes from the exact documented example. Whole game-free suite on the final commit: 509 passed, 0 failed, 19 skipped. Existing source rules and table/prompt checks are retained; no C fixture source or compiler command changed. **Not tested:** a game build or game run, a real Clawshot seed, progression, real controller prompts, native Windows execution, macOS filesystem behavior, or an unknown mod's safety. This driver already skips filesystem routing on Windows and macOS; a passing table-only run there would not prove the new example. No native-module, chest-table, checked-location, grant or Archipelago compatibility claim. No performance claim. **Needs other testers:** - Windows: confirm the documented full-path precedence and disable/restart behavior with a locally generated mod and disposable copied saves, expecting the same selected replacement and the original file after all mods are disabled (request to open) ## Screenshots Nothing on screen. The example uses invented paths the game never requests, so a screenshot would not prove the contract. ## Risk and rollback The risk is misleading documentation or a fixture expectation that drifts from the loader. The example is read from the guide itself and checked through the real loader; the guide separates established behavior from proposals and warns that file mods are not a sandbox. Reverting this commit removes only docs and test coverage. It changes no installed files, saves or runtime defaults. ## Reviewer notes Start with `documented_example_checks` and the example table in `docs/mods.md`; compare `host/mods.c`'s `find_mods`, `mod_cmp`, `hos_mods_init` and `hos_mods_romfs`. The nested example deliberately makes a folder named `00-nested` win over `02-override`, demonstrating that the whole relative path is compared. [Pull request 51](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/51) is the shared menu text editor, not the earlier Mods-tab proposal. [Pull request 68](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/68) builds core co-op on it; `coop_on_event` is not a native-module ABI. This branch needs neither head. They also edit the README, but not its Mods section; this change does not touch their code. Maintainers still need to choose the chest-table versus bridge-first direction, progression/save/conflict ownership, community recipes versus an official catalog, trusted native modules versus companions, and co-op module-first versus core-first. This test-only slice does not choose for them. No game proof is needed to establish this documentation/test change: production behavior is unchanged. Before claiming current Longshot/Clawshot compatibility, a separate proof should run a locally generated seed for up to five minutes with a copied save, check a known changed reward and controller prompts, then restart for one minute with the mod disabled and confirm the original route. That run has not been requested or performed here and would not prove the higher tiers. ## 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). - [ ] Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported. This documentation/test slice has one named run per source and one suite per commit, not interleaved timing pairs. - [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). Existing behavior only; no new switch-reference entry. - [x] A check under `tests/` that reads changed source updated in this PR, not weakened. - [x] Docs updated. --- New on this forge; not opened on GitHub. Assisted-by: GPT-6 Astra Co-Authored-By: GPT-6 Astra <[email protected]>
test(mods): exercise the documented RomFS file contract
Some checks are pending
Linux AppImage / appimage (pull_request) Blocked by required conditions
Windows builder / Longshot Builder zip (pull_request) Blocked by required conditions
Windows builder / Windows Vulkan back end (llvm-mingw) (pull_request) Blocked by required conditions
Windows builder / dav1d for Windows (llvm-mingw) (pull_request) Blocked by required conditions
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 / Python source checks (windows-2022, part 1/2) (pull_request) Blocked by required conditions
CI / Python source checks (windows-2022, 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) Waiting to run
36539b7fa0
Document the existing file-mod route and run its authored example through the real loader fixture. Cover full-path precedence, nested discovery, disabling across restarts, both root selections and ignored ExeFS without changing runtime policy.

See #42

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Some checks are pending
Linux AppImage / appimage (pull_request) Blocked by required conditions
Windows builder / Longshot Builder zip (pull_request) Blocked by required conditions
Windows builder / Windows Vulkan back end (llvm-mingw) (pull_request) Blocked by required conditions
Windows builder / dav1d for Windows (llvm-mingw) (pull_request) Blocked by required conditions
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 / Python source checks (windows-2022, part 1/2) (pull_request) Blocked by required conditions
CI / Python source checks (windows-2022, 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) Waiting to run
This pull request is marked as a work in progress.
Some workflows are waiting to be reviewed.
This branch is out-of-date with the base branch
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u 'test/mods-romfs-contract:mooki-test/mods-romfs-contract'
git switch 'mooki-test/mods-romfs-contract'
Sign in to join this conversation.
No description provided.