emulators.scummvm
ScummVM launcher: folder ROMs, ini pinning, and GMM-driven save states.
ScummVM launcher: folder ROMs, ini pinning, and GMM-driven save states.
ScummVM has no control socket and no state hotkeys, so the broker drives it the
way a player does. A game is a folder of data files rather than a single image:
scummvm --add --path=<folder> registers it in scummvm.ini under a generated
target name (monkey, gob1-cd-fr), and that target, not the path, is what
boots the game and what every save file is named after. The exit code of
--add is not a detection signal (it returns success having added nothing), so
the ini is read back afterwards and a folder with no domain is what "not
bootable" means here.
Save and load go through the Global Main Menu with xdotool: the menu key opens
it, the bare-letter Save/Load hotkey picks the button, Down walks the chooser
to the slot and Return activates it. Three things are pinned in scummvm.ini
for that to work at all. The chooser is forced to list mode, because the
default grid has no keyboard path to a numbered slot. The menu key is bound on
the global keymap, because ScummVM's own C+F5 loses its modifier on the way
through this container's Xwayland and the unmodified F5 belongs to the engine
keymap, which an engine may take for itself (see MENU_KEY). The button
letters follow the GUI translation, so they are read back from gui_language.
The macro is silent about its result, so a save is only reported once the
slot's file has actually changed on disk.
ScummVM's saves are its states: there is no separate state format, so the
save archive and the working slot both come out of the same directory and
save_file_kind tells them apart by filename. Slot 0 is ScummVM's autosave and
its own chooser marks it write protected, so the broker never saves there; it
works in STATE_SLOT (default 1) and, like every launcher here, resolves
whatever slot RomM asks for to that one.
Two more ini settings are pinned for the stream rather than for ScummVM's own
sake. fullscreen, pinned off whatever the launch, because going fullscreen
makes SDL grab and confine the pointer against a stack that feeds it
absolutely; the window is grown to the display afterwards instead, which SDL
reads as an ordinary resize (SCUMMVM_FILL_SCREEN). gfx_mode=surfacesdl
because
the OpenGL renderer scales mouse coordinates through getSdlDpiScalingFactor
(backends/platform/sdl/sdl-window.cpp), which divides by
SDL_GL_GetDrawableSize and only means anything for a GL window, so against an
injected absolute pointer the game takes clicks while the cursor stops moving.
Surface SDL has no such path and is ScummVM's own default.
attributelog= logging.getLogger(__name__)attributeROM_ROOT= Path(os.environ.get('ROM_ROOT', '/romm'))Root of the RomM library mount (env ROM_ROOT, default /romm).
A resolved game folder must sit under it; anything resolving outside is discarded.
attributeCONFIG_DIR= Path(os.environ.get('SCUMMVM_CONFIG_DIR', '/config/.config/scummvm'))ScummVM's config directory (env SCUMMVM_CONFIG_DIR, default /config/.config/scummvm).
Safe to move, because every ScummVM the broker runs is told where it is:
--config names INI_PATH on the launch command line and on --add's.
Without that the knob would move only the file the broker patches while
ScummVM kept resolving its own, so the pinned settings and the registered
game targets would land in a file nothing opens.
attributeINI_PATH= CONFIG_DIR / 'scummvm.ini'The config the broker pins before every launch and reads game targets back out of.
attributeDATA_DIR= Path(os.environ.get('SCUMMVM_DATA_DIR', '/config/.local/share/scummvm'))ScummVM's data directory (env SCUMMVM_DATA_DIR, default /config/.local/share/scummvm).
attributeSAVE_DIR= DATA_DIR / 'saves'Where ScummVM writes every save, which is also where the working slot lives.
attributeSCUMMVM_LOG_PATH= Path(os.environ.get('SCUMMVM_LOG_PATH', '/config/scummvm.log'))Log file the broker appends this emulator's output to (env SCUMMVM_LOG_PATH).
attributeCACHE_DIR= Path(os.environ.get('SCUMMVM_CACHE_DIR', str(DATA_DIR / 'extracted')))Where archived games are extracted and booted from (env SCUMMVM_CACHE_DIR, default DATA_DIR/extracted).
An extraction is kept and reused by every later launch of the same archive, and its folder is what gets registered in scummvm.ini, so the target and the saves named after it stay the same from one launch to the next.
attributeCACHE_MAX_GB= float(os.environ.get('SCUMMVM_CACHE_MAX_GB', '10'))Cap on the extraction cache in GB (env SCUMMVM_CACHE_MAX_GB, default 10).
The least recently booted games are evicted to make room for a new one.
attributeARCHIVE_LIST_TIMEOUT= 60.0Seconds 7z or unrar gets to list an archive's members before it is considered hung.
attributeAUTOSAVE_SLOT= 0ScummVM's own autosave slot.
MetaEngine::getAutosaveSlot returns 0 and MetaEngine::listSaves marks it
setWriteProtectedFlag(true), so the save chooser refuses to write there. It is
never the working slot, and a save request that names it lands in STATE_SLOT
like any other.
attributeSTATE_SLOT= int(os.environ.get('SCUMMVM_STATE_SLOT', '1'))The one slot the broker saves into (env SCUMMVM_STATE_SLOT, default 1).
Low on purpose: the chooser is walked with one Down per slot, so a high slot
would spend a keystroke per row getting there. Slot 0 is unavailable (see
AUTOSAVE_SLOT), which makes 1 the first usable row.
attributeSTATE_WAIT= float(os.environ.get('SCUMMVM_STATE_WAIT', '10'))Seconds to wait for the save macro's write to land (env SCUMMVM_STATE_WAIT).
attributeSTATE_STABLE= float(os.environ.get('SCUMMVM_STATE_STABLE', '1.0'))Seconds size and mtime must both hold still before a save counts as written.
From env SCUMMVM_STATE_STABLE, default 1.0. Long enough that a write still
in progress on a loaded host does not read as a finished one.
attributeKEY_DELAY= float(os.environ.get('SCUMMVM_KEY_DELAY', '0.8'))Seconds between the macro's steps (env SCUMMVM_KEY_DELAY).
The GMM animates itself in and its chooser fades in after it, and a keystroke sent into either transition is dropped without a trace.
attributeADD_TIMEOUT= float(os.environ.get('SCUMMVM_ADD_TIMEOUT', '120'))Seconds scummvm --add gets to scan a folder (env SCUMMVM_ADD_TIMEOUT).
Generous because detection reads through every file in the folder, which on a network mount holding a CD rip is not fast.
attributeRESUME_LOAD_WAIT= float(os.environ.get('SCUMMVM_RESUME_LOAD_WAIT', '45'))Seconds a deferred resume waits for RomM to push its state (env SCUMMVM_RESUME_LOAD_WAIT).
attributeRESUME_LOAD_SETTLE= float(os.environ.get('SCUMMVM_RESUME_LOAD_SETTLE', '8'))Seconds a deferred resume gives the game to reach a menu-able state (env SCUMMVM_RESUME_LOAD_SETTLE).
attributeFILL_SCREEN= os.environ.get('SCUMMVM_FILL_SCREEN', 'true').lower() not in ('false', '0', 'no', 'off')Whether the game is grown to fill the stream (env SCUMMVM_FILL_SCREEN, default on).
A ScummVM window is its game's own resolution, 640x480 for most, which is a
postage stamp in the middle of the stream. The obvious fix is ScummVM's own
fullscreen, and it is the wrong one: going fullscreen makes SDL grab the
pointer and confine it to a rect (SdlWindow::createOrUpdateWindow:
shouldGrab = ... || fullscreenFlags, then SDL_SetWindowMouseRect) and ask
for an explicit display mode on the way in. Both are harmless on a real X
server and both fight a streaming stack that feeds an absolute pointer and owns
the display size itself: the pointer stops tracking while clicks still land,
and these games are nothing but mouse. fullscreen is therefore pinned off in
the ini whatever this setting says.
What works is asking the window manager for the size instead. The window is
resized to the display after launch, SDL sees an ordinary resize and scales its
output into it, letterboxing to keep the game's aspect on its own, and never
sets the flag that triggers the grab. The cost is the title bar, which no tool
in this image can remove (xdotool 3.2016 has no windowstate, and there is no
wmctrl); a labwc window rule in the image would.
window_maximized is deliberately not used either: labwc leaves the window at
its own size regardless, so it fills nothing.
attributeFILL_SCREEN_WAIT= float(os.environ.get('SCUMMVM_FILL_SCREEN_WAIT', '10'))Seconds to wait for the game window before giving up on resizing it.
attributeFILL_SCREEN_POLL= float(os.environ.get('SCUMMVM_FILL_SCREEN_POLL', '2'))Seconds between checks that the window still matches the display.
The display is resized by whichever client is connected, so the window has to follow it rather than being sized once at launch.
attributeMENU_KEY= os.environ.get('SCUMMVM_MENU_KEY', 'F11')Key that opens the Global Main Menu (env SCUMMVM_MENU_KEY, default F11).
Bound to the global keymap in scummvm.ini on every launch rather than
trusting a default, for two reasons. ScummVM ships the global menu on C+F5,
and a modifier does not survive injection into this container's Xwayland: the
key arrives without its Ctrl, so the menu never opens. The unmodified F5 that
does work belongs to the engine keymap, which an engine is free to take for
itself (gob answers it with Gobliiins' own panel, which has no Save). Pinning
one unmodified key on the global keymap avoids both.
attributeSCUMMVM_LANGUAGES= {'ar', 'bg', 'ca', 'zh', 'cn', 'tw', 'hr', 'cs', 'da', 'nl', 'en', 'gb', 'us', 'et', 'fi', 'be', 'fr', 'fr-ca', 'de', 'el', 'he', 'hu', 'it', 'ja', 'ko', 'lt', 'lv', 'nb', 'fa', 'pl', 'br', 'pt', 'ru', 'sr', 'sk', 'es', 'eu', 'sv', 'tr', 'uk'}The language codes ScummVM itself accepts (Common::parseLanguage).
Not ISO-639: ScummVM keeps a few of its own spellings (Brazilian Portuguese is
br, Chinese splits into zh/cn/tw), so a code from outside has to be
translated before it means anything on the command line.