Skip to content

Sync saves with Argosy: upgraded Sigil, local naming, DC/GC/PSP/3DS/Eden units - #31

Open
Covin90 wants to merge 8 commits into
devfrom
claude/determined-edison-14llai
Open

Covin90 wants to merge 8 commits into
devfrom
claude/determined-edison-14llai

Conversation

@Covin90

@Covin90 Covin90 commented Oct 4, 2026

Copy link
Copy Markdown
Collaborator

What this changes

Makes Ludo's save sync interoperate with Argosy, the Android RomM client, for the popular platforms. It does this by reading games the same way Argosy does (argosy-sigil) and shaping saves as Argosy's handlers do. One commit per step:

  • Bundle current argosy-sigil (8a3b008) and read every disc through it. The full upstream build adds zipped games, CHD, Dreamcast, PSP and 3DS; it still needs only libc at the same GLIBC_2.33 floor. title_ids now passes the platform, from the caller or the roms/<platform>/ folder. Without one, Sigil declines every .iso. The Python disc readers are removed. build_sigil.sh now builds with cmake and actually installs into the package; it never did before.
  • Name a downloaded save after this device's copy of the game. A server save took the uploader's filename, so an Argosy save, or a zipped ROM extracted under another name, landed where RetroArch never reads it. Saves now take this device's content name: the launched file, the ROM itself, the game inside an extracted zip, an .m3u or a .cue. Regional-variant saves keep their own name. Saves that can't be placed yet wait for the next launch.
  • Dreamcast: the port A1 VMU syncs in the autosave slot as Argosy does, restored as <product id>.A1.bin. Ludo's legacy vmu-a1 rows are no longer downloaded over it.
  • GameCube: all of a game's .gci files sync as one save: one file raw, several as a flat zip, matching the server hash of Argosy's zip. Before this Ludo kept one file per game, or found none: the libretro Dolphin card folder sat one level below the save scan.
  • Upload a game's autosave as autosave.<ext>, Eden included. Argosy recognises an autosave by name: its own ROM name, argosy-latest or autosave. Ludo's names were often none of these. Version restore and Eden restore no longer depend on the filename; Eden reads the title ID from RomM, else from the pack itself.
  • PSP and 3DS: synced for the first time.
    • PSP: PPSSPP's SAVEDATA/<serial>* folders, zipped with each folder as a root.
    • 3DS: Citra or Azahar data/ + extdata/, zipped under those two root names.
    • Both are found under every RetroArch save sorting. A 3DS restore replaces only the parts the zip contains.
  • Version restore of a packed save (PS2, GameCube, PSP, 3DS, Eden) now unpacks it through the same restore the sync uses, instead of writing the zip next to the game.

Saves already on the server under the old names stay as they are; Argosy recognises each game's next upload.

Which shells it affects

Both. The changes are in engine/ plus one call in app/ludo_app/backend.py (version restore).

How you tested it

  • cd engine && tests/run.sh: all 34 files pass. New: test_save_naming.py, test_dreamcast_sync.py, test_gamecube_sync.py, test_folder_saves.py; test_title_ids.py was updated for Sigil.
  • cd app && pytest: 21 passed.
  • desktop untouched; not run.
  • The new tests run the real run_negotiated_save_sync and build_sync_inventory against a stubbed server. Fixture zips are built the way Argosy's SaveArchiver writes them, and the tests assert the server hashes them the same as Ludo's.
  • Paths were checked against the cores' sources: libretro Dolphin Boot.cpp, PPSSPP libretro.cpp, Citra/Azahar citra_libretro.cpp and archive.h.
  • Sigil's upstream unit tests pass (29/29). Its integration tests skip without sample ROMs.

Not tried by hand: a real round trip with Argosy on hardware. Worth checking one game each for Melee (GameCube), a Dreamcast game, a PSP game, a 3DS game, a zipped SNES game, and a Switch game, Deck → phone → Deck.

🤖 Generated with Claude Code

https://claude.ai/code/session_014P4FnbXeH6kbQAgBJ6BDgG


Generated by Claude Code

claude added 8 commits October 3, 2026 17:21
The bundled libsigil was an early build: it read only Switch containers
and PS1/PS2/PSP/Wii images, and declined every ".iso" because Ludo never
told it the platform. A Python reader covered GameCube/Wii and ISO 9660
beside it, and nothing at all read a zipped game, a CHD or a Dreamcast
disc.

Sigil is now upstream 8a3b008, built in full with cmake (3DS and Wii U
included, deps vendored and static, still libc-only at GLIBC_2.33).
title_ids passes the platform -- named by the caller, or by the
roms/<platform>/ folder the ROM sits in -- and exposes identify(), which
adds the emulator's on-disk save name and layout to the title ID. That
is the same identity Argosy reads with the same library. The Python disc
readers are gone; what stays is what Sigil cannot see: IDs in the names
of games not on this device, and Switch base/update/DLC arithmetic.

