8 Reference Mod Object
bryanthaboi edited this page 2026-10-20 17:01:40 -04:00

The mod object

Every member of the mod table the loader hands your entry function, as constructed in src/mods/Loader.lua (_api). Nothing outside this surface, the registries, the cataloged events/hooks, and the documented data schemas is supported.

The same table is built for every game (Red, Blue, Yellow, Gold, Silver, Crystal, FireRed, LeafGreen, Ruby, Sapphire, Emerald). A manifest with "api": 3 replaces mod.content, mod.events, mod.hooks, mod.world, mod.battle and mod.datasets with the portable services and adds mod:supports; those differences are marked API 3 below and explained in Portable API 3. Everything else is identical at every api level.

Your chunks run inside a per-mod sandbox (src/mods/Sandbox.lua): _G is your own table, io / os / love.filesystem / love.system / love.event answer through compat stand-ins (src/mods/LegacyCompat.lua) that log a migration warning per call, and love.thread, ffi and debug are refused outright. The members below are the sanctioned replacements.

Start from the task

You want to Use
Add or change imported game content Content
React when gameplay happens or alter a calculation Events and hooks
Save quest progress or player preferences Persistence
Keep an independent record or checkpoint Durable storage and checkpoints
Read files bundled with your mod Assets and files
Add a menu, screen, or widget UI toolkit
Work with a running map or NPC World facade
Read a running battle or submit a menu choice Battle facade
Read a user-supplied file, an asset pack or another imported game Assets and files, Imported datasets
Fetch data or run background work Background work

The signatures below use parentheses for arguments and -> for returned values. A | B means either value may be returned. A value such as { id, version } is a Lua table with named fields. Check the section's timing note before calling a facade: content is entry-file work, and mod.world requires the live game after game.ready.

Identity

