Content registries
How mods add, tweak, and remove game content. A registry is a named table
of ids (pokemon, moves, items, ...) reachable during a mod's entry
chunk as mod.content.<name>. Registrations are collected while every mod
loads, then folded into the live data once, at the merge boundary described
in Lifecycle. The catalog itself - every registry's
merge semantics, Data target, and value schema - lives in one place,
src/mods/Schemas.lua (47 registries), and the
registry reference is generated from it. Regenerate
the reference after changing the schema so the committed documentation stays
aligned with the engine.
The same registry name works on every generation. Each registry is routed per
game: it may keep the Gen 1 target, land in a gen2* or gen3* table with
its own record shape, or have no home there (the write is dropped and
reported). The per-game routing is in Data Model
and each entry of the reference. A mod with "api": 3 gets canonical shared
records for the core registries; see Portable API 3.
Choose the operation first
Choose by the result you want, before reading the exact schema:
| You want to | Use | Why |
|---|---|---|
| Change one or a few fields on an existing record | patch |
keeps fields you did not mention |
| Add a record with a new id | register |
requires the new record's required fields |
| Replace an existing record as a whole | override |
deliberately discards the old record before applying yours |
| Make a record unavailable | remove |
leaves a tombstone so consumers treat the id as absent |
| Inspect the merged result | get or each |
includes base data and loaded mods |
Code placement: call these methods from your mod entry file, before the merge phase. Status: the snippets below show the method shapes; adapt ids and fields to your mod. Start with Choose a Registry if you know the outcome you want but not the registry name.
mod.content.pokemon:register(id, value) -- add a new record; duplicate ids error
mod.content.pokemon:override(id, value) -- replace a whole record
mod.content.pokemon:patch(id, partial) -- deep-merge onto the current record
mod.content.pokemon:remove(id) -- tombstone; consumers treat it as absent
mod.content.pokemon:get(id) -- the effective value, base data included
mod.content.pokemon:each() -- iterate the merged view (base + mods)
mod.content.pokemon:has(id) -- api 3 only: true when the id exists
has exists on the api = 3 content handles only. Reads and iteration there
return detached copies, so change a record through a write verb.
register collides with the imported base data too: adding "PIKACHU"
errors, because changing an existing record is what override and patch
are for. remove followed by a later register resurrects the id.
Patch, not restate
The classic v1 mistake was re-stating a whole species record to change one
stat, which silently dropped every field the author forgot. patch merges
only what you provide:
mod.content.pokemon:patch("MEWTWO", { catchRate = 1 })
mod.content.pokemon:patch("MEW", { baseStats = { attack = 120 } }) -- hp etc. intact
Patch rules, in order:
- Dictionary values merge per key, recursively.
- Array values replace wholesale in a record registry (element-wise merging of a learnset is ambiguous). In a deep registry they append instead, so two mods adding rows to the same list both land.
- To extend a list in a record registry, wrap it:
{ __append = { row } }or{ __prepend = { row } }. The wrapper is unwrapped during the merge and never reachesData;__prependis also how a deep registry reaches the front of a list. mod.DELETEunsets a field:patch("EEVEE", { evolutions = mod.DELETE }).- Patches stack across mods in load order.
Two small merge examples
A record field that is a dictionary keeps the keys you did not name:
before: baseStats = { hp = 35, attack = 55, speed = 90 }
patch: baseStats = { attack = 70 }
after: baseStats = { hp = 35, attack = 70, speed = 90 }
A plain list means different things in the two registry families. In a
record registry it replaces the existing list; use __append or __prepend
when you want to retain it. In a deep registry it appends, because those
registries combine one shared data tree:
record registry: before { "TACKLE" }, patch { "GROWL" } -> { "GROWL" }
deep registry: before { "A" }, patch { "B" } -> { "A", "B" }
Merge semantics
Each registry declares one of three behaviors in Schemas.lua
(constants, field and text_pointers are the deep ones; map_scripts
and migrations are the compose ones; the other 42 are records):
- record - id maps to a whole record; the verbs behave as above. Most registries are records.
- deep - the registry is one deep-mergeable tree (
constants,field,text_pointers) whose ids are the top-level keys of the target table.registerandpatchare synonyms: both merge into the key, with lists appending.overridereplaces the whole key, which is the total-conversion verb. See Data Model. - compose - registrations accumulate into per-id chains instead of
replacing each other (
map_scripts,migrations). Duplicateregistercalls are legal by design: two mods adding NPCs to the same map both land, ordered bypriority(higher first) then load order. Anoverridechain replaces the engine's own base contribution too, and afalseentry in amap_scriptstalk/scripts slot suppresses every lower-precedence entry.patchis an error on a compose registry.
Registries merge shallowest target first, so a whole-table registry such as
audio lands before the granular ones nested under it (music, sfx,
cries, map_songs); the name breaks ties, so the same content always
merges the same way.
Validation
Every register/override/patch is checked against the registry's
schema. A violation is a load error attributed to the mod - named
registry, id, field path, expected type - instead of a nil-index crash
three systems downstream. A near-miss field name gets a suggestion
(did you mean "baseStats"?). After the merge, a cross-reference pass
resolves id-typed fields (a learnset move, an evolution target) against
the merged world and reports dangling references the same way.
Unknown top-level fields are allowed and preserved - extensible records are a feature - but a key that is only a case/underscore variant of a schema field is the classic typo and is rejected with the suggestion.
Mods with "api": 1 in their manifest (or no api field) downgrade all
of this to logged warnings, so every v1 mod keeps loading exactly as
before.
A registry with no home on the booted game takes the write, drops it and
reports it once per mod in the manager's error feed; it is never a crash.
A registry is gated this way in both directions (a Red boot writing to
apricorns, a Gold boot writing to rulesets).
If you use the schemas from your own tools, note the argument order:
Schemas.check(spec, registryName, id, value, mode, generation) - the spec
comes FIRST, fetched from Schemas.REGISTRIES[name]. Passing the name first
silently validates nothing. The optional generation picks the Gen 1, Gen 2
or Gen 3 record shape.
The strict level follows the manifest api: 2 and 3 reject, 1 warns.
Namespaces
A registry whose base data module never shipped a table merges into a
freshly created namespace (Data.screens, Data.map_scripts,
Data.audio.songs when absent) instead of being silently dropped - the
fate of the v1 scripts and ui registries.
Vanilla content is registrations too
The engine registers its own built-ins - statuses, move effects, balls,
growth curves, evolution methods, script commands, text tokens, rulesets,
AI classes, battle transitions, the type chart - under the owner engine
before any mod runs (src/mods/Builtins.lua). Which built-ins exist depends
on the booted generation: Gold seeds its own statuses, balls and AI records
instead of Red's, and the Gen 3 battle registries (move_effects,
statuses, ai_classes, battle_anims) are seeded empty because the cart's
own effects are code, not records. Three consequences:
each() always yields the whole world, register on an existing vanilla
id collides and forces an explicit override, and the behavioral
namespaces (Data.statuses, Data.commands, ...) are populated on every
boot whether or not a mod is installed. Each registration hands over the
table the engine already reads, so the registry and the consumer are the
same object and nothing about vanilla behavior changes.
Renames and deprecations
| v1 name | Now |
|---|---|
scripts |
map_scripts (compose). v1 registrations, previously dropped, now work. |
ui |
screens (record). Same: previously dropped, now merged. |
audio |
Kept, deprecated. Whole-key replacement of Data.audio; the granular sfx / cries / map_songs / music registries supersede it. |
The old names keep working as aliases of the same registry and log a one-time deprecation warning per mod.
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.