- Introduced PresentSync module to manage display synchronization.
- Updated love.run() to handle display changes and resizing events with PresentSync.
- Enhanced FrameCap logic to accommodate PresentSync requirements.
- Modified VSync to report effective states and handle driver quirks.
- Updated options menus to reflect PresentSync availability and restrictions.
- Added tests to validate PresentSync functionality and its interaction with VSync settings.
should address issue #1910 and issue #1958
Closes the round trip. Gold, Silver and Crystal progress writes back to a
cartridge save the real game boots.
Writes into the image the save came from. Gen 2 SRAM holds a great deal
this codec does not model and the real game trusts it on CONTINUE, so a
save with no cartridge behind it is refused rather than built from nothing.
That refusal is the known limitation: a game begun in this project has no
lineage to write back into yet.
The image lives BESIDE the slot as saves/<version>/<id>.cart, not in the
save table. 32 KB of binary in the serialized table is 40 KB of Lua source
reparsed on every save and load, for every imported slot, forever.
Only the primary copy is written. TryLoadSaveFile rewrites the backup from
the primary the moment VerifyChecksum passes, so a stale backup heals
itself on the first load and this does not need a second offset mapping.
Crystal arranges its backup differently from Gold and Silver, so that
matters.
What encode now reaches that it did not:
* the bag, all four pockets, bucketed by each item's own pocket, plus
wCurBox and the box names. It used to leave them at whatever the
template carried, so a potion bought in a session never arrived.
* the RTC footer. importToSlot truncates to 32768, so the image is kept
at its full length and the footer is carried through. Dropping it
resets the clock and costs the player daily events, the bug contest
and a clock-adjustment penalty.
* 0x1C-0x1E, pokerus and caught data. Left to the template they survive
POSITIONALLY, so reordering the party gives slot 1 the previous
occupant's caught level and location.
A bag that cannot be bucketed is refused rather than written short: without
the item table every item falls into ITEM, which holds 20, and a real bag
is bigger than that. Silently dropping the overflow would be worse than
the bug this fixes.
Two bugs in the text encoder that only real names caught: the cart's table
carries the ligature halves PO and KE, so a name containing "PO" became
0x70 where the cart had a plain P; and #glyph counts BYTES in Lua, so every
multi-byte glyph was dropped and came back as "?", which is NIDORAN and
every name with an accent.
Tests
The round trip CHANGES things first, in each place export has to reach, and
reads them back through a fresh decode. Exporting onto the buffer a save was
decoded from proves nothing, because every region encode does not write
matches by construction.
The fixture-gated audit exports a real cart save too, and pins that the
image keeps its size, RTC and all.
./scripts/test.sh passes end to end, and luacheck is clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Gold, Silver and Crystal battery saves import now. Export is still refused.
GenSave.lua is pokered's SRAM window and nothing else, which is why the
guard refusing Gen 2 was right to be there. This adds Gen2Save.lua beside
it, covering pokegold and pokecrystal.
Every offset is generated, not transcribed. tools/gen2_sram_offsets.py
reads pokegold.sym and pokecrystal.sym from a pret build and emits
Gen2Layout.lua, including the text table from constants/charmap.asm and
Crystal's backup-save layout. Gen 2 copies a contiguous WRAM block into
SRAM bank 1, so a field's file offset is sPlayerData + (wField -
wPlayerData); the generator asserts that relation against sPokemonData
rather than assuming it, and range-guards anything outside
sGameData..sGameDataEnd.
Gold and Crystal are separate tables because they disagree about nearly
every field. Reading a Crystal save with Gold's numbers gives a party
count of 133 and 13113 hours played, with a checksum that validates.
The cart stores numbers and the engine is keyed by name, so the codec
crosswalks species, moves and items through the generated tables the same
way GenSave.crosswalks does for Gen 1. Without that, an import looks
perfect and the engine cannot read a byte of it.
Shapes that have to match what the engine reads:
* events is byte index -> packed byte, which Save.scrubEvents validates
with tonumber. A set of booleans is silently emptied.
* the bag is one flat save.inventory keyed by item id, which PackMenu
buckets by each item's pocket. Nothing reads save.keyItems or
save.balls, and the TM/HM pocket lands here too.
* position carries the map id, or Save.summary falls through to
save.spawn and the player resumes somewhere else at their old
coordinates.
* mon.status is an ItemEffects.STATUS_CLASS key with statusTurns beside
it, nil when healthy. 0 is truthy in Lua.
A save the real cartridge would open is not refused: TryLoadSaveFile falls
back to VerifyBackupChecksum, so this does too. Crystal's backup is
contiguous and laid out like the primary; Gold and Silver split theirs
across three sections and have none to offer.
Three suites that pinned Gen 2 import being refused now pin what refuses
instead. Tests live in tests/engine so the ROM-free tier actually runs
them.
./scripts/test.sh passes end to end, and luacheck is clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CI caught this: tests/engine/save_import_retry_bug420.lua replaces
SaveConvert with a minimal double, and importToSlot now asks it
importSupported before it measures the bytes, so the double answered nil
and the call died.
The double stands in for the real module, so it grows with it. Answering
true keeps that case about the thing it is testing, which is that
importToSlot names the game whose cache to read.
./scripts/test.sh passes end to end locally now, every ROM-free tier.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Drop the pointless zip wrapper around the x86_64 AppImage, harden HostShell
so bundled curl keeps APPDIR libs while host curl scrubs LD_LIBRARY_PATH /
Steam LD_PRELOAD, and add a Flatpak channel with --device=all, bundled
curl, and AppStream releases metadata. Portable mode now probes writability
with a unique temp file and falls back soft on RO mounts / Flatpak.
bootGame's require("src.core.Game") was resolving to Gen2Compat's Game
facade after a Gold session, so the next Gen 1 boot got an empty proxy
whose rawget(load) is nil (iOS: "src.core.Game missing load after
reload"; Android: Game:load called on a nil value).
Two independent causes, both needed:
* callerIsMod decided "not under src/" meant "a mod", and main.lua is
not under src/, so the engine's own require was gated as a mod's.
Root chunks main.lua and conf.lua are now matched exactly, which a
mod's own mods/<id>/main.lua cannot collide with.
* devShim.generation was only ever set, never cleared, so the
generation Gold declared outlived Gold's session. Loader.endSession
drops it from SessionLifecycle.endMountedSession, and the facade
gate now requires generation == 2 rather than ~= 1.
Only platforms that return to the launcher in-process (Android, and iOS
since #1808) keep the shim alive across sessions; everywhere else the
process restart cleared it.
Drop Data:load from #1635/#1743 engine tests so headless CI without
data/generated/ can run them, and relax dig's #916 hide-ratio check for
white fade-in under spinDrop (#1644).
Seed Mt Moon Super Nerd dialogue (#1743), play item-use heal jingles
(#1635), use white fades for fly/teleport warps (#1644), add a
Yellow-only Advanced palette overlay (#1639), parse Yellow Super Rod
data including Safari Dragonair (#1074), and apply title rOBP0 when
baking Pikachu eye OAM so pupils stay black with white glints.
Stop joining ChipAudio on every EXIT GAME, always rebuild the Gen1 Game
module on Play-again, and treat iOS like Android for return-to-launcher so
release/mobile LOVE restarts no longer wipe Game.load or crash on quit.
Stop fanning out arbitrary field:release() during Game/Game2 reset — shared
modules use :release as a handle API, and that teardown left the Gen1
singleton unbootable (Game:load nil) on Play-again. Harden bootGame and
endGameSession to rebuild the module if load is missing.
PcMenu.lua's own CHANGE BOX save confirmation stayed in English on every
non-English build: its overwrite/saving/done prompts and its YES/NO choice
were bare literals, invisible to a translation mod's `strings` registry,
even though the overwrite/saving prompts are the exact same two cart
messages Gold's SAVE screen (SaveMenu.lua) already routes through
Strings().
SaveMenu.lua now exports OVERWRITE_PROMPT_SOURCE, SAVING_PROMPT_SOURCE and
twoLines() so PcMenu:savePrompt() can call through them instead of
duplicating the two shared prompts. The now-unused plain
OVERWRITE_PROMPT/SAVING_PROMPT tables (PcMenu's old direct-read contract)
are gone along with them, removing a second copy of the same literal text
that had to be kept in sync by hand. The CHANGE BOX confirm prompt itself
("#MON BOX, data\nwill be saved. OK?") gets its own new Strings.source()
key -- it is not shared with SaveMenu, and the port's own English text is
already a same-shape truncation of the cart's three-line
_ChangeBoxSaveText, confirmed against poke-corpus GoldSilver
en_msg.txt:4897. The done message and YES/NO now go through the exact
same Strings() keys SaveMenu.lua already uses ("%s saved\nthe game.",
"Could not save.", "YES", "NO"), so a translation covering SaveMenu's
screen needs no PcMenu-specific fork for any of those.
Added tests/engine/gen2_pcmenu_changebox_save_translation_test.lua,
mirroring gen2_save_menu_translation_test.lua's technique for PcMenu's own
CHANGE BOX flow, and updated the stale cross-file-contract regression case
in gen2_save_menu_translation_test.lua to match the new shared-export
shape instead of the old plain-table one.