Also: the GCI filename fallback expected a six-character code where
Dolphin writes "<maker>-<game code>", two then four; and build_sigil.sh
resolved the repo root after cd-ing away from it, so it never installed
its build into the package.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014P4FnbXeH6kbQAgBJ6BDgG
Both download paths wrote a server save under the uploader's filename.
Argosy names a game's autosave after its own ROM file, and the same game
is routinely called something else here -- a zip Ludo extracted, a
different revision tag, a disc set booted through its .m3u -- so the
save landed beside the game as a file RetroArch never loads.

A battery save now takes the name RetroArch reads on this device: the
uploader's name when it is one of this ROM's own files (a regional
variant keeps its region), otherwise the launched file, the ROM itself,
the one game inside an extracted folder, a disc set's playlist or a
dump's descriptor. A folder ROM that leaves a choice and has not been
launched defers the background download; the pre-launch sync, which
runs after the launch is noted, places it. The pre-launch path also
groups server saves by that local name, so another client's save and
ours are one save, newest wins, instead of two writes to one file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014P4FnbXeH6kbQAgBJ6BDgG
Argosy now syncs flycast's port A1 card, "<product id>.A1.bin", in the
"autosave" slot, uploaded under its own ROM's name and restored by
product id. Ludo gave every port a "vmu-<port>" slot, so the two never
met, and a "Crazy Taxi (USA).bin" from Argosy would have been written
beside the card as a file flycast never reads.

A1 uploads as the autosave; the other ports keep their slots. A
Dreamcast autosave from the server is written as the game's A1 card: the
card already on disk names it, else the product number Sigil reads off
the disc (zipped or not), else the content name flycast falls back to.
Old "vmu-a1" rows hold an earlier copy of the same card and are no
longer downloaded over the autosave.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014P4FnbXeH6kbQAgBJ6BDgG
Dolphin, the libretro core included, keeps each save a game writes as
its own .gci in "<user dir>/GC/<region>/Card A/". Ludo reported every
GCI on the game's (rom, "autosave"), so the dedupe kept the newest and
dropped the rest; a game with two save files synced one. Under core or
content sorting the card folder sat one level below the save scan, so
nothing was found at all.

A game's GCIs are now one unit, as Argosy's GciSaveHandler has them:
one GCI travels raw, several as a flat zip of their file names, which
the server hashes the same as Argosy's own. A restore replaces that
game's GCIs in the folder Dolphin reads -- the one holding them, else
the one the core would create -- keeps the old ones as backups, names a
lone GCI as Dolphin does, refuses another game's save, and waits while
RetroArch runs. A legacy per-GCI row from an older Ludo replaces only
the save file it is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014P4FnbXeH6kbQAgBJ6BDgG
Argosy finds a game's autosave on the server by its name: the name of
its own ROM file, "argosy-latest" or "autosave"
(SaveSyncApiClient.isLatestSaveFileName). Ludo uploaded under its own
name for the game, which often is not Argosy's -- a zip extracted here,
a revision tag -- so Argosy saw Ludo's save only while it was the lone
one on the server.

Ludo places downloads by its own rule now, not by the server's name, so
the name can serve Argosy: a game's autosave goes up as
"autosave.<ext>". VMU ports, named channels and a ROM of several
regional games keep their names, the last because the name is all that
says which region's save it is. Version restore takes the local name
too, and an Eden save whose name carries no title ID -- Argosy's, or
ours as "autosave.zip" -- restores under the ROM's own ID from RomM.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014P4FnbXeH6kbQAgBJ6BDgG
Eden packs went up as "<title id>.zip", a name Argosy reads as this
game's autosave only while it is the lone save on the server. They now
go up as "autosave.zip", like every other game's autosave. The title ID
still travels inside the pack ("<title id>/…", Argosy's layout too), and
a restore whose name carries none -- ours now, or Argosy's ROM-named
packs -- reads it from the ROM's RomM title ID, else from the pack.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014P4FnbXeH6kbQAgBJ6BDgG
Neither console's save is a file: PPSSPP keeps a game's save as folders
under PSP/SAVEDATA named by its serial, Citra and Azahar as a title's
data and extdata folders on their virtual SD card. Ludo's save walk
looks for save-file extensions, so neither was ever synced.

Both are now discovered where the libretro cores write them, under any
of RetroArch's sortings, and packed as Argosy packs them -- every
SAVEDATA folder starting with the serial as a root of its own, minus
installed game data; a 3DS title's "data/" and "extdata/" -- so the
server hashes the two clients' zips alike. A restore replaces the game's
folders (a 3DS component only when the zip carries it), keeps backups,
waits while RetroArch runs, and finds the place to write from the save
already there, else where the core would create it. Games are matched
by serial or title ID; RomM's "00040000/00033500" spelling of a 3DS
title now folds onto its 16-hex ID.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014P4FnbXeH6kbQAgBJ6BDgG
Picking a version in the save history wrote the server file beside the
game under its own name -- right for an .srm, but a PS2 card's folders,
a GameCube game's GCIs, a PSP or 3DS game's save folders and an Eden
pack all arrive as zips that only mean something unpacked into the
emulator's tree. Those versions now go through the sync's own restore,
with its backups and its refusal while the emulator is open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014P4FnbXeH6kbQAgBJ6BDgG
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants