The menu entry borrowed the Ports icon, which is what "give it a theme
folder that already has artwork" buys you. EmulationStation offers nothing
better on its own: a system's logo is resolved through the theme
(SystemData::getProperty("image") -> theme's system/logo, path built from
the system's theme folder), and there is no user-level override anywhere —
not in the settings, not in the ROM folder, not in the binary.
So `menu.logo` installs the file where the themes look. The logo folder is
found rather than assumed: it is the folder holding the most logos of
systems every theme has (ports, snes, nes, …), because themes keep them in
art/logos, _inc/systems/logos, _art/Colorlogos and worse. On the box that
found 7 of 9 themes; the other two do not use ${system.theme} at all, and
there ES falls back to drawing the store's name as text, which is a fine
answer.
Two safeguards, since this writes outside the store's own folder for the
first time: the file is named after `menu.theme`, which now defaults to the
store id so no theme's own artwork can be overwritten, and every path
installed is recorded in state.json — an uninstall removes exactly those,
a pre-existing file of that name is reported and left alone.
Themes that ship with the system image are under /usr/share, and Batocera's
root is a RAM overlay: the copy works but does not survive a reboot. The
sync puts it back, and the log says so, naming batocera-save-overlay.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
On a real box (Batocera 43.1) the entry did not show up, and switching
themes seemed to decide whether it did. Neither was a theme problem — two
EmulationStation behaviours nobody documents:
* A system with no games of its own is not merely hidden, it is thrown
away: loadSystem logs `System "ttg" has no games! Ignoring it.`. The
menu entry was deliberately an empty shelf (`.ttg-none` extension, a
`true` command), so it could never have survived.
* ES marks a directory it has enumerated with a `<dir>/*` entry in its
file cache, and it adds that mark *before* reading the contents. From
then on, any path under that directory which is not itself cached
answers "does not exist" (FileSystemUtil.cpp, getCacheEntry). Systems
load in a thread pool, so an entry whose path sat above the systems'
paths lost that race for a couple of them, and ES dropped those with
`System "ttg-c64" path does not exist !` — a different couple on every
start, which is what made it look theme-dependent.
Both are fixed by pointing the entry at the `store/` folder: it holds the
updater, so it has a game and ES keeps it, and it is a sibling of the
platform folders rather than their parent, so there is no cache race to
lose. The separate `<id>-store` child system is gone with it, and the
"Update <store>" item now sits directly in the entry instead of in a
"Store" subfolder — one level less to walk.
Also: the migration removed the old Ports script but left its node in the
box's ports gamelist, which ES then complained about on every start. It
goes now, and only that node.
Verified on the box: three consecutive ES restarts with no system dropped,
all four gamelists parsed, and the log clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The games were scattered across the box's own systems — a folder inside
c64, another inside tic80, the launchers inside Ports, and the sync a
fourth entry somewhere else. There was no one place where the store
appeared, and living in the box's folders is what forced the gamelist
merging, the backups and every "never step outside your subfolder" check.
Now the store gets a folder of its own, /userdata/roms/<subfolder>, with a
directory per platform, and an es_systems_<id>.cfg overlay declares one ES
system per platform under a shared <group> — which EmulationStation draws
as a single carousel entry holding a folder per system. The gamelists are
ours, the box's systems are untouched, and the sync is startable from
inside the entry: Store -> "Update <store>".
Nothing about emulators is hardcoded. The launch command, the extensions,
the platform, the theme and the emulator list are copied off the box's own
es_systems.cfg — a file Batocera generated for this image, so it lists
exactly what got built into it. %SYSTEM% is resolved to the source system
name on the way, since ES would otherwise substitute ttg-c64, which
configgen has never heard of.
The old behaviour is emulationstation.menu.mode: "merge", and switching
either way migrates: the layout the files are in is recorded in
state.json, and a mismatch removes the old one and downloads again rather
than shuffling files half-way. purge follows that record too, so an
uninstall after an upgrade whose first sync never ran still finds the
games where they actually are.
Every path now goes through a Layout, so the two modes cannot drift apart.
Also: a gamelist or es_systems file whose content has not changed is not
rewritten at all, and the tags ES owns — favourite, playcount, lastplayed
— are carried over onto the nodes we rewrite, which merge mode used to
lose.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`purge` is what `remove` does for one game, for all of them at once, plus the
directories the store made: the shell script that wraps it has no way of knowing
which ROMs, box art, Ports payloads and gamelist entries were ours, and
state.json does. uninstall.sh runs it first and only then removes the Ports
entry, the launcher and the store home; if the engine cannot finish, nothing
else is touched.
The gamelists are merged rather than deleted — they belong to the box, and the
user's own games, play counts and favourites stay in them. Two things the tests
caught:
- The backup was being made on the *uninstall's* first touch of a gamelist we
had created ourselves, so a purge produced a backup of our own file. It is now
skipped when purging: by then any copy worth having was made on the first sync.
- With that fixed, "no backup next to it" means we created the file, so a
gamelist that is left empty is deleted too. The box ends up as it was.
- The Ports entry cannot be derived from the config: a store repository sets
BATOCERA_PORT_NAME to whatever it likes (ours is "Teletype Games Store" while
store.name is "Teletype Games"), so the uninstaller looked for a file that was
never there. It now finds the entry by looking inside the Ports scripts for
this store's home — which also works for boxes installed before this existed,
and picks up entries left over from an earlier name.
install.sh and uninstall.sh are POSIX sh now, checked with dash, so `curl … | sh`
works where /bin/sh is not bash. The repository moved from the tools org to
stores; the old raw URLs still redirect, but every reference is updated.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The engine was 1073 lines, of which the catalog, release selection,
architecture matching, state and HTTP had nothing to do with Batocera. A
second engine now needs exactly that code, so it moved to engines/warpstore
and this repository keeps only the Batocera adapter: ROM folders, Ports
launchers, gamelist.xml, EmulationStation. 426 lines went, 115 came back.
Nothing about the CLI or the on-disk layout changes. The installer now
places two files side by side, store.py and warpstore.py, and the engine
imports the core from its own directory.
Behaviour was checked against the live catalog with a fake ROMs root: the
cartridge path, the Ports path, box art, gamelist merging, idempotent
re-sync, prune and uninstall all produce what they did before. A state.json
written by the previous version — records with `system` but no `scope` —
keys the same way, so an installed box neither re-downloads nor prunes
anything on the first sync after the upgrade.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The store could only offer what an emulator boots, which meant cartridges
and nothing else — a little under half the catalog. Batocera runs Linux
and its Ports system takes .sh launchers, so a native build can be
installed too; it just is not a ROM.
`install: "port"` unpacks the zip, finds the executable and writes a
launcher that cd's into its directory first, because a game loads assets
relative to the working directory. Python's extraction drops the
executable bit, so it is restored from the mode the archive records.
The payload lives under ports/.data/<subfolder>/, whose leading dot is
what hides it from EmulationStation, while the launchers sit in a normal
subfolder that gets scanned like any other system's. Both carry the store
subfolder, so an uninstall or prune cannot reach another store's files —
verified by pointing a state entry at a foreign namespace and watching it
be refused.
Verified end to end on aarch64 against the live catalog: bevydemo
installs as a port with an executable launcher and binary, re-syncs
without re-downloading 29 MB, and uninstalls taking its 106 MB payload
with it. `install` defaults to rom, so existing configs are unaffected.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Batocera runs on x86_64 mini-PCs, on the Raspberry Pi and on the ARM
handhelds alike. A cartridge is data for an emulator and runs anywhere,
which is why this store has been portable so far — but a native build
only starts on the architecture it was built for, and an x86_64 binary
installed on a Pi is worse than one never offered.
`kind` and `ext` may now be a map from architecture to value, with `*`
as a fallback; a plain string still means "the same everywhere", so
existing configs are untouched. The detected architecture is reported at
the top of `list` and as `detected_arch` in `config`, and a title with no
asset for the running machine is skipped with a reason that names it.
Verified on aarch64 against the live catalog: the cartridge config
behaves exactly as before, a bevy entry mapped per architecture pulls
bevydemo-1.0.0-linux-arm64.zip, and the binary inside is an AArch64 ELF.
A map without an entry for the machine skips and says so.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>