6 Guide Modkit
bryanthaboi edited this page 2026-10-20 17:01:40 -04:00

The modkit CLI

tools/modkit.py is the mod-author command line: scaffold, validate, gen2check, gen3check, lint, pack, bounce, translation, docs, set-github, add-release-workflow. Python 3; the validate path also needs luajit on your PATH (override with MODKIT_LUAJIT).

python3 tools/modkit.py <subcommand> [args]

Global flags: --repo PATH (repo root override), --json, --quiet. Exit codes: 0 success, 1 validation/lint failure, 2 usage error.

First mod workflow

Run these commands from the engine repository root, the directory containing tools/modkit.py. Replace my_mod with a short id using letters, numbers, _, or -.

python3 tools/modkit.py scaffold my_mod
python3 tools/modkit.py validate my_mod
python3 tools/modkit.py pack my_mod -o my_mod-1.0.0.zip

Edit mods/my_mod/manifest.json and mods/my_mod/main.lua between scaffold and validate.

scaffold creates the starting files under mods/my_mod/. validate checks the mod with the headless loader; pack writes the installable package only after validation and linting pass. If a command reports a finding, fix the file and line named in that finding, then rerun the same command. Do not use --force until you have confirmed which existing file the command will replace.

scaffold

python3 tools/modkit.py scaffold my_mod
python3 tools/modkit.py scaffold my_tc --profile total_conversion
python3 tools/modkit.py scaffold my_mod --github owner/repo --experimental
python3 tools/modkit.py scaffold my_mod --games gen1,gen2,gen3

Creates mods/<id>/ with a manifest (api: 2 by default, or the level you pass with --api; the current engine's game_version range), a commented main.lua, a README.md, a .luarc.json, and an assets/ directory. The total_conversion profile adds a transforms.lua and wires assets_transforms. Flags: --profile content|overhaul|total_conversion, --games (default gen1; gen1, gen2, gen3, frlg, rse, all, or a comma list of version ids such as red,gold,firered), --github owner/repo (optional; enables launcher auto-update, see Manifest), --experimental (off until the player confirms enable), --dest DIR, --force.

--api sets the manifest api (default 2). Use --api 3 for a portable API 3 mod; mods/examples/portable_api/ is a working reference.

validate

python3 tools/modkit.py validate my_mod [--strict] [--base auto|fixture|imported]

Drives the real engine loader headlessly, so a mod that passes will not surface load errors in-game: manifest shape (MK001-MK004), permission vocabulary (MK005) and undeclared engine requires (MK006), schema violations, dangling cross-references, vanilla-id checks (MK1xx) and map_scripts problems (MK201). --base auto folds over the player's imported dataset when one exists and falls back to the ROM-free fixture (tests/fixture_data/) otherwise, which keeps the tool runnable on a CI box with no ROM. Rules that need the real id space (MK103) are reported as skipped over the fixture, never guessed.

github is optional: if the field is missing, validate prints a tip that setting "github": "owner/repo" enables launcher auto-update. A present but malformed value is an error.

gen2check and gen3check

python3 tools/modkit.py gen2check my_mod [more_mods...] [--strict] [--notes]
python3 tools/modkit.py gen3check my_mod [more_mods...] [--strict] [--notes]

Answer whether a mod runs on a Gen 2 (Gold, Silver, Crystal) or Gen 3 game and how far it gets: first the manifest gate (MK400: games claims no game of that generation; MK401: a dependency claims none), then a static read of the mod's Lua against what src/mods/Gen2Compat.lua or src/mods/Gen3Compat.lua backs, member by member (MK402-MK410). It is a scan, not an interpreter: what it cannot follow is listed as unresolved. Mods named together are read as one install set, so a mod and its dependencies answer each other. --notes adds the adapter's note for every backed member the mod touches. Exits non-zero on a fatal finding.

-- example_portable_api: api 3, profile content, games gen1+gen2+gen3, ...
ok example_portable_api on gen 3: will load

lint

python3 tools/modkit.py lint my_mod

The no-ROM-content distribution gate:

Rule Refuses
MK301 files in (or pointing into) the generated trees
MK302 byte-identical or perceptually near-duplicate assets vs the player's cache
MK303 ROM images and ROM-hack patch formats (.gb, .gbc, .ips, .bps)
MK304 raw chip-audio banks (ROM-derived)
MK305 bulk dumps of an imported data table (register individual records instead; warns when no imported dataset exists to diff against)
MK306 lang/strings.lua keys that are FireRed/LeafGreen ROM English (key them by ROM label; translation --refresh moves them)
MK307 anything under baseroms/: user-supplied import files are installation state, never distributed (Publishing)

pack

python3 tools/modkit.py pack my_mod [-o out.zip] [--base auto|fixture|imported]

Runs validate and lint at strict level (any finding, warning included, refuses the package), then writes a reproducible zip with the mod files at the archive root plus .modkit/pack.json. Without -o the name is <id>-<version>.modpkg; the launcher's Import mod .zip and drag-and-drop only take a .zip name, so pass -o <id>-<version>.zip for a file players import directly. See Publishing.

bounce

Renders chip-program songs only to WAV headlessly: the fast iteration loop for ChipAsm authoring without booting the game. File-backed songs are skipped because this command exercises the chip renderer. Run from the engine repository root; without --out, WAV files go under bounce/ in that repository. The command needs imported/generated data, so an error saying it needs an imported dataset means you must import a game before using bounce. A missing id prints no such song.

python3 tools/modkit.py bounce Music_ModChip --seconds 30
python3 tools/modkit.py bounce --all --out renders/

Look for the printed wrote .../<song-id>.wav line and listen to that WAV. The command's success means a WAV was rendered; it does not verify that a file-backed cue, map assignment, or in-game mix sounds right.

translation

python3 tools/modkit.py translation my_lang --language "Francais"
python3 tools/modkit.py translation my_lang --refresh

Scaffolds a translation mod: manifest.json, main.lua, README.md, TRANSLATING.md and empty lang/*.lua catalogs holding every player-visible string the engine and the imported dataset know about. --refresh re-harvests the catalogs and keeps existing work, --pixel-font renders through the bundled Plain Pixel TTF instead of the tile font, and --base, --dest, --force work as elsewhere. See Translations.

docs

python3 tools/modkit.py docs [--out DIR]

Regenerates docs/modding/reference/registries.md from src/mods/Schemas.lua (via tools/gen_registry_docs.lua), so the registry reference cannot drift from the engine; --out DIR also copies it to DIR/registries.md. To write the wiki page directly, run luajit tools/gen_registry_docs.lua <wiki checkout>. The wiki registry reference mirrors that file.

set-github

python3 tools/modkit.py set-github my_mod owner/repo
python3 tools/modkit.py set-github my_mod https://github.com/owner/repo

Writes or updates the optional github field on an existing manifest.json (for mods scaffolded before the field existed). Same normalization as the loader: owner/repo or a github.com URL.

add-release-workflow

python3 tools/modkit.py add-release-workflow my_mod
python3 tools/modkit.py add-release-workflow my_mod --force

Copies the standard GitHub Actions release workflow into mods/<id>/.github/workflows/release.yml, stamped with the mod's id. On each push to main (or a manual run) it packs a root-level installable .zip and publishes a GitHub Release: the layout the launcher's Import mod .zip / Update / Versions paths expect. Use --force to overwrite an existing workflow. See Publishing.

Check and recover

The first workflow should leave a package at the -o path after validate and lint pass. If a command reports a finding, use its file and line, rerun that same command, and keep the last working mod directory; if bounce reports no imported dataset or no song, import the dataset or correct the song id before trying again.