7.2 KiB
Updater
A fused build (love.filesystem.isFused() true) ships a bundled game.love
baked into the executable, but that bundled copy is only ever the fallback.
On every launch, before anything else runs, Boot.run (src/update/Boot.lua)
looks in the save directory's updates/ folder for a downloaded
gen1recomp-X.Y.Z.love payload that is both strictly newer than the bundled
engine version and runnable on this shell. If one qualifies, it is mounted
over / (so its files win over the fused source for every subsequent
require) and chainloaded in place: the payload's main.lua and love.load
run as if they had shipped in the executable. A dev/source checkout is never
fused, so Boot.run no-ops there and the working tree always runs itself.
The pieces are deliberately layered so the risky part is small. Boot.select
is a pure function (no love.* calls) that, given probed candidates and the
bundled engine/shell, decides what to run and what stale payloads to
delete. Boot.probePayload mounts one archive at an isolated mountpoint and
reads its src/core/Version.lua with loadstring (never require, so it is
never cached as a module) to learn its engine and minShell. Boot.run
orchestrates the crash guard, enumeration, selection, and the mount +
chainload, with full rollback on any failure. Checking for and fetching a
new payload is a separate, slower path: src/update/Check.lua is a thin
main-thread state machine the launcher screen polls, while the curl calls,
JSON parsing, and sha256 verification run on a background love.thread
(src/update/check_worker.lua) so a hung network call never blocks a frame.
Version.lua fields
src/core/Version.lua carries three fields the updater reads directly (the
existing modApi, linkProtocol, saveFormat, and cache fields are
untouched):
engine- the semver release, e.g."1.4.0". The repo default is the"0.0.0-dev"placeholder; CI stamps the realX.Y.Zinto the packedgame.loveonly, never the working tree. A"0.0.0-dev"engine always reports itself up to date (it never chases a release, and it never counts as a valid payload to chainload).shell- the native-shell contract this build's fused executable implements.minShell- the lowest shell contract required to run this payload.
Bump minShell only when a payload needs something the currently-shipped
native shell cannot provide, for example a LOVE version bump, a new required
system binary, or a change to love.run itself (see Known limitations
below). An older shell refuses to chainload a payload whose minShell
exceeds the shell it provides; Boot.select keeps that payload in updates/
rather than deleting it, in case a future shell upgrade can run it, and
Check's worker reports needs_full so the player is pointed at a full
installer instead. Do not bump minShell for an ordinary Lua/data release;
that is exactly the case the updater exists to avoid a reinstall for.
Release assets
Each tagged release vX.Y.Z carries the existing per-platform archives
(gen1recomp-X.Y.Z-macos.zip, -windows.zip, -linux.zip,
-android.apk) plus two assets the updater itself consumes:
gen1recomp-X.Y.Z.love- the payload, matched by the exact patterngen1recomp-<version>.love(seeisPayloadNameinBoot.luaandCheck.parseRelease).sha256sums.txt-shasum -a 256output (<hex> <filename>, bare filenames) covering at least the.lovepayload.Check.parseSumstolerates a leading*binary marker and a./prefix but expects the filename otherwise to match the asset name exactly.
A release missing either asset is treated as "no in-place update available":
Check reports needs_full and sends the player to Check.releaseUrl()
(https://github.com/bryanthaboi/gen1recomp/releases/latest).
Save-directory layout
Under the save directory (identity pokemon-love2d):
updates/gen1recomp-<X.Y.Z>.love downloaded payload(s)
updates/pending.txt crash-guard marker
pending.txt holds the filename of the payload currently being chainloaded.
Boot.run's chainload writes it immediately before mounting, and removes it
on both a successful handoff and a clean rollback. If it is still present the
next time Boot.run starts, the previous boot died mid-handoff, so that
named payload is distrusted: it and the marker are deleted before candidates
are enumerated. Boot may still fall back to an older valid payload, or to the
bundled game, in that case.
Update flow
- Boot (every launch, fused builds only): crash-guard check, enumerate
and probe every
updates/*.love, pick the highest engine that is strictly newer than the bundled one and whoseminShellthis shell satisfies, delete stale payloads, chainload the winner (or run the bundled game if none qualifies). - Check (launcher screen):
Check.start()kicks off an async check against the GitHub releases API; safe to call every frame, it is a no-op once a check is in flight or has reached a terminal state.Check.state()reportsidle | checking | uptodate | available | downloading | ready | needs_full | errorplus the latest version and download progress. - Download + verify: on
available,Check.download()tells the worker to fetch the payload, polling the growing.partfile for progress. On completion the worker re-fetchessha256sums.txt, verifies the payload's sha256, and probes it withBoot.probePayloadto gate itsminShellagainst this shell'sshell. A verified, runnable payload is renamed into place and reported asready; anything else reportserrororneeds_fulland leavesupdates/clean. - Restart to apply: a
readypayload just sits inupdates/until the player relaunches; the next launch's Boot step (1) is what actually mounts and runs it. There is no in-session hot-swap.
Known limitations
love.runpersists across handoff. By the timechainloadruns, the bundledlove.runhas already returned its stepper to LOVE; redefining the globallove.runfrom the payload'smain.luadoes not affect the loop already driving the frame. A payload that must changelove.runitself needs aminShellbump so an older shell refuses to chainload it rather than running with half its intended behavior.- Android has no in-app download transport yet.
check_worker.luashells out to curl for both the release check and the download; curl is absent on Android, soCheckdegrades tostatus = "error"there (the launcher UI hides on that status) and the player is directed to the releases page viaCheck.releaseUrl()instead. - Dev/source runs never self-update.
Boot.runreturns immediately whenlove.filesystem.isFused()is false, and a working tree'sengineis the"0.0.0-dev"placeholder that always reports up to date, so a source checkout is always "the game" itself; updating it means pulling the repo. - Nintendo Switch does not use this LÖVE self-updater. On NX,
Platform.networkValidated()isfalse, soBoot.run/Checknever download.lovepayloads. In-console OTA uses the native OTA launcher (DEVKITPRO), documented in switch-install.md. Wire format:src/update/SwitchOta.lua. Manual zip install remains the fallback.