mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 00:10:56 +02:00
847 lines
49 KiB
Markdown
847 lines
49 KiB
Markdown
# Mods and Gen 2 (Gold)
|
|
|
|
The mod API is one API across both generations. Hook names, event names,
|
|
registry names and the `mod.*` facade are shared on purpose: a mod that runs on
|
|
Red should be able to run on Gold without learning a second vocabulary.
|
|
|
|
What differs is how much of it Gold can actually serve, and that is why Gen 2
|
|
support is something a mod **declares** rather than something it inherits.
|
|
|
|
## What you can rely on today
|
|
|
|
The short version, for an author deciding what to write:
|
|
|
|
- **Every registry name, hook name and event name means the same thing in both
|
|
games.** Nothing is prefixed, renamed or repurposed per generation. Where Gen
|
|
2 genuinely carries more, the record or the payload gains a *field*.
|
|
- **40 of the 46 registries are available on Gold.** 17 keep their Gen 1 target
|
|
outright (`commands`, `tokens`, `growth_rates`, `battle_sprite_scales` and
|
|
`render_pipelines` among them), 16 route to a Gen 2 table under the same
|
|
name, 6 are Gen 2-only systems Red has no counterpart for, and `migrations`
|
|
is a code registry with no data target in either game. The other 6 are gated,
|
|
and are listed below with the consumer change each one still needs.
|
|
- **A registry with no home in a generation is reported, never silently
|
|
merged.** The write is taken, dropped, and named once per mod in the same
|
|
error feed the mod manager shows -- in both directions, so a Red boot writing
|
|
to `decorations` is told exactly as a Gold boot writing to `map_scripts` is.
|
|
- **40 event names and 43 hook names have a call site in both generations**, so
|
|
one subscription serves both games. `tests/engine/gate_gen2_mod_api.lua`
|
|
reads those names back out of the source and fails if a site is renamed or
|
|
deleted on either side, and fails again if a new shared site appears without
|
|
being listed here.
|
|
- **24 further names are Gen 2-only** (friendship, breeding, the Pokegear, the
|
|
radio, Pokerus, the roamers, Kurt, the Bug Contest, the Unown puzzle, mail,
|
|
held items, shininess, gender, and the five cards of the GS boot cinema).
|
|
They are plain names, not a `gen2.` namespace, so if Red ever grows the
|
|
system the name is already right.
|
|
- **Every Gen 2 seam is guarded** by `Runtime.wants` / `Runtime.wantsHook`, so
|
|
a boot with no mod subscribed allocates nothing at any of them.
|
|
- **A mod is loaded on Gold only if it says so.** See `gen2compat` below.
|
|
|
|
`src/mods/Schemas.lua` is authoritative for routing;
|
|
`tests/engine/gate_gen2_mod_api.lua` holds this document to it.
|
|
|
|
## Declaring which games a mod is for
|
|
|
|
```json
|
|
{
|
|
"id": "my_mod",
|
|
"name": "My Mod",
|
|
"version": "1.0.0",
|
|
"entry": "main.lua",
|
|
"api": 2,
|
|
"games": ["gen1", "gen2"]
|
|
}
|
|
```
|
|
|
|
`games` is an optional array of version ids (`"red"`, `"blue"`, `"yellow"`,
|
|
`"gold"`), generations (`"gen1"`, `"gen2"`, case-insensitive) or `"all"`.
|
|
`src/mods/ModTargets.lua` resolves the tokens off `GameVersion.ORDER` and
|
|
`GameVersion.generation`, so nothing anywhere restates the game list.
|
|
`Manifest.validate` stores the resolved, ORDER-sorted ids on `manifest.games`
|
|
and **derives** `manifest.gen2compat` from them, which is the one field the
|
|
loader's gate reads.
|
|
|
|
Nothing moves on disk for any of this. A mod is installed once, into
|
|
`mods/<id>/`, and that directory serves every game: there is no `mods/gen1/`
|
|
and no per-generation copy. Targeting is declared, not filed.
|
|
|
|
`"gen2compat": true` is the legacy spelling and is still accepted. It is purely
|
|
additive -- it *adds* the Gen 2 games to whatever `games` says -- so no shipped
|
|
manifest can lose a game it already ran on. A manifest with neither key is Gen
|
|
1 only, which is exactly what it always meant. An unknown token warns and is
|
|
dropped under `api` 1 and refuses the manifest under `api` 2; a `games` array
|
|
that names no game this engine knows falls back to the default rather than
|
|
orphaning the mod; a non-array `games` is a hard error.
|
|
|
|
Every token is enforced, per game. `Loader:_gateGeneration` gates on
|
|
`ModTargets.supports(manifest, version, generation)`, the same call both mod
|
|
surfaces make, so `"games": ["blue"]` really does not load on Red and the
|
|
loader's skip line is the launcher's line, `For Blue, not Red`. A manifest with
|
|
no `games` and no `gen2compat` still covers every Gen 1 game, so nothing
|
|
written before the key existed changes behavior.
|
|
|
|
On a Gold boot, a mod claiming no Gen 2 game is **not loaded at all**: no
|
|
registrations, no subscriptions, no entry chunk. The manager still lists it,
|
|
showing `ENABLED (NOT THIS GAME)` and the reason, and the player's enable flag
|
|
is left alone so it comes straight back on Red.
|
|
|
|
Both mod surfaces derive what they show from `ModTargets` rather than from
|
|
their own copy of the rule. The launcher's mod panel carries a `Show for:` game
|
|
chip row and a per-mod tag (`GEN 1`, `GEN 1+2`, `RED/GOLD`), greyed with `Not
|
|
for this game` and the detail `For Gen 1, not Gold` when the mod does not run
|
|
on the selected game; the in-game manager shows the same verdict as
|
|
`ENABLED (NOT THIS GAME)` plus an inert `FOR GEN 1+2` row on the detail screen.
|
|
The launcher asks the same question of a mod's dependencies: one whose hard
|
|
dependency does not run on the selected game reads `Needs <id> (not for Gold)`,
|
|
matching the loader's contagious skip.
|
|
|
|
A separate overlay, `options.modsByVersion[version][id]`, is where a per-game
|
|
enable flag will live. It is a preview: `SaveData.PER_VERSION_MODS` is `false`,
|
|
so `SaveData.modScope` answers nil for every caller and the launcher panel, the
|
|
in-game manager and the loader all read *and* write the one shared
|
|
`options.mods` flag. Nothing consults the overlay for enablement until that
|
|
flips, which is deliberate: the overlay is plantable from an imported
|
|
`.g1rmodlist`, and a reader scoped differently from the writers would show a
|
|
mod set no boot would honour.
|
|
|
|
That is deliberate. Gold reimplements the battle engine, the overworld, the
|
|
script VM and the save format, so a Gen 1 mod dropped into a Gold boot would
|
|
find a small fraction of its call sites live. A mod that half-applies reads to
|
|
a player as a broken mod. Not running is the honest state, and naming a Gen 2
|
|
game is the author saying "I have tested this there".
|
|
|
|
Adding a Gen 2 game does not opt out of anything on Gen 1, because `games` is a
|
|
union: `["gen1", "gen2"]` covers everything it covered before. What does change
|
|
is that the gate now runs on a Gen 1 boot too, so a manifest that names *only*
|
|
Gen 2 games no longer loads on Red, Blue or Yellow. Say `["all"]` or list both
|
|
generations if you want both.
|
|
|
|
Two riders. **A hard dependency that does not run here takes the dependent down
|
|
with it**, as a skip rather than a failure and carrying the dependency's own
|
|
wording (`depends on X, which does not run here (For Blue, not Red)`), so the
|
|
whole chain has to cover the same games. And **the claim is yours, not the last
|
|
word**: it is the manager's `TRY HERE ANYWAY` row that lets a player run a mod
|
|
whose author never opted in, which is the only route for a mod written before
|
|
the field existed. The override is per game -- `options.modsGen2[id]` is a
|
|
`{ [version] = true }` table, so forcing a mod onto Red does not force it onto
|
|
Gold, and a legacy `options.modsGen2[id] = true` reads as "the Gen 2 games",
|
|
the only set it could ever have affected. It applies on the next boot; a forced
|
|
mod loads normally and keeps a note saying it was never verified here. Where
|
|
the choice cannot be persisted the manager says `COULD NOT SAVE` instead of
|
|
promising a restart.
|
|
|
|
If you are writing new code, still prefer the API: take the live game from
|
|
`mod.game` (or the `game.ready` payload, or a `ui.*` hook's first argument) and
|
|
the world from `mod.world`. Those are the names that mean the same thing in
|
|
both games. What follows is for the mods that were written before Gold existed
|
|
and reach past it.
|
|
|
|
## Gen 1 module facades
|
|
|
|
A mod with `engine_internals` reaches engine modules by name, and under Gold
|
|
those names used to resolve to Gen 1 modules nothing instantiates -- so the
|
|
patch landed on dead code and the mod was inert with no symptom but silence.
|
|
|
|
On a Gen 2 boot, **a require made from a mod's own chunk is answered by an
|
|
adapter**: the Gen 1 API, backed by Gen 2 internals. `src/mods/Gen2Compat.lua`
|
|
is the table, `src/mods/Loader.lua`'s require shim is where the swap happens,
|
|
and `tests/engine/gate_gen2_mod_facade.lua` holds both to it. Engine code is
|
|
not affected -- the shim only substitutes when the calling chunk is outside the
|
|
engine tree, so `src/render/PaletteFX.lua` still gets the real Gen 1 module on
|
|
both generations.
|
|
|
|
Fifteen names are served. **alias** means the adapter *is* the Gen 2 module, so
|
|
a monkey-patch, a `rawset` sentinel and a `getmetatable(x) == M` check all land
|
|
on the table Gold runs; **facade** means a translating wrapper over it.
|
|
|
|
| the Gen 1 name a mod requires | kind | what it gets on Gold |
|
|
| --- | --- | --- |
|
|
| `src.core.Game` | facade | a live proxy onto the Game2 instance |
|
|
| `src.world.OverworldController` | facade | over `src/world/gen2/World.lua`; `World:step` / `:interact` / `:interactBody` dispatch through it |
|
|
| `src.world.Map` | alias | `src/world/gen2/Map.lua`, grown Gen 1's statics and instance methods |
|
|
| `src.world.NPC` | alias | `src/world/gen2/Npc.lua`; `NPC.new` sniffs the Gen 1 argument order |
|
|
| `src.pokemon.Boxes` | facade | over `src/core/gen2/Boxes.lua`, plus Gen 1's `COUNT` / `CAPACITY` / `ensure` / `active` / `deposit` |
|
|
| `src.battle.BattleState` | facade | over `src/ui/gen2/BattleState.lua`, write-through |
|
|
| `src.ui.PartyMenu` | facade | over `src/ui/gen2/PartyMenu.lua`, write-through |
|
|
| `src.world.WorldAPI` | alias | `src/world/gen2/WorldAPI.lua` |
|
|
| `src.world.PikachuFollower` | alias | `src/world/gen2/Follower.lua` |
|
|
| `src.script.ScriptRunner` | facade | over `src/script/gen2/Vm.lua` |
|
|
| `src.ui.OptionsMenu` | facade | over `src/ui/gen2/OptionsMenu.lua`, write-through |
|
|
| `src.world.FieldDefaults` | facade | the `playerSprites` answer, and a named refusal for the rest |
|
|
| `src.world.Collision` | facade | `DELTA` / `target` / `occupied` / `canMove` |
|
|
| `src.ui.StartMenu` | facade | over `src/ui/gen2/StartMenu.lua`, write-through |
|
|
| `src.ui.BoxMenu` | alias | `src/ui/gen2/PcMenu.lua` |
|
|
|
|
Two entries in that table are not the pairing they look like.
|
|
`src.ui.BoxMenu` resolves to `PcMenu`, not to `src/ui/gen2/BoxMenu.lua`: Gen 1's
|
|
`BoxMenu` is Bill's PC *top menu*, whose Gold counterpart is `PcMenu`, while
|
|
Gold's `BoxMenu` is the withdraw/deposit *list* Gen 1 builds inline. And
|
|
`src.script.ScriptRunner` is served narrowly rather than fully: `scanLabels`
|
|
and `validate` forward verbatim, with the default verb lookup swapped to
|
|
`game.data.commands` so a script of Gen 1 built-ins cannot validate clean and
|
|
then run as nothing, while the lifecycle half is a thin handle onto the one
|
|
`world.vm` with `resume` and `update` refused rather than double-driving it.
|
|
The `script.started` / `script.ended` / `script.command` seams are the
|
|
supported route and already work on Gold.
|
|
|
|
`src.script.Commands` and `src.ui.OptionRows` have **no** adapter and are the
|
|
two names a require of which still lands in the boot error feed the manager
|
|
shows, with the module named. Both load fine under Gold and both are traps: the
|
|
first hands back 61 Gen 1 verbs none of which Gold can run, the second paints
|
|
Red's four-box options chrome over Gold's single 18x16 one.
|
|
|
|
`docs/preparing-your-mod-for-gen2.md` is the migration guide for an author
|
|
working through this, and `python3 tools/modkit.py gen2check <id>` reports a
|
|
mod's own findings against the coverage table below.
|
|
|
|
Three rules the adapters keep, because a plausible wrong answer is worse than
|
|
the module being missing:
|
|
|
|
- **Live, never a snapshot.** A mod captures `require("src.core.Game")` at file
|
|
scope, before a save or a world exists. The facade is a proxy that reads the
|
|
live instance on every touch, so `Game.save` is nil during the entry chunk
|
|
and correct forever after. It aliases the two names Gold spells differently
|
|
(`Game.overworld` is `Game2.world`, `Game.writeOptions` is
|
|
`Game2:persistOptions`) and the one data table that was renamed
|
|
(`game.data.sprites` is `data.gen2Sprites`).
|
|
- **A member with no backing says so.** `game.data.field` does not exist on
|
|
Gold, so it reads nil *and* logs once, naming the mods holding the facade.
|
|
`BattleState.newWild` is absent rather than invented, because a `newWild`
|
|
that took a species and a level would be a lie about what Gold's battle
|
|
screen is.
|
|
- **One stable table for the run.** Where the Gen 2 arm can serve the name
|
|
outright the adapter *is* that module, so a mod's monkey-patch, its
|
|
`rawset` sentinel and its `==` idempotency check all land on the table Gold
|
|
actually runs.
|
|
|
|
### What the adapter says it covers
|
|
|
|
The adapter publishes its own coverage, versioned by
|
|
`Gen2Compat.COVERAGE_VERSION` (1), and `modkit gen2check` consumes that table
|
|
rather than a second copy of the same knowledge:
|
|
|
|
```lua
|
|
Gen2Compat.modules() -- the 15 names, sorted
|
|
Gen2Compat.serves(name) -- boolean
|
|
Gen2Compat.memberStatus(name, member) -- "backed" | "warned" | "absent" | nil
|
|
Gen2Compat.coverage(name) -- a fresh table per call:
|
|
-- { module, kind = "facade"|"alias", target, members = { [name] = status },
|
|
-- notes = { [name-or-topic] = "one line" } }
|
|
```
|
|
|
|
The status vocabulary is frozen at three values, and a member listed as both
|
|
resolves to the weaker claim:
|
|
|
|
| status | means |
|
|
| --- | --- |
|
|
| `backed` | present, and it does the Gen 1 job on Gold |
|
|
| `warned` | present, answers nil or degrades, and names itself once with the mod attributed |
|
|
| `absent` | deliberately not served; a nil read is the honest failure |
|
|
|
|
Today that is 288 backed, 32 warned and 161 absent across the fifteen modules.
|
|
`notes` keys are documentation topics rather than a member list -- dotted paths
|
|
(`save.money`), field names (`warpAt`), hook names (`hook ui.pc.items`) and
|
|
bare topics (`identity`, `iteration`, `rawset`) all appear there. `members` is
|
|
the authoritative set, and a member it does not record is not a promise either
|
|
way: on an alias it resolves to whatever the Gen 2 module has, on a
|
|
write-through facade it falls to the Gen 2 class, on the `src.core.Game` facade
|
|
it reads nil and says so, and on the `src.world.OverworldController` facade it
|
|
reads nil silently.
|
|
|
|
**The follower.** Gold's cart has no trailing companion at all, so
|
|
`src/world/gen2/Follower.lua` is new Gen 2 code rather than a facade: the
|
|
entity, the trail loop, and a `shouldSpawn` a mod replaces. `World:step` calls
|
|
`Follower.update(game, world)` once per logic frame after the body, and
|
|
`World:setMap` calls `Follower.onMapEntered` before it emits `map.entered` --
|
|
the same two call sites `src/world/OverworldController.lua` gives the Gen 1
|
|
arm, which is what makes a Gen 1 follower mod's wrappers tick.
|
|
|
|
Vanilla never spawns one: `shouldSpawn` answers false until something replaces
|
|
it. `Follower.setShouldSpawn(fn)` is the supported way, and it writes the same
|
|
file-local the Gen 1 mods reach through `debug.setupvalue` on the upvalue named
|
|
`shouldSpawn`, so the two cannot disagree.
|
|
|
|
Two Gen 2 engine changes came with it, both general rather than follower-only:
|
|
an entity with `passable` set never blocks a step (the Gen 1 name and meaning,
|
|
`src/world/Collision.lua`), and `World:rebuildPeople` now preserves **guests** --
|
|
anything in the people list it did not put there. A rebuild runs on every zoom
|
|
and every time-of-day roll, so without that a follower vanished at the top of
|
|
the hour.
|
|
|
|
**What the facades cannot fix.** A mod that allow-lists version strings
|
|
(`GameVersion.get() == "red" or ...`) excludes itself from Gold by construction,
|
|
and no adapter should special-case it. Neither is a Gen 1 screen id: Gold's
|
|
builtins carry a `Gen2` prefix, so a mod matching `id == "BoxMenu"` matches
|
|
nothing. A write to a field on a live Gen 2 menu instance is inert where Gen 1
|
|
read it back (`menu.onSwitch`, `menu.swapFrom`, `StartMenu`'s box geometry),
|
|
and `map.warpAt` is a name collision rather than a rename -- Gen 1's is a table
|
|
keyed by cell, Gold's is a method, so indexing or iterating it raises. All of
|
|
these are mod-side edits, each with a route that works on both generations;
|
|
`docs/preparing-your-mod-for-gen2.md` walks through them.
|
|
|
|
## What works on Gold today
|
|
|
|
**Screens.** The `screens` registry serves both generations. Gold's screens
|
|
are registered under `Gen2`-prefixed ids so a mod that replaces Gold's party
|
|
menu does not also replace Red's; `Screens.GEN2_IDS` in `src/ui/Screens.lua`
|
|
is the full list. Every screen Gold opens goes through an id, including the
|
|
boot cinema and the START menu.
|
|
|
|
**Asset overrides.** `overrides/` shadowing and asset transforms work
|
|
unchanged: Gold's screens load art through `src/render/Assets.lua`, the same
|
|
choke point Gen 1 uses.
|
|
|
|
**Content registries at the shared path.** `pokemon`, `moves`, `items`,
|
|
`type_chart`, `strings`, `font`, `screens`, `commands`, `tokens`,
|
|
`growth_rates`, `battle_sprite_scales`, `render_pipelines`, and the audio
|
|
family (`audio`, `music`, `sfx`, `cries`, `map_songs`). These keep their Gen 1
|
|
target path, so one mod source targets both generations.
|
|
|
|
The last two are the newest and each carries one caveat worth stating before
|
|
you write against it:
|
|
|
|
- **`battle_sprite_scales`.** `src/ui/gen2/BattleState.lua:imageScale` walks
|
|
the merged table for a record whose `path` matches the pic being drawn,
|
|
skipping the registry's own `_owners` row, and `picScale` falls through to
|
|
the species record's `battleScaleFront` / `battleScaleBack` after it -- the
|
|
same image-then-species-then-default order Gen 1 resolves in. Because the key
|
|
is the asset path it also reaches the pics that are nobody's species: the
|
|
player's trainer back, the DUDE's, an opponent's frontpic. The **default**
|
|
differs and is not a registry record either side: Red's 32x32 back pics draw
|
|
at 2x, Gold's 48x48 ones fill their 6x6 box at 1x, so a scale that looks
|
|
right on Red is twice as large on Gold. At any scale the pic stays centred in
|
|
its box and standing on the same ground line.
|
|
- **`render_pipelines`.** `src/core/Game2.lua:load` installs
|
|
`src/render/Pipelines.lua` on Gold's dataset *after* `mods:load`, so the
|
|
merged table is the one it walks, and `Game2:draw` composites the
|
|
whole-frame half through `Pipelines.wantsPresent` / `Pipelines.present` with
|
|
the Gen 1 ctx keys (`width`, `height`, `scale`, `dpi`, `dpiX`, `dpiY`). The
|
|
**`drawWorld` half is inert on Gold**: its overworld draws straight to the
|
|
window rather than into a canvas the way `src/world/OverworldController.lua`
|
|
hands one to `Pipelines.drawWorld`. A drawWorld-only pipeline is not left
|
|
switched on and drawing nothing -- `Game2:load` retires a restored level for
|
|
one, leaving `options.pipelines` untouched so the mode comes back the day
|
|
Gold grows a world canvas. Gold also has no OPTION row for a pipeline
|
|
(`Pipelines.rows` is read only from `src/ui/OptionsMenu.lua`), so a Gold
|
|
player reaches one by its `hotkey`.
|
|
|
|
**Content registries at a Gen 2 path.** `maps`, `tilesets`, `sprites`, `text`,
|
|
`encounters`, `trainers`, `palettes`, `icons`, `battle_anims`, `constants`,
|
|
`statuses`, `move_effects`, `item_effects`, `balls`, `ai_classes` and
|
|
`evolution_methods`. Same registry name, same verbs, a Gen 2 table underneath
|
|
(`data.gen2Maps`, `data.gen2Encounters`, `data.gen2Statuses`, ...).
|
|
`src/core/Game2.lua` loads the extracted ones into `game.data` before it
|
|
calls `mods:load`, and every consumer takes them by reference and never
|
|
copies, so what a mod merges is what the game walks: a registered map is a map
|
|
Gold can warp into, a patched tileset is the one `Map.new` reads, a patched
|
|
encounter table is the one the grass rolls.
|
|
|
|
The battle-rule six are the newer half and work slightly differently: there is
|
|
no table on disk for them at all. They come into existence *as* the merge, and
|
|
each consumer reads a record through a lookup that falls back to its own module
|
|
records when no loader ran, so a mod-free Gold boot behaves identically:
|
|
|
|
| registry | who reads it |
|
|
| --- | --- |
|
|
| `statuses` | `Battle.statusRecordFor` / `statusPenaltyFor`, `Catching.statusBonus`, `ItemEffects.healClassOf` |
|
|
| `move_effects` | `Battle.moveEffectRecordFor` (`useMove`'s dispatch) |
|
|
| `balls` | `Catching.recordFor` |
|
|
| `ai_classes` | `Ai.layersFor` (the ten `scoring.asm` passes, plus mod layers) |
|
|
| `evolution_methods` | `Evolution.methodFor` |
|
|
| `item_effects` | `ItemEffects.recordFor` / `partyAction` |
|
|
|
|
`src/mods/Builtins.lua` seeds those six with **Gold's** records under Gen 2
|
|
rather than Red's. It has to: both games call it `GREAT_BALL`, and Red's record
|
|
carries no `multiplier`, so seeding Red's would leave Gold's x1.5 reading nil.
|
|
|
|
**Content registries that exist because Gold does.** Six systems Red has no
|
|
counterpart for, so there is no Gen 1 table to share and none of these carries
|
|
a Gen 1 target at all. The routed Gen 2 path is their only home, and
|
|
`Schemas.GEN1` gates them on a Red boot the way `Schemas.GEN2` gates
|
|
`map_scripts` on a Gold one -- reported, not silently merged.
|
|
|
|
| registry | id space | who reads it |
|
|
| --- | --- | --- |
|
|
| `held_items` | item ids | `ItemEffects.heldItemFor`; the merged rows are written back onto `data.items` for `Battle:itemDef` |
|
|
| `phone_contacts` | `PHONE_*` (`data.gen2Constants.phoneContactOrder`) | `Phone.useRegistry`, folded onto the contact table |
|
|
| `decorations` | `"deco:<n>"` | `Decorations.attributes`, the single read point for an attribute row |
|
|
| `apricorns` | apricorn item ids | `Apricorns.useRegistry`, which rebuilds all three lookups and Kurt's menu order |
|
|
| `landmarks` | `LANDMARK_*` | `Nests.landmarkId` / `Nests.landmark`, which resolve a map header's landmark byte |
|
|
| `radio_channels` | station ids | `MapRadio.channelRecord`, which puts a registered station on the dial |
|
|
|
|
`Game2:load` calls `Phone.useRegistry`, `Decorations.useRegistry`,
|
|
`Apricorns.useRegistry` and `ItemEffects.applyHeldItems` immediately after
|
|
`mods:load`, so the merge is live before the first frame. `landmarks` and
|
|
`radio_channels` need no such call: their consumers take `data` at call time.
|
|
|
|
`landmarks` merges onto the cache's own `gen2Landmarks.landmarks` and
|
|
`held_items` onto the view `Game2` builds from `data.items`, so both fold
|
|
against the vanilla row -- a `register` for an existing id collides, a
|
|
`patch` stacks. The other four come into existence as the merge, seeded from
|
|
their module's literals by `src/mods/Builtins.lua`.
|
|
|
|
Four honest limits on that surface:
|
|
|
|
- `held_items` reaches the battle by being written back onto `data.items`, so a
|
|
held row for an id with no `data.items` record lands nowhere. To invent a
|
|
held item, register the `items` record too. The write-back is a diff against
|
|
a pre-merge snapshot, which is what lets `items` and `held_items` compose
|
|
instead of one reverting the other.
|
|
- `decorations` ids are `"deco:<n>"`, not `DECO_*` names: the cart's decoration
|
|
constants are a bare `const_def` block with no name table behind them, so
|
|
there is nothing in the ROM to spell them by. `battle_anims` addresses its
|
|
unnamed rows the same way. `n` is the attribute row's index, which is
|
|
`wMenuSelection`.
|
|
- `phone_contacts` does not register the four `PHONE_UNUSED` `const_skip` holes
|
|
(contact bytes 8, 9, 10 and 25). The manifest gives all four the same id, and
|
|
one id cannot key four rows. They stay copies of the wrong-number filler,
|
|
which is what the cart does with them.
|
|
- `radio_channels` and `phone_contacts` register *content*, not new UI: a
|
|
registered station gets a dial position and a name, and a registered contact
|
|
gets a row the Pokegear indexes, but neither invents a screen.
|
|
|
|
**Record shapes.** A registry whose Gen 2 records genuinely differ carries a
|
|
Gen 2 schema beside its Gen 1 one (`gen2Fields` / `gen2Keys` / `gen2Write` in
|
|
`src/mods/Schemas.lua`, resolved by `Schemas.shapeFor`). The registry name, the
|
|
verbs and wherever possible the ids stay shared; only the record changes. The
|
|
differences an author meets:
|
|
|
|
- **`pokemon`.** Gen 2 splits `special` into `specialAttack` /
|
|
`specialDefense`, names the level-up table `levelMoves` and the pic size
|
|
`picSize`, has no separate `level1Moves`, and points an evolution at `into`
|
|
rather than `species`. It also carries the breeding block (`eggGroups`,
|
|
`eggMoves`, `eggSteps`, `genderRatio`) and the wild held-item pair.
|
|
- **`encounters`.** The id is the encounter *kind*, not the map:
|
|
`mod.content.encounters:patch("grass", { ROUTE_29 = { rates = { NITE = 40 } } })`.
|
|
A map's row carries a `rates` set per time of day and one slot list.
|
|
`fishGroups`, `trees` / `treeSets`, `rocks`, `bugContest` and `roamMaps` are
|
|
ids of their own.
|
|
- **`trainers`.** The id is the trainer *class*, and the record is
|
|
`{ name, index, attributes, baseMoney, encounterMusic, trainers, items }`,
|
|
with one entry per named trainer of the class. The registry writes one level
|
|
in, into `data.gen2Trainers.classes`, so the call shape is unchanged.
|
|
- **`icons`.** Two id forms in one registry, routed by the `ICON_` prefix a
|
|
sheet name carries: a species id names an assignment (a string, the sheet's
|
|
name), an `ICON_*` id names a sheet.
|
|
- **`palettes`, `battle_anims`, `constants`.** The id is a subtable of the
|
|
target: `pokemon` / `trainers` / `bg` / `objects` / `roofs` for palettes,
|
|
`scripts` / `moves` / `objects` / `framesets` / `oamsets` / `gfx` for
|
|
battle_anims, and one of Gold's 42 ordered ROM name lists (plus `mapGroups`,
|
|
`trainerClassMembers`, `types`) for constants. Those lists are ordered and
|
|
position *is* the id a script byte resolves through, so they replace rather
|
|
than append.
|
|
|
|
Four more id-space notes, because the records at those paths came out of a
|
|
Gen 2 ROM:
|
|
|
|
- Gold's `text` ids are ROM pointer strings such as `"55:4067"`, not the
|
|
`TEXT_*` names Red uses. `override` them by pointer; there is no name table.
|
|
- A Gen 2 tileset carries its walkability as `collision` where Gen 1 says
|
|
`walkable`. Both fields validate; only `collision` is read on Gold.
|
|
- A Gen 2 warp row carries `destGroup` / `destMapNum` beside the `destMap` /
|
|
`destWarp` pair Gen 1 also has. Both are optional in the shared schema, so a
|
|
Gen 1 warp row and a Gen 2 one both validate, and patching one of Gold's own
|
|
maps does not mean restating the ROM's map-group numbers.
|
|
- Gold writes `"burn"` / `"sleep"` into `mon.status` where Red writes `BRN` /
|
|
`SLP`. The `statuses` registry is the same registry; only the ids differ, and
|
|
they have to.
|
|
|
|
**`mod.commands`.** Works on Gold. `src/script/gen2/Vm.lua` runs the cart's own
|
|
bytecode, so there is no opcode byte to hand a mod -- the seam is a row the
|
|
cart cannot write. `Opcodes.MOD_COMMAND` (`"modcommand"`) is an op *name* with
|
|
no byte behind it, and the VM dispatches it through the same merged
|
|
`data.commands` table Gen 1's runner resolves by name. Two row shapes reach it:
|
|
|
|
```lua
|
|
{ op = "modcommand", verb = "mymod:shake", args = { 4, 2 } } -- native
|
|
{ "mymod:shake", 4, 2 } -- Gen 1 row
|
|
```
|
|
|
|
The second is the Gen 1 row shape verbatim, so one row list can serve both
|
|
games as long as every row in it is the mod's own verb. The handler is called
|
|
`fn(ctx, unpack(args))` with `ctx.vm` where Gen 1 has `ctx.runner`; it may
|
|
block on `ctx.vm:showText` / `:waitFrames`, and its return value speaks Gen 1's
|
|
control vocabulary (`"end"`, a row number, or nil). A missing or raising verb
|
|
is warned once per name and the rest of the list still runs. The engine's own
|
|
Gen 1 verbs are **not** seeded on Gold: a row-list verb handed Gold's ctx would
|
|
find no runner on it, so `data.commands` under Gen 2 is the mod verbs alone.
|
|
|
|
**`mod.save`, `mod.options`, `mod.log`, `mod.assets`, `mod.find`, exports.**
|
|
Generation-agnostic; nothing to adapt.
|
|
|
|
**`mod.world`.** Same method set, resolved against Gold's world
|
|
(`src/world/gen2/WorldAPI.lua`). Two differences show through and are
|
|
documented on the module: Gold's world is not a stack state, and Gen 2 event
|
|
flags are numeric ids into `wEventFlags` rather than string keys.
|
|
`spawnNpc` / `removeNpc` append onto the map def's own object list, the way the
|
|
Gen 1 arm does, so a spawned actor is pooled, drawn, walked and talked to like
|
|
an extracted one and survives a map reload; it is not serialized, so a mod
|
|
respawns on `map.entered`. `queueScript` takes a small allowlist of verbs Gold
|
|
has its own entry points for (`start_battle "wild" species level`, `warp`,
|
|
`text`, `setflag`, `clearflag`) and refuses a list containing anything else
|
|
**by name, before the first row runs**, so a mod never gets a half-run queue.
|
|
`marchInPlace` still has no Gen 2 equivalent (the Gen 2 movement stream has no
|
|
byte for it) and returns `nil, reason` rather than approximating one.
|
|
|
|
**Hooks and events that fire on Gold.** Every name below is the Gen 1 name
|
|
carrying the Gen 1 payload keys, because Gold's call sites reuse them rather
|
|
than defining a parallel vocabulary; where Gen 2 carries more, the payload
|
|
gains a field instead of the name gaining a prefix.
|
|
|
|
- *Engine-wide, from the shared modules:* `game.ready`, `screen.pushed`,
|
|
`screen.popped`, `screen.render_visible`, `music.started`, `music.stopped`,
|
|
`music.select`, `music.volume`, `sound.played`, `zoom.range`,
|
|
`assets.transformed`, `mods.loaded`, `mod.options_changed`.
|
|
- *Overworld (`src/world/gen2/`):* `map.entered`, `map.exited`,
|
|
`map.reloaded`, `player.warped`, `world.stepped`, `world.interacted`,
|
|
`world.npc_spawned`, `world.trainer_engaged`, `world.blacked_out`,
|
|
`world.block_replaced`, `world.boulder_moved`, `world.tod_changed`,
|
|
`world.object_toggled`, `flag.changed`; hooks `warp.destination`,
|
|
`movement.collision`, `movement.speed`, `encounter.roll`,
|
|
`encounter.species`, `encounter.fishing`, `world.tod`, `map.palette`,
|
|
`fieldmove.eligibility`. `flag.changed` carries the numeric `wEventFlags`
|
|
id under Gen 1's `name` key, which is the one payload difference the
|
|
numeric flag space forces.
|
|
- *Menus (`src/ui/gen2/`):* `ui.start_menu.items`, `ui.title_menu.items`,
|
|
`ui.options.rows`, `ui.party.submenu`, `ui.naming.grid`, `ui.pc.items`,
|
|
`ui.list_menu`, `transition.style`. `ui.list_menu` covers Gold's script
|
|
menus (`ScriptMenu.lua`); the `Chrome.List` widget the START and title
|
|
menus draw with does not raise it yet, so those two are composed through
|
|
their own hooks only.
|
|
- *The Oak speech (`src/ui/gen2/OakSpeech.lua`):* `intro.oak_speech.started`,
|
|
`intro.oak_speech.step`, `intro.oak_speech.answered`,
|
|
`intro.oak_speech.finished`, and the `intro.oak_speech.build` hook. Gold has
|
|
a real Oak speech, so it is the same extension point rather than a second
|
|
one: same names, same payload keys, same moments in the sequence. The beats
|
|
are a data table with the same step vocabulary (`say` / `pic` / `name` /
|
|
`choice` / `yesno` / `shrink` / `fn`, plus Gold's own `initclock` and
|
|
`demo`), and the step *ids* match Gen 1's wherever the moment is the same --
|
|
`oak_welcome`, `demo_mon`, `world_spiel`, `ask_player_name`, `name_player`,
|
|
`legend`, `shrink` -- so `ModUI.insertStepBefore(steps, "name_player", ...)`
|
|
lands in the right place in both games. The two ids with no Gen 1
|
|
counterpart are Gold's own beats, `init_clock` (the `farcall InitClock` the
|
|
speech opens with) and `oak_study` (the return to Oak for `_OakText5`). Gold
|
|
has no rival-naming or name-confirmation beats, so it raises no anchors for
|
|
them: the rival is named by `CopScript` in `maps/ElmsLab.asm`, hours later.
|
|
- *Battle (`src/battle/gen2/`):* `battle.started`, `battle.ended`,
|
|
`battle.turn_started`, `battle.turn_ended`, `battle.move_used`,
|
|
`battle.damage_dealt`, `battle.fainted`, `battle.status_inflicted`,
|
|
`battle.battler_switched`, `battle.ball_thrown`, `battle.exp_gained`,
|
|
`pokemon.level_up`, `pokemon.move_learned`; hooks `battle.damage`,
|
|
`battle.crit`, `battle.accuracy`, `battle.turn_order`,
|
|
`battle.enemy_action`, `battle.run`, `battle.exp_award`, `exp.gain`,
|
|
`catch.rate`, `trainer.party`, `battle.overlay`, `battle.low_health_alarm`
|
|
and `battle.catch_exp`. One payload difference: Gen 1's vanilla
|
|
`battle.low_health_alarm` link reads `ctx.battle.data`, and Gold's battle
|
|
screen has no `.data` field, so the Gen 2 site **adds** `ctx.data` beside the
|
|
Gen 1 keys. A mod that calls `nextFn` is unaffected; one that reaches through
|
|
`ctx.battle.data` instead gets nil on Gold.
|
|
- *The catch and the evolution:* `pokemon.caught`, `pokemon.evolved`; hook
|
|
`evolution.check`. `src/ui/gen2/BattleState.lua:pushCaught` emits
|
|
`pokemon.caught` once the mon is in the party or the box, and
|
|
`src/core/gen2/Evolution.lua` emits `pokemon.evolved` from `apply` and wraps
|
|
each row's decision in `evolution.check`. The hook passes `data` where Gen 1
|
|
passes `game`; positions 2-4 (mon, row, trigger) match.
|
|
- *The frame (`src/core/Game2.lua`):* hooks `input.step`, `input.pointer`,
|
|
`render.zones`, `render.compose`, `render.letterbox`, `render.hud`. Each sits
|
|
at the same moment `src/core/Game.lua` and `src/render/Renderer.lua` raise it
|
|
-- the logic tick before the pad is read, a pointer the touch overlay gets
|
|
first refusal on, the palette zone list handed to the present pass, the
|
|
letterbox, and the finished playfield rect -- and carries the same payload.
|
|
`render.hud`'s `gameX` / `gameY` really is where Gold's dialogue boxes and
|
|
menus land, because `Chrome.fitScale` / `fitOrigin` and `World:fitScale`
|
|
compute the same number. `render.zones` is handed `nil` in GBC mode (Gold
|
|
computes no zone of its own there) and the engine's own one-rect list in
|
|
CLASSIC mode; a rect that clamps to nothing is skipped rather than throwing,
|
|
which is what `src/render/Renderer.lua:scissorClamped` does on the Gen 1 side.
|
|
- *Sprites (`src/pokemon/Sprites.lua`, shared):* `pokemon.sprite`,
|
|
`pokemon.icon` and `player.sprite`. `pokemon.icon` is reached from
|
|
`src/ui/gen2/PartyMenu.lua` through the shared module, so it is one call site
|
|
serving both games. `player.sprite` is raised by `Sprites.playerPic`, which
|
|
Gold's battle back pic (`src/ui/gen2/BattleState.lua`), Hall of Fame and
|
|
intro call with an already-resolved path: Gold's trainer art is not in
|
|
`field.playerPics`, so the path is found first and the hook raised over it,
|
|
with the Gen 1 `ctx` keys (`side`, `kind`, `demo`, `battle`, `data`)
|
|
unchanged. The Gen 2 trainer card is the one player-art read still outside
|
|
it: its portrait is a tile sheet that also carries the frame tiles, not a
|
|
swappable pic.
|
|
`pokemon.sprite` has a second site of its own in
|
|
`src/ui/gen2/BattleState.lua`, which adds `letter` (Unown) and `shiny` to the
|
|
Gen 1 ctx keys -- both concepts Red does not have.
|
|
- *Save and the script VM:* `save.created`, `save.loaded`, `save.loading`,
|
|
`save.writing`; hooks `save.write`, `save.new_game`, `script.command`, and
|
|
the `script.started` / `script.ended` pair off `src/script/gen2/Vm.lua`.
|
|
`script.command` reports a mod's own row under the name `"modcommand"` with
|
|
the row's real operands, and may rewrite them, on the same path it wraps a
|
|
cart row.
|
|
|
|
## New in Gen 2
|
|
|
|
These have no Gen 1 analogue -- Red has no friendship byte, no day care egg,
|
|
no Pokegear, no radio, no held items -- so they are the only places a new name
|
|
is justified. They are **live**, guarded by `Runtime.wants` /
|
|
`Runtime.wantsHook`, and each is driven through a real bus by
|
|
`tests/engine/gen2_new_seams.lua`.
|
|
|
|
### Events
|
|
|
|
| event | raised from | payload |
|
|
| --- | --- | --- |
|
|
| `happiness.changed` | `Happiness` (`ChangeHappiness`, `StepHappiness`) | `mon`, `event`, `reason` (`"event"` / `"step"`), `delta`, `from`, `to` |
|
|
| `breeding.egg_created` | `Breeding` (`DayCare_InitBreeding`) | `egg`, `mother`, `father`, `compatibility`, `stepsToEgg` |
|
|
| `egg.hatched` | `Breeding` | `mon`, `egg`, `slot`, `species`, `nickname` |
|
|
| `phone.call_received` | `PhoneRing.script` | `call`, `contact`, `name`, `className`, `special`, `scriptKey` |
|
|
| `clock.day_changed` | `Clock` | `day`, `previous`, `reason` |
|
|
| `pokerus.infected` | `Pokerus` | `party`, `slot`, `mon`, `strain`, `days`, `source` |
|
|
| `roamer.moved` | `Roamers` | `index`, `slot`, `species`, `from`, `to`, `reason` |
|
|
| `roamer.encountered` | `Roamers` | `index`, `slot`, `species`, `level`, `mapId` |
|
|
| `apricorn.converted` | `Apricorns` (Kurt) | `apricorn`, `ball`, `event` |
|
|
| `bug_contest.scored` | `BugContest` | `mon`, `score`, `place`, `results` |
|
|
| `unown.unlocked` | `Unown` (`UpdateUnownDex`) | `letter`, `name`, `word`, `count` |
|
|
| `radio.channel` | `MapRadio` | `station`, `channel`, `name`, `source` |
|
|
| `mail.written` | `Mail` | `entry`, `slot`, `mon`, `message`, `author`, `source` |
|
|
| `mail.read` | `Mail` | `entry`, `message`, `author`, `top`, `bottom` |
|
|
| `intro.boot.copyright` | `CopyrightSplash:enter` | `screen`, `game` |
|
|
| `intro.boot.gamefreak` | `GameFreakPresents:enter` | `screen`, `game` |
|
|
| `intro.boot.movie` | `GoldSilverIntro:enter` | `screen`, `game` |
|
|
| `intro.boot.movie_ended` | `GoldSilverIntro:finish` | `screen`, `game`, `skipped`, `frames` |
|
|
| `intro.boot.title` | `TitleState:enter` | `screen`, `game` |
|
|
|
|
The four `intro.boot.*` cards are the GS boot cinema, and they are the one part
|
|
of Gold's intro with no Gen 1 moment to share a name with: Red boots into
|
|
`IntroMovie` with no copyright card, no GAME FREAK splash and no attract movie.
|
|
The Oak speech immediately after them is the opposite case and reuses
|
|
`intro.oak_speech.*` verbatim (see the shared table above).
|
|
|
|
Each card raises its name the frame it comes up, because that is the moment a
|
|
mod can act on. Only the movie has an `_ended` name, and only because it
|
|
carries a fact nothing downstream does -- `skipped` is the difference between a
|
|
player who watched all 2335 frames and one who pressed START. The other three
|
|
cards chain straight into the next card, whose own event is their end.
|
|
|
|
`delta` on `happiness.changed` is `to - from`, not the table's column, because
|
|
the 0 and $ff carry clamps are part of what the cart applied: a mon at 254
|
|
gaining "5" gained 1.
|
|
|
|
`clock.day_changed` compares against a process-local latch, so the first read
|
|
after a boot has nothing to compare against and raises nothing. That is by
|
|
design; it is a day *change*, not a day report.
|
|
|
|
`unown.unlocked` is raised from `UpdateUnownDex` -- a form first entering the
|
|
`#DEX` list -- not from the four `ENGINE_UNLOCKED_UNOWNS_*` puzzle flags. Those
|
|
flags are written by the cart's own `setflag`, so there is no Lua transition at
|
|
the puzzle solve to hang a second event on yet.
|
|
|
|
`mail.read` rides `Mail.lines` with a per-struct latch, because the read page
|
|
redraws every frame. The latch is re-armed by `Mail.get` / `Mail.mailbox`,
|
|
which is how both readers pick the letter they are about to open, so reopening
|
|
the same letter raises a second event.
|
|
|
|
### Hooks
|
|
|
|
| hook | wraps | ctx | vanilla answer |
|
|
| --- | --- | --- | --- |
|
|
| `held_item.trigger` | `Battle:heldEffect` | `battle`, `mon`, `item`, `def`, `effect`, `parameter`, `trigger` | `ctx.effect, ctx.parameter` |
|
|
| `breeding.compatibility` | `Breeding.compatibility` | `data`, `mon1`, `mon2`, `dayCare` | the vanilla byte |
|
|
| `phone.contact_list` | `Phone`'s `wPhoneList` read | called `(save, list)`, the shape the other list hooks use | the same list |
|
|
| `shiny.roll` | `Mon` | `dvs`, `species`, `def`, `level` | the DV-derived boolean |
|
|
| `gender.roll` | `Mon` | `def`, `dvs`, `ratio`, `species`, `level` | the DV-derived gender |
|
|
|
|
`held_item.trigger` is one hook over eight call sites, because on the cart
|
|
those eight *are* one routine (`GetUserItem` / `GetOpponentItem` loading b and
|
|
c, and the caller comparing b against the `HELD_*` it cares about). `trigger`
|
|
says which comparison is about to happen: `"priority"` (Quick Claw),
|
|
`"damage"` (Scope Lens and the type-boost family), `"endure"` (Focus Band),
|
|
`"flinch"` (King's Rock), `"accuracy"` (BrightPowder), `"confuse"`,
|
|
`"residual"` (the end-of-turn Leftovers / Berry / cure arm), and `"check"` for
|
|
any other read. Return nil to make the item do nothing at that trigger, or
|
|
another `HELD_*` name to substitute one -- every call site compares against a
|
|
name, so substitution is the whole mechanism.
|
|
|
|
`held_item.trigger` wraps the *read*, so a mod can suppress or substitute an
|
|
effect from any item. Defining a **new** held item is the `held_items`
|
|
registry's job, and the two compose: register the row, then steer it from the
|
|
hook.
|
|
|
|
`phone.contact_list` refuses an answer of the wrong length or with an unknown
|
|
contact id (unknown ids blank to 0 on purpose, so the Pokegear never indexes a
|
|
nil). It reorders and blanks the ten save slots; registering a contact id the
|
|
game does not know is `phone_contacts`' job.
|
|
|
|
`shiny.roll` does not override a forced-shiny battle (`opts.shiny`), which is
|
|
how the cart's own scripted shiny Gyarados stays shiny.
|
|
|
|
## Registries with no Gen 2 home
|
|
|
|
Writing to one of these while Gold is running takes the write, drops it, and
|
|
reports it once per mod into the same error feed the manager shows. It is not
|
|
fatal: a mod that supports both generations registers its Gen 1 content
|
|
unconditionally and still loads the half that applies. The report is worded
|
|
from the boot's own generation, because the gating runs both ways.
|
|
|
|
`rulesets`, `transitions`, `field`, `text_pointers`, `link_fields`,
|
|
`map_scripts`.
|
|
|
|
`Schemas.GEN2` in `src/mods/Schemas.lua` is the authoritative table, and
|
|
`tests/engine/gate_gen2_mod_api.lua` holds it to the catalog.
|
|
|
|
The list used to have three causes behind it and now has one. "No Data path
|
|
exists" closed when the overworld tables stopped loading off disk into World
|
|
fields. "The shape differs" closed when a registry gained the option of
|
|
carrying a Gen 2 record schema beside its Gen 1 one. What is left is one cause:
|
|
|
|
**Gold reimplements the system without reading a registry.** The Gen 1 target
|
|
is still built and merged into, but nothing in a Gold boot ever looks at it, so
|
|
routing the registry would be a merge into a table with no reader -- exactly
|
|
the silent no-op the gate exists to prevent. Closing one of these is a consumer
|
|
change in the Gen 2 module first and a routing row second:
|
|
|
|
- `rulesets`: no Gen 2 ruleset dispatch exists.
|
|
- `transitions`: Gold draws its own battle intro
|
|
(`src/ui/gen2/BattleTransition.lua`), and its `STYLES` is a boolean *set* of
|
|
the four cart wipes (`spin`, `speckle`, `zoom`, `sine`) rather than the
|
|
`{ frames, draw, sound, flash }` record this registry carries. There is no
|
|
styleDef lookup for a registered id to reach, so a mod style would fail the
|
|
`STYLES` membership test and fall back to vanilla -- routing it would be the
|
|
silent no-op, not the fix.
|
|
- `field`: the Gen 1 overworld's data grab bag. Gold's equivalents live in
|
|
`data.gen2Maps` and the VM's own tables.
|
|
- `text_pointers`: Gen 1's `TEXT_*` indirection. Gold's text *is* pointers.
|
|
- `link_fields`: link play is Gen 1 only.
|
|
- `map_scripts`: `data.gen2Scripts` is the cart's bytecode pool keyed by ROM
|
|
pointer, and a Lua row list merged into it is not something
|
|
`src/script/gen2/Vm.lua` can run. Routing it needs a Gen 2 side dispatcher in
|
|
`World`, not just the verb table `mod.commands` already has. The
|
|
`script.started` / `script.ended` / `script.command` seams do fire, so a mod
|
|
observes and can veto a script it cannot yet author whole.
|
|
|
|
Four of this list closed after it was written, and how they closed is the
|
|
pattern for the rest:
|
|
|
|
- **`growth_rates`** now routes to the SHARED Gen 1 target. Gold's curves are
|
|
coefficient rows in the extracted `pokemon.lua`, so `src/mods/Builtins.lua`'s
|
|
Gen 2 registrant wraps each as the `{ expForLevel }` record Gen 1's registry
|
|
uses, and `src/battle/gen2/Mon.lua:growthFor` is the one accessor all six
|
|
readers go through (`Mon` twice, `BattleState`, `SummaryMenu`, `Breeding`,
|
|
`ItemEffects`). One record shape, one id space, one mod source for both
|
|
games. Because it is routed, the `pokemon` schema's `growthRate` reference is
|
|
now checked rather than skipped, and it resolves: both sides say
|
|
`GROWTH_MEDIUM_SLOW`.
|
|
- **`tokens`** was on the list by mistake rather than by cause. `TextBox.new`
|
|
runs `TextBox.substitute` on every box in both generations and `substitute`
|
|
reads `game.data.tokens`, so the shared target was live on Gold the whole
|
|
time. A `{NAME}` a mod registers expands in the world, the menus and the VM's
|
|
pages alike.
|
|
- **`battle_sprite_scales`** closed consumer-first, the `growth_rates` way:
|
|
`src/ui/gen2/BattleState.lua` grew `imageScale` / `picScale`, a faithful
|
|
mirror of Gen 1's `BattleState.imageBattleScale` / `resolveBattleScale` down
|
|
to skipping `_owners` and the image-then-species-then-default order, so the
|
|
registry now routes to the SHARED Gen 1 path and one record serves both
|
|
games. Only the default is generation-specific, and neither side reads that
|
|
from the registry.
|
|
- **`render_pipelines`** closed because the reader moved, not the registry:
|
|
`src/core/Game2.lua:load` installs `src/render/Pipelines.lua` on Gold's
|
|
merged dataset after `mods:load` and `Game2:draw` composites `present`. The
|
|
`drawWorld` half is still inert, which is why this one is worth reading the
|
|
caveat above for -- it is routed on the strength of the half that works, and
|
|
Gold retires a drawWorld-only level rather than pretending.
|
|
|
|
## Hooks and events Gold does not raise yet
|
|
|
|
Gold has its own draw path, intro, evolution and sprite lookups, so the call
|
|
sites in those Gen 1 modules are not on Gold's path. The names are not taken
|
|
and not reserved for Gen 1: when a Gen 2 call site lands it uses the existing
|
|
name and the existing payload, plus fields where Gen 2 genuinely carries more
|
|
(the split special stats, held items on a trainer roster).
|
|
|
|
The list is much shorter than it was. What is outstanding, in descending value:
|
|
|
|
- `pokemon.before_give` / `pokemon.received`: Gold has no give-mon seam of its
|
|
own yet.
|
|
- `link.*` and `trade.completed`: a Gold boot offers no link menu at all. The
|
|
Gen 2 fingerprint and handshake exist (`src/link/Fingerprint.lua` hashes a
|
|
Gen 2 surface and a cross-generation pairing is refused by name), but nothing
|
|
in `src/ui/gen2/` opens onto the protocol, so these raise nowhere.
|
|
|
|
Four groups that used to sit here have since landed and moved to the shared
|
|
table above: the frame seams (`render.compose` / `render.hud` /
|
|
`render.letterbox` / `render.zones`, `input.step` / `input.pointer`), the three
|
|
battle seams (`battle.overlay`, `battle.low_health_alarm`,
|
|
`battle.catch_exp`), the two sprite lookups (`pokemon.sprite`,
|
|
`pokemon.icon`), and the catch/evolution trio (`pokemon.caught`,
|
|
`pokemon.evolved`, `evolution.check` -- `src/ui/gen2/BattleState.lua` emits
|
|
`pokemon.caught` from `pushCaught` once the mon is in the party or the box, and
|
|
`src/core/gen2/Evolution.lua` emits `pokemon.evolved` from `apply` and wraps
|
|
each row's decision in `evolution.check`).
|
|
|
|
Three partial coverages worth knowing about, because "the hook exists" is not
|
|
the same as "the hook sees everything":
|
|
|
|
- `encounter.roll` / `encounter.species` are wired into the grass/water step,
|
|
`randomwildmon`, the Bug Contest and SWEET SCENT, but **not** into
|
|
`World:tryHeadbutt`, `World:rockMonEncounter` or `Roamers.checkEncounter`.
|
|
Those three read row shapes that are not `{ species, level }` slot lists, so
|
|
a mod that reskins encounters misses headbutt trees, rock smash and the
|
|
roamers.
|
|
- `src/ui/gen2/BattleState.lua` builds a flat `opts` for `Catching.attempt`
|
|
with no `data` in it, so a mod-registered ball is readable through
|
|
`Catching.recordFor` but is not yet resolved at the real throw site.
|
|
- Three Gold UI files carry their own copy of the status HUD labels the merged
|
|
`statuses` records now hold as `hudLabel`, so a mod status shows no label in
|
|
the battle HUD, the party menu or the summary page until they read
|
|
`Battle.statusRecordFor(data, status).hudLabel`. The values are identical
|
|
today, so nothing vanilla is affected.
|
|
|
|
## Gen 2 tables with no registry
|
|
|
|
`Game2:load` assigns 24 `data.gen2*` tables and 12 of them are registry-backed,
|
|
so twelve sit in `game.data` on a Gold boot with no registry pointing at them:
|
|
`gen2Marts`, `gen2Roofs`, `gen2StdScripts`, `gen2EventTables` (the phone book,
|
|
in-game trades, elevator labels, decoration descriptions), `gen2InitialEvents`,
|
|
`gen2Pokedex`, `gen2MenuGfx`, `gen2Intro`, `gen2Credits`, `gen2Diploma`,
|
|
`gen2Trade`, and `gen2Scripts` (which the `map_scripts` registry does reach, so
|
|
it is the one of the twelve that is not out of reach). Naming registries for the
|
|
rest is new API surface rather than a routing change, so it is deliberately not
|
|
done yet.
|
|
|
|
## Testing a Gen 2 mod
|
|
|
|
Static first. `gen2check` reads the manifest, scans every `.lua` the package
|
|
carries and cross-references what it finds against the coverage table above:
|
|
|
|
```sh
|
|
python3 tools/modkit.py gen2check my_mod # or a path
|
|
python3 tools/modkit.py gen2check my_mod --notes # + the caveat on each backed member
|
|
```
|
|
|
|
It reports one of `will load`, `will load but degrade` or `will not work`, with
|
|
a `MK4xx` finding per site and an `unresolved:` note, carrying a file and a
|
|
line, for every reach a static scan could not follow. Exit 0 clean, 1 on a
|
|
fatal finding (or any finding under `--strict`), 2 on usage; `--json` emits the
|
|
whole batch as one document, and `--quiet` prints the findings alone, so a
|
|
clean mod prints nothing and the exit code is the answer. The rule ladder is
|
|
`MK400`-`MK410` and is listed in `tools/modkit.py`'s section header.
|
|
|
|
Then the headless harness, which takes the generation without booting Gold:
|
|
|
|
```lua
|
|
local run = T.sdk.loadMod("mods/my_mod", { generation = 2 })
|
|
T.eq(run.mod and run.mod.state, "loaded",
|
|
"runs on gen 2: " .. tostring(run.mod and run.mod.skipReason))
|
|
T.eq(#run.errors, 0, "and loads with no boot errors")
|
|
```
|
|
|
|
Everything else is the production path: same loader, same validate, same
|
|
topological sort, same merge. Assert the state as well as the error count: a
|
|
gate skip is deliberately not an error, so `#run.errors == 0` passes for a mod
|
|
that never ran a line.
|
|
|
|
Neither substitutes for a real Gold boot, and the two output channels there are
|
|
not the same. The adapter's own warnings (`Gen2Compat.warnOnce`) go to the log
|
|
only, each attributed to the mod holding the facade. The boot error feed the
|
|
manager shows is `loader.errors`: a failed mod, a duplicate id, a registry with
|
|
no Gen 2 target, a cross-validation problem, and a require for a Gen 1 module
|
|
the adapter does not serve. A skipped mod and a degraded member are on neither
|
|
list, by design.
|