6 Reference Manifest
bryanthaboi edited this page 2026-10-21 17:34:48 -04:00

Manifest reference

Every mod is a directory under mods/ with a manifest.json beside its entry file. A manifest written for an earlier API still validates unchanged: api 1 is lenient, api 2 is strict, and api 3 is strict plus the portable cross-generation surface (Portable API 3).

Smallest complete manifest

Where it goes: save this as manifest.json in your mod folder, beside the Lua file named by entry. Status: this is a complete starting file for a new API 2 mod; add fields only when your mod needs their behavior. Use "api": 3 instead for a mod that should run on Gen 1, Gen 2 and Gen 3 from one entry file, and add a games list.

{
  "id": "my_first_mod",
  "name": "My First Mod",
  "version": "1.0.0",
  "entry": "main.lua",
  "api": 2
}

id is the internal name other mods and your save namespace use. name is what players see. version identifies this release, and entry is the Lua file the loader runs. JSON uses double quotes, and a comma separates one field from the next. See Modding Basics for a complete folder example.

Fields

Field Type Default Meaning
id string required [A-Za-z0-9_-]+, unique across installed mods
name string required display name in the manager
version string required your own version; semver if you want other mods to range-check it
entry string required Lua file run at load time, relative to the mod dir
api integer 1 mod API this mod is written against: 1 lenient, 2 strict, 3 portable (see API levels); the engine provides 3
priority number 0 lower loads first, after dependencies
games array all Gen 1 games which games the mod runs on (tokens below)
gen2compat boolean false legacy spelling that adds the Gen 2 games to games; never needed with games
language boolean false declares a translation; online play checks the claim against what the mod writes (text, strings and font records only, no hooks, events or permissions)
dependencies array [] dependency entries (formats below); enforced
optional_dependencies array [] same formats; ordering and mod.find only
dependency_sources object none { "dep_id": "owner/repo" } GitHub fallbacks for entries that carry no repo hint of their own
conflicts array [] "id" or "id@<range>"; enforced
incompatible array [] alias for conflicts; merged into it at validate (same "id" / "id@<range>" syntax)
category string "OTHER" manager grouping
game_version string none semver range the engine must satisfy
description string "" manager detail text
github string none "owner/repo" or a github.com URL; enables launcher auto-update and Other versions
experimental boolean false when true, the mod stays off until the player enables it (with a confirm dialog)
profile string "content" content, overhaul or total_conversion
affects_link boolean true unless profile is content or language is true low-level cart fingerprint metadata; never permission for freely enabled loose-mod link play
log_url string none https:// URL that mod.postLog reports to; needs the network permission (api 2 and up)
permissions array [] network, filesystem, engine_internals, steps, background, compute
required_imports array [] user-supplied files the mod needs; the mod does not load while one is missing (details below)
optional_imports array [] user-supplied files that unlock optional functionality; same flow, never block loading
required_assets array [] installed importer asset packs the mod needs, { importer, pack, version? }; the mod fails at load while one is missing or outside its range (details below)
optional_assets array [] same shape; never blocks loading
options_schema string none file declaring the mod's options; must exist
assets_transforms string none file declaring asset transforms; must exist
force_enable_env string none environment variable name; when set to "1" it re-enables the mod over a saved disable (for platform-bridge mods bundled with one build's launcher)

category is a closed vocabulary (TWEAK, BALANCE, CONTENT, QUEST, MECHANIC, GRAPHICS, LANGUAGE, AUDIO, UI, TOOL, TOTAL_CONVERSION, OTHER; GAMEPLAY is an alias of TWEAK). An unknown value is a warning, not an error. Manifest strings are scrubbed to valid UTF-8, and every path field (entry, options_schema, assets_transforms, import file) is refused if it climbs out of the mod directory.

API levels

api pins the contract the mod was written against. Absent means 1.

api Validation Surface
1 lenient: an unknown profile, permission or games token is an attributed warning and falls back (content, no permission, dropped token) the original mod object; Gen 1 names and shapes
2 strict: the same violations are load errors adds the later facades (mod.storage, mod.fetch, mod.job, checkpoints, log_url)
3 strict, plus portable mod.content, mod.events, mod.hooks, mod.world and mod.battle become the portable services, and mod:supports exists

api above the number this engine provides (3, Version.modApi) is refused with requires mod API <n>; this engine provides <m>. api 3 is opt-in per mod; the 1 and 2 contracts do not change.

The games field

games is an array of tokens, case-insensitive, resolved against the game list in src/core/GameVersion.lua (src/mods/ModTargets.lua):

Token Covers
"gen1" Red, Blue, Yellow
"gen2" Gold, Silver, Crystal
"gen3" FireRed, LeafGreen, Ruby, Sapphire, Emerald
"frlg" FireRed, LeafGreen (a layout-family token)
"rse" Ruby, Sapphire, Emerald (a layout-family token; the Hoenn games)
a version id "red", "blue", "yellow", "gold", "silver", "crystal", "firered", "leafgreen", "ruby", "sapphire", "emerald"
"all" every game

The resolved list is stored in launcher order. A manifest with no games and no gen2compat covers Red, Blue and Yellow only, which is exactly what it meant before the key existed; it is never inferred for Gen 2 or Gen 3. The loader gates per version, so "games": ["blue"] does not load on Red; a mod that names no game of the running generation is left out of that boot entirely (the manager shows ENABLED (NOT THIS GAME)), and the player can force it with TRY HERE ANYWAY. A hard dependency that does not run on the current game takes its dependents down with it as a skip. Details: Compatibility.

Link setup belongs in the launcher. Select a vanilla cart or a sealed custom cart; the latter runs its pinned mod set and is the supported modded route. Freely enabled loose mods, including cosmetic and translation mods, are not a supported player link setup. affects_link remains in the manifest because low-level cart fingerprint code reads it; it is not a compatibility opt-in for loose mods.

Version ranges

The same grammar serves game_version and the @range half of a dependency or conflict entry.

Form Matches
1.2.3 exactly that version (=1.2.3 is the same)
>=1.0 that version or later; also >, <, <=
^1.2 >=1.2 <2.0; the caret pins the leftmost non-zero component, so ^0.2 is >=0.2 <0.3
>=1.0 <2.0 space-separated comparators all have to hold
^1.4 || ^2.0 || separates alternatives

Missing components default to zero (1.2 is 1.2.0) and a pre-release sorts before its release (1.0.0-beta < 1.0.0). A malformed range fails manifest validation, so a typo is caught before the mod runs.

Dependencies and conflicts

Dependency entry formats

An entry in dependencies or optional_dependencies is any of:

  1. "mod_id"

  2. "mod_id@^1.2.0" (version-pinned)

  3. "mod_id#owner/repo" or "mod_id@^1.2.0#owner/repo" (repository-hinted: the launcher can offer the dependency's GitHub releases as a download)

  4. a structured object:

    { "id": "mod_id", "range": "^1.2.0", "games": ["gen2"], "github": "owner/repo" }
    

The object form also accepts version for range and repo for github. A repo hint that fails to normalize is dropped rather than failing validation; dependency_sources at the manifest top level supplies the same hint for entries that carry none (src/mods/Manifest.lua, parseSpecs).

games on a dependency scopes when it is enforced. A mod declaring "games": ["gen1", "gen2"] can mark a hard dependency "games": ["gen2"]: booting Red ignores it entirely, booting Gold requires it. Optional integrations that are optional everywhere stay in optional_dependencies.

Resolution

Resolution runs over the enabled mods and fails only the mods that cannot be satisfied. A hard dependency that is missing, disabled, failed, or outside its declared range fails the mod that declared it, and anything depending on that mod fails in turn. optional_dependencies never fail a mod: they order the load so a present target is already loaded when mod.find asks for it.

A conflicts entry that is co-enabled fails the declaring mod -- it asserted the incompatibility, so the outcome does not depend on priority. Two mods naming each other both fail, and the manager offers to switch one off. incompatible is the same list under a friendlier name: both arrays are merged (first-wins on duplicate entries) before conflict checks run.

A dependency cycle fails exactly the mods in the cycle. Mods beside it load normally.

Imported asset packs

required_assets and optional_assets name packs that the launcher's IMPORTERS tab exported from a dump the player supplied; one import serves every mod that declares it. Each entry is { importer, pack, version? } where importer and pack are lowercase [a-z0-9_-] ids and version is an optional semver range on the pack's export version. A missing or out-of-range required pack fails the mod at load with a pointer to the IMPORTERS tab; an optional pack is yours to feature-detect. Read them with mod.packs (Mod Object), which can only address packs you declared. Duplicate importer/pack pairs are refused.

Required user-supplied files

required_imports and optional_imports keep copyrighted or otherwise user-owned source material out of mod archives: the player supplies the file, the launcher validates it, and the mod reads the private copy. Each entry is an object:

Key Required Meaning
id yes stable identifier for the import
name yes display name in the launcher's import panel
file yes destination filename (a filename, never a path)
md5 yes one MD5 digest, or an array of accepted digests
format no "raw" (default) or "n64"
description no dump or region guidance shown to the player
size no exact canonical byte length
max_size no per-import ceiling when an exact size is wrong

Validated bytes land in mods/<mod-id>/baseroms/<file>, and the mod reads them with its ordinary scoped mod:read("baseroms/<file>"): no host path and no new filesystem permission. For "n64", the launcher accepts .z64 / .v64 / .n64 byte orders, strips a recognized 512-byte copier header, converts to canonical big-endian .z64 order, and hashes the result. Every import also has an engine-enforced 128 MiB ceiling.

Each selection is a private grant to that one mod; the launcher never copies another mod's imported files just because a manifest names the same digest. A missing required_imports entry blocks the mod before its entry chunk runs; a missing optional_imports entry shows in the same launcher panel but never blocks. MD5 identifies a known dump (it is what ROM databases publish); it is not a security guarantee, and it is not the SHA-1 the game-ROM importer uses. Mod archives must not contain anything under baseroms/: modkit pack refuses with finding MK307 (tools/modkit.py). Implementation: src/mods/RequiredImports.lua.

GitHub and experimental

github is optional. When set, the launcher MODS panel can check that repo's GitHub Releases for installable .zip assets: Update installs a newer release, Versions lets the player pick any published release (including older ones) to roll forward or back. Accepts owner/repo or a https://github.com/owner/repo URL; the loader normalizes both to owner/repo. A malformed value fails validation. Absent or empty means no auto-update UI for that mod.

experimental defaults to false. When true:

  • a missing options.mods[id] entry means disabled (normal mods default to enabled)
  • enabling in the launcher or in-game manager asks for confirmation ("this mod is marked experimental")
  • the MODS panel badges the row EXPERIMENTAL

Permissions

Mod code runs in a per-mod sandbox (src/mods/Sandbox.lua), and the manager shows a mod's permissions before the player enables it. Some permissions genuinely gate a facade; the rest are disclosure over the sandbox's fixed rules.

Permission Covers
network low-level cart/link APIs, require("socket"), and mod.fetch (background HTTP); it does not make an arbitrary enabled mod set eligible for launcher link play
steps mod.steps, the native real-world step bridge on iOS/Android; without it available() answers false and sync/poll raise
background mod.job, background compute on an engine-owned worker
filesystem legacy disclosure; there is no raw filesystem grant, because mod.storage and the asset-transform root cover every legitimate write
compute allows love.thread, which is otherwise refused in the sandbox
engine_internals requiring or patching modules outside the mod API

Run with POKEPORT_DEV=1 while developing: a mod that requires a private engine module without declaring the matching permission logs one warning per module, naming the mod and the module. Declare the permission or move to the supported surface.