Save archives
Moving save archives in and out of the container, and syncing a whole memory card, over the import, export and memory-card routes.
Save archive transfer
When you launch a game, the save.archive field has to be a file path
inside the container. But RomM runs as its own separate service: it only
has the raw save file, not a way to write directly into the container's
file system. So a save archive has to travel between RomM and the
container over these endpoints, in both directions.
PUT /streaming/api/session/imports/{name}.zip body: raw zip
GET /streaming/api/session/exports
GET /streaming/api/session/exports/{name}.zip
DELETE /streaming/api/session/exports/{name}.zipAll four routes need the X-Broker-Secret header. The upload returns
{"status": "stored", "name": ..., "path": ..., "size": ...}; feed that
path back as save.archive on activate. The name must be a plain .zip
filename with no folders in it, the uploaded data must actually be a zip
file (checked by its file signature), and it can't be bigger than the same
256 MiB limit the exit dump uses.
Pulling an archive through these routes covers two cases the automatic exit upload can't: dev mode, where uploading is turned off and the archive only ever ends up on disk, and a failed upload, where the archive on disk is the only copy of the save data left. Delete each archive once RomM has stored it.
# hand a save archive to the container, then launch with it
curl -X PUT http://localhost:3000/streaming/api/session/imports/4471.zip \
-H 'X-Broker-Secret: <shared secret>' --data-binary @4471.zip
# -> {"status": "stored", "name": "4471.zip", "path": "/config/broker-imports/4471.zip", "size": 12345}
# list, fetch and discard what exit left behind
curl http://localhost:3000/streaming/api/session/exports -H 'X-Broker-Secret: <shared secret>'
curl http://localhost:3000/streaming/api/session/exports/abc123-1723600000.zip \
-H 'X-Broker-Secret: <shared secret>' -o abc123.zip
curl -X DELETE http://localhost:3000/streaming/api/session/exports/abc123-1723600000.zip \
-H 'X-Broker-Secret: <shared secret>'An upload larger than SAVE_FILE_MAX_BYTES answers 413,
and a body that isn't a zip answers 422.
Members under .import/ are not restored path for path; they are placed by
the emulator's own rules. See Declared imports.
What an archive holds
An archive is a zip file of the emulator's save subtrees (the specific
folders inside its save data that actually hold saves and states). The
paths inside the zip are relative to the emulator's save root folder: for
PCSX2, that means paths like memcards/... and sstates/.... When an
archive is restored, a file already on disk is never overwritten by an
older copy from the archive. A file outside those folders refuses the whole
restore before anything is cleared (it is listed as unrecognised_layout in
an archive with imports), and so does an encrypted file, one with a
compression method the broker cannot read, or one whose data is corrupt. The
broker reads every file in full to check before it clears anything. Only a
synced memory card in an older archive is skipped.
That's what keeps an old archive from rolling a session backward. When an
archive is built at exit, only the files that changed since launch go into
it, so an archive holds just the changes, not a full copy of the save data.
The archive manifest
Every emulator lays its save directories out differently, and there are
fourteen of them. So RomM doesn't have to keep its own table of those
layouts, an archive built at exit also holds a .broker-manifest.json file
at its root, naming the session and labelling each file:
{
"version": 1,
"created_at": 1787880008.5,
"session": {
"emulator": "retroarch",
"core": "mgba",
"platform": "gba",
"rom_id": 5,
"rom": "Advance Wars",
"rom_file": "/romm/library/roms/gba/Advance Wars (USA).gba",
"state_slot": 0
},
"files": [
{ "path": "states/mGBA/Advance Wars (USA).state", "kind": "state" },
{ "path": "states/mGBA/Advance Wars (USA).state.png", "kind": "state_screenshot" },
{ "path": "saves/mGBA/Advance Wars (USA).srm", "kind": "save" }
]
}kind is one of state, state_screenshot, memcard or save. core is
only set for a launcher that can run several different emulator cores, like
RetroArch (mgba above), so a RetroArch PSP archive (core: "ppsspp") can
be told apart from a standalone PPSSPP one. state_slot is null when the emulator has no save states at
all. When the session placed declared imports, an
imported list names the placed files, which ship whether or not the game
changed them.
The manifest describes what's in the archive, it isn't save data itself: it
doesn't appear in the save_dump.files list, and when an archive is
restored, the manifest file is dropped instead of being written into the
emulator's save data. Archives made before this manifest existed simply
carry none, and they still restore exactly as they always did.
Memory cards
Some emulators use a memory card: a single file or folder holding save data, the way a real memory card would. The broker manages this file directly for PCSX2 (its Slot 1 folder card) and Dolphin (its GameCube memory card). Instead of bundling it inside every save archive, RomM can sync the whole card against the player using its own routes:
GET /streaming/api/session/memory-card?emulator=pcsx2
PUT /streaming/api/session/memory-card?emulator=pcsx2 body: raw zip of the card
GET /streaming/api/session/memory-card?emulator=dolphin&platform=ngc
PUT /streaming/api/session/memory-card?emulator=dolphin&platform=ngc body: raw zip of the cardBoth routes need the X-Broker-Secret header. The GET sends the card back
as a zip file, with an X-Memory-Card-Slot: 1 header (this is always 1,
not a real slot number for every emulator). A 404 response carrying an
X-Memory-Card: absent header is the broker confirming the card is
genuinely empty, which is what tells RomM it's safe to wipe its own copy.
Every other kind of failure reads as "could not be captured", so a card
RomM never managed to read is not mistaken for an empty one and deleted the
next time it tries.
The PUT wipes the existing card and writes the one you send in its place.
It's refused with 409 while a session is running: the emulator keeps the
card file open for as long as the game runs, and replacing it underneath
would corrupt it. RomM sends the card down before activate and pulls it
back out after exit, so the card is only ever swapped while nothing is
running. A card larger than SAVE_FILE_MAX_BYTES answers 413.
The platform parameter says which card to sync, for an emulator whose card
depends on which platform it's currently running. Dolphin is the case that
needs it: the same emulator handles both GameCube and Wii, but only
GameCube has a physical card (Wii saves to its internal flash storage,
called NAND, which has no memory-card equivalent and always travels inside
the regular save archive instead), so this route requires platform=ngc to
say which one you mean. Leave platform off, or send anything else, and the
route answers 400, the same as it would for an emulator with no card at
all. The card you get back is Dolphin's whole GC/ folder rather than one
region's Card A folder, since a game library can mix regions and a sync
has to cover all of them. PCSX2 needs no platform value, since its Slot 1
card doesn't depend on one.
Dolphin only reads a GameCube save that sits at <USA|EUR|JAP>/Card A/<name>.gci
inside that folder, so a card pushed to Dolphin is tidied on the way in. A
leading GC/ folder is dropped, and a .gci anywhere else (loose, under a
bare Card A/, or under Card B/) moves to the Card A folder of the
region Dolphin boots its game in, read from the last letter of its game code
the same way Dolphin reads it: E, B and N are USA; J, and the
Korean K, Q, T and W, are JAP; D, F, H, I, L, M, P,
R, S, U and V are EUR. SRAM.raw, Dolphin's console settings file,
stays at the top. A move never lands on a path another file in the zip
already uses, packed or moved, so a copy already in place keeps its spot.
A .gci the broker cannot place stays where it was packed: one coded X,
Y or Z, which Dolphin files by the disc's region rather than the code, or
one whose spot is already taken. So does a whole card image (.raw, .mcd,
.gcp), which the folder card never reads: export its saves as .gci files
with Dolphin's Memory Card Manager first. The push still answers 200,
because a refused push makes RomM abort the launch, and names each such file
in an unread list:
{"status": "ok", "written": 2, "unread": ["Card A/01-GZLX-zelda.gci"], "slot": 1}written counts every file laid down, unread included. For PCSX2, and for
a Dolphin card that needed no help, unread is empty.
When you sync a card this way, tell activate about it with
save.memory_card_synced: true. The card then travels as its own file,
separate from the save archive: a restore will skip a stale card folder if
an older archive still has one, and the exit dump leaves the card's subtree
out entirely. A 400 from either memory-card route means the named
emulator has no memory card for the given platform; a 422 means the
emulator name itself isn't recognised.