TITLE=WarpEngine Batocera Store ROUTE=/stores/warp-engine-batocera-store LOCALE=en MODIFIED=1792413440 URL=https://wiki.teletypegames.org/stores/warp-engine-batocera-store L=1 #22 L=2 https://batocera.org L=3 #24 L=4 #47 L=5 #43 L=6 https://git.teletypegames.org/stores/warp-engine-batocera-store L=7 #57 L=8 #63 L=9 #64 q│ cwarp-engine-batocera-storeq pulls games from a 1WarpEngine catalog onto a q│ 2Batocera box, with EmulationStation metadata and box art, and gives the q│ store its own entry in the EmulationStation menu. This is the engine only: q│ it knows the WarpEngine API and nothing about any particular site. The host, q│ the store name and where games land come from a cconfig.jsonq in a separate q│ bstore repositoryq. The half that is not about Batocera is 3warpstore, shared q│ with the 4RetroArch engine. p warpstore the shared core — warpstore.py p ▲ p warp-engine-batocera-store this engine — store.py, install.sh p ▲ p │ config.json p │ p ┌─────┴─────┬───────────────┐ p │ │ │ p ttg-… my-… other-… store repositories p │ │ │ p ▼ ▼ ▼ p teletypegames.org my.example other.example The reference store is 5ttg-batocera-store. bRepository:0 6https://git.teletypegames.org/stores/warp-engine-batocera-store bCatalog side:0 1WarpEngine bShared core:0 3warpstore bSibling engine:0 4RetroArch 4store engine bBatocera:0 2https://batocera.org 1What you get 1════════════ • bIts own menu entry0 — one carousel entry named after the store, holding a folder per platform. Written as an ces_systems_.cfg0 overlay; the launch command and emulator list are copied off the box's own systems, so nothing about emulators is hardcoded. • bSite-agnostic0 — any host mounting WarpEngine can have a store; only the config changes. • bSeveral stores per box0 — each owns a ROM folder, a menu entry and its own state, so one store's prune cannot reach another's games. • bThe box is left alone0 — the games live in the store's folder, not in the box's systems. Nothing outside it can be touched, and inside it only what cstate.json0 records is ever deleted. • bSafe next to EmulationStation0 — the in-ES sync defers the gamelist write until ES exits, and the tags ES owns (favourites, play counts) are carried over on every rewrite. • bZero dependencies0 — Python 3 standard library only. Batocera ships cpython3 and no cpip0. 1How it works 1════════════ p "" ▸ "Update " p │ p ├─ GET /api/software the whole catalog p ├─ keep platforms this box can run c64 → c64, plus4 → cplus» p 4, tic80 → tic80 p ├─ pick the newest non-dev release that carries the right a» p sset p ├─ GET /api/download?path= .prg / .tic into the sto» p re's folder p ├─ GET box art p ├─ write gamelist.xml title, desc, author, ima» p ge p └─ write es_systems_.cfg the store's menu entry r────────────────────────────────────────────────────────────────────────────── 1The store's own menu entry 1══════════════════════════ Everything the store installs lives in one folder of its own, c/userdata/roms/0, with a directory per platform inside it. An ces_systems_.cfg0 in EmulationStation's user config declares one ES system per platform, all carrying the same c0 — and ES turns a group into a single carousel entry holding a folder per system: p main carousel: … ▸ Teletype Games ▸ ┌ Commodore 64 p ├ TIC-80 p ├ Ports p └ Update Teletype Games Nothing about emulators is hardcoded. The engine reads the box's own c/usr/share/emulationstation/es_systems.cfg0 and copies the launch command, the extensions, the platform, the theme and the emulator list off the system it borrows — a file Batocera generated for this image, so it lists exactly the emulators the box has. The one thing rewritten is c%SYSTEM%0: ES replaces it with the name of the system being launched, which for us would be cttg-c640, a name configgen has never heard of. The box's own system name goes in instead, so a game of ours is launched exactly as the box would launch it. Five details, all of them EmulationStation's, and two of them cost an afternoon on a real box: • bThe menu entry is declared, not implied.0 ES would invent the group parent itself, but its theme folder would then be the group's name, which no theme has — and cHideUniqueGroups0 (on by default) dissolves a group with a single system in it unless a system by that name exists. The declared parent fixes both: it carries a c0 (cports0 by default, present in every theme) and keeps the entry in place on a box that only has c64 games. • bA system with no games of its own is dropped0, not merely hidden: cloadSystem logs cSystem "..." has no games! Ignoring it.0 and throws it away. So the entry cannot be an empty shelf — it iis0 the cstore/0 folder, and the updater in it is the game that keeps it alive. That is also why iUpdate …0 sits directly in the entry rather than in a subfolder. • bThe entry must not be the directory above the systems.0 ES marks an enumerated directory with a c/*0 entry in its file cache, and adds the mark ibefore0 reading the contents; from then on any path under it that is not itself cached answers "does not exist" (cFileSystemUtil.cpp0, cgetCacheEntry0). Systems load in a thread pool, so an entry sitting above the systems loses that race for a random couple of them, and ES drops those with cSystem "..." cpath does not exist !0 — a different couple on every start, which looks exactly like a theme problem. cstore/0 is their sibling, so there is no race to lose. • bAn empty system does not show0, so the entry never has a dead end in it. • bA new system needs an ES restart0, which the engine does after a sync that added one. If the box already has a system named like the store id — a store called cnes0, say — the entry is called c-store0 instead and the log says so: an overlay whose c0 matches an existing system imodifies that system0 rather than adding one. 2Its logo 2──────── EmulationStation resolves a system's carousel logo through the btheme (cSystemData::getProperty("image")0 asks the theme for csystem/logo0, and the theme builds the path from the system's theme folder — carbon tries cart/logos/${system.theme}.png0, then c.svg0). There is no user-level override, so a custom system either borrows a theme folder that already has artwork, or the file goes where the themes look. Both are supported: • without cmenu.logo0 the entry borrows cports0, which every theme has, so it has ian0 icon — just not the store's; • with cmenu.logo0 (a path or URL, or a list of them for several formats) the engine installs it into each theme's logo folder. It finds that folder by looking for the logos of systems every theme has (cports0, csnes0, cnes0, …) rather than by knowing the layouts, writes the file as c.0 — cmenu.theme0 then defaults to the store id, so nothing of the theme's own is overwritten — and records every path in cstate.json0, so an uninstall takes exactly those back out and a name clash is reported and skipped; • a theme it cannot reach shows the store's bname as text0, which is EmulationStation's own fallback when a logo image is missing. Batocera wrinkle: themes that ship with the system image are under c/usr/share0, and Batocera's root is a RAM overlay — the copy shows up immediately but is gone after a reboot. So the engine (4.3.0+) keeps a marked block in c/boot/postshare.sh0, the one boot hook that runs before EmulationStation, which copies the cached logo back where it is missing; c/boot0 is remounted read-write only for the write, the rest of the file is left alone, and cpurge0 takes the block out. Themes in c/userdata/themes0 keep the logo anyway. For the old behaviour — games inside the box's own systems, merged into its gamelists — set cemulationstation.menu.mode0 to cmerge0. r────────────────────────────────────────────────────────────────────────────── 1Upgrading a store installed before the menu entry 1═════════════════════════════════════════════════ Engine 4.0 is the layout change. The first sync after the upgrade takes the old install off the box — ROMs, box art, port payloads, our nodes in the box's gamelists, the empty folders, the Ports entry — and then downloads everything again into the store's own folder. Files are not carried across on purpose: the catalog is small, and a half-moved install is worse than a slightly longer sync. c-n0 shows the whole thing without doing any of it. r────────────────────────────────────────────────────────────────────────────── 1Installing a store 1══════════════════ The installer takes the store as a parameter, so it works with any config: p curl -fsSL https://git.teletypegames.org/stores/warp-engine-batocera-store/r» p aw/branch/master/install.sh | p STORE_CONFIG=https://git.example.org/tools/my-store/raw/branch/master/conf» p ig.json sh From a checkout, with a local config: p scp -r warp-engine-batocera-store root@batocera:/tmp/ p ssh root@batocera 'STORE_CONFIG=/tmp/my-config.json /tmp/warp-engine-batocer» p a-store/install.sh' It reads cstore.id0 from the config, installs the engine and the config into c/userdata/system/batocera-store//0, writes a c-store0 launcher, and runs the first sync — which is what creates the menu entry. EmulationStation has to restart to see it. t┌──────────────────────┬─────────────────────────────────────────────────────┐ t│h Environment variable t│h Meaning t│ t├──────────────────────┼─────────────────────────────────────────────────────┤ t│0 cSTORE_CONFIG0 t│0 brequired0 — path or URL of the store's cconfig.json0 t│ t├──────────────────────┼─────────────────────────────────────────────────────┤ t│0 cBATOCERA_STORE_ROOT0 t│0 where stores live (default t│ t│0 t│0 c/userdata/system/batocera-store0) t│ t├──────────────────────┼─────────────────────────────────────────────────────┤ t│0 cBATOCERA_PORTS_DIR0 t│0 Ports folder (default c/userdata/roms/ports0) t│ t├──────────────────────┼─────────────────────────────────────────────────────┤ t│0 cBATOCERA_PORT_NAME0 t│0 Ports entry name, for a store that still wants one t│ t│0 t│0 (default: the store's cname0) t│ t├──────────────────────┼─────────────────────────────────────────────────────┤ t│0 cENGINE_RAW_BASE0 t│0 where to fetch cstore.py0 from t│ t├──────────────────────┼─────────────────────────────────────────────────────┤ t│0 cWARPSTORE_RAW_BASE0 t│0 where to fetch cwarpstore.py0 from t│ t├──────────────────────┼─────────────────────────────────────────────────────┤ t│0 cWARPSTORE_SRC0 t│0 a local cwarpstore.py0 to install instead of t│ t│0 t│0 downloading it t│ t└──────────────────────┴─────────────────────────────────────────────────────┘ Every install also drops the logos cached in the store home, so a store that changes its logo gets the new one onto the box; with a local cSTORE_CONFIG0, the logos named in cmenu.logo0 are taken from beside it. r────────────────────────────────────────────────────────────────────────────── 1Writing a store repository 1══════════════════════════ Three files: p my-batocera-store/ p ├── config.json the store: URL, name, subfolder, platform mapping p ├── install.sh a wrapper that hands that config to the engine installer p └── README.md cinstall.sh0 in full: p #!/bin/sh p set -eu p FORGE=https://git.teletypegames.org p ENGINE_REF="${ENGINE_REF:-commit/}" p WARPSTORE_REF="${WARPSTORE_REF:-commit/}" p export ENGINE_RAW_BASE="${ENGINE_RAW_BASE:-$FORGE/stores/warp-engine-batocer» p a-store/raw/$ENGINE_REF}" p export WARPSTORE_RAW_BASE="${WARPSTORE_RAW_BASE:-$FORGE/engines/warpstore/ra» p w/$WARPSTORE_REF}" p export STORE_CONFIG="${STORE_CONFIG:-https://git.example.org/tools/my-batoce» p ra-store/raw/branch/master/config.json}" p TMP_DIR="$(mktemp -d)" p trap 'rm -rf "$TMP_DIR"' EXIT p curl -fsSL --retry 3 "$ENGINE_RAW_BASE/install.sh" -o "$TMP_DIR/install.sh" p sh "$TMP_DIR/install.sh" Three things in it are deliberate. The engine and cwarpstore0 are bpinned0 to commits (craw/commit/0) the config was tested with, so a push to either cmaster0 cannot change what a fresh install gets — after an engine release the store bumps both. The installer is bdownloaded to a file0 rather than piped into csh0, so a failed download fails loudly. And that file sits balone in a fresh bdirectory0: the engine installer installs any cstore.py0 or cwarpstore.py0 it finds next to itself, and a bare cmktemp0 file would make that c/tmp0. The 5TTG store is a complete example, with a CI that has the pinned engine load its config. Pick a cstore.id0 and a cpaths.subfolder0 nobody else uses — they are what keeps two stores on the same box out of each other's way, and the id is what names the menu entry and its ces_systems0 file. r────────────────────────────────────────────────────────────────────────────── 1Use 1═══ bOn the device.0 i"\" ▸ "Update \"0. The script downloads anything new, then restarts EmulationStation. A script launched by ES gets no console, so the output goes to cstore.log0 in the store home. bOver SSH0, through the launcher the installer wrote: p S=/userdata/system/batocera-store/example-store p p $S list # compatible catalog entries; * installed, ^ update » p available p $S sync # download everything new, refresh gamelists and the» p menu p $S sync blessingofra # just one title (never prunes) p $S -n sync # dry run p $S remove c64:demo # uninstall (bare name works too) p $S purge # uninstall everything this store installed p $S config # effective configuration, and where the menu entry » p lives Global flags go bbefore0 the subcommand: c$S --roms-root /tmp/roms sync0. r────────────────────────────────────────────────────────────────────────────── 1Uninstalling 1════════════ p ssh root@batocera 'curl -fsSL https://git.teletypegames.org/stores/warp-engi» p ne-batocera-store/raw/branch/master/uninstall.sh | sh' The engine does the removing, not the shell script: cpurge0 first, then any Ports entry, the launcher and the store home. Only cstate.json0 knows which ROMs, box art, port payloads, gamelist entries and ES systems were ours — and if it cannot finish (an unmounted ROMs root, say) nothing else is touched, so you are never left with the files but without the engine that knows them. t┌─────────────────────────────────────┬──────────────────────────────────────┐ t│h Environment variable t│h Meaning t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cSTORE_ID0 t│0 which store to remove; needed only t│ t│0 t│0 when several are installed t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cSTORE_CONFIG0 t│0 alternative to cSTORE_ID0 — the id is t│ t│0 t│0 read out of the config t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cDRY_RUN0 t│0 c10 to print what would go and remove t│ t│0 t│0 nothing t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cKEEP_HOME0 t│0 c10 to keep the store home (state, t│ t│0 t│0 catalog cache, log) for a reinstall t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cBATOCERA_STORE_ROOT0, t│0 as for the installer t│ t│0 cBATOCERA_PORTS_DIR0, t│0 t│ t│0 cBATOCERA_PORT_NAME0 t│0 t│ t└─────────────────────────────────────┴──────────────────────────────────────┘ In the store's own layout the uninstall is total: the ROM folder, the gamelists and the ces_systems_.cfg0 all go, and the box is as it was. In cmerge0 mode these stay behind on purpose: • bROMs the user put in the store's subfolder0 — a directory goes only when empty, so one stray file keeps it. cports/.data0 also stays: another store may keep a payload there. • bthe gamelists0 — they belong to the box. Our c0 and c0 nodes go; the user's own games, play counts and favourites are written back untouched. A gamelist we icreated0 and left empty is deleted, since it was never theirs. • cgamelist.xml.-backup0, listed at the end of the run: the gamelists as we first found them. A dry run lists files but not directory removals: it cannot know which directories will end up empty. To take bevery0 store off a machine — both engines, the shared core, the launchers — use the script in 3warpstore: p curl -fsSL https://git.teletypegames.org/engines/warpstore/raw/branch/master» p /uninstall.sh | sh r────────────────────────────────────────────────────────────────────────────── 1Where things land 1═════════════════ p /userdata/system/batocera-store/ p ├── example-store launcher for the store below p └── example/ p ├── store.py, warpstore.py the engine and its shared core p ├── config.json the store that selected it p └── state.json, catalog.json, store.log p p /userdata/system/configs/emulationstation/es_systems_example.cfg the menu » p entry p //example.png with men» p u.logo: the entry's logo p /boot/postshare.sh a marked block restoring the logo in» p /usr themes at boot p p /userdata/roms/example/ the store's own ROM folder p ├── c64/ p │ ├── blessingofra-2.2.0.prg, rabbit-1.0.0.prg, … p │ ├── images/blessingofra.png, … p │ └── gamelist.xml p ├── ports/ p │ ├── .sh, images/.png, .data// p │ └── gamelist.xml p └── store/ the menu entry itself p ├── update.sh the sync, as the menu starts it p └── gamelist.xml The box's own system folders are not touched at all. In cmerge0 mode — the old behaviour, still available — the same content goes to c///0, the first cgamelist.xml0 touched is copied to cgamelist.xml.-backup0, and every merge rewrites only the nodes under that store's subfolder, so two stores can share one gamelist. r────────────────────────────────────────────────────────────────────────────── 1Configuration 1═════════════ cconfig.json0 in the store home; the template is cconfig.example.json0 in the repository. t┌────────────────────────┬─────────────────────────┬─────────────────────────┐ t│h Key t│h Default t│h Meaning t│ t├────────────────────────┼─────────────────────────┼─────────────────────────┤ t│0 cstore.id0 t│0 cwarp0 t│0 slug: names the store t│ t│0 t│0 t│0 home, log prefix and t│ t│0 t│0 t│0 gamelist backup t│ t├────────────────────────┼─────────────────────────┼─────────────────────────┤ t│0 cstore.name0 t│0 cWarpEngine Store0 t│0 display name of the t│ t│0 t│0 t│0 menu entry t│ t├────────────────────────┼─────────────────────────┼─────────────────────────┤ t│0 cstore.base_url0 t│0 — t│0 WarpEngine host t│ t├────────────────────────┼─────────────────────────┼─────────────────────────┤ t│0 cstore.api.catalog0 t│0 c/api/software0 t│0 catalog endpoint, if t│ t│0 t│0 t│0 the engine is mounted t│ t│0 t│0 t│0 elsewhere t│ t├────────────────────────┼─────────────────────────┼─────────────────────────┤ t│0 cstore.api.download0 t│0 c/api/download0 t│0 download endpoint t│ t├────────────────────────┼─────────────────────────┼─────────────────────────┤ t│0 cpaths.roms_root0 t│0 c/userdata/roms0 t│0 where systems live t│ t├────────────────────────┼─────────────────────────┼─────────────────────────┤ t│0 cpaths.subfolder0 t│0 cwarp0 t│0 the store's own folder t│ t│0 t│0 t│0 inside croms_root0 (in t│ t│0 t│0 t│0 cmerge0 mode: its t│ t│0 t│0 t│0 subfolder inside each t│ t│0 t│0 t│0 system) t│ t├────────────────────────┼─────────────────────────┼─────────────────────────┤ t│0 cemulationstation.menu.0 t│0 csystem0 t│0 csystem0: the store gets t│ t│0 cmode0 t│0 t│0 its own menu entry. t│ t│0 t│0 t│0 cmerge0: install into the t│ t│0 t│0 t│0 box's own systems t│ t├────────────────────────┼─────────────────────────┼─────────────────────────┤ t│0 cemulationstation.menu.0 t│0 cnull0 → cstore.name0 t│0 label of the menu entry t│ t│0 cname0 t│0 t│0 t│ t├────────────────────────┼─────────────────────────┼─────────────────────────┤ t│0 cemulationstation.menu.0 t│0 cnull0 → the store id t│0 theme folder the entry t│ t│0 ctheme0 t│0 with a logo, cports0 t│0 takes its logo from, t│ t│0 t│0 without t│0 and the logo file's t│ t│0 t│0 t│0 name t│ t├────────────────────────┼─────────────────────────┼─────────────────────────┤ t│0 cemulationstation.menu.0 t│0 cnull0 t│0 the entry's own logo: a t│ t│0 clogo0 t│0 t│0 path or URL, or a list t│ t│0 t│0 t│0 of them; installed into t│ t│0 t│0 t│0 every theme's logo t│ t│0 t│0 t│0 folder t│ t├────────────────────────┼─────────────────────────┼─────────────────────────┤ t│0 cemulationstation.menu.0 t│0 ctrue0 t│0 an iUpdate …0 entry t│ t│0 cupdater0 t│0 t│0 inside the menu that t│ t│0 t│0 t│0 runs the sync t│ t├────────────────────────┼─────────────────────────┼─────────────────────────┤ t│0 cemulationstation.menu.0 t│0 c{}0 t│0 per-system folder t│ t│0 clabels0 t│0 t│0 labels; a system not t│ t│0 t│0 t│0 listed keeps the box's t│ t│0 t│0 t│0 own c0 t│ t├────────────────────────┼─────────────────────────┼─────────────────────────┤ t│0 cemulationstation.0 t│0 cnull0 → cfalse0 in csystem0 t│0 also write a Ports t│ t│0 cports_entry0 t│0 mode t│0 entry that runs the t│ t│0 t│0 t│0 sync t│ t├────────────────────────┼─────────────────────────┼─────────────────────────┤ t│0 cemulationstation.0 t│0 cWarpEngine Store0 t│0 cmerge0 mode only: t│ t│0 cfolder_name0 t│0 t│0 display name of our t│ t│0 t│0 t│0 folder in the box's t│ t│0 t│0 t│0 gamelist t│ t├────────────────────────┼─────────────────────────┼─────────────────────────┤ t│0 cemulationstation.0 t│0 ctrue0 t│0 restart t│ t│0 crestart0 t│0 t│0 EmulationStation after t│ t│0 t│0 t│0 a change t│ t├────────────────────────┼─────────────────────────┼─────────────────────────┤ t│0 cemulationstation.0 t│0 cnull0 → t│0 where ES keeps its user t│ t│0 cconfig_dir0 t│0 c/userdata/system/0 t│0 config t│ t│0 t│0 cconfigs/0 t│0 t│ t│0 t│0 cemulationstation0 t│0 t│ t├────────────────────────┼─────────────────────────┼─────────────────────────┤ t│0 ccatalog.statuses0 t│0 c["released",0 t│0 catalog cstatus0 values t│ t│0 t│0 c"archived"]0 t│0 to install t│ t├────────────────────────┼─────────────────────────┼─────────────────────────┤ t│0 ccatalog.owner_id0 t│0 cnull0 t│0 restrict to one t│ t│0 t│0 t│0 publisher t│ t│0 t│0 t│0 (c/api/software?owner_0 t│ t│0 t│0 t│0 cid=0) t│ t├────────────────────────┼─────────────────────────┼─────────────────────────┤ t│0 ccatalog.only0 / t│0 c[]0 t│0 software-name allow / t│ t│0 ccatalog.exclude0 t│0 t│0 deny lists t│ t├────────────────────────┼─────────────────────────┼─────────────────────────┤ t│0 cplatforms0 t│0 c64, plus4, tic80 t│0 platform → system, t│ t│0 t│0 t│0 asset kind, extension, t│ t│0 t│0 t│0 enabled t│ t├────────────────────────┼─────────────────────────┼─────────────────────────┤ t│0 cbehavior.prune0 t│0 ctrue0 t│0 remove games that left t│ t│0 t│0 t│0 the catalog or the t│ t│0 t│0 t│0 filters t│ t├────────────────────────┼─────────────────────────┼─────────────────────────┤ t│0 cbehavior.timeout0 t│0 c300 t│0 HTTP timeout, seconds t│ t├────────────────────────┼─────────────────────────┼─────────────────────────┤ t│0 cbehavior.insecure0 t│0 cfalse0 t│0 skip TLS verification t│ t│0 t│0 t│0 (self-hosted test t│ t│0 t│0 t│0 instances) t│ t└────────────────────────┴─────────────────────────┴─────────────────────────┘ 2Platform mapping 2──────────────── A platform is installable when its release asset is a file a system this box has can boot directly — "has" meaning declared in the box's own ces_systems.cfg0, which is an emulator that got built into this image (a ROM folder can be there with nothing behind it): t┌──────────────────┬────────────┬──────────────────────┬───────────┐ t│h Catalog platform t│h Asset kind t│h Batocera system t│h Extension t│ t├──────────────────┼────────────┼──────────────────────┼───────────┤ t│0 7cc640 t│0 ccartridge0 t│0 cc640 (VICE) t│0 c.prg0 t│ t│0 8cplus40 t│0 ccartridge0 t│0 ccplus40 (VICE cxplus40) t│0 c.prg0 t│ t│0 9ctic800 t│0 ccartridge0 t│0 ctic800 t│0 c.tic0 t│ t└──────────────────┴────────────┴──────────────────────┴───────────┘ The others (cebitengine0, clove0, cgodot0, cbevy0, cphaser0) ship chtml0 and per-OS zips, not a ROM a system launches, so they are not mapped by default. Adding one is a config edit: p "platforms": { "godot": { "system": "godot", "kind": "linux_x64", "ext": ".z» p ip", "enabled": true } } r────────────────────────────────────────────────────────────────────────────── 1How a sync stays safe next to EmulationStation 1══════════════════════════════════════════════ EmulationStation keeps gamelists in memory and writes them back on exit, so a merge done while it runs can be clobbered. Launched from the menu (csync-from-es0) the engine downloads first, then hands the merge to a detached capply-gamelists --wait-pid 0 child that waits for the old ES process to die. From SSH, with no ES running, the merge is inline. The ces_systems0 overlay needs none of that: ES reads it and never writes it. A re-run is idempotent: existing files are left alone, a release bump (c1.10 → c2.0.00) deletes the old asset before fetching the new one, and a gamelist or ces_systems0 file whose content has not changed is not rewritten at all. Downloads go through cGET /api/download0 rather than c/file/0, with a cwarpstore/ ()0 user agent; whether they count towards the catalog statistics is the host's call — teletypegames.org deliberately leaves cwarpstore/*0 out (cignored_user_agents0). A sync aborts when it cannot tell which systems the box has — an unreadable ces_systems.cfg0 with an unmounted c/userdata/roms0 behind it would otherwise look like "nothing is compatible any more" and prune every installed game. 1State 1═════ cstate.json0 records what the store installed, keyed c:0, so the same software name on two systems stays two entries. clayout0 says which directory layout those records belong to: that is how the upgrade to the store's own folder knows to run once — it removes the old install, and the sync downloads it again into the new place. A cversion: 10 file from the pre-split cttg-store0 is migrated on first run.