Skip to content

Repository files navigation

Lodor

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 Android app

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.

What it does

  • 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.json manifest, 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).

Design

  • CGO-free, standard-library only. No database driver, no SDL, no third-party modules — go.mod has zero requires. 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 reports downloaded=0 and 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.

Build

# 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-sync

A 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).

Usage

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.

Status

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.

License

MIT — see LICENSE. Acknowledgements in CREDITS.md.

About

Lodor — a CGO-free, self-hosted RomM sync engine for retro handhelds (Linux CFW + Android): zero-byte stub mirroring, fetch-on-launch, automatic save + save-state sync.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages