A small, CGO-free Go engine that syncs a RomM library to a retro handheld — the headless core behind LodorOS, the per-CFW ports, and the Lodor Android app.
Lodor's wedge is transparent mirroring: instead of browsing and downloading, your whole library appears on the device as zero-byte stub files. Tap a game and the real ROM is fetched on demand; saves sync back to the server automatically around each session. The library "isn't there until you touch it," so a 128 MB handheld can front a 500 GB server.
This repository's source is the engine only — a single binary plus shell-callable subcommands,
no UI; the front-ends (menus, onboarding, progress) live in the OS and port repos that embed it.
This repo's releases are the umbrella for the whole
project: each release carries every lane's artifacts — the LodorOS update overlays, the muOS
.muxapp, the Knulli zip, the OnionOS zip, the Android APK, and the signed versions.json
self-update manifest.
The same engine also drives Lodor for Android (Lodor-Android-<version>.apk on the releases
page): an app that brings stub mirroring, download-on-launch, and save/state sync to Android
handhelds under an existing frontend. It registers as a proxy emulator — the frontend launches
Lodor, Lodor materializes the ROM and saves, forwards to RetroArch, and syncs back at session end.
One-tap setup configures ES-DE, Cocoon, and Daijishō. The engine ships inside the APK
(built GOOS=android GOARCH=arm64); nothing about the server setup differs from the CFW lanes.
- Stub mirror — write a zero-byte placeholder for every not-yet-downloaded ROM, into the device's per-system folders, and emit a resolution index (no database needed).
- Fetch on launch — resolve a tapped stub to its RomM ROM, stream the real file to disk, verify it.
- Save sync — push local saves to RomM (additive, versioned) and pull/restore newer ones, with a pending queue for offline writes. A restore preserves the current save first — pushing it, or staging it for deferred upload when offline — so it can never trade away unsynced progress.
- Save-state sync — the same discipline for emulator save states: push, pull, queue offline, restore any past point (this is what powers LodorOS Flashback and cross-device Handoff).
- Self-update — check and fetch per-lane update assets from the signed
versions.jsonmanifest, sha256-verified and resumable; a lane with no published asset honestly reports "no update". - Catalog & collections — mirror platforms and RomM collections to the device.
- Box art & BIOS — fetch covers (lazily) and per-platform firmware.
- Onboarding — pair to a server with a one-time code (no admin credentials on the device).
- CGO-free, standard-library only. No database driver, no SDL, no third-party modules —
go.modhas zerorequires. Builds to a single static binary that cross-compiles cleanly for ARMv7/ARM64 on the CFW lanes (the Android build of the same code ships inside the APK instead). - Honest results. Every subcommand prints a machine-parseable
RESULT …line and exits with a code the caller switches on. A partial or failed transfer reportsdownloaded=0and leaves the stub in place — it never fabricates success or writes a fake placeholder in a real ROM's spot. - Newest-wins, additive saves. Uploads never delete server history; conflicts insert a new timestamped revision. Pulls back up the local file before overwriting.
# host build
CGO_ENABLED=0 go build -o lodor-sync ./cmd/lodor-sync
# static ARMv7 (most Linux handhelds)
CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build -trimpath -o lodor-sync ./cmd/lodor-syncA static binary uses Go's pure resolver, so on-device it expects a usable /etc/resolv.conf and a CA
bundle (ship ca-certificates.crt and point SSL_CERT_FILE at it).
Configuration lives in config.json next to the binary (see config.json.example). Subcommands:
| Flag | Does |
|---|---|
--set-server <url> [--port N] [--insecure] |
Persist the server URL before pairing. |
--pair <code> |
Exchange a RomM pairing code for a client token; write config.json. |
--register-device <name> / --rename-device <name> |
Register/rename this device. |
--validate |
Check reachability + auth. Prints reachable=<0|1> auth=<0|1> pairing_expired=<0|1>. |
--mirror-catalog |
Stub every not-downloaded ROM into Roms/ (only platforms with an installed emulator pak — never games the device can't launch); write the resolution index. |
--mirror-collections |
Write Collections/<name>.txt per RomM collection. |
--download <rom> |
Fetch one ROM's real file (resolve → stream → verify). Multi-disc aware. |
--download-bios |
Fetch BIOS/firmware for every mapped platform. |
--sync-save <rom> |
Pull-then-push the save for one ROM. |
--list-saves <rom> / --restore-save <rom> <id> |
List / restore server save revisions. |
--push-pending |
Upload every queued pending save. |
--recent |
Print the single most-recently-played game across devices (powers the Continue tile). |
--sync-feed |
List recent server saves, newest first. |
Each prints a RESULT … summary line.
That table is the everyday core; the full surface is larger. Run lodor-sync -h for the
authoritative per-flag reference. The other mode groups:
| Group | Modes |
|---|---|
| Save states (Flashback / Handoff) | --list-states --pull-state --push-states --push-all-states --push-pending-states --queue-state (with --state-id / --state-slot) |
| Self-update | --check-update --fetch-update (sha256-verified, Range-resumable; per-lane assets from the signed versions.json) |
| Downloads & discs | --download-queue --fetch-discs --fetch-next-disc --prefetch-discs --check-rom --check-bios --remove-downloads --evict |
| Continue / recency | --recent --sync-continue --sync-feed |
| Multi-user profiles | --list-users --login-user --login-profile --pair-profile --list-profiles --login-device |
| Playtime & metadata | --sync-playtime --report-session --session-start / --session-end · --set-favorite / --unset-favorite --set-rating --set-status --set-props |
| RetroAchievements | --ra-login --ra-status --ra-cmd |
| Housekeeping | --reconcile --pull-saves --push-save --track-save / --untrack-save --write-gamelists --uninstall-mirror --version |
Common modifiers: --dry, --full, --include-deleted, --cancellable (honest B-press
cancellation on device UIs), --insecure, --port.
Exit codes: 0 ok · 2 config/flag error · 3 unreachable / could not resolve · 4 ran but
one or more items errored · 6 pairing expired — the server rejected this device's client token
(expired or revoked); re-pair the device. On exit 6, modes that print a RESULT line also print a
final PAIRING_EXPIRED line on stdout; list-output modes signal via the exit code alone. The engine
never deletes or rewrites its config on an auth failure, so a transient server misconfig can't wipe
a valid pairing.
Save-sync integrity: an upload is reported pushed only after the server-side copy is verified
(the create response's content hash and stored size, else a byte-for-byte re-download); an
unverifiable upload is retried once and then left pending — never silently marked synced. Server
save records with missing/zero-length bytes ("ghost saves") are never pulled over a real local save,
never offered for restore, and are counted (ghosts=<N> in --sync-save) so a UI can surface them.
A zero-byte local save file is never uploaded.
The engine is built and proven end-to-end against a live RomM server (pairing, mirror, download — including multi-disc — and save round-trips). It targets the MinUI save-directory conventions today; per-CFW save-path data is data-driven so other front-ends can adopt it.
MIT — see LICENSE. Acknowledgements in CREDITS.md.