- 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
Drives NamingScreen:drawPanel() with a mod-loaded Strings catalog and
checks the translated text reaches Font.draw for the prompt, a
keyboard cell, the lower/END bottom-row labels, and both lines of the
nickname header (checking the mod's word order actually took, not just
the mon name splicing in), plus a vanilla no-mod case proving the
fallback is unchanged. Reverting src/ui/gen2/NamingScreen.lua to its
pre-fix content fails 6 of the 9 checks, reproducing the reported gap
exactly.
src/ui/gen2/NamingScreen.lua had zero Strings() calls: every prompt
(YOUR NAME?/RIVAL'S NAME?/MOTHER'S NAME?/BOX NAME?/NICKNAME?), the
on-screen keyboard's own letters, and the lower/UPPER/DEL/END bottom
row were bare literals, invisible to a translation mod's `strings`
registry (reported against a real Gold build, gen1recomp#1642). The
Gen 1 naming screen (src/ui/NamingScreen.lua) already routes its title
and every keyboard cell through Strings(); this mirrors that.
NamingScreen.TYPES and the bottom-row label tables are built once at
require time, before any mod's Strings.load has a catalog to answer
from, so their literals are wrapped in Strings.source -- an identity
marker that keeps the catalog generator's harvest of them -- and
drawPanel resolves them live through plain Strings() calls, the same
split used for Gold's OPTION screen and PR #1450's clock screens.
The nickname header ("<MON>'S" then "NICKNAME?", two lines because
Chrome.printThrough draws one row at a time) folds the mon name into
the first line's own format string, Strings("%s'S", self.monName),
rather than translating just the "'S" suffix: a language whose
possessive is not a bare suffix appended to the name can restructure
that whole line instead of being stuck splicing one on.
No real shader runs headless (love_stub does not stub newShader), so
this cannot check a rendered pixel. Font.encode/drawCode/advanceOf/
width are replaced with fakes that hand printThrough a fixed list of
glyph codes, so what is checked is the two things that decide the
outcome: which glyphs skip the shade-remap shader, and what colour is
active when each one draws -- for an all-tile string, an all-TTF one,
a string that mixes both (proving the switch is per glyph, not per
call), an inverted call, and DMG mode's own resolved palette.
Reverting src/ui/gen2/Chrome.lua to its pre-fix content fails every
TTF-mode check. A per-string version of the fix (Font.ttfActive()
checked once for the whole call instead of per glyph) also fails the
mixed-string case here, since it would flatten the naming screen's
tile-based <PK>/<MN> cells to a plain ink tint whenever a TTF is
loaded at all, losing their shader-remapped colour even though they
never draw from the TTF.
Chrome.printThrough (src/ui/gen2/Chrome.lua) runs a string through the
GbcPalette shade-remap shader whenever a palette is given, so a screen
whose background is not white -- the naming/keyboard screen, Diploma,
Pokegear, Credits -- gets its text colour resolved from the same
palette as everything else on it. The shader recovers a shade by
reading the RED CHANNEL of an already-rasterized 2bpp tile pixel
(src/render/GbcPalette.lua's SHADER_SOURCE), which assumes the texture
underneath is one of the four flat GB shades.
A TTF glyph is not: LÖVE's font rasterizer stores glyph coverage as
alpha over a plain white texture and lets the current tint carry the
ink colour, so that same red-channel read always comes back 1.0 --
shade 0 -- painting every TTF character the exact colour of the paper
rect printThrough had just drawn behind it. Since a mod loads a TTF
through src/core/Strings.lua's font registry precisely so a
translation is not boxed into a handful of tile-page glyphs, every
translation mod that ships one hit this: reported against a real Gold
build where the naming screen's on-screen keyboard, Diploma and the
Pokegear all went blank the moment the mod's TTF took over rendering
(gen1recomp#1642). Gen 1 has no such shader, which is why the same mod
draws fine there.
A TTF glyph carries no discrete shade to recover in the first place,
so skip the shader for one and tint it with the palette's own ink
colour directly -- the same shade-3 entry the shader's `rgb = pal3`
branch would have mapped a black tile pixel to.
The switch is per GLYPH rather than per string: a TTF-mod build still
keeps multi-byte charmap sequences (the naming screen's own <PK>/<MN>
cells, the 'd/'l/'s ligatures) and anything a mod names in ttf.tiles on
their ROM tiles (src/render/Font.lua's Font.split), so one call can mix
both kinds of glyph, and a tile glyph drawn alongside a TTF one still
needs the shader to pick up a mod's COLOR/rBGP settings correctly.
Drives OptionsMenu:drawPanel() with a mod-loaded Strings catalog and
checks the translated text reaches Font.draw for a cart-original row
(label and its display value), a port-added row's label, and CANCEL,
plus a vanilla no-mod case proving the fallback is unchanged. Reverting
src/ui/gen2/OptionsMenu.lua to its pre-fix content fails 4 of the 9
checks, reproducing the reported bug exactly.
src/ui/gen2/OptionsMenu.lua had zero Strings() calls: every row label
and every cart-original display value (FAST/MID/SLOW, ON/OFF,
SHIFT/SET, MONO/STEREO, the PRINT contrast ladder, :TYPE) was a bare
literal baked into the module-level ROWS table, invisible to a
translation mod's `strings` registry (reported against a real Gold
build, gen1recomp#1642). The Gen 1 OPTION screen (src/ui/OptionsMenu.lua)
already routes every one of its equivalent rows through Strings().
ROWS is built once at require time, before any mod's Strings.load has
a catalog to answer from, so wrapping its literals directly in
Strings() would freeze the English (src/core/Strings.lua's own note on
this). Labels and cart-original display strings are wrapped in
Strings.source instead -- an identity marker that keeps the catalog
generator's harvest of them, without changing when the lookup runs --
and drawPanel resolves them live through plain Strings() calls, the
same split PR #1450 used for the clock and day-of-week screens.
Also routed the two port-added rows (SCREEN POS, VIBRATION) and the
touch pad ON/OFF row through Strings() at their value call sites,
matching how the Gen 1 screen already treats the same shared
ScreenPosition.label/TouchControls.hapticLabel helpers -- both return
raw, untranslated text on their own. Left MUSIC VOL/SFX VOL's OFF/level
readout and the MUSIC FILTER ladder (OFF/1X/2X/3X) untranslated, since
that mirrors the Gen 1 screen's own established choice for the same
two helpers.
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>.
skins again skins again skins again skins again skins again skins again skins again skins again skins again skins again skins again skins again skins again skins again skins again skins again
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.