mod.content.encounters:patch on a Gold boot keys by encounter KIND
(encounters.grass.ROUTE_29), but the id space was open: a key the
catalog did not describe was treated as a mod's own data and merged
as-is. A Gen 1 encounters mod ported unchanged writes the MAP where
Gold wants the kind, so the call was accepted, merged into
data.gen2Encounters.ROUTE_29 and read by nothing -- vanilla game, no
error, nothing in the Mod Manager (#2369).
That key cannot be a mod's own data the way an extra palette id can:
src/battle/gen2/Encounter.lua, src/core/gen2/Roamers.lua,
src/core/gen2/BugContest.lua and src/world/gen2/World.lua read this
table by name and the set of names is fixed, so an unknown id is a
write nothing reads. Add an opt-in `keysClosed` shape slot (folded by
Schemas.shapeFor like the other gen2* slots) that Schemas.check honours
in its keys/keyValue branch, and set it on encounters' Gen 2 spec. An
unknown id now names the ids that do exist instead of silently
no-opping; api 2 fails the mod, api 1 keeps the warning.
Also catalogue roamMons, which the extractor has always emitted
(RomExtractorGen2:readRoamMons) and Roamers.roster has always read, but
which was missing from gen2Keys.
Only encounters opts in: palettes, battle_anims and constants keep
their open id spaces, and Red's encounters id space is untouched.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Kanto Companion's Edit Mode can't run during battle -- there's no way
to stop a D-pad press from also driving the battle menu, so the only
option today is pausing the game outright. This adds the same input
precedence a mod had before the sandbox changes: vanilla is the whole
existing callback body, and the hook fires before any of it runs.
input.wheel stays a plain observer like input.pointer, since nothing
depends on suppressing it.
Battle.MOVE_EFFECT_RECORDS only lists effects with a standalone handler --
by design, per its own comment: a move whose effect is just "deal damage"
(EFFECT_NORMAL_HIT and the multi-hit/recoil/drain families) has none and
falls through to the generic damage path. But src/mods/Schemas.lua's
`moves.effect = f.id("move_effects")` cross-check treats move_effects as
the complete id space for the field regardless of generation, so every
"full" effect the real Gold/Silver movedex uses read as a dangling
reference the instant any mod's `moves` patch touched the registry --
caught building the Gold/Silver translation mod, whose move_names patch
(name only, never effect) was enough to trigger the scan on all ~130
moves.
registerMoveEffectsInto now also registers a bare `{kind="primary"}`
marker for every effect id data.moves actually uses that MOVE_EFFECT_RECORDS
doesn't already cover. Both of moveEffectRecordFor's call sites already
treat a handler-less record exactly like a missing one (nil-checking
.run/.status before use), so this is a validation-only change with no
battle behavior difference -- a real typo in a mod's own effect patch is
still caught, since the widened set is seeded from the pre-merge data.moves,
not from whatever a mod patches in afterward.
GEN2_STATUS_IDS (psn/brn/frz/par/paralysis/slp -> Gen 2's own registry ids)
was declared verbatim in both PartyMenu.lua and SummaryMenu.lua; move it to
Status.GEN2_ID_ALIASES so a future status alias fix only has one copy to
update.
SummaryMenu.TYPE_NAMES was left behind after this branch switched its two
former internal uses to the shared TypeChart.displayName/DISPLAY_NAMES; it
has no remaining callers anywhere in src/ or tests/.
Lets a mod ask what a map's wild-encounter odds look like right now,
composed with any live encounter.table wrapper, without rolling the RNG.
encounter.roll/encounter.species can only transform one draw; this is the
missing read side. Guarded query method (mod.world:effectiveEncounters),
no changes to the real roll path, docs and a public-API modkit case.
Crystal boots from a user-supplied ROM, imports a full cache and is playable:
copyright, the Crystal intro movie, the animated title, gender select, Oak,
and out into Johto. 122 of the cart's 169 script specials are implemented.
Import and data
- tools/make_crystal_manifest.py derives the manifest by importing
make_gold_manifest as a library, with three additive keyword seams. Gold and
Silver still regenerate byte-identical, which is the standing requirement for
touching that generator.
- crystal_symbol_deltas.py and crystal_movie_symbols.py carry the symbol delta:
Crystal renames the credits mons, splits the trainer card, Pokegear and
pack-pal blocks by gender, and replaces the intro and title outright.
- Crystal-only manifest keys: engineFlagOrder (162 flags to Gold's 93, so the
badge block sits one higher) and unownCharmap (the main charmap parser stops
at the first newcharmap so the two cannot contaminate each other).
Extractor
- RomExtractorGen2 becomes three-edition. Crystal corrections: PAL_MAP_BANK
0x13, a flat PICS_FIX pic bank, audio bank 0x5e, the mapSongs id-100 hole,
seven NPC trades, a TradeTexts stride of 8, the five Crystal tileset anim
steps with per-row degrade, and the column-major trainer card portraits.
- New: animated front sprites (frames, bitmasks, play and idle scripts), the
Battle Tower roster, Kris assets, Mobile System GB art, and the Crystal
intro and title via src/import/CrystalMovie.lua.
Engine
- GameVersion gains engine(id) and fixes(id). Gold and Silver keep their
original bugs where the bug is not hardware dependent; Crystal gets the fixes
Crystal shipped: Lucky Number boxes 10-14, surfing onto an NPC, and the
Reflect and Light Screen defence overflow.
- Crystal story: Suicune and Eusine, Celebi behind the GS Ball flag, the Ruins
of Alph chambers, Buena, the Move Tutor, the Poke Seer, and the Battle Tower
including the wInBattleTowerBattle badge-boost guard.
- Kris and the gender flag, animated fronts in battle and the summary screen,
and mon caught data.
Verification
- Every extracted asset is pixel-compared against pret's own source PNGs.
- Gold caches are byte-identical before and after, file for file.
- New Crystal suites plus a T2 Gen 2 tier; the full suite passes.
Silver: derived import manifest (tools/make_silver_manifest.py re-resolves
the Gold manifest's symbols from pokesilver.sym), silver GameVersion row,
generation-keyed extractor routing, required-files override, edition save
stamping (a Silver playthrough no longer writes into the Gold save),
checkver-driven edition data, SILVER/KAMON/OSCAR/MAX presets, GOLD rival
default, edition credits banner, Lugia title screen (OAM layouts, bob,
trail, palettes as title.lua data keys with Gold defaults so old caches
need no re-import), packaging for every build target, docs, and tests.
Launcher: the installed-mods list is one continuous scroll (rows culled to
the viewport) instead of a pager with an inner scroll viewport; the pad
cursor's edge-scroll no longer runs it to the bottom. The game dropdown
shows just the initial and caret. Find-tab behavior unchanged.
Title tempo: a sprite-anim frame shows duration+1 ticks
(engine/sprite_anims/core.asm GetSpriteAnimFrame), which locks both
editions' 64-tick wing beat to the 64-tick sine bob; the title screens no
longer run fast and out of phase.