Repository navigation
Concepts Portable API 3
One entry file, every game. A mod that declares "api": 3 and lists the games
it claims runs on Red, Blue, Yellow, Gold, Silver, Crystal, FireRed, LeafGreen,
Ruby, Sapphire and Emerald with the same names, record shapes and units.
Gen 1 is the source of truth: Gen 2 and Gen 3 add capabilities and keep the
inherited contracts. Shared names are registrable everywhere, but a feature only
does something where the cart has it. The API never invents gameplay a game
lacks (no Gen 3 phone, radio or apricorns).
api 1 and api 2 mods behave exactly as before. API 3 is a per-manifest
opt-in, not a rewrite of the mod object: mod.content, mod.events,
mod.hooks, mod.world and mod.battle keep their names and are replaced by
portable services for api >= 3 mods only. The engine provides modApi = 3
(src/core/Version.lua) and refuses a manifest with a higher number.
{
"id": "portable_example",
"name": "Portable Example",
"version": "1.0.0",
"api": 3,
"games": ["gen1", "gen2", "gen3"],
"entry": "main.lua"
}return function(mod)
if mod.content.moves:get("TACKLE") then
mod.content.moves:patch("TACKLE", { power = 45 })
end
mod.events:once("world.stepped", function()
local ok, reason = mod.world:setFlag("mod:visited", true)
if not ok then mod.log:warn("%s", reason) end
end)
endThe entry chunk may use local mod = ... or return function(mod) ... end.
Content changes belong there, before the loader freezes the registries. The
runnable mods/examples/portable_api in the repository shows the pattern.
games takes gen1, gen2, gen3, rse, frlg, a version id or all
(Manifest). A manifest that says nothing
is Gen 1 only; gen2compat is not a Gen 3 claim. Claim a game only after
running on it.
API 3 routes the mod object through engine services. You never require them.
| Service | Facade | What it does |
|---|---|---|
UnifiedContent |
mod.content, mod.datasets
|
canonical records for the core registries; native shapes for the rest |
UnifiedRecords |
records inside content, hooks and events | converts species, move, item, trainer and encounter rows between the Gen 1 shape and the native one |
UnifiedIds |
ids inside records | Gen 1 spellings for species (FARFETCHD, MR_MIME) and Gen 3 types (PSYCHIC, MYSTERY) |
UnifiedEffects |
effect on a move |
maps Gen 1 effect ids to the native effect; an effect with no verified mapping is refused |
UnifiedCallbacks |
mod.events, mod.hooks, mod:supports
|
boundary conversion of record-valued payloads; the capability query |
UnifiedWorld |
mod.world |
the common world method set |
UnifiedBattle |
mod.battle |
snapshots and intents |
UnifiedScripts |
mod.world:queueScript |
portable script rows |
The portable registries are pokemon, moves, items, trainers and
encounters. They keep register, override, patch, remove, get and
each, and add has. Reads and iteration return detached records; apply
changes through a write method.
-
Species:
baseStats.hp,attack,defense,speed,special;level1Moves;learnset = { { level = 9, move = "TACKLE" } };evolutions = { { method = "LEVEL", level = 16, species = "CHARMELEON" } }; string species, move, item, type and growth-rate ids. - Gen 2
levelMovesandinto, and Gen 3 numeric tables, are translated to that shape. Writingspecialsets both later Special stats; optionalspecialAttackandspecialDefensecarry the split, and Gen 1 requires them equal. -
Moves use Gen 1 effect ids such as
NO_ADDITIONAL_EFFECT. -
Trainers:
parties = { { { species = "MEW", level = 5 } } }pluspartyNames. Encounter slots:{ species = "MEW", level = 5 }; a shared encounter write applies to every Gen 2 time of day. -
Text keeps the Gen 1 control characters with symbolic placeholders such
as
{PLAYER}. - A new registration needs no Gen 3 storage index; the adapter allocates a free slot and checks explicit collisions.
patch keeps untouched fields; lists replace or use __append / __prepend;
mod.DELETE deletes. An operation the selected game cannot represent returns
nil, reason before anything is recorded. A malformed record raises a schema
error attributed to the mod, and a failed entry chunk rolls back its
contributions. The native extension on a record is read-only data for
diagnostics.
Registries whose Gen 2 or Gen 3 shape differs from Gen 1 and that have no
canonical adapter are written by api 3 mods as pass-through records in the
native shape of the running game, and mod:supports("content", name) says
per game whether the registry has a target. Where each registry lands on
each game, and the Gold and Gen 3 record differences, is in
Registry Fields; Gold serves 44 of the 47 registries,
Emerald 34 and FireRed and LeafGreen 33. A registry with no home is reported,
never silently merged.
local ok, reason = mod:supports("content", "pokemon")
local canFly = mod:supports("world", "flyTo")
local hasHook = mod:supports("hooks", "core.logic_speed")
local hasEvent = mod:supports("events", "world.stepped")Surfaces are content, world, hooks and events. Every content handle also
has supports() and an available boolean. Content and world answers describe
implemented interfaces; game state may still refuse a call.
For hooks and events the answer is native per version: a name returns
true only on versions that have a call site. Names with a call site in every
generation (for example pokemon.level_visible, battle.field_residual) are
true on every game; Gen 1 only names (battle.style, catch.nickname,
catch.party_full) and Gen 1 and Gen 2 names (transition.style, world.tod,
map.palette) answer from their per-generation call sites; mod.options_changed
is true everywhere; Gen 2
additions follow the cart (phone.call_received, radio.channel,
phone.contact_list and the boot intro are Gold, Silver and Crystal only;
shiny.roll, gender.roll, held_item.trigger, breeding.compatibility,
egg.hatched, happiness.changed, pokerus.infected and the roamer events are
true on Gen 2 and Gen 3). Custom mod.<id>.* events are always true. The tables
in Reference: Hooks and Reference: Events
show the call sites per generation.
hooks:wrap, events:on and events:once accept any catalogued name on any
game, so one source file loads everywhere; a subscription to a name the game
does not raise simply never fires. Unknown names raise. Check mod:supports
before relying on a callback.
mod.hooks:wrap(name, function(next, ...) end) and mod.events:on / once
keep their forms. API 3 converts record-valued payloads at the boundary
(including the mutable pokemon.before_give). Priorities, unsubscribe closures,
owner rollback and the run-vanilla-at-most-once rule are unchanged. Game,
battle and context handles stay native. A mod may subscribe to another mod's
events but only emit under its own mod.<id>.* prefix; engine events cannot be
forged. The start-menu hook exposes executable onSelect rows on every
generation.
Handles can be captured at entry time and resolve the current game when used. A
call with no active world or save returns nil, reason.
The method set: current, party, canReorderParty, reorderParty,
availableFieldActions, useFieldAction, canFly, flyTo, mapOverview,
warpTo, toggleObject, setFlag, getFlag, effectiveEncounters,
activeBlockAt, replaceBlock, spawnNpc, liveMaps, removeNpc, npc,
queueScript, startWildBattle, invalidateMap and overworld. Actions
validate arguments and native busy state before starting.
-
mod:<key>flags are private booleans in the mod's save bucket on every generation. Symbolic native flags need authoritative constants for the version; Crystal does not reuse Gold's mapping. -
party()returns detached Pokemon withnickname,stats.special, string ids, short statuses (SLP,PSN,BRN,FRZ,PAR) and moves as{ id, pp, maxPp }. -
activeBlockAt(mapId, x, y)returns a native block id plustileSize,cellSize,coordinateBaseandkind. Blocks are 32 px on Gen 1 and Gen 2 and 16 px on Gen 3, zero-based. It is never a collision byte. -
spawnNpc(mapId, objDef)takes a detachedmaps[].objectsshape and returns a session idmapId .. "_obj_" .. index;npc(mapId, id)resolves it. Handles expose movement, facing, placement, passability andsetAppearance(spriteId).removeNpcremoves only the calling mod's runtime NPCs. Runtime NPCs survive map reloads in a session but are not saved; respawn on load. A permanent NPC belongs in a map content patch. -
world.talkreceives the native game or overworld and the target; callnext(gameOrWorld, target)for the normal interaction (it runs at most once). On Gen 3scriptKeyruns the native script VM; Gen 1 talk scripts through this path are refused with an error.
Method signatures: Mod Object.
queueScript(rows, { onDone = function(ok, reason) end }) validates the whole
list before running anything, waits for native completion between asynchronous
rows, reports completion once and cancels with a replaced session. Rows are
{ verb, arg... }, at most 1024. The verb tables (common verbs on all three
generations, adapted verbs per game, and the refusal messages) are in
Script commands. Native script
bytecode stays version-specific.
For single actions use warpTo(mapId, x, y, facing, options) or
startWildBattle(species, level, onDone); warp options (arrive, onDone,
keepMusic) are read only for api 3 mods and have generation limits. api 1
and 2 mods keep the legacy warpTo contract, which ignores opts.
local snapshot = mod.battle:snapshot()
if snapshot and snapshot.prompt == "menu" then
mod.battle:submit({ id = 1, revision = snapshot.revision, kind = "menu", choice = "fight" })
endUse a fresh, increasing intent id per mod; native prompt, stale-revision and
replay checks still apply. Snapshots have player, enemy, party, moves,
items, revision, prompt, turn, catchable, plus double, active,
battlers and targets. Singles use battler ids 0 and 1; Gen 3 doubles use 0
through 3; targets is empty outside target selection. An item's catchChance
is a 0 to 100 percentage and reading it rolls nothing. Mutating a snapshot does
not change the battle. See Mod Object.
mod.ui.push(game, id, ...) pushes a screen registered in content.screens. The
Gen 3 bridge is what an api 3 mod gets from mod.ui; api 1 and 2 mods keep
the Screens-based push and the Gen 1 TextBox. Gen 3
has no builtin Gen 1 screens, so an unregistered id raises. A screen state
table (enter, update, draw, exit, optional isOpaque) draws into the
160x144 interior of the 240x160 GBA frame, translated to (40, 8); set
gbaCanvas = true on the state to draw in the full 240x160 space instead.
screen.pushed and screen.popped are raised. mod.ui.TextBox.new(game, text, onDone, opts) opens the native message window; opts.choice and opts.stay
raise an error on Gen 3, so use the ask or choice script rows for
questions.
Gen 3 reuses the Gen 1 and Gen 2 hook, event and registry names and invents
none. It has no phone, radio, apricorns, Bug Contest, mail or Unown puzzle, so
those registries and events are never raised from Gen 3 code. Gen 3 registries
that have a native consumer route to their own tables (gen3Pokemon,
gen3Moves, gen3Items, gen3Encounters, gen3Text, and others); the
battle registries (move_effects, statuses, ai_classes, battle_anims)
hold mod content only, because the cart's own effects are battle-script code.
The registry-by-registry routing, with the Gold and Gen 3 subsections, is in
Registry Fields. The per-version call-site lists are in
Hooks and Events.
luajit tests/run_modkit.luaRun the same body against every claimed version and assert the loader result, native data changes, unsupported results, rollback and frozen content. Registering a callback only proves the registration contract; native coverage needs the real call site and its payload. For runtime NPCs also check pool membership, collision, interaction, owner-scoped removal and session cleanup.
See also: Portable mods guide, Compatibility, Registries, Events and Hooks, Manifest.
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.
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