Cartwright
A cartwright builds carts. Field-tested on a TrimUI Brick Pro (tg5040)
running NextUI: 19 systems, 2,718 games, every file verified bit for bit. The
arcade folder started at 375 romsets of which none ran, and finished at 541
working: part rebuilt from the original bytes by CRC, part from a replacement
set fetched once the core's naming era was known. Every path, command and
number here came from a real build.
Scope: paths and conventions are NextUI's. The method and all five tools
apply to any Linux handheld running libretro cores. If the device is not a
TrimUI, read references/porting.md first, say so once, and keep working.
The one rule
Never trust a database, a filename, a wiki, or this document. Interrogate the binary.
Documentation, DAT files and ROM sets routinely disagree with what the emulator on the device actually wants. Every hard problem in this domain dissolved the moment the core was asked directly. Every assumption cost an hour.
scripts/corespec.py is how you ask.
Start here: symptom to first move
Most people arrive with one broken thing, not an empty card.
| Symptom | First move |
|---|---|
| Arcade games say "missing files for THIS VERSION of FBNeo" | roms-and-arcade.md. Fetch the DAT, run fbneo.py validate, and only then rebuild. Do not touch a ROM first |
| One game or one system will not launch | Read .userdata/$PLATFORM/logs/<TAG>.txt over SSH (device.md). The reason is in there and it is almost never what you guessed |
| Games listed by filename instead of title | map.txt, tab separated, no BOM (roms-and-arcade.md) |
| Box art missing | libretro.py art (presentation.md). A system reporting Queued 0 ROMs is missing its .media folder |
| Every PlayStation game appears twice | A redundant .m3u next to each single-disc .chd (roms-and-arcade.md) |
| Device frozen inside a game | kill -9 the emulator over SSH (device.md) |
| An emulator opens its config screen instead of the game | Its bundled pad map does not match TRIMUI Player1. The user maps it once (device.md) |
Card full of ._ files |
xattrs went to exFAT (card-and-verify.md) |
| Building a card from nothing | Phase order below |
Before anything: recon
Never guess the platform tag from the product name. The Brick Pro is
tg5040, not tg5050. Wrong tag, nothing loads.
Run this against the card's root, either mounted on your computer or over SSH
at /mnt/SDCARD. On a factory card the file is already there; this is recon on
the card you have, before you reformat anything.
grep -A2 'PLATFORM=' /Volumes/<CARD>/trimui/app/.tmp_update/updater
Then map every ROM folder tag to its emulator and every emulator to its
accepted extensions and BIOS names. See references/recon-and-bios.md.
Phase order
Steps 1 and 5 are the real work. The rest is plumbing.
| # | Phase | Reference |
|---|---|---|
| 1 | Platform tag, folder tags, core extensions, core BIOS names | recon-and-bios.md |
| 2 | Install NextUI all.zip into staging (not onto the card) |
recon-and-bios.md |
| 3 | BIOS by ranged extraction, validated against the cores | recon-and-bios.md |
| 4 | Sort ROMs into tagged folders, unzip everything but arcade | roms-and-arcade.md, sourcing.md |
| 5 | Rebuild arcade against the core's CRCs, generate map.txt |
roms-and-arcade.md |
| 6 | Build the manifest, format exFAT/MBR, rsync, verify | card-and-verify.md |
| 7 | Boot, install paks from the Pak Store, verify each landed | device.md |
| 8 | Scrape box art, fill gaps from libretro-thumbnails | presentation.md |
| 9 | Generate collections from libretro-database | presentation.md |
| 10 | Apply a theme, rebuild whatever it does not cover | presentation.md |
| 11 | Configure backups | device.md |
| 12 | Sync the card back to staging, rebuild the manifest | card-and-verify.md |
Not a TrimUI? references/porting.md says which parts hold and how to derive
the rest from the device instead of guessing from a wiki.
Read references/gotchas.md once before starting. It is short and every entry
in it cost real time to learn.
Tools
Run each with --help. All of them accept --selftest.
| Script | What it does |
|---|---|
scripts/ziprange.py |
Index and extract single entries from a remote ZIP over HTTP range requests, without downloading it. Handles split .zip.001 archives. This is how you get 391 BIOS files out of multi-GB packs, or 496 arcade romsets out of a 9.6 GB archive. |
scripts/corespec.py |
Interrogate a libretro core .so: accepted extensions, expected BIOS filenames, FBNeo naming era, embedded-ROM detection. |
scripts/fbneo.py |
Parse an FBNeo DAT, prove it matches your core (validate), index romsets by CRC, rebuild broken zips with the filenames the core wants, rename misidentified sets, generate map.txt. Its --help names the DAT source. |
scripts/manifest.py |
Build a size+CRC32 manifest of a source tree, verify a copy against it. |
scripts/libretro.py |
Fill box-art gaps from libretro-thumbnails; build genre and franchise collections from libretro-database. |
Getting ROMs
You do the searching. The user names what they want; you search, vet the candidates and come back with two or three options, their sizes and a recommendation. Making the user go find a URL and paste it back is the wrong division of labour.
Decide the convention before searching — FBNeo for arcade, No-Intro for
cartridges, Redump/.chd for discs — because downloading the wrong kind of
set is the more common failure. Then vet with the archive.org metadata API,
pull only the entries needed with scripts/ziprange.py, and verify what
arrived before building on it.
Full procedure in references/sourcing.md. No list of sites lives in this
skill: they rot, and a live search is better every time.
What you cannot do from here
Say so plainly and hand the user exact steps instead of pretending.
| Task | Why | What to tell the user |
|---|---|---|
| Installing paks | The Pak Store writes a SQLite row that drives update notifications. Copying folders over SSH does not. | Install from the Pak Store on the device. If it fails mid-install, the DB row already exists — finish the file copy over SSH and updates still work. |
| Saturn pad mapping | SDL derives the controller GUID from a CRC of the device name (TRIMUI Player1). It cannot be hand-written. |
Launch any Saturn game once, map the pad on the config screen that appears. Saved to Saves/SS/.yabasanshiro/keymapv2.json. |
| Formatting / mounting the card | Physical. | Disk Utility needs View → Show All Devices to expose the partition-scheme selector. exFAT + MBR. |
| ScreenScraper credentials | The user's account. | Without them ScrapeGoat does ~1 request/minute, unusable at scale. With them, 100+/min. |
Rules that prevent damage
- Never delete from the device's
/tmp. The Pak Store stages downloads there. Cleaning it mid-session breaks installs already in flight. - Read the whole Pak Store catalog before calling a system impossible.
A wrong "Dreamcast can't work" cost a user 38 GB of deleted ROMs. The
DCpak exists.https://raw.githubusercontent.com/LoveRetro/nextui-pak-store/refs/heads/gh-pages/storefront.json - Verify your own tooling before alarming the user. Several false alarms in
the original build traced to bugs in the checker, not the data —
os.path.splitexton names containing dots, the wrong string pulled from a binary. When a scan reports something alarming, confirm it by hand on one file first. - Explain what is lost before any destructive step, in two lines. Users approve readily and regret slowly.
- Say the numbers. File counts, CRC results, before/after. This domain is full of silent failures that only surface weeks later.
Verification is the deliverable
A 240 GB copy that silently drops one file is the worst outcome in this domain — it surfaces when a game will not boot, long after the context is gone.
Build the manifest from the source, verify the card against it, state the
count. scripts/manifest.py does both. Full CRC over 240 GB takes ~45 minutes
at 90 MB/s and is worth it.
When a game will not launch, the device already wrote the reason to
.userdata/$PLATFORM/logs/<TAG>.txt. Read it before forming a theory.
If the superpowers plugin is installed, verification-before-completion and
systematic-debugging pair well with this skill. Neither is required: the
verification discipline above stands on its own.