Member Value
mod.id the manifest id
mod.version the manifest version string
mod.path the mod's directory ("mods/")
mod.manifest a deep copy of the validated manifest - mutating it changes nothing
mod.generation 1, 2 or 3: the generation of the running game
mod.developer true when the engine was started in developer mode (POKEPORT_DEV=1); plain data, so a mod can register diagnostics without reading the environment
mod.game the live game service owner, resolved per generation (the Gen 1 singleton, Gold's Game2 instance, the Gen 3 shell); read on every touch, nil before it exists. Hold this instead of requiring src.core.Game
mod.DELETE the tombstone sentinel for patch (patch(id, { field = mod.DELETE }))
mod:supports API 3 only. (surface, name) -> ok, reason; surfaces "content", "world", "hooks", "events"

Content

Member Signature Notes
mod.content.<registry>:register (id, value) add; duplicate id errors
mod.content.<registry>:override (id, value) replace whole record
mod.content.<registry>:patch (id, partial) deep-merge
mod.content.<registry>:remove (id) tombstone
mod.content.<registry>:get (id) -> value merged view, base data included
mod.content.<registry>:each () -> iterator merged view

All 47 registries plus the v1 aliases (scripts, ui) hang off mod.content; see Reference: Registries. A registry with no home on the running game takes the write, drops it and reports it once per mod. Content verbs are entry-chunk-only (Lifecycle).

API 3: mod.content is the portable facade. The registries pokemon, moves, items, trainers and encounters take the canonical Gen 1 record shape on every game and add has(id); reads and iteration return detached records, and an operation the game cannot represent returns nil, reason before anything is recorded. Every other registry keeps its native shape. Each content handle also has supports() and an available boolean.

Imported datasets

Member Signature Notes
mod.datasets:open (version) -> view | nil, reason read-only view of another imported game: view.content.<registry> with get / has / each (detached, schema-checked, ids in lexical order), view.assets:path(rel) / info(rel) for assets/generated/..., plus view.version and view.generation. reason is "unknown_version", "not_imported" or "invalid_cache". Never changes the active game

Events and hooks

Member Signature
mod.events:on (name, callback, priority) -> unsubscribe
mod.events:once (name, callback, priority) -> unsubscribe
mod.events:emit (name, payload) - only "mod.<id>.*" names
mod.hooks:wrap (name, callback, priority) -> unsubscribe

Catalogs: Reference: Events, Reference: Hooks. Both list which games raise each name.

API 3: the same four verbs accept any catalogued name on any game (a name the game never raises just never fires) and raise on an unknown name. Record valued payloads (species, moves, items, parties, the mutable pokemon.before_give) are converted to the portable shape at the boundary; game, battle and context handles stay native. A mod may subscribe to another mod's events but only emit under its own prefix.

Persistence

Member Signature Backed by
mod.save:get (key, default) -> value save.modData[modId]
mod.save:set (key, value) save.modData[modId]
mod.options:define (rows) rows render in the manager
mod.options:get (key) -> value options.modOptions[modId], falling back to the row default
mod.migrations:add (since, fn) runs on load for saves older than since

Row shapes and migration ordering: Save Model.

Durable storage and checkpoints

Independent of mod.save (which rides the normal SAVE), mod.storage writes per-mod, per-playthrough records through the engine's persistence backend, and mod.checkpoints captures/restores engine-owned runtime state. Implementations: src/mods/Storage.lua, src/core/Checkpoint.lua.

Member Signature Notes
mod.storage:context (game) -> { engineVersion, gameVersion, playthroughId } identity is opaque; no slot or path ever crosses
mod.storage:write (game, key, value) -> ok, code, message data-only tables; staged and decode-verified
mod.storage:read (game, key) -> value, code, message recovers from a valid staged/backup generation
mod.storage:writeBytes (game, key, bytes) -> ok, code, message opaque byte strings (NULs included), 512 MiB per key, staged and byte-verified; never decoded or executed
mod.storage:readBytes (game, key) -> bytes, code, message the byte counterpart of read
mod.storage:list (game, prefix) -> keys, code, message keys are slash-separated [A-Za-z0-9_-] segments
mod.storage:delete (game, key) -> deleted, code, message table and byte values share one key space; delete before switching a key's type
mod.storage:selected (game) -> facade | nil, code, message title screen only: a bound facade over the launcher-selected playthrough, same methods minus the game argument; non-allocating
mod.checkpoints:inspect (game) -> capability capability.canCapture before capturing
mod.checkpoints:capture (game) -> checkpoint, code, message data-only; store it through mod.storage
mod.checkpoints:restore (game, checkpoint) -> ok, code, message validates before mutation, verifies by recapture, rolls back on failure; emits checkpoint.restored on success
mod.checkpoints:resume (game, checkpoint) -> ok, code, message title-session counterpart of restore; rebuilds a usable title session on failure
mod.checkpoints:ensureNormalSave (game, checkpoint) -> anchored, code, message make a never-saved playthrough title-bootable exactly once; true, "already_exists" after that

The playthrough identity is allocated lazily on the first storage/checkpoint call, so an unused API changes no save bytes. A checkpoint's identity.engineVersion is compatibility metadata, not runtime state: a checkpoint captured by an older engine release restores on a newer one (src/core/Checkpoint.lua, normalizeVerificationMetadata). Full contracts and error codes: the Durable tool storage section of docs/modding.md and RFCs 0003-0006.

Assets and files

Member Signature Notes
mod.assets:path (relative) -> "mods/<id>/<relative>" the path helper every sprite field wants
mod.assets:image (relative) -> Image cached love.graphics.newImage; needs a graphics context
mod.assets:list / mod.assets:info (relative) same as mod:list / mod:info
mod.assets.<registry> - v1 alias of mod.content.<registry>
mod:read (relative) -> contents, err read a file from the mod directory
mod:list (relative) -> names sorted directory listing inside the mod directory
mod:info (relative) -> { type, size } | nil type is "file" or "directory"; nil when absent
mod.imports:info (id) -> { id, name, file, size, md5, required } | nil, err a validated required_imports / optional_imports entry; undeclared ids are refused
mod.imports:read (id, offset, length) -> bytes | nil, err zero-based range read, 8 MiB per call; no host path or handle
mod.packs:list () -> { { importer, pack, version, kind, required, installed }, ... } every pack you declared, installed or not
mod.packs:info (importer, pack) -> { importer, pack, kind, version, entries, source } declared packs only
mod.packs:entries / entry (importer, pack[, entryId]) entry metadata (id, file, size, width, height, frames, ...)
mod.packs:read (importer, pack, entryId) -> bytes | nil, err 8 MiB cap; refuses a size mismatch
mod.packs:metadata (importer, pack, entryId) the entry's importer-written metadata
mod.cache:write / read / info / exists / delete (relative[, bytes]) installation-scoped generated data under mod_cache/<id>/; 64 MiB per write; not rewound by checkpoints, not scoped to a game or playthrough

All path arguments join to your own directory: .., absolute paths and drive letters are refused (src/mods/SafePath.lua). Validated required_imports land under baseroms/ and are read the same way (Manifest).

Scripting sugar

Member Signature Notes
mod.commands:register (verb, fn) sugar over mod.content.commands; replacing an engine verb must say override on the registry

UI toolkit

mod.ui is the shared widget facade (src/ui/ModUI.lua, picked per manifest by ModUI.forApi(api)); widgets load on first touch. An api 1 or 2 mod gets the Screens-based push and the Gen 1 TextBox on every game. An api 3 mod gets the Gen 3 bridge (src/ui/game3/mod_ui.lua) when the running game is a Gen 3 game.

Member What it is
mod.ui.Menu, mod.ui.ListMenu, mod.ui.ChoiceBox, mod.ui.QuantityBox, mod.ui.NamingScreen, mod.ui.PicBox, mod.ui.TextBox, mod.ui.Font, mod.ui.Theme the stable widget set for building screens
mod.ui.push(game, screenId, ...) instantiate through the screens registry and push. On Gen 3 (api 3 only) a screen state table (enter, update, draw, exit, optional isOpaque) draws into the 160x144 interior of the 240x160 frame (translated to 40, 8); set gbaCanvas = true to draw the full frame. An unregistered id raises, because Gen 3 has no builtin Gen 1 screens
mod.ui.insertBefore(items, anchorLabel, item) anchored menu insertion (missing anchor appends)
mod.ui.insertAfter(items, anchorLabel, item) same, after the anchor
mod.ui.removeLabel(items, label) remove a row by label
mod.ui.PokemonIcon.draw(game, summary, x, y, opts) draw a party icon from the detached { species, hp, maxHp } summary; opts.selected / opts.counter request the native selected animation. Composes with icons content and the pokemon.icon hook; invalid summaries return false, code, message
mod.ui.insertStepBefore(steps, anchorId, step) Oak-speech step insertion (intro.oak_speech.build)
mod.ui.insertStepAfter(steps, anchorId, step) same, after the anchor id
mod.ui.removeStep(steps, id) drop a step by id

On Gen 3 mod.ui.TextBox.new(game, text, onDone, opts) opens the native message window; opts.choice and opts.stay raise an error there, so use the ask or choice script rows for questions. screen.pushed and screen.popped are raised for Gen 3 mod screens too.

World facade

mod.world materializes on first touch, once the live Game exists (after game.ready); nil before that and in headless runs without a game. Implementation per generation: src/world/WorldAPI.lua (Gen 1), src/world/gen2/WorldAPI.lua (Gold, Silver, Crystal) and src/world/game3/WorldAPI.lua (Gen 3). The method set is the same where the game has the feature; the table notes the exceptions.

API 3: mod.world is the portable service (src/mods/UnifiedWorld.lua). It can be captured at entry time and resolves the live game when used; with no active world or save a call returns nil, reason. Its method set is exactly: 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, adapted to every generation (including Fly and startWildBattle on Gold, which the native Gold facade lacks).

Member Signature Notes
mod.world:current () -> { mapId, x, y, facing } the player's position
mod.world:warpTo (mapId, x, y, facing, opts) scripted warp. api 1 and 2 keep the legacy contract: opts is accepted and nothing in it is read (no arrive FX, no onDone, no keepMusic). api 3 reads opts.arrive = "fly" | "teleport" (arrival FX), opts.onDone and opts.keepMusic, with generation limits
mod.world:toggleObject (mapId, objName, visible) persisted; emits world.object_toggled
mod.world:setFlag / getFlag (name, value) / (name) -> bool the shared story-flag namespace; a real change emits flag.changed. A "mod:<key>" name is a private boolean in the mod's save bucket on every generation
mod.world:replaceBlock (bx, by, block) current map, block grid; on Gen 3 the block is a 16 px metatile id (0..1023)
mod.world:activeBlockAt (mapId, x, y) -> blockId, { tileSize, cellSize, coordinateBase, kind } the native block id at a cell, never a collision byte. Blocks are 32 px on Gen 1 and Gen 2 and 16 px (metatiles) on Gen 3, zero-based. Native on Gen 1 and Gen 3; Gold gets it through API 3
mod.world:liveMaps () -> { { mapId, ox, oy, active }, ... } the active map plus every connected map being drawn (ox/oy are pixel offsets). Bracketed by world.live_maps_preparing / world.live_maps_updated on Gen 1 and Gen 2
mod.world:effectiveEncounters (mapId, terrain[, opts]) -> { chance, dist } the wild distribution after encounter.table, with no RNG and no side effects; terrain is "grass", "water" (Gen 1 also "indoor"); Gold takes { daytime = "NITE" } to preview a time of day. Reflects a Gold swarm but not a roaming legendary
mod.world:canFly / flyTo () -> bool / (mapId) -> true | nil, reason FLY needs a destination picker; flyTo accepts only a visited Fly town. Native on Gen 1 and Gen 3; Gold gets them through API 3
mod.world:overworld () -> overworld | nil the live overworld object for the running game; engine-owned, version specific
mod.world:spawnNpc (mapId, objDef) -> npcId runtime NPC, same shape as maps[].objects; returns the object id, allocated at spawn time; not serialized - respawn on map.entered
mod.world:removeNpc (npcId) the id spawnNpc returned; refuses a mapped object and another mod's
mod.world:npc (mapId, indexOrName) -> handle active map only (and ghosts on a live neighbor map); accepts def.index, def.name or the object id. handle.id is the object id. Handle methods below
mod.world:queueScript (rows, extra) run command rows through the script queue. API 3 validates the whole list first, accepts the portable verbs on every game, and reports completion once through { onDone = fn(ok, reason) } (Portable scripts)
mod.world:invalidateMap (mapId) drop the built map so edits show
mod.world:startWildBattle (species, level[, onDone]) -> true | nil, reason push a wild battle; refuses mid-warp, mid-battle, and an unhealthy party. Native on Gen 1 and Gen 3; Gold gets it through API 3
mod.world:mapOverview () -> overview read-only snapshot: collision rows (cell grid), optional tileRows (2x) and tileDetailRows (4x) of GB shades "0"-"3" with matching width/height fields, and markers of { kind, x, y } for warp / visible item / untaken hidden (src/world/MapOverview.lua); same contract on Gold and Gen 3
mod.world:canReorderParty () -> bool true only during idle overworld play with 2+ mons
mod.world:reorderParty (fromSlot, toSlot) -> true | nil, reason one-based slots; menus, movement, scripts, battles and transitions refuse
mod.world:availableFieldActions () -> { { id, label, rods? }, ... } the field items and moves that can start right now (bicycle, fish, cut, surf, strength, flash, dig, teleport; Gold adds headbutt, whirlpool, waterfall, sweet_scent, squirtbottle; Red adds softboiled with sources and targets; Gen 3 offers bicycle, cut, surf, strength, flash, rock_smash, waterfall, dig, teleport, sweet_scent, plus softboiled and milk_drink with sources); empty while the world is busy. Render what you understand and ignore unknown ids
mod.world:useFieldAction (id, opts) -> true | nil, reason perform a listed action through the game's own field-item path; fishing takes opts.rod and auto-picks a sole rod. Stale or busy requests change nothing

NPC handles

scriptMove, marchInPlace, face and position drive an NPC through the overworld's scripted-movement list, which locks player input while it walks. The other methods drive an actor without that lockout (a networked ghost, an ambient walker). All of them work the same on Gen 1, Gen 2 and Gen 3.

Method Signature Notes
handle:scriptMove (dir, tiles, onDone) queued scripted walk; gates player input until it finishes
handle:marchInPlace (dir?) step animation in place
handle:face (dir) turn
handle:position () -> x, y current cell
handle:stepNow (dir) one tile now, with no queue; deliberately does not check collision (a replayed, already-decided move must not be re-judged)
handle:canStep (dir) -> bool the map's opinion; ask first when you want collision
handle:placeAt (x, y, facing) snap with no animation and clear any step in flight
handle:isMoving () -> bool pace a driver instead of stomping a move in flight
handle:setPassable (flag) the flag collision skips; a passable object still draws and can be talked to
handle:setAppearance (spriteId) swap to another registered sprite without touching identity, cell, step or passability

An object spawned at runtime carries no TEXT_* id, so the vanilla talk path has nothing to say for it; answer the A press with the world.talk hook. On Gold and Gen 3, spawnNpc takes (mapId, objectDefinition) and returns the generated id (for example CHERRYGROVE_CITY_obj_2), which mod.world:npc(mapId, id) resolves; the later world.interacted payload carries the live object in ev.target, so read ev.target.id.

Battle facade

mod.battle is the read-only snapshot and menu-intent surface. It materializes on first touch like mod.world and is nil with no live game. Per generation: src/battle/BattleAPI.lua, src/battle/gen2/BattleAPI.lua, src/battle/game3/BattleAPI.lua; on API 3 it is the portable service (src/mods/UnifiedBattle.lua).

Member Signature Notes
mod.battle:snapshot () -> snapshot | nil, reason detached copy: player, enemy, party, moves, items, revision, prompt, turn, catchable; on API 3 also double, active, battlers, targets. Mutating it changes nothing. An item's catchChance is a 0 to 100 percentage; reading it rolls nothing
mod.battle:submit (intent) -> true | nil, reason apply a validated menu choice to the snapshot you just read. Every intent needs a strictly increasing positive integer id (per mod) and the latest revision; stale, replayed, covered or invalid intents change nothing

Intents: { kind = "menu", choice = "fight" } (party, item, run), { kind = "move", slot = 1..4 } and { kind = "back" } on every game. Gen 1 also has { kind = "safari", action = "ball" } (bait, rock, run) and { kind = "mimic", index = 1 }. Singles use battler ids 0 and 1; Gen 3 doubles use 0 through 3. Tutorial, link, forced, stale and covered battle states refuse core intents. Use mod.input for ordinary text advance.

Scripted input

Source-safe GB button presses through the engine's multi-source input bookkeeping: releasing a mod's hold can never drop a button the keyboard, a pad, the touch overlay or another mod still owns.

Member Signature Notes
mod.input:tap (game, btn) queue exactly one wasPressed edge for the next fixed step; holds nothing
mod.input:press (game, btn) -> token hold until released
mod.input:release (token) -> bool idempotent; refuses tokens taken by another mod. Outstanding tokens release on rollback, hot reload and input recovery

Buttons are up, down, left, right, a, b, start, select. Needs the live game (game.ready).

Device and platform

Member Signature Notes
mod.device:powerInfo () -> state, percent read-only battery state; state is LÖVE's "unknown" / "battery" / "nobattery" / "charging" / "charged", percent is 0-100 or nil. The rest of love.system stays sandboxed
mod.datetime:date / time / dateTime (game, timestamp) -> string format a timestamp under the player's DATE FORMAT / TIME FORMAT options; invalid timestamps return "----"
mod.steps:available () -> bool false on builds without the native step bridge and for mods without the steps permission, so a probe is always safe
mod.steps:sync () -> bool ask iOS/Android to refresh its step count (async; OS consent sheet on first use)
mod.steps:poll () -> { steps, from, to } | nil the next delivery for this mod; each permissioned mod gets its own copy, and the same walk is never delivered twice

sync and poll without the steps permission raise an error naming it (src/mods/Steps.lua).

Background work

These facades exist because love.thread is refused (it needs the compute permission): the workers run engine code, so a mod gets asynchrony without a Lua state the sandbox cannot reach. poll never blocks; call it from a hook or update.

Member Signature Notes
mod.fetch:available () -> bool false without a transport or the network permission
mod.fetch:get (url, opts) -> handle | nil, reason http/https only, on the URL and every redirect; opts.accept, opts.maxSeconds (max 30); four in flight per mod
mod.fetch:poll (handle) -> { status, body, err, progress } status: "pending", "ok", "error", "cancelled"
mod.fetch:release / cancel (handle) free a finished job / drop an unwanted result
mod.job:available () -> bool false without threads or the background permission
mod.job:run (script, arg, opts) -> handle | nil, reason script is a file in your mod dir, run in your sandbox with no mod object and no require; plain data in, plain data out; opts.maxSeconds (default 5, max 30); two per mod, four per machine
mod.job:poll (handle) -> { status, result, err } same statuses as fetch
mod.job:release / cancel (handle) cancel drops the result; it cannot stop the thread, so write jobs that terminate
mod.postLog (body, opts) -> handle one-way crash-log report to the https:// log_url the manifest declares; needs the network permission and log_url, never returns the response body. Same handle/poll/release shape as mod.fetch

The full rules (User-Agent attribution, handle ownership, budget semantics) are in the Background HTTP and Background jobs sections of docs/modding.md.

Inter-mod API

Member Signature Notes
mod.exports a table you fill (or assign) what other mods see
mod.find (otherId) -> { id, version, exports } | nil nil when absent, disabled, failed, or not yet loaded

Logging

Member Signature
mod.log:info / warn / error (fmt, ...) - attributed [mod-id] lines

Supported requires

Mod code may require exactly three engine modules without the engine_internals permission: src.mods.Semver (range-check another mod's exports), src.audio.ChipAsm (author chip music and sfx), and src.pokemon.Stats (Stats.isShiny / Stats.calc for indicator mods). Everything else under src. is unsupported-and-may-break; under POKEPORT_DEV=1 an undeclared require logs a warning naming the mod and module.