Getting started
Install the game, make a mod directory, enable it, restart. This page is the durable reference for that loop; the tutorial ladder builds on it.
Run the game first
Open the packaged desktop app and, on first boot, choose your legally
obtained ROM. The launcher has one tab per game: Red, Blue, Yellow, and
Gold (Beta), and each accepts only its canonical US cartridge, verified
by SHA-1 (Red is ea9bcae617fdf159b045185467ae58b2e4a48b9a; the Gen 1
carts are 1 MiB, Gold is a 2 MiB .gbc). Import takes a few seconds and
the game starts. From a source checkout, love . after setup: see
Developer Setup.
Where mods live
The loader scans one virtual mods/ root. LÖVE fuses two real locations
into it (src/mods/Loader.lua:_discover):
mods/inside the game folder (a source checkout or an unpacked app)mods/inside the LÖVE save directory — the same placesave.lualives:- macOS:
~/Library/Application Support/LOVE/pokemon-love2d/mods/(without theLOVE/segment in a packaged build) - Linux:
~/.local/share/love/pokemon-love2d/mods/ - Windows:
%APPDATA%\love\pokemon-love2d\mods\
- macOS:
Either location works; the save directory is the one that survives app updates.
The minimal mod
mods/my_first_mod/
├── manifest.json
└── main.lua
manifest.json:
{
"id": "my_first_mod",
"name": "My First Mod",
"version": "1.0.0",
"entry": "main.lua",
"api": 2
}
api: 2 opts into strict schema validation — typos become load errors with
suggestions instead of silent misses. Every other field is optional; the
full list is in the manifest reference.
main.lua returns an entry function that receives the mod object:
return function(mod)
mod.log:info("hello from a native mod")
-- patch deep-merges onto the imported record; only speed changes
mod.content.pokemon:patch("GROWLITHE", {
baseStats = { speed = 90 },
})
-- react to the one gift-mon seam every story gift passes through
mod.events:on("pokemon.before_give", function(gift)
mod.log:info("giving %s at level %d", gift.species, gift.level)
end)
end
Record fields are camelCase (baseStats, catchRate, spriteFront) —
they come from the importer, not from JSON. Writing base_stats is the
classic mistake; under api = 2 the loader rejects it with
did you mean "baseStats"?. See Data Model.
Scaffold instead of hand-writing
The modkit CLI seeds the same layout with a valid manifest and a commented entry file:
python3 tools/modkit.py scaffold my_first_mod
python3 tools/modkit.py validate my_first_mod
validate drives the real engine loader headlessly, so a mod that passes
will not surface load errors in-game. See Modkit CLI.
Enabling, disabling, and restarting
Mods load enabled by default, and enablement is answered per game: the
launcher's MODS panel draws a checkbox for Red, Blue, Yellow, and Gold
on every row, so a Gen 1 balance patch can stay off in Gold without
uninstalling it. Answers persist in the normal options.lua beside
audio and display settings (options.modsByVersion[game][id] = false
means disabled; missing means enabled), and starting a new game never
resets them. Toggle from the launcher, from the in-game mod manager
(Options menu), or edit the file. Content changes take effect on the
next launch: content registries freeze after the boot merge
(Lifecycle).
The MODS panel is also where mods are installed and managed: Import
mod .zip (or drag-drop an archive onto the window) validates the
manifest, installs, and then flags any missing or disabled dependency,
offering its download when the manifest names the dependency's GitHub
repository. A "Show for" filter scopes the list to one game, and
profiles let you save the current enablement set under a name and
switch between sets (saved in options.modProfiles). A mod whose
manifest declares required user-supplied files
(Publishing) shows an import control on its row and
stays unloaded until every required file has been supplied and
verified.
While developing, run with POKEPORT_DEV=1: F5 hot-reloads mod content
without restarting, and the loader warns when a mod requires private engine
modules without declaring the matching permission.
When something breaks
A broken mod never takes the game down. Load failures — bad manifest, schema violations, missing dependencies, an entry chunk that throws — are reported in the mod manager against the failing mod, and every registration that mod made is rolled back. Fix the reported field and restart.
The rule that has no exceptions
Do not put ROM-derived content in a mod. Ship original assets, or an asset
transform that regenerates derived art from the player's own imported cache
(Art Pipeline). The loader never imports or executes
ROM-hack patches; the supported content source remains the verified base
ROM plus native mods. modkit pack refuses to package cache-derived bytes.
Where to next
| Goal | Page |
|---|---|
| See a change on screen in ten minutes | Tutorial 01 |
| Understand what loads when | Lifecycle |
| Learn the five registry verbs | Registries |
| Look up one task | Cookbook |
| Look up a record's fields | Reference: Registries |
Start here
Concepts
Tutorials
- The ladder
- 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
Reference
Guides
- Art Pipeline
- Audio Authoring
- Preparing Your Mod for Gen 2
- Translations
- Total Conversions
- Link Compatibility
- Publishing a Mod
- Custom Carts
- Docs Style Guide
Playing
Engine 1.0.0 · mod API 2 · repository · Discord
This project ships no ROM data. All game content is decoded on the player's machine from their own verified Pokemon Red ROM; mods ship recipes and original assets, never extracted content.