Both features landed independently and each was green on its own, so the gap
only appears in the combination.
- tools/cartkit.py BASES did not list crystal, so cartkit rejected a Crystal
cart as an unknown base game. The Lua validator never had this problem: it
asks GameVersion.VERSIONS, so it accepted Crystal the moment the row existed.
- Two self-tests used the literal "crystal" as their never-a-game sentinel and
invert now that Crystal is real: tests/engine/cart_manifest.lua and
cartkit's own t_identity. Both re-pointed at "nonesuch", the token the rest
of the suite already uses for this.
A custom cart pairs an identity (title, shell colour, label art) with a base
game, a list of mods pinned to exact builds with their option values frozen,
a load order, and a seal. It ships no code of its own: every mod it names is
a separately published mod, which is what keeps a cart auditable before it
runs and reproducible after an author's repo disappears.
Format and storage:
- src/carts/CartManifest.lua parses and validates cart.json, canonicalises it
for hashing and reads/writes the .g1rcart bundle. The bundle is a data-only
serialised table read through SaveSerializer, so an imported cart can never
execute code. Canonical strings are length-prefixed because option keys and
values are author-controlled and could otherwise forge a record boundary and
collide two different carts onto one hash.
- Pins name a public source: a GitHub release with its sha256, a GameBanana
file id with its md5, or "local" for a capture that only exists on this
install. A local pin is unpublishable by construction, which is what makes
"build it here, publish later" possible without inventing a hash.
- Label art rides alongside the manifest rather than inside its identity, so
re-arting a cart does not tell every player their run is out of date.
src/core/Base64.lua decodes it; strict, with no whitespace tolerance.
Saves:
- Cart playthroughs live in the cart's own slot namespace (saves/cart_<id>/),
so a cart's file never sits beside a vanilla one and uninstalling a cart
never orphans a save. Every save records the cart build it was made under.
The seal:
- A sealed cart loads its pinned list, in its order, with its options, and
nothing else. A pinned mod with no frozen options gets an empty bucket so
unfrozen keys fall to schema defaults, identical for everyone; otherwise two
players on one cart quietly run different games.
- A sealed cart refuses to load when a pin is missing or installed at another
version. Playing a subset of the cart is the exact dishonesty the seal
exists to prevent, so the refusal loads nothing at all.
- Breaking the seal is permanent, marked per save slot, and downgrades that
playthrough to open behaviour. It cannot be cleared through any public API.
Launcher:
- A game's page carries a Custom Carts control and a picker; choosing a cart
turns the page into that cart's page, with its own cartridge, title and save
slots. The rail of five games never grows and a cart id never reaches
imp.tab or imp.panelVersion.
- Loader.planCart runs before boot so a refusal is visible on the page instead
of being discovered as an error after launch.
- Save as cart captures the enabled mods for a game and names, before the
player confirms, every mod that could only be pinned to this install and
whether the result can be shared at all.
Authoring:
- tools/cartkit.py scaffolds, validates, pins and packs a cart, and installs a
release workflow. Its writer is byte-identical to the engine's serialiser.
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.
- Introduced `teardownMountedSession` to handle cleanup of mounted versions, generated data, and mod states during transitions between the editor and launcher.
- Added `flushEditorPackageLoaded` to evict save-editor modules from `package.loaded` dynamically, ensuring a clean state for subsequent sessions.
- Implemented `Game:reset` and `Game2:reset` methods to clear session-specific fields, allowing for a fresh start when returning to the launcher.
- Enhanced `Renderer` and `TileRenderer` to release GPU resources immediately, preventing memory leaks during rapid transitions.
- Updated `MagnetTrainRide` to support OAM priority overlays and manage background rendering with new shader functionality.
This commit improves the stability and performance of the application during in-process transitions, particularly on Android.
fixes Yellow color palette for title screen
fixes#1662#1643#1536 and finishes fixing #1597
Gold and Silver had no label-keyed string table at all. The manifests
carried no text section, RomExtractorGen2 had no extractText, and
game.data.text was never assigned, so every call through
src/core/RomText.lua fell back to the literal written beside it. The only
Gen 2 text the cache held was the script text in data/generated/text.lua,
keyed by bank:address for the overworld VM, which nothing can look a battle
line up in.
make_gold_manifest.py now walks data/text/'s five dialogue files for their
labels, the way make_rom_manifest.text_metadata walks pokered's, and embeds
each one's symbol. 889 labels, all of them resolving in both editions.
make_silver_manifest.py inherits the list unchanged and re-resolves the
addresses from pokesilver.sym.
RomExtractorGen2:extractText decodes them into data/generated/rom_text.lua.
The mechanism is the one extractOakSpeech already used for _OakText1-7:
resolve the label, decode from the cart, key by name. What is new is that
the list comes from the manifest rather than being written out in Lua, so
all of data/text/ arrives instead of seven strings.
decodeGen2Text also emits the three runtime name slots it used to drop.
PlaceMoveUsersName, PlaceMoveTargetsName and PlaceEnemysName (home/text.asm)
substitute a battler's name as the line prints, so <USER>, <TARGET> and
<ENEMY> are markers, not glyphs. Skipped as control glyphs, SubTookDamageText
decoded as "The SUBSTITUTE / took damage for" with nothing after it.
Game2:load assigns the table to self.data.text, which is what makes the
existing shared RomText helper work on Gold and Silver at all.
The new cache file is listed in the Gold override rather than bumping
CACHE_FORMAT, so caches built before this stage re-import themselves and Red,
Blue and Yellow are left alone.
findPendingRom answered with the first dump in the save directory whose SHA-1
mapped to any not-yet-ready version. On a device with no file picker that
scan IS the import, so with four dumps in the folder, choosing Red imported
and decoded Blue (#1274).
It now takes an optional version and narrows to it. The two Choose paths
pass self.chooseVersion, since a Choose names the cart it is for. The two
Android USB-drop scans pass nothing and still take the first pending cart of
any version, which is what they are for.
A chosen version with no dump present now imports nothing rather than the
wrong cart, and falls through to the notice that says where to put the file.
Drives a real turn per case and compares the emitted line to the label in
data/text/battle.asm. Six of the eleven checks fail against the text as it
stood.
The tail case wraps each changed line through Chrome.wrap at the box's own
width and holds it to the two rows printMessage draws. That is the check
that caught SpikesText needing a third row, which is why Spikes is not in
this change.
Five messages in the Gen 2 battle code are written by hand rather than taken
from data/text/battle.asm, and each has drifted from what the game prints:
SuperEffectiveText lost its hyphen and its line break
NotVeryEffectiveText ended on three periods, not the
ellipsis glyph the charmap carries
BattleText_TheresNoPPLeftForThisMove dropped "There's"
PlayerHitTimesText/EnemyHitTimesText printed "Hit 3 time(s)!", showing
the parenthetical on screen; Gen 1
already prints "Hit 3 times!" via
_HitXTimesText
StartPerishText printed a sentence no cart prints
The break is \n, which is what RomExtractorGen2 decodes the cart's own $4e
into, so these read as an extracted line would. Each goes through Strings
now, which is what the rest of this file already does with its messages.
SpikesText is left alone and the reason is written down beside it: its third
row is a `cont`, and engine messages set self.message directly rather than
going through showPages, so printMessage would cut the row carrying <TARGET>.
battle.exp_award hands a mod ctx.applyShare(mon, split, announce) on both
generations, and on Gen 1 the third argument decides whether the mon's
GainedText box is printed -- which is how a mod paying the whole party
prints ONE summary line instead of a box per recipient. Gold accepted
the argument and ignored it, so the same mod source printed one line on
Red and one per party member on Gold.
The Exp Share mod is the live case: it declares games gen1+gen2 and its
description promises "a single shared-exp line instead of one message per
Pokemon", passing true for the fighters and nil for the bench exactly as
the Gen 1 seam asks. On Gold every nil call announced anyway, so a
five-mon party turned every KO into six boxes. There was no mod-side
fix: the emit sits behind no hook, and the argument meaning "quietly" was
discarded.
Gold now reads it, and ONLY when it is actually passed -- by argument
count, not by value. select("#", ...) counts an explicit nil, so
applyShare(mon, split) is distinguishable from applyShare(mon, split,
nil); the first is a Gen 2-era call written against a seam that always
announced and keeps announcing, the second is a deliberate "pay this one
quietly" and is now silent on both games. No mod that exists today
changes behaviour, and a mod that passes the argument gets parity.
Only the { kind = "experience" } event is affected. A silent award is
still a whole award: exp, stat exp, battle.exp_gained, "grew to level",
learned moves and the forget prompt are untouched, in the same order.
giveExperiencePass takes a sixth `silent` parameter that defaults to
announcing, so both of the cart's own passes are unchanged.
RFC: docs/rfcs/0012-gen2-exp-award-announce.md
Docs: docs/mod-api-gen2-compat.md gains the reading and the residual
omitted-argument difference beside the existing payload note.
Tests: tests/gen2_exp_share_test.lua grows the two Route B tests -- the
no-mod parity case and the seam driven through hooks:wrap -- and
its 23 existing checks are unchanged.
Implemented a new deinterleave function in ImageWriter to restore row-major order for PNGs. Updated RomExtractorGen2 to utilize this function for extracting Slot Machine graphics, and added new functions for padding and writing graphics sheets. Updated the ROM manifests to include necessary symbols for Slots and Card Flip assets.
Added functions to write raw graphics assets for the Slot Machine and Card Flip games, ensuring that the necessary files are generated when the corresponding symbols are present in the ROM. Updated the manifest to include required symbols for these assets, preventing fallback to labelled cells when graphics are missing.