From c03fcf4746dae3a161b6dfec9136b67cf08546c5 Mon Sep 17 00:00:00 2001
From: Matthew Stanley <1379tech@gmail.com>
Date: Thu, 30 Jul 2026 14:54:34 -0700
Subject: [PATCH] docs: add mod authoring guide
---
README.md | 164 +++++++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 162 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 2178391d..6ea297c1 100644
--- a/README.md
+++ b/README.md
@@ -180,10 +180,170 @@ turned off as easily as it was turned on.
-Tomba! ships three today — widescreen, skip-FMV, and a warp debug menu that
-re-enables the developers' own hidden menu. Full format in
+### Add a mod
+
+Start with a source directory containing a manifest and, when needed, payload
+files:
+
+```text
+my-mod/
+├── manifest.toml
+├── README.txt
+└── assets/
+ └── replacement.bin
+```
+
+A minimal guarded executable patch looks like this:
+
+```toml
+format_version = 1
+id = "example.quick-start"
+version = "1.0.0"
+name = "Quick-start example"
+author = "Your name"
+description = "One independently toggleable gameplay change."
+resolver = "declarative"
+save_compatibility = "shared"
+
+[[target]]
+game_id = "SLUS-00000"
+# Replace this with the SHA-256 of the exact supported stock disc image.
+disc_sha256 = "0000000000000000000000000000000000000000000000000000000000000000"
+
+[[feature]]
+id = "quick-start"
+name = "Quick Start"
+description = "Skips the game's startup delay."
+group = "Gameplay"
+default_enabled = false
+
+[[patch]]
+feature = "quick-start"
+target = "main_exe"
+address = 0x80041234
+expected = "2a 00 02 24"
+replace = "00 00 02 24"
+```
+
+`main_exe` addresses are PSX guest virtual addresses. The complete `expected`
+bytes are checked after the BIOS loads the executable and before any write is
+made, so a package fails closed on the wrong revision. For disc changes use
+`disc_raw` or `disc_user`; for a larger asset use a file-backed `[[overlay]]`
+with payload and expected-range SHA-256 hashes.
+
+From a PSXRecomp checkout, pack the directory into a deterministic archive:
+
+```sh
+python tools/psxmod_pack.py my-mod my-mod-1.0.0.psxmod
+```
+
+Install the resulting `.psxmod` through the launcher's Mods manager. A game can
+also ship reviewed, default-disabled packages by placing unpacked sources at
+`mods/preloaded/packages///` and copying
+`mods/preloaded` beside the game executable as `mods`; see Tomba's build wiring
+below.
+
+Choose the narrowest mechanism that describes the change:
+
+| Change | Package mechanism |
+|---|---|
+| Fixed code or data bytes | Guarded `[[patch]]` on `main_exe`, `disc_raw`, or `disc_user` |
+| A player-selectable boolean, choice, or number | Feature-local `[[option]]` plus `when`, `replace_from`, sparse `fields`, or `when_integer` |
+| Artwork, script, audio, or another large disc asset | Hashed file-backed `[[overlay]]`; do not rebuild the player's stock image |
+| Host setting or live game behavior | Trusted static `[[plugin]]`, compiled into the game and selected by a stable id |
+| Several features composing one shared table, bitfield, routine, or allocation | Game-owned `resolver = "builtin:"`, only when declarative operations cannot express the composition |
+
+Format versions 2–4 add bounded integers, ordered constraints, linked MIPS
+`LUI`/`ORI` values, sparse owned fields, and integer predicates. Format 5 adds
+trusted static plugins. The full schemas and resolution rules are in
[`docs/MOD_PACKAGES.md`](docs/MOD_PACKAGES.md).
+The enabled feature set is resolved before boot. Changing it may require a
+relaunch, but never asks the player to patch a disc or compile the game.
+
+### Trusted game code
+
+A `.psxmod` never supplies or loads native code. If a feature needs a per-VBlank
+callback or must configure a host-side facility, the game registers an
+implementation that is already linked into its executable:
+
+```c
+#include "mod_plugins.h"
+
+static void example_vblank(void) {
+ if (psx_mod_game_started())
+ psx_mod_write_byte(0x1F8001B4u, 1u);
+}
+
+PSX_MOD_CONSTRUCTOR(register_example_mod) {
+ (void)psx_mod_register_vblank_plugin(
+ "example.quick-start", example_vblank);
+}
+```
+
+The manifest activates it with:
+
+```toml
+format_version = 5
+
+[[plugin]]
+feature = "quick-start"
+id = "example.quick-start"
+```
+
+Activation callbacks are for one-time pre-renderer configuration; VBlank
+callbacks are deterministic guest-time work. Plugins receive only the narrow
+services in
+[`runtime/include/mod_plugins.h`](runtime/include/mod_plugins.h), can read
+validated feature options, and are selected by registry id—not by a library
+path or symbol supplied by an archive. Prefer ordinary patches and overlays
+whenever they are enough.
+
+### Converting an existing patch or patcher
+
+Large legacy mod suites should be converted feature by feature:
+
+1. Pin the exact clean disc revision and keep the original patcher or patched
+ output as a byte-for-byte **test oracle**, not as the runtime payload.
+2. Split its UI into independent features and typed, feature-local options.
+ Enabling one feature must not silently select another.
+3. Map each owned change back to the stock image: executable writes become
+ guarded `main_exe` patches, small disc writes become guarded disc patches,
+ and large assets become content-addressed overlays.
+4. Use sparse fields when features own adjacent bytes in one record. Introduce
+ a trusted built-in resolver only when independent selections must synthesize
+ one shared semantic object.
+5. Test every feature alone, representative combinations, all-off identity,
+ wrong-revision rejection, collision diagnostics, and byte parity with the
+ oracle. Record authorship and redistribution permission for every imported
+ asset.
+
+Do not ship a prepatched disc or use the legacy `derived_disc`/VCDIFF path for
+new packages. It remains conversion scaffolding only.
+
+### Real examples
+
+- [Tomba's merged catalog](https://github.com/mstan/TombaRecomp/tree/master/mods/preloaded/packages)
+ is the compact starting point. Its
+ [trusted plugins](https://github.com/mstan/TombaRecomp/tree/master/src/mods)
+ show activation callbacks, guest-VBlank behavior, launcher options, display
+ features, controller policy, and skip-FMV behavior; its
+ [CMake file](https://github.com/mstan/TombaRecomp/blob/master/CMakeLists.txt)
+ shows how to compile the plugin sources and preload the package catalog.
+- [Mega Man X6's unmerged Tweaks package branch](https://github.com/mstan/MegaManX6Recomp/tree/integrate/mmx6-mod-packages-current)
+ is the large conversion example. The
+ [package generators](https://github.com/mstan/MegaManX6Recomp/tree/integrate/mmx6-mod-packages-current/tools)
+ preserve patcher parity while producing guarded stock-targeted operations and
+ content-addressed assets; the
+ [trusted resolvers](https://github.com/mstan/MegaManX6Recomp/tree/integrate/mmx6-mod-packages-current/src/mods)
+ demonstrate deterministic composition of shared records and injected code.
+ The earlier
+ [`feature/mmx6-tweaks`](https://github.com/mstan/MegaManX6Recomp/tree/feature/mmx6-tweaks)
+ branch records the patcher-parity and write-classification work that feeds
+ this conversion; its patched-disc/pre-bake experiments are history, not the
+ recommended package delivery model. Both branches are review material rather
+ than a released compatibility promise.
+
## Swap language on the fly
A general localization layer captures the game's own strings out of memory and