Add manifest-driven required mod imports

This commit is contained in:
anxiousintrovert
2026-08-14 14:22:46 -05:00
parent 545a99d86d
commit 542856c83d
19 changed files with 1224 additions and 14 deletions
+41
View File
@@ -44,6 +44,23 @@ Every mod contains a root `manifest.json` defining its metadata, supported games
"optional_dependencies": [
"gen1_modern_ui"
],
"required_imports": [
{
"id": "stadium2",
"name": "Pokemon Stadium 2 ROM",
"file": "stadium2.z64",
"format": "n64",
"md5": ["00000000000000000000000000000000"]
}
],
"optional_imports": [
{
"id": "bonus_source",
"name": "Optional bonus source",
"file": "bonus.bin",
"md5": "00000000000000000000000000000000"
}
],
"conflicts": [],
"permissions": ["engine_internals"],
"description": "A brief description of the mod.",
@@ -67,6 +84,8 @@ Every mod contains a root `manifest.json` defining its metadata, supported games
| `priority` | `integer` | Load priority order (lower numbers load earlier; dependencies always precede dependents regardless of priority). |
| `dependencies` | `array` | Hard required dependencies. A mod will not load if a required dependency is missing or disabled for the active game. |
| `optional_dependencies` | `array` | Soft dependencies. Guarantees that if the target mod is present and active, it loads *before* this mod without blocking load if absent. |
| `required_imports` | `array` | User-supplied files required by this mod. The launcher validates and copies each file into this mod's `baseroms/` directory; the mod does not load while one is missing. |
| `optional_imports` | `array` | User-supplied files that unlock optional mod functionality. They use the same validation and private-copy flow but never block the mod from loading. |
| `conflicts` / `incompatible` | `array` | List of mod IDs that cannot run concurrently with this mod. |
| `permissions` | `array` | Requested privileges (e.g. `["engine_internals"]`, `["network"]`, `["filesystem"]`). |
| `github` | `string` | GitHub repository (`"owner/repo"`) used for update checks and dependency download links. |
@@ -91,6 +110,28 @@ Dependencies in `dependencies` and `optional_dependencies` can be declared in se
#### Version-Scoped Dependencies
When a mod supports multiple games (`"games": ["gen1", "gen2"]`), a dependency can specify `"games": ["gen2"]` to indicate it is only required when booting Gen 2. When booting Gen 1, the engine will ignore the dependency, preventing unnecessary boot blocks on games that do not need it.
### Required user-supplied files
`required_imports` and `optional_imports` keep copyrighted or otherwise user-owned source material
out of mod archives while giving every platform the same installation flow.
Each object requires a stable `id`, a display `name`, a destination `file`
(a filename, never a path), and one MD5 digest or an array of accepted MD5
digests. `format` is either `"raw"` (the default) or `"n64"`.
For `"n64"`, the launcher recognizes `.z64`, `.v64`, and `.n64` byte orders,
strips a recognized 512-byte copier header, converts the bytes to canonical
big-endian `.z64` order, and then checks MD5. The canonical bytes are written
to `mods/<mod-id>/baseroms/<file>`. Another installed mod with an overlapping
accepted MD5 automatically supplies a copy, so the player only selects a ROM
once. Mods read the result with their existing scoped `mod:read` API, for
example `mod:read("baseroms/stadium2.z64")`; no host path or new filesystem
permission is exposed. Missing `required_imports` block the mod before its
entry chunk runs; missing `optional_imports` remain visible in the same
launcher panel but do not block loading.
MD5 here identifies a known dump; it is not used as a security or authenticity
guarantee. Mod archives must not include anything beneath `baseroms/`.
## Mods and Gold (Gen 2)
The mod API is one API across both generations, but Gold runs its own battle
+105
View File
@@ -0,0 +1,105 @@
# RFC 0010 — Manifest-declared user imports for mods
## Status
Proposed. Engine: `Manifest.lua`, `RequiredImports.lua`, `Loader.lua`,
`LauncherMods.lua`. Launcher: `RomImporter.lua`, `LauncherView.lua`. Native
picker bridges: Android and iOS.
## Motivation
Some mods derive presentation data from another cartridge the player owns.
Shipping those bytes in a mod is not acceptable, while asking each mod to
escape the sandbox, implement native pickers, and maintain platform-specific
paths defeats the sandbox's purpose. Pokemon Stadium and Pokemon Stadium 2 are
the first concrete consumers, but the ownership/import problem is generic.
## The decision it extends
This extends the manifest as the engine-owned declaration of mod dependencies
and preserves the sandbox rule in `src/mods/Sandbox.lua`: mods do not receive
raw host filesystem access. It also extends the launcher's established ROM,
save, and mod archive picker/inbox flows instead of introducing a second host
integration.
## Exact manifest delta
Additive `required_imports` and `optional_imports` arrays are accepted:
```json
{
"required_imports": [{
"id": "stadium2",
"name": "Pokemon Stadium 2 ROM",
"file": "stadium2.z64",
"format": "n64",
"md5": ["00000000000000000000000000000000"]
}]
}
```
Both arrays use the same object schema. A missing `required_imports` entry
blocks the mod; a missing `optional_imports` entry only leaves that bonus
functionality unavailable.
- `id` is unique within the manifest and uses the mod-id vocabulary.
- `name` is launcher-facing text.
- `file` is one safe filename below the mod's `baseroms/` directory.
- `md5` is one 32-digit hexadecimal digest or a non-empty array of accepted
digests. MD5 is a known-dump identity convention, not a trust primitive.
- `format` is `raw` by default. `n64` recognizes a canonical big-endian dump,
pair-byte-swapped and little-endian-word dumps, with or without a recognized
512-byte copier header. Validation and stored output use canonical big-endian
bytes.
The launcher copies a validated file to
`mods/<manifest.id>/baseroms/<file>`. Missing required imports make the launcher
row need attention and make the loader refuse that enabled mod before its entry
chunk runs. Missing optional imports remain selectable without changing load
status. A matching validated import owned by another installed mod is
copied automatically. Replace and remove remain explicit per-mod actions.
Desktop and UWP use their existing native/host picker routes. Android and iOS
add a `required_import` picker kind which stages
`picked_required_import.bin`. NX scans the engine-owned
`imports/baseroms/` MTP inbox. All writes continue through `CacheFs`, preserving
portable-mode placement.
`modkit validate` and `modkit pack` report MK307 for every file beneath a
source mod's `baseroms/` directory, so the packaging path cannot accidentally
distribute a file the launcher placed there. The installer also rejects a mod
archive containing `baseroms/` files, covering packages built without modkit.
## Mod-facing API and sandbox statement
There is no new runtime API and no new permission. A mod reads its own copied
file through the existing `mod:read("baseroms/<file>")` capability. It never
learns the selected host path, cannot browse another mod's tree, and receives
no raw `io`, `love.filesystem`, or platform-picker access.
## Migration and compatibility
Existing manifests omit both import arrays and behave exactly as before. The
fields are additive for both manifest API levels. Mods currently maintaining
their own cross-platform picker can declare the source file and remove that
host integration; their processing code changes only to read the declared
`baseroms/<file>` path.
## Parity tests
- Empty/absent `required_imports` leaves existing manifests and loader behavior
unchanged.
- Manifest validation refuses path traversal, duplicate ids/files, malformed
MD5 values, and unknown normalization formats.
- N64 byte orders and the recognized copier-header form produce identical
canonical bytes before MD5 validation.
- A mismatched selection is never written.
- An enabled mod with a missing declared file never executes its entry chunk.
- A matching import in another installed mod is copied into the requesting
mod's own `baseroms/` directory.
- The packaging gate refuses every `baseroms/` file.
- The launcher refuses an archive that contains `baseroms/` files.
## Deprecation etiquette
Nothing deprecated or removed.