Supported emulators
Every launcher the broker registers, what it can do, and the details of how each one is driven.
An activate request picks its emulator with the emulator field. Each emulator
has its own launcher in the broker (in Python terms, a subclass of the
broker's shared Emulator code). A launcher knows how to start the game,
where that emulator keeps its save data, and whether it supports save states:
saving or loading your exact progress on demand while the game is still
running, not just when you quit. If you haven't got the container itself
running yet, see the docker-compose example
and wiring it into RomM.
emulator | System | Save states | Disc swap | Save subtrees shipped in the archive |
|---|---|---|---|---|
retroarch | anything with a libretro core | yes, unless the platform's core opts out | yes | states/, saves/ under RETROARCH_DATA_DIR |
pcsx2 | PlayStation 2 | yes | no | memcards/, sstates/ |
duckstation | PlayStation | no (a resume state is written at exit) | no | memcards/, savestates/ |
dolphin | GameCube, Wii | yes | no | StateSaves/, GC/, Wii/ |
ppsspp | PSP | yes | no | SAVEDATA/, PPSSPP_STATE/ under PSP/ |
cemu | Wii U | no | no | usr/save/ under the mlc01 tree |
azahar | 3DS | no | no | the SD title/ and extdata/ trees and the NAND extdata/ and sysdata/ trees |
eden | Switch | no | no | nand/user/save/ plus the profile store under nand/system/save/ |
shadps4 | PlayStation 4 | no | no | home/1000/savedata/ |
rpcs3 | PlayStation 3 | no (a resume state is written at exit) | no | the save directories under dev_hdd0/ |
xemu | Xbox | no | no | extracted from the HDD image into a staging directory |
xenia | Xbox 360 | no | no | content/ under XENIA_DATA_DIR |
scummvm | LucasArts and Sierra era adventure games | yes, over the Global Main Menu | no | saves/ under SCUMMVM_DATA_DIR |
flycast | Sega Dreamcast | no (a resume state is written at exit) | no | the flat $XDG_DATA_HOME/flycast (VMU saves and the savestate) |
desktop | the webstation desktop itself | no | no | none |
The "Save states" column above is what the broker's status response reports
as supports_states. When it's off, the emulator can't save or load state
while the game is running, and the save-state routes return 400 (an error
code meaning the request is rejected). DuckStation, Flycast and RPCS3 are
worth calling out here: supports_states is off for all three, but each one
writes a state when it exits cleanly and can boot straight into that state on
the next launch, so resume_slot still matters for them even though live
saving and loading doesn't work. DuckStation and RPCS3 also hand that exit
state out through the
state-file GET once the exit
has confirmed it, so RomM can keep it in its state library; Flycast doesn't
yet. For every other "no" in the table,
resume_slot really is ignored and the game's own save data is all that's
kept. Each emulator's exact save paths are listed as the save_subtrees
attribute of its class in the
Python reference.
RetroArch, Flycast, DuckStation, PPSSPP, Dolphin, PCSX2, RPCS3, ScummVM, Cemu, Azahar, Eden, shadPS4, xemu and Xenia also take declared imports: save files made outside the broker, such as another emulator's, placed into the session at activate. What each emulator takes lists the files each one accepts.
RetroArch
emulator: "retroarch" is the general-purpose launcher. RetroArch itself is
a front end that can run many different emulator engines, called cores, so
one RetroArch launcher here covers many systems. The rom.platform field
picks which core to use (ngc and wii both map to dolphin, plus snes, n64,
dc, saturn, psp, nds, 3ds, arcade and genesis). The
RetroArch core manifest lists the full
platform-to-core mapping and what BIOS, firmware, or key file each one needs
before it'll boot. If a core is missing, the broker downloads it from the
libretro buildbot (libretro's server that hosts precompiled cores) into your
own libretro_directory, and puts the core's .info file beside it. A core
without its info file crashes RetroArch mid-session when the broker checks its
status (GET_STATUS): a segfault, a crash from the program touching memory it
isn't allowed to. So the broker's settings layer points RetroArch's
libretro_directory and libretro_info_path at that same folder for the
session, whatever your retroarch.cfg says. The broker installs a missing .info and leaves an
existing one alone unless it has no systemid value, which crashes
GET_STATUS the same way (NeoCD, FreeChaF and EmuSCV only gained one in
August 2026). Then the broker replaces it with its catalog's copy when that
has a systemid, and otherwise adds systemid = "<core>" to the file, logging
a warning either way. A few dozen of libretro's own .info files have no
systemid, so a core installed from one gets the same line. After writing an
.info it also drops a core_info.refresh file there, so RetroArch rebuilds
its core_info.cache on the next start. The broker never edits your
RetroArch config directly; it only layers extra settings on top with
--appendconfig. That layer also turns off
config_save_on_exit. Left on (RetroArch's default), a clean quit writes the
merged config, broker settings included, back over your retroarch.cfg.
Earlier broker versions left it on and could leave keys such as
savestate_directory = "~/.retroarch/states" or
core_options_path = "~/.retroarch/broker-core-options.cfg" in that file. They
don't affect streamed sessions, but if you also run RetroArch on the desktop,
check your config for them.
Save states work, but RetroArch has no direct "save to slot number n"
command. Instead, the broker steps the slot counter all the way down to -1
(the floor) and back up to
RETROARCH_STATE_SLOT (default
0), to make sure it lands on the right slot. This happens once per launch,
and again only if a save shows up in a different slot, which means the
player changed slots with their own hotkeys. The first save of a session
takes about 3 seconds; later saves take under 1 second. Loading uses
LOAD_STATE_SLOT, which can jump straight to a slot number, so it doesn't
need the same trick. Two settings control the timing:
RETROARCH_SLOT_STEP_DELAY (0.1
seconds between presses, by default) and
RETROARCH_SLOT_HOME_STEPS (24
presses to reach the floor, by default).
Setting RETROARCH_STATE_SLOT to -1 makes the auto slot the working slot:
the presses stop on the floor, saves write <content>.state.auto, and loads
send LOAD_STATE_SLOT -1. RetroArch's own auto save and auto load stay off
either way, so only the broker writes or reads that file.
RetroArch's menu ignores stdin save commands while it's open, so a save sent
with the Quick Menu up writes nothing. When a save writes nothing to any
slot, the broker sends MENU_TOGGLE to close the menu, parks the slot again
(the menu has its own State Slot setting), and retries. If the retry writes
nothing either, it toggles the menu back, so a menu the player opened stays
open and one the broker opened is closed again. A retry that lands is
followed by one more save: if that one writes nothing, the first save was
only slow and the toggle opened a menu, so it's toggled closed. A save
that lands in a different slot is a slot change, not a menu, and only gets
the re-park.
RETROARCH_MENU_SETTLE (0.5
seconds, by default) is the pause after the toggle.
The menu can also be opened on purpose. Players on a phone have no F1 key
and often no stick buttons, so the room shows the host an Emulator menu
button that sends MENU_TOGGLE (the
menu route is what it calls). For a
physical gamepad, the layer sets RetroArch's menu combo to L3 + R3 (both
sticks pressed in), but only when your retroarch.cfg has no combo of its
own. RETROARCH_MENU_COMBO
changes which combo that is, and 0 turns it off. The layer also hides the
menu entries that don't belong in a streamed session: Configuration File
(which loads, saves and resets whole configs), the core, game and
content-directory override saves, Close Content, Load
Content, Load Core, Load Disc, Dump Disc, the History, Playlists and Explore
tabs, Restart RetroArch and Quit RetroArch. Add to Favorites, the
Favorites tab, the Online Updater and Download a Core are hidden as well,
since what they write outlives the session: a
favorite is a game the next host could launch in place of their own, and
the updater replaces the cores every later session runs. It hides four
Settings pages too: Configuration (where Save Configuration on Quit
lives), Directory and Saving (both can move states away from where the
broker looks for them), and User Interface (which could un-hide all of
this). Save states, shaders, cheats, core options, controls and Disc
Control stay visible. A remap file saved under Controls, or a shader preset
saved under Shaders, goes into the container's RetroArch config and is
loaded again in later sessions of that core or game, whoever hosts them,
while auto_remaps_enable and auto_shaders_enable are on (RetroArch's
default). To undo one, delete it from the RetroArch config directory:
remaps live under config/remaps/<core>/, shader presets under
config/<core>/.
Only the player on gamepad 1 can steer the menu, unless your
retroarch.cfg turns on all_users_control_menu. A keyboard works too,
since its keys belong to player 1. Opening it is different: during play,
RetroArch reads the combo from every gamepad, so any player can open the
menu, and with RetroArch's default menu_pause_libretro the game pauses
for everyone while it's open. Once it's open only gamepad 1 is read, so
gamepad 1, the keyboard or the host's button has to close it.
The menu drops disc commands the same way it drops saves, and RetroArch's
status still reports the game as playing. So before a
disc swap moves the tray, the broker makes
a check save: if it lands, the menu is closed; if it writes nothing, the
broker closes the menu and checks again, this time with two saves in a row.
The second one is there for a core that was only slow: its first save can
land late, after the toggle has opened a menu, and look like the menu
closing. When the checks don't land, it toggles the menu back and refuses
the swap rather than record a disc change that never happened. The check
saves are undone afterwards, file bytes and timestamps included, so the
exit upload never sees them. If a file can't be put back, the broker logs
an error naming it, and that file goes to RomM with the session's other
states. Loading a state doesn't
need any of this: LOAD_STATE_SLOT works with the menu open.
Not every core can save a state (some can't turn their memory into a save
file, i.e. "serialize" it). A platform whose core can't do this can be marked
"savestate": false in the table. When that's set, supports_states reports
false for that platform, the save-state routes return 400, resume_slot
is ignored, and exit saves no state. RetroArch also empties its working slot
when a game activates, the same as PCSX2 does below.
For multi-disc games, rom.path can point at an .m3u playlist file, or a
folder that contains one; on disc-based platforms the broker prefers the
playlist when one exists. While the game is running, you can switch discs
through the swap-disc route.
A declared import brings in one
.srm on a platform whose core loads one, Wii title folders on wii, or
Azahar's SD card and NAND folders on 3ds. ngc still refuses a save: its
GCI shape has not been verified, so import through dolphin instead. See
RetroArch cores.
The full list matching each platform to its core lives in
retroarch_platforms.json, next to the launcher's code. To pin exactly where
one specific core downloads from, without editing that file, set
RETROARCH_CORE_URL_<CORE>.
PCSX2
The broker talks to PCSX2 over its PINE socket
($XDG_RUNTIME_DIR/pcsx2.sock), PCSX2's own interface for letting outside
programs send it commands, for saving, loading, checking status, and reading
the game's serial number. PCSX2 saves into slot 10, which is PCSX2's own
built-in autosave slot. State files (.p2s) are named after the disc they
came from, and the game serial can only be read off the disc that's
currently running, so a leftover state file from an earlier session can't be
told apart from the current game's just by looking at it. PCSX2 therefore
clears its working slot at the very start of activation, before the save
archive is restored: RomM already keeps its own copies of these states, and
clearing the slot first is what stops an old player's save from being served
as if it belonged to the new session.
The Slot 1 memory card is a folder-based card the broker manages itself
(PCSX2_SLOT1_CARD, default
romm-slot1). If save.memory_card_synced is
set, the whole memory card is handled through the
memory card routes instead of being
bundled inside the regular save archive.
A declared import can bring in
one folder memory card, which becomes the Slot 1 card, as long as the archive
carries no card of its own. States still come in through the
state-file PUT, which refuses
a state named for another game's serial once RomM has sent the game's
title_id.
DuckStation
DuckStation has no mid-session save states: it doesn't give the broker
anything to hook into while a game is running, so the save-state routes
return 400. What it does have is a graceful shutdown: when it closes
cleanly, it writes a resume state, and the broker reports that file as the
exit save state and serves it through the state-file GET until the next
activate. Getting that clean shutdown takes longer than for other
emulators, which is why
DUCKSTATION_STOP_WAIT, the
time the broker waits after asking DuckStation to quit (a SIGTERM signal,
the standard request a program can ignore or handle gracefully) before
forcing it closed, is the longest of any emulator here. Memory cards and
that resume state both live
under the data root that the save archive carries.
Memory card 1 is one shared card, memcards/shared_card_1.mcd, that every
game uses. DuckStation's own default names the card after each game, which
the broker can't know before the disc boots, so the broker pins the shared
card in DuckStation's settings on every launch. An archive saved before that
pin holds a per-game card (<title>_1.mcd) instead. While the shared card
doesn't exist, a launch copies the newest per-game card to it. It's a copy,
never a move, so the per-game card stays on disk and in the archive. If the
copy fails, the launch fails rather than boot the game on a blank card.
A declared import can bring in
a raw memory card, which becomes the shared card, or a resume state, which
needs resume_slot and which the game boots straight into.
Dolphin
Save and load are done by sending Dolphin the same keyboard shortcuts a
player would use, through xdotool, a tool that lets the broker simulate key
presses inside the container: Shift+Fn saves slot n, and Fn loads it.
The broker waits for Dolphin's game window to appear before sending these,
and confirms a save actually happened by watching the StateSaves/ folder
for the new file. The video backend is left to Dolphin's own config, so
a backend picked in Dolphin's settings survives the next launch. For
GameCube, memory card Slot A is set
to a folder-based card device, so the card is a normal directory the save
archive can carry along.
A declared import can bring in
GameCube .gci saves, which go onto the Slot A card, Wii title saves, which
go into the NAND, or one state, which needs resume_slot. The state has to
open with the launched game's id, and the
state-file PUT holds a pushed
state to the same game.
PPSSPP
Like Dolphin, PPSSPP is driven with keyboard hotkeys, and it always saves to
slot 1. Save data and save states both live under PSP/ in the config
folder.
For PSP games, the RetroArch ppsspp core is generally the better choice.
It's the same emulator engine underneath, but running it through RetroArch
gets you RetroArch's controller handling and its proper save-state
interface, instead of the standalone app's hotkeys-into-its-own-menu approach
and its shakier controller support. PSP games aren't demanding enough for
RetroArch's extra overhead to matter. The standalone PPSSPP launcher is still
here and supported, for anyone who prefers PPSSPP's own interface and
settings.
A declared import can bring in
PSP save folders, each with its PARAM.SFO, or one state with its
screenshot, which needs resume_slot. The RetroArch ppsspp core takes no
imported PSP save: RetroArch refuses one and names ppsspp in the refusal's
suggest_emulator.
With PPSSPP_CACHE_ENABLED=true, a PSP image stored as a .7z, .zip, or
.rar archive boots too. PPSSPP can't read one directly, so the first launch extracts it into the
extraction cache and later launches
reuse that copy. rom.path can name the archive itself, or a folder that
holds exactly one archive and no loose image; an archive in a subfolder
beside it, such as extras, doesn't count. A folder with several archives
side by side is refused, since there's no telling which one is the game, and
so is an archive whose image sits more than one folder down. Save data is
keyed by the game's serial, not its file path, so zipping a game you already
have saves for keeps them.
Cemu
Cemu has no save states; mlc01/usr/save/ is where its save data lives.
Cemu identifies controllers by a GUID, a unique ID string that SDL (the input
library it uses) generates for each device. So the launcher works out what
GUID the room's virtual gamepad will report, based on
CEMU_PAD_NAME, and writes a matching
controller profile before each launch
(CEMU_PAD_UUIDS can override that
derived list if you need to set the GUIDs yourself). Cemu doesn't respond to
a SIGTERM shutdown request at all, so
CEMU_STOP_WAIT only covers the time
the broker allows for tearing down Cemu's process group, not for Cemu to
shut itself down cleanly.
A declared import can bring in a Wii U save for the game being launched. It is placed under Cemu's own account, in place of the account the save was made under.
Azahar
Azahar has no save states and no way for the broker to send it commands
while it runs, so quitting it means killing the process outright: only
whatever the game itself already wrote to disk survives. Single-game
launches also start it windowed instead of fullscreen, for a reason covered
in Azahar setup. For 3DS games, the
RetroArch core is the better option, and it's what a 3ds launch uses by
default.
A declared import can bring in the save data of a 3DS title from an unencrypted SD card or NAND folder.
Eden
Eden has no save states; nand/user/save/ is where saves persist. A running
game takes longer than the broker's usual shutdown grace period to close,
which is why Eden gets its own
EDEN_STOP_WAIT setting.
A declared import can bring in Switch save folders from a NAND export. An account save has to come with the profile store it needs.
shadPS4
emulator: "shadps4" launches the shadPS4 PlayStation 4 emulator. shadPS4
has no save states (resume_slot is ignored); the game's own save data,
under home/1000/savedata/, is what persists. The broker picks the shadPS4
binary from $HOME/.local/share/shadPS4QtLauncher/versions: it uses the
Pre-release/ build if one is present (a pre-release always wins over a
numbered release), otherwise the newest release folder by version number
(vX.Y.Z - ... - <date>/, each holding a Shadps4-sdl.AppImage file). You
can override that search with
SHADPS4_BIN (an exact path to the
binary), SHADPS4_VERSIONS_DIR, or
SHADPS4_BIN_NAME. The broker
controls shadPS4 through its own IPC, a channel for sending it commands
while it runs, over stdin (SHADPS4_ENABLE_IPC=true): a RUN then START
command boots the game headlessly, and on exit the broker sends STOP, which
triggers shadPS4's own
quit event (the same thing that happens when its window is closed normally),
for a clean shutdown before the changed save data is written to disk.
shadPS4 doesn't respond to SIGTERM at all, so SIGTERM is only used as a
fallback if STOP hasn't finished within
SHADPS4_STOP_WAIT (default 20
seconds).
rom.path can be a game folder that contains eboot.bin, a bare
eboot.bin file, or a .zar archive. A .pkg file, or a
.7z/.zip/.rar archive containing one, only boots if
SHADPS4_CACHE_ENABLED is turned
on, since it first has to be unpacked into a cache folder. With the cache
turned off, those formats are rejected up front instead of failing partway
through, and they're left off the emulator's list of supported file types
entirely. See shadPS4 setup for sizing the
cache and the disk space an archive needs.
A game folder with no eboot.bin is searched (its top level and one
subfolder down) for a .pkg or archive, which then boots as if RomM had
pointed at it directly. A .pkg whose header says it's the base game is
picked first, and one whose header says it's a patch or DLC is picked last.
Between those (archives, and a .pkg with no readable header), anything with
update, patch or dlc in its file or subfolder name loses, since a PS4
patch can be bigger than the game it patches. After that a .pkg wins over
an archive, and when there are several the largest is picked.
A declared import can bring in PS4 save folders. They are placed under the default user, whoever made them.
RPCS3
Like DuckStation and Flycast, RPCS3 has no live save states: nothing here
lets a game be saved or loaded while it keeps running. What it has instead
is an exit-time save. Sending RPCS3 its own save-state hotkey
(RPCS3_SAVE_STATE_KEY, ctrl+alt+1
by default) always closes the game once the state finishes writing, so the
broker triggers that hotkey and waits for the process to end rather than
keeping it open. A resume works by pointing the next launch straight at the
newest saved state instead of the game itself: RPCS3 tells a save state
apart from a normal boot target by its file contents, not its extension, so
booting into one works the same way booting a ROM does. States are kept one
per title, so resume_slot always finds the right one. The state an exit
confirms is served through the state-file GET until the next activate, the
same way DuckStation's is. The game's own save
data, under dev_hdd0/home/, persists separately from all of this.
A declared import can bring in
PS3 save folders, each with its PARAM.SFO, or one savestate, which needs
resume_slot and which the game boots straight into. The savestate has to
belong to the game being launched, and a rom the broker can read no title id
from, a bare .iso, takes none.
rom.path can be a PKG file, which the launcher installs first
(RPCS3_INSTALL_TIMEOUT limits how
long decryption is allowed to take), an already-extracted game folder, or a
disc image.
A .7z/.zip/.rar archive of a game folder only boots if
RPCS3_CACHE_ENABLED is turned on,
since it has to be extracted first. With the cache off, that format is
rejected up front rather than accepted and failing partway through, and it's
left off the emulator's list of supported file types. A PKG file isn't
affected either way, since its installer is separate from the cache. See
RPCS3 setup for sizing the cache and the disk
space an archive needs.
xemu
xemu has no save states. It keeps its saves inside a single virtual hard
drive image, in the qcow2 format (a type of virtual disk file), rather than
as separate files. After a clean shutdown, the launcher pulls the saves out
of that image (working through the plain raw image the container prepares
the first time it starts) and puts them back before the next launch; the
save directories are matched using whatever letter-casing the virtual disk
itself uses. The renderer is set in xemu.toml before each launch
(XEMU_RENDERER; set it to KEEP to
leave your own choice alone), and
XEMU_SOFTWARE_GL forces rendering on
the CPU instead of the GPU, for hosts where neither GPU rendering path
works.
A declared import can bring in
the UDATA and TDATA folders of the game being launched. They reach the
hard drive image at launch.
Xenia
emulator: "xenia" launches the Xenia Edge Xbox 360 emulator. Xenia has no
save states (resume_slot is ignored) and no way for the broker to send it
commands at all while it's running, no IPC, socket, or stdin channel of any
kind. So the game's own save data is what persists, and exiting means
sending a plain SIGTERM, which Xenia treats as an ordinary kill. That's safe
here because the game writes its saves straight through to the host's files
as it plays, so nothing sits in memory waiting to be lost.
The save archive is the entire content/ folder tree under
XENIA_DATA_DIR. Xbox 360 save paths
are tied to the player's profile ID
(called an XUID), and the profile data itself lives in that same tree, so
both travel together and still line up correctly when restored into a fresh
container.
rom.path can be an XISO disc image (.iso), a bare .xex file, an
extracted dump (a folder containing default.xex), or an XBLA / Games on
Demand title folder. For that last case, the broker walks the same folder
layout the console itself uses ([Content/<XUID>/]<TITLE_ID>/000D0000 or
00007000/<package>) and boots whichever file is a genuine game
package in Xbox 360's own STFS format (Microsoft's package format for Xbox
Live content), confirmed by checking the start of the file rather than its
name. That way DLC, title updates, and the extra
.data file that sits beside a Games on Demand package are correctly
skipped.
A declared import can bring in Xbox 360 saves for the game being launched. They are placed under the profile that is signed in, which the broker reads from Xenia's config, so a profile has to exist first.
Flycast
Flycast has no control socket and doesn't respond to SIGTERM either, so the
only way to shut it down cleanly is a graceful window-close request: the
broker switches focus to Flycast's own window and sends Alt+F4, the same
shortcut a player would use. That reaches Flycast's own quit handling, and
it's the only moment a save state gets written
(FLYCAST_STOP_WAIT gives it that
window of time before the broker escalates to a SIGTERM, which skips the
save entirely). Like DuckStation, there's no saving or loading
mid-session: a launch resumes from whatever state was written the last time
it exited cleanly, and the current session's state is written exactly once,
on exit.
The save state is always a single, predictably-named file, <rom>.state,
sitting directly in $XDG_DATA_HOME/flycast (no subfolders). VMU (Dreamcast
memory card) saves live loose in that same folder and travel with it as one
save subtree, since
nothing separates them from the save state, or from a leftover first-run
setup file. A RetroArch core also covers the Dreamcast (dc uses it by
default), for anyone who'd rather use RetroArch's shared save-state menu and
hotkeys instead.
A declared import can bring in
VMU images, the console's system flash (dc_nvmem.bin), or a save state.
An imported state needs resume_slot. It is renamed after the game being
launched, and the game boots straight into it, as it does with the state
Flycast writes on exit. A RetroArch .srm isn't a VMU image, so Flycast
refuses one.
ScummVM
For ScummVM, rom.path points at a folder of game files, not a disc
image. ScummVM works out what game a folder holds by scanning its contents,
so launching runs scummvm --add --path=<folder> and then reads back the
target name it generated (something like monkey or gob1-cd-fr) from
scummvm.ini. A target is ScummVM's own internal name for a registered
game. It's the target, not the folder path, that actually boots the game and
that every save file is named after. If ScummVM adds nothing for a folder,
that means it found nothing bootable there, but --add still reports
success even when it added nothing, so its exit code alone can't tell you
whether it worked. If rom.path points at a single file instead, a
.scummvm marker file, for example, ScummVM registers the folder that file
sits in.
A game can also be stored as a single .7z, .zip, or .rar archive,
either named directly by rom.path or sitting alone in the game's folder.
ScummVM can't detect a game inside an archive, so the first launch extracts
it into the extraction cache and
registers the extracted folder instead. A single wrapper folder inside the
archive (Monkey Island/, say) is walked into, and macOS __MACOSX clutter
is ignored. Later launches reuse the same extraction and the same target, so
saves keep their names. Replacing the archive with a different file extracts
it again, under the same target. A second archive of the same game is
refused, the same way a second folder copy is, so two copies never share
one set of saves. An archive with no game files in it is refused before
anything is extracted.
A folder holding a game in multiple languages registers one target per
language ScummVM detects (gob1-cd-fr, gob1-cd-de), and it's the target,
not a command-line flag, that decides which language's files actually get
loaded: passing --language on its own does not switch a launch to a
different variant. The language field on the activate payload is what
picks between the targets, and the broker also passes it on the command
line, so the running game agrees with the target it was launched from.
Language codes get translated into ScummVM's own spellings first (pt-br
becomes br, jp becomes ja, zh-hans becomes cn); if the exact one
isn't available, a close match is used instead (a us variant will answer
an en request), and a code ScummVM wouldn't recognize is simply dropped
rather than failing the whole launch. If a ROM entry doesn't specify a
language at all, the broker falls back to gui_language, the player's own
interface language setting, since RomM usually doesn't know what language a
game is in, and a multilingual folder is exactly the case where that matters
most. If neither is available, ScummVM just picks alphabetically, but it does
so consistently, which keeps the <target>.sNN save file names stable
across relaunches.
ScummVM has no control socket and no dedicated save/load hotkeys, so the
broker drives its Global Main Menu directly using xdotool (the same
simulated-keypress tool used for Dolphin and PPSSPP): it presses the menu
key to open the menu, presses the Save or Load button's own letter hotkey,
presses Down to walk the list down to the right slot, and presses Return
to activate it. When saving, the first Return starts editing the slot's
text description, and a second Return confirms it.
Three settings are pinned in scummvm.ini to make all of this work
reliably. First, gui_saveload_chooser=list, because the default menu shows
saves as a grid, and there's no way to arrow-key your way to a specific
numbered slot in a grid. Second, keymap_global_MENU is bound to
SCUMMVM_MENU_KEY (default F11).
ScummVM's own default menu shortcut is
C+F5, but a Ctrl modifier doesn't survive being simulated inside the
container's Xwayland (the compatibility layer that lets this X11-based
software run under Wayland) display server, so the key press arrives
without its Ctrl and the menu never opens. The plain, unmodified F5 that does work is
normally reserved for each game engine's own shortcuts, and some engines
grab it for themselves (the gob engine, used by the Gobliiins games, opens
its own panel on F5 instead, which has no Save option). Third, the Save
and Load button labels follow ScummVM's own translated menu text (so
~S~ave becomes ~S~auvegarder in French, for example), matching whatever
gui_language is set to. The activate payload's gui_language field pins
this when the request supplies one, and otherwise leaves it as the user
already set it themselves. Only that one keymap binding is changed; any
other keyboard shortcut you've set yourself is left alone.
The sequence of key presses doesn't report back whether it actually worked, so the broker only counts a save as successful once it sees the slot's file actually change on disk. Some engines don't support saving while running at all; for those, ScummVM pops up a message dialog instead of a save menu, and the broker dismisses that dialog rather than leaving the game stuck behind it. Gobliiins is the clearest example: its gob engine has no save handler whatsoever, because in that game, passwords are how you track progress instead of save files. Games like this still have their own saves included in the save archive, and resuming them at boot time still works fine.
In ScummVM, saves and save states are the same thing: there's no separate
save-state format, so the broker's working slot and the game's own manual
saves live in the same folder and are only told apart by their file names.
Slot 0 is ScummVM's own autosave slot, and ScummVM's own save menu marks it
write-protected (MetaEngine::listSaves sets the flag), so the broker never
writes there. Instead it uses
SCUMMVM_STATE_SLOT (default 1).
When a state
is loaded back in, it's renamed to match the currently booted target,
because ScummVM finds a save purely by name, and a multilingual folder can
register a separate target per language.
A declared import can bring in
<game>.NNN saves, named for a game in the rom folder, or one state, which
needs resume_slot. The state is renamed onto the booted target in the
working slot, like a state loaded back in.
Two more settings are pinned for the sake of the video stream, not because
ScummVM itself needs them. fullscreen is pinned off, no matter how the
game is launched. A ScummVM window always renders at the game's own native
resolution, so a 640x480 game would otherwise show up as a tiny postage
stamp in the middle of the stream. Using ScummVM's own fullscreen mode to
fix that would be the wrong move: internally, going fullscreen makes SDL
(the graphics and input library ScummVM is built on) grab the mouse pointer
and confine it to a fixed rectangle (shouldGrab = ... || fullscreenFlags,
then SDL_SetWindowMouseRect), and also request a specific display
resolution. Both of those fight against a video-streaming stack that feeds
in an absolute mouse position and controls the display size itself: the
result is a pointer that stops tracking the stream even though clicks still
register, which is a serious problem since these games are controlled almost
entirely by mouse.
So instead, the desktop's window manager handles it directly. After launch,
the game's window is moved to the top-left corner and resized to fill the
whole display
(SCUMMVM_FILL_SCREEN, on by
default). SDL treats that as a
normal window resize: it scales its own picture to fit and adds black bars
(letterboxing) to keep the game's 4:3 shape correct, all without ever
triggering the pointer-grab behavior that fullscreen mode causes. This is
best-effort and purely cosmetic: if the display size can't be read, or the
window never appears, the game just keeps running at its own native size
instead of the launch failing outright. One thing it doesn't remove is the
window's title bar, since nothing in the container image is able to (the
xdotool version shipped, 3.2016, has no window-state command, and there's no
wmctrl either; a labwc window rule could do it, but isn't set up).
ScummVM's own window_maximized setting isn't used either, since labwc, the
window manager here, leaves the window at its own size regardless, so that
setting wouldn't fill anything. The broker also pins gfx_mode=surfacesdl
instead of ScummVM's OpenGL renderer, because the OpenGL renderer scales
mouse coordinates through a call (SDL_GL_GetDrawableSize) that only means
anything inside a real OpenGL window. Against the stream's injected absolute
pointer, that mismatch means clicks still land in the game, but the
on-screen cursor stops moving. Finally, SDL is forced onto X11 at launch, so
the broker's simulated key presses have an Xwayland window to talk to.
None of the above fixes one deeper pointer problem that isn't something this
launcher alone can solve. While a game is running, it holds what's called a
pointer lock: ScummVM hides the normal mouse cursor and moves (warps) it
itself. Xwayland emulates that lock, but the desktop's window manager,
labwc, drops the absolute mouse-position updates the browser sends while a
lock is active. It still forwards relative movement, though, so the result
is an in-game cursor that freezes in place while clicks still land where you
click. To work around this, the container image patches selkies' own input
handler (input_handler.py) so that every absolute mouse move is sent as a
relative movement followed by the absolute position, and so that it reads
the actual pointer position back afterward (using the python-xlib library)
to correct for any warp the game just issued. Without that patch, the mouse
pointer would be unusable even in windowed mode, no matter what this
launcher pins.
Desktop
emulator: "desktop" streams the full webstation desktop, so you can
configure emulators through their own graphical interface. It doesn't use a
rom or save field, but everything else, the room interface and how a
session exits, works the same as any other launcher. This is how you do the
one-time setup.
Troubleshooting
Symptom-first fixes for the failures that come up wiring a container behind a reverse proxy: cross-origin breakage, broker secret mismatches, dead-on-load rooms, and stale sessions.
Emulator setup
The one-time desktop session each emulator needs before single game launches work: BIOS, firmware, keys and controllers.