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.
Player link rule
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:
-
"mod_id" -
"mod_id@^1.2.0"(version-pinned) -
"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) -
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.
Start here
Playing
Tutorials
- Choose a tutorial
- 01 Sprite and Text Tweak
- 02 Balance Patch
- 03 New Species
- 04 New Item and Ball
- 05 New Move
- 06 NPC and Dialogue
- 07 New Map
- 08 Quest
- 09 Custom Music
- 10 New Mechanic
- 11 Custom UI Screen
- 12 Mini Total Conversion
Creative guides
Concepts
Recipes and reference
Author and developer tools
Home · Start Playing · Glossary · Troubleshooting · Repository · Community
Game data is imported locally from a supported ROM supplied by the player. Shared mods contain original work or transform recipes, never extracted game content. Check each guide's target game before using its examples.