Publishing a mod
The path from a working mods/<id>/ directory to something other people
can install, and the rules that bind it.
First release: local package
This route is for a mod author with a working mod. Run it from the engine
repository root, the directory containing tools/modkit.py, and replace
my_mod with the directory or id of your mod.
python3 tools/modkit.py validate my_mod --strict
python3 tools/modkit.py lint my_mod
python3 tools/modkit.py pack my_mod -o my_mod-1.0.0.zip
Check the named file and line for each finding, repair it, and rerun the failed command. A package is ready for a local player when the commands pass and its archive contains the mod files at the archive root. Test that result from a clean mods folder before sharing it.
Player install
Give the player the .zip, the games it targets, and the names of any
required import files. The player uses MODS > Import mod .zip (or drops
the .zip on the launcher window), selects the intended game, and enables
the mod for that game. Unpacking the archive into its own folder under the
save directory's mods/ also works. The launcher's import and drop only
recognize a .zip name, so do not hand out a bare .modpkg. A required
import is supplied through the launcher's import control; it must not be
bundled inside the archive. Link play uses the launcher's approved
vanilla/custom-cart flow; an ordinary mod package is not a link cart.
The checklist
-
Manifest hygiene. A real
version(semver, so dependents can range-check you), adescriptionthe manager can show, an honestprofile, andgame_versionpinned only if you rely on something version-specific. If the release is intended for link play, include it in a sealed custom cart and follow Link Compatibility; manifest link fields are author metadata, not a player permission switch. Setgithubtoowner/repoif you want launcher Update / Versions. Mark unfinished workexperimentalso it stays off until the player confirms enable (Manifest). Stategameshonestly (["gen1"],["gen2"],["gen3"],["frlg"],["rse"], specific version ids, or["all"]) so the launcher's per-game checkboxes and each game's mod list behave. A manifest with nogamesis Gen 1 only, and a mod is not loaded on a game it does not claim. Claim a game only after testing there;modkit gen2checkandgen3checkreport what a mod would hit (Modkit). For one entry file across generations, use"api": 3(Portable Mods across Gen 1, 2 and 3). Dependencies can carry a repository hint ("dep_id@^1.0.0#owner/repo", or a structured object withgithub), which lets the launcher offer the download when the dependency is missing, and agamesscope ({ "id": "dep_id", "games": ["gen2"] }) so a dependency only your Gold content needs never blocks a Gen 1 boot. -
README.md: what the mod does, how to install it, and the regenerate instructions for any transform-derived art. -
DIFFERENCES.md: what you change from vanilla, in the format of the engine'sdocs/known-differences.md. Your divergences are yours to document; the base game's ledger stays "None currently." (Compatibility). -
Validate and lint clean.
python3 tools/modkit.py validate my_mod --strict python3 tools/modkit.py lint my_mod -
Pack or publish Releases from GitHub.
python3 tools/modkit.py pack my_mod -o my_mod-1.0.0.zippackre-runs both gates at strict level (any finding refuses the package) and zips the directory with files at the archive root. Players install it with Import mod .zip (Getting Started).For GitHub-hosted updates, put the mod in its own repo, set
"github": "owner/repo"on the manifest, and add the release workflow:python3 tools/modkit.py set-github my_mod owner/repo # if the field is missing python3 tools/modkit.py add-release-workflow my_modEach push to
main(or a manual workflow run) publishes amy_mod-<version>.zipGitHub Release with files at the archive root, the shape Import mod .zip, Update, and Versions install. Bumpmanifest.json'sversion, or put[release X.Y.Z]in the commit message, or pass a version on a manual run; otherwise the patch number is bumped past the latest tag.
The content rule
A distributed mod contains no ROM-derived content: no extracted
PNGs, no chip-audio banks, no ROM images, no IPS/BPS patches
(lint rules MK301-MK304, MK307 for baseroms/). lint refuses .gb,
.gbc, .ips and .bps by extension; a .gba image is just as
forbidden even though no rule names it. Derived art ships as an asset transform that
regenerates it from the player's own cache
(Art Pipeline). Because nothing you distribute
contains Nintendo/Game Freak content, your mod is shareable as ordinary
open-source Lua and PNGs, the same posture the engine itself claims.
Branding is your responsibility: name and present your mod as your own work, not as an official product.
User-supplied files (required_imports)
When your mod needs source material you cannot ship (another game's
ROM, say), declare it instead of bundling it. required_imports and
optional_imports in the manifest each list objects with a stable
id, a display name, a destination file (a filename, never a
path), one MD5 digest or an array of accepted digests, and optionally a
description, an exact size (or max_size), and a format ("raw"
default, or "n64", which accepts .z64/.v64/.n64 byte orders and
canonicalizes them).
The launcher does the rest on every platform: the mod's row shows an
import control, the player picks the file through the platform's native
picker (desktop chooser, Android SAF, iOS Files, and so on), the
launcher verifies size and digest, and the accepted bytes are copied
into mods/<id>/baseroms/<file>. Your code reads them with the normal
scoped API, mod:read("baseroms/<file>"), or for large files the bounded
mod.imports:info(id) / mod.imports:read(id, offset, length) (8 MiB per
call); no host path is ever exposed. A mod with missing required_imports does not load until they
are supplied; optional_imports never block loading.
Never ship anything under baseroms/ in your archive: it is the
player's installation state, and modkit lint/pack refuse it
(MK307).
Versioning etiquette
- Bump patch for fixes, minor for compatible additions, major when you break your own save shape or exported API.
- Ship a
mod.migrations:addstep whenever yourmodDatashape changes (Save Model); old saves are your users' progress. - Treat
mod.exportsas a public API: additive changes only within a major. - Declare
conflicts(or the aliasincompatible) when you know two mods cannot coexist; the declaring mod loses at load time, so state it in the mod that knows.
Play-testing the failure paths
Before shipping, test what your users will actually hit: enable alongside popular mods (merge-order surprises), disable mid-save (your content quarantines and reclaims cleanly), and load a save from your previous version (migrations fire). A mod that fails politely (attributed errors, no crashes, clean rollback) is one the manager can help users live with.
Check and recover
The local release checkpoint is a package that passes strict validation and
lint, installs from a clean mods/ folder, and reaches the expected in-game
change. If installation or loading fails, remove only the new package, read
the manager's attributed error, repair the named file, and rebuild before
sharing another archive.
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.