webstation-broker
DeveloperPython referenceemulatorsemulators.scummvm

emulators.scummvm.Scummvm

ScummVM sessions, driven through the launcher's own config and menus.

ScummVM sessions, driven through the launcher's own config and menus.

A launch registers the game folder with scummvm --add, pins the settings the macros and the stream depend on, and boots scummvm <target>. A resume whose state is already on disk goes in on the command line with --save-slot, which every engine reads in its startup path, so the game never shows its title screen first; a state RomM pushes after activate returns is delivered by a deferred thread over the GMM instead.

Saving and loading drive the Global Main Menu with xdotool, and the macro is silent, so a save is confirmed by watching the slot's file rather than by the keystrokes going out. Engines without runtime save support (gob's password-based games are the canonical case) put up a message dialog instead: the write never lands, the dialogs are dismissed, and the failure is reported rather than left as a game paused behind a menu.

Saves and states share saves/, so save_file_kind splits them by name rather than by subtree, and the archive carries both.

A multilingual folder registers one target per language it detects, so the language the session was activated for decides which of them boots; without one the pick is alphabetical, which hands a French player a German game.

Declared imports take a save file per member, and one of them as the state. The stem has to name a game of the folder, and the target the session boots is worked out the way launch does, registering the folder when it is new.

Attributes

attributename
= 'scummvm'

Registry key, scummvm.

attributedisplay_name
= 'ScummVM'

Human-readable name shown in the UI.

attributesave_root
= DATA_DIR

ScummVM's data directory, which the save subtree hangs off.

attributesave_subtrees
= ('saves',)

saves, holding the game's saves and the working slot alike.

attributestate_subtrees
= ()

Empty, because states are not in a subtree of their own.

attributeclears_stale_saves
= True

On; activate empties the save directory.

attributesupports_states
= True

True, over the Global Main Menu.

attributestate_slot
= STATE_SLOT

The one slot the broker works in, echoed back as the effective slot.

attributestate_dir
= SAVE_DIR

Where ScummVM writes every save.

attributelog_path
= SCUMMVM_LOG_PATH

The ScummVM output the broker exposes.

attributeterm_timeout
= float(os.environ.get('SCUMMVM_STOP_WAIT', '10'))

Seconds SIGTERM gets, which ScummVM spends flushing its config.

attribute_rom_dirOptional[Path]
= None

What resolve_rom_file picked: the folder launch registers, or an archive it extracts first.

attribute_targetOptional[str]
= None

The target the running game booted under, and every save is named after.

Kept after the process stops: the state routes are read once the game is already gone, and they have no other way to name its files.

attribute_launch_seq
= 0

Bumped per launch, so a deferred resume can tell it has been superseded.

attributerom_extensionstuple[str, ...]

The .scummvm marker file some libraries use; a game is a folder.

Functions

func__init__(self) -> None

Start with no game folder, no target, and no launch behind it.

paramself

Returns

None
funcresolve_rom_file(self, path) -> Optional[Path]

Resolve a RomM path to the game folder to register, or the archive to extract.

Kept cheap: the detection pass that can actually answer "is there a game in here" takes seconds, and extracting an archive can take longer, so both run in launch. A folder that holds files is accepted here and only a folder ScummVM detects nothing in fails, later, as a launch failure.

paramself
parampathPath

The ROM as RomM delivered it: the game folder, a file inside it (a .scummvm marker, or the single file a library pointed at), or an archived game.

Returns

typing.Optional

The game folder, or the archive when the game is archived (a loose

func_resolve_archive(self, archive) -> Optional[Path]

Accept an archived game for launch to extract.

paramself
paramarchivePath

The archive, a loose ROM file or the lone file in its folder.

Returns

typing.Optional

The resolved archive, or None when the cache is off, it is not a

func_xdotool(self, *args, *, quiet=False) -> Optional[str]

Run one xdotool command against the session display.

paramself
paramargsstr
= ()
paramquietbool
= False

Suppress the failure warning. For a caller polling for something that is not there yet, where search exiting non-zero is the expected answer rather than a fault.

Returns

typing.Optional

Its stdout, or None if it could not be run, timed out, or exited non-zero.

func_window(self, quiet=False) -> Optional[str]

The X window this launch's ScummVM renders into.

Matched on this process's own pid, so a window left behind by an earlier session cannot swallow the macro. The last id wins: xdotool lists in creation order and ScummVM's game window is created after the transient ones.

paramself
paramquietbool
= False

Suppress the "no window" warning. For a caller polling for a window that is not mapped yet, where absence is the normal answer until it is not.

Returns

typing.Optional

The window id as xdotool prints it, or None when this process has

func_activate(self, win_id) -> bool

Give win_id the input focus before keys are sent at it.

The menu key and the button hotkey go through XTEST, which delivers to whatever holds focus, so a key sent at an unfocused game lands on the desktop instead.

paramself
paramwin_idstr

The window to focus.

Returns

bool

True once the window is active.

func_open_gmm(self) -> bool

Open the Global Main Menu with the pinned menu key.

One unmodified key, bound to the global keymap in the ini before the launch: see MENU_KEY for why neither ScummVM's own C+F5 nor the engine-level F5 can be relied on here.

paramself

Returns

bool

True when the keystroke went out.

func_type(self, text) -> bool

Type text into the focused window.

type rather than key: the GMM's button hotkeys follow the GUI translation and can be Cyrillic, Greek or Hebrew, and only type synthesizes the keysym whatever the X keyboard layout is.

paramself
paramtextstr

The hotkey letter to send.

Returns

bool

True when the keystroke went out.

func_keys(self, win_id, keys) -> bool

Send several keys to win_id in one xdotool call.

One call rather than one per key: each costs a process spawn, and the chooser walk sends one key per slot.

paramself
paramwin_idstr

The window to send to.

paramkeyslist[str]

Key names in xdotool's syntax.

Returns

bool

True when the keystrokes went out.

func_wait_for_write(self, before, deadline) -> bool

Poll the working slot until this save's write settles, or deadline passes.

The first change is not the finished save. The caller stops ScummVM as soon as this returns and the broker zips the save directory right after, so returning on the first differing stat sends SIGTERM into a write still in progress and ships whatever landed to RomM as the player's progress. A write therefore only counts once the slot differs from before, holds at least one non-empty file, and has held every size and mtime still for STATE_STABLE.

paramself
parambeforedict[str, tuple[float, int]]

The slot's (mtime, size) per filename, from before the macro.

paramdeadlinefloat

A time.monotonic() value to give up at.

Returns

bool

True once a slot file appeared or changed and its write settled,

funclaunch(self, rom_path, resume_slot) -> None

Register the game, pin the ini, and boot ScummVM on its target.

The folder is registered with --add and the target read back out of the ini; a folder ScummVM detects nothing in is a launch failure, since there is nothing to boot. With resume_slot set and the working slot already holding this target's save, the state loads at boot through --save-slot; otherwise a deferred thread waits for RomM's push and loads it over the menu.

An archived game is extracted into the cache first (or its earlier extraction reused), and the game folder inside it is what gets registered.

Raises

  • RuntimeError: When no ROM folder was resolved, an archive could not be extracted, or ScummVM detects no game in it.
  • RegisteredElsewhere: When another copy of the game holds its target.
paramself
paramrom_pathOptional[Path]

The game folder or archive, as returned by resolve_rom_file.

paramresume_slotOptional[int]

The slot to resume from, or None to boot clean.

Returns

None
func_fill_screen(self, seq) -> None

Keep the game window the size of the display, for as long as it runs.

Resizing once at launch is not enough: the display is sized by the streaming client, so a game that starts before a browser has connected is grown to whatever the last session left behind and then sits in the top left corner of a screen that changed under it. The same happens mid-session when a viewer resizes their browser. So this follows the display instead of photographing it, and exits when the launch is superseded or the game is gone.

Purely cosmetic, so every failure is logged and swallowed: a game running at its own size is worth more than a launch reported as broken.

paramself
paramseqint

The launch sequence number this belongs to.

Returns

None
func_display_size(self) -> Optional[tuple[str, str]]

The display's current size as a (width, height) pair of digits.

paramself

Returns

typing.Optional

The pair, or None when xdotool could not be asked or answered

func_deferred_load_state(self, seq) -> None

Wait for a pushed state to arrive, then load it through the menu.

Gives the file RESUME_LOAD_WAIT to turn up and then the game RESUME_LOAD_SETTLE to be far enough into its startup to answer the menu. Abandons itself as soon as seq no longer matches the current launch, so a superseded launch never gets a stray load.

paramself
paramseqint

The launch sequence number this load belongs to.

Returns

None
funcsave_state(self, slot) -> bool

Save the running game into the broker's slot through the GMM.

slot is what RomM asked for and is ignored: the save lands in STATE_SLOT and the caller reads the effective slot back off state_slot. The chooser opens with its list focused and nothing selected, so the first Down lands on slot 0 and slot N needs N+1 of them; the list index is the slot number because empty slots still take a row. In save mode the first Return starts editing the slot's description and the second commits it, which is also the save.

paramself
paramslotint

The slot RomM requested; not used.

Returns

bool

True once the slot's file has changed on disk within STATE_WAIT,

funcload_state(self, slot) -> bool

Load the broker's slot into the running game through the GMM.

In load mode the list is not editable, so a single Return activates the selected slot and the GMM closes itself.

paramself
paramslotint

The slot RomM requested; STATE_SLOT is what gets loaded.

Returns

bool

True when the slot holds a save and the keystrokes went out, False

funcstate_path(self) -> Optional[Path]

The working slot's save file for the booted target, or None when empty.

paramself

Returns

typing.Optional[pathlib.Path]
funcclear_working_slot(self, excluded=()) -> None

Empty the save directory before a new session boots.

The target only exists once a game has been registered and booted, so at activate time a leftover cannot be told apart from the save of the game about to start. Anything still here belongs to a session that has already exited and whose saves RomM holds; the incoming archive restores whatever should be here.

The whole directory goes, not just the broker's slot. ScummVM names a save <target>.<NNN> for the slot the player chose in the game's own menu, and nothing in that name says whose session wrote it, so clearing only the broker's slot leaves every save the last player made from inside the game readable by this one and swept into their dump.

paramself
paramexcludedtuple[str, ...]
= ()

Subtrees carried by the whole-card routes. ScummVM has no memory card, so this is always empty.

Returns

None
funcstate_target(self, filename) -> Optional[Path]

Where a pushed state called filename belongs.

The name is rewritten onto the booted target and the broker's slot: ScummVM finds a save by name alone, so a state captured under another target (a multilingual folder registers one per language) has to be renamed or the engine will not see it. The suffix form the state arrived in is kept, since which of the two an engine writes is the engine's business.

paramself
paramfilenamestr

The name RomM stored the state under.

Returns

typing.Optional

The path to write to, or None when the name is not a ScummVM save

funcsave_file_kind(self, rel) -> str

Classify an archive member for the manifest.

ScummVM has no state format of its own, so the working slot's file is a save like every other and only its name says otherwise. Naming the target as well as the slot is what keeps another game's save in the same slot from being filed as this session's state.

paramself
paramrelstr

The member path, relative to save_root and posix-separated.

Returns

str

state for the booted target's working-slot save, save for

funcimport_spec(self) -> imports.ImportSpec

Declare what ScummVM takes: save files, and one of them as the state.

Saves and states share saves/, so the two kinds have the same shapes. The state rides the archive and resumes through save.resume_slot, which boots the game with --save-slot.

paramself

Returns

webstation_broker.imports.ImportSpec

The spec.

funcplace_import(self, member, spec, ctx) -> Union[imports.Placement, imports.ImportRefusal]

Place one declared member: a save, or the state.

paramself
parammemberimports.ImportMember

The member, already past the kind gate.

paramspecimports.ImportSpec

This emulator's spec.

paramctximports.ImportCtx

The launch context.

Returns

typing.Union

The placement, or a refusal.

funcvalidate_import_plan(self, plan, ctx) -> list[imports.ImportRefusal]

Refuse a state that shares the working slot with a save the archive carries.

check_plan counts the archive's own states by save_file_kind, which answers save for everything until a game has booted. So a v1 save in the working slot under the target's name goes uncounted there, and would be restored on top of the state, or beside it under the other spelling.

paramself
paramplanlist[imports.Placement]

The placements that passed every per-member check.

paramctximports.ImportCtx

The launch context.

Returns

list

One destination_conflict for the state, or nothing when the state

funcidentity_source(self) -> Optional[imports.IdentitySource]

Read the target the folder is registered under, for the session's identity.

paramself

Returns

typing.Optional

The source.

func_read_target(self, rom) -> Optional[str]

Look up the target registered for rom, without registering it.

Only a lookup: a launch that ran no preflight reads this in a worker thread, and registering can take up to two minutes. The identity is informational, so an unregistered folder reads as no identity.

paramself
paramromPath

The game folder, or an archived game.

Returns

typing.Optional

The target, or None when the ini has no domain for the folder, or

funcsave_and_exit(self, slot) -> dict[str, Any]

Save through the menu if asked, then stop ScummVM.

paramself
paramslotOptional[int]

The slot RomM asked to save into, resolved to STATE_SLOT, or None to exit without writing a state.

Returns

dict

A dict with state_saved (bool), state_slot (the effective slot,

On this page