emulators.base.Emulator
Contract every launcher implements, plus the process plumbing they share.
Contract every launcher implements, plus the process plumbing they share.
A subclass describes itself through the class attributes below and must
override launch and resolve_rom_file; both raise NotImplementedError
here. Everything else is an optional hook with a safe default:
save_state,load_state,state_path,state_targetandwait_for_stateare the save-state hooks. The broker only calls the first two whensupports_statesis on; the defaults report an empty slot. The frame RomM shows beside a state is not the emulator's to produce: the broker captures the streamed desktop on the way into a save and keeps it onstate_screenshot.swap_discis only called whensupports_disc_swapis on.toggle_menuis only called whensupports_menuis on.memory_card_pathpairs withmemory_card_subtreefor emulators whose whole memory card travels on its own routes.clear_working_slotandprepare_restoreboth run at every activate. Any subclass with real save data has to clear the previous session's saves in one of them and declareclears_stale_saves; seeclear_working_slotfor the contract.save_and_exitis the exit path; the default writes no state.
The lifecycle as the broker drives it:
- Activate:
clear_working_slotdrops the previous session's leftover saves and states,prepare_restoreruns (always, archive or not), and the incoming save archive is extracted intosave_root, scoped tosave_subtrees. launchspawns the process through_spawn, which captures output tolog_path, starts it in its own session and records its pid so a later broker process can reap it (seereap_orphan).- Mid-session:
save_stateandload_statework the one slot instate_slot. The state-file routes servestate_pathand write throughstate_target, withwait_for_statebridging a resume state that arrives after launch. save_and_exitsaves when asked and stops.stopsends SIGTERM to the process group, escalates to SIGKILL afterterm_timeout, and_forgetdrops both the handle and the pid record once the process is confirmed gone.
Attributes
attributenamestr= 'base'Registry key and log label for the emulator.
attributedisplay_namestr= 'Webstation'Human-readable name the UI shows.
attributeplatformOptional[str]= NoneThe RomM platform slug the session was activated for, or None.
attributecoreOptional[str]= NoneThe core RomM asked for (rom.core), for a launcher that fronts many; None for its default.
Set by the activate route and the import-spec route, like platform.
attributeexperimental_coresbool= FalseWhether RomM opted this launch in to known-broken cores (rom.experimental_cores).
attributelanguageOptional[str]= NoneThe language the rom was activated for, or None.
attributegui_languageOptional[str]= NoneThe player's own interface language, or None.
attributerequires_rombool= TrueWhether a launch needs a ROM; the desktop session does not.
attributerom_cacheablebool= FalseWhether the ROM cache may boot this emulator from a local copy of its ROM.
attributesave_rootPath= Path('/config')Root of the emulator's writable data.
attributesave_subtreestuple[str, ...]= ()Subtrees under save_root that hold save data; save
restore and dump are scoped to these.
attributestate_subtreestuple[str, ...]= ()The subset of save_subtrees holding savestates, for
labelling an archive's members.
attributerom_extensionstuple[str, ...]= ()File extensions the emulator will boot, in preference order.
attributesupports_statesbool= FalseWhether the emulator can save and load state mid-session.
attributesupports_disc_swapbool= FalseWhether the emulator can change the mounted disc without restarting.
attributesupports_menubool= FalseWhether the room can open and close the emulator's own menu.
attributestate_slotint= 0The one slot the broker saves into.
attributestate_dirPath= Path('/config')Where that slot's file lives.
attributelog_pathPath= Path('/config/broker-app.log')Where the emulator's stdout and stderr are appended.
attributeterm_timeoutfloat= _DEFAULT_TERM_TIMEOUTSeconds SIGTERM gets before escalating to SIGKILL.
attributekill_timeoutfloat= _DEFAULT_KILL_TIMEOUTSeconds SIGKILL gets before the process is written off.
attributeclears_stale_savesbool= FalseThe subclass's declaration that it wipes the previous session's save data before a restore.
attributememory_card_subtreeOptional[str]= NoneThe save subtree holding the whole memory card, or None for emulators without one.
attributememory_card_markerOptional[str]= NoneA file the emulator needs inside the card directory before it treats it as a card, or None.
attribute_procOptional[subprocess.Popen[bytes]]= Noneattributeboot_failedbool= FalseSet by an emulator that can tell its process is alive but never reached a running game.
attributeextraction_phaseOptional[str]= NoneSet while a slow pre-launch extraction is running, else None.
attributestate_screenshotOptional[bytes]= NoneThe frame captured just before the working slot's state was written, as PNG bytes.
Set by the broker on every confirmed save and replaced, or cleared, by the next one, so it never outlives the state it was taken with.
attributeexit_codeOptional[int]The spawned process's exit code once it has exited, else None.
Negative for a process killed by a signal, as subprocess reports it:
-11 is a segfault.
attributeimport_identityOptional[imports.SessionIdentity]= NoneThe game id this session runs as, set on every activate.
Preflight sets it when the archive has imports; resolve_activate_identity
sets it when it has none. The push route's identity checks read it.
attributelink_rootstuple[Path, ...]Directories a save subtree may be a symlink to although they lie outside save_root.
The chain check and the write check refuse a path through a link that resolves outside the save root. An emulator whose data is split across two trees declares the second tree here, and a subtree link that resolves exactly to it is allowed.
attributerestore_subtreestuple[str, ...]The subtrees an archive may restore into, read before the working slot is cleared.
Functions
func__init__(self) -> NoneStart with no process handle, no boot failure, no extraction running, and no frame.
paramselfReturns
Nonefunc_spawn(self, cmd, env, stdin_pipe=False, stdin_tty=False) -> NoneStart the app in its own process group with output captured.
A launch banner and then the child's stdout and stderr are appended to
log_path; if the log cannot be opened the output is discarded. The pid
is recorded through _record_pid once the process is up.
Raises
ValueError: When bothstdin_pipeandstdin_ttyare set.OSError: When the process started but its pid could not be recorded. The process is killed first: an emulator no record names outlives the next broker restart with nothing able to find it, so the launch fails rather than leaving one behind.
paramselfparamcmdlist[str]The argv to run.
paramenvdict[str, str]The environment to run it in, normally base_launch_env().
paramstdin_pipebool= FalseKeep the child's stdin as a pipe so emulators with a stdin control protocol (shadPS4 IPC) can be driven headlessly.
paramstdin_ttybool= FalseGive the child a pseudo-terminal for stdin, for apps that print an error to stdout and exit when stdin is a terminal but raise a blocking dialog when it is not (Xenia).
Returns
Nonefuncalive(self) -> boolWhether a spawned process exists and has not exited.
paramselfReturns
boolfunc_forget(self) -> NoneDrop the handle on the emulator and the record of it on disk.
Every path that ends with the process gone has to go through here.
A graceful exit that only clears _proc leaves a record pointing at a
pid nobody owns, and the next broker start would hunt it.
paramselfReturns
Nonefuncstop(self) -> NoneTerminate the running emulator, if any, and forget it.
The process group gets SIGTERM, escalating to SIGKILL once
term_timeout passes, and SIGKILL gets kill_timeout to land. A
process that is already gone is a no-op.
The handle and the pid record are only dropped once the process is
confirmed gone. An emulator that outlived SIGKILL, or that refused the
signal outright, keeps both: dropping the record while it still runs
would leave it with nothing able to find it, which is the orphan
PID_FILE exists to prevent, and every subclass launch opens with a
stop that would then be its next chance to try again.
This reaches the process and its group, which is every emulator here.
An app that starts other apps and detaches them needs more than a
signal to one group, and overrides this (see Desktop.stop).
paramselfReturns
Nonefuncprepare_restore(self) -> NoneHook run at activate, after clear_working_slot and before any extract.
Called on every activate, whether or not there is an archive to restore: a session that starts with no archive is exactly the one where a stale save left in place would be picked up as the player's own.
Default: nothing. Override to clear anything that would block or outrank the restore: a process holding a save file open, or an existing file the newer-file guard would wrongly keep over the archived one.
paramselfReturns
Nonefunclaunch(self, rom_path, resume_slot) -> NoneStart the emulator on rom_path, optionally resuming a state.
Raises
NotImplementedError: Always; every subclass overrides this.
paramselfparamrom_pathOptional[Path]The file to boot, as returned by resolve_rom_file, or
None for an emulator that does not require a ROM.
paramresume_slotOptional[int]The slot to load once the game is up, or None for a fresh start.
Returns
Nonefuncsave_state(self, slot) -> boolSave the running game to slot.
Only called when supports_states.
Raises
NotImplementedError: When the emulator does not support states.
paramselfparamslotintThe slot RomM asked for; implementations may resolve it to
state_slot.
Returns
boolTrue once the state is confirmed written.
funcload_state(self, slot) -> boolLoad slot into the running game.
Only called when supports_states.
Raises
NotImplementedError: When the emulator does not support states.
paramselfparamslotintThe slot RomM asked for; implementations may resolve it to
state_slot.
Returns
boolTrue once the load was delivered to the emulator.
funcstate_path(self) -> Optional[Path]The file the working slot holds right now, or None if it is empty.
This is what the state-file GET serves, so it has to be the file the emulator just wrote, not the newest state in the directory: another slot or another game's state would otherwise be filed in RomM as this save.
paramselfReturns
typing.OptionalThe state file's path, or None. The default reports an empty slot.
funcnote_state_handout(self) -> NoneRecord that the state-file GET route is about to read the file state_path returned.
That read moves the file's access time, which some implementations
also watch to confirm a load actually happened. Called before the
route reads the file's bytes, so the window it opens covers that read
instead of starting after the access time has already moved; callers
of load_state can then tell their own load's read from this one.
The default does nothing, for implementations that confirm loads some
other way.
paramselfReturns
Nonefuncnote_broker_state_write(self, path) -> NoneRecord that the broker itself just wrote path as a state file.
Called right after a mid-session state push (the state-file PUT route) lands the file on disk. An implementation that infers where a core actually saves by watching the filesystem needs to tell its own writes apart from the core's, since a push the broker made itself proves nothing about the core's own behavior. The default does nothing, for implementations with no such inference to protect.
paramselfparampathPathThe state file's path, as returned by state_target.
Returns
Nonefunclock_for_state_write(self) -> boolTake whatever lock a pushed state file should hold before it overwrites the working slot.
A push lands as a raw file replace with no confirmation step of its own, so nothing else stops it landing in the middle of another implementation's own confirmation window for that same file (a resume retry, an in-flight load) and being mistaken for whatever that other read or write was watching for. The default has no such window to protect and always succeeds.
paramselfReturns
boolTrue once locked (or when there is nothing to lock); False when
funcunlock_state_write(self) -> NoneRelease whatever lock_for_state_write took. No-op by default.
Only called after a lock_for_state_write call that returned True.
paramselfReturns
Nonefunc_clear_subtree(self, subtree, keep=None) -> NoneEmpty one of save_subtrees without removing the directory itself.
The directory stays because an emulator that finds its save directory missing at launch writes its saves somewhere else entirely, or refuses to boot at all.
paramselfparamsubtreestrA path relative to save_root, as save_subtrees names it.
paramkeepOptional[Callable[[Path], bool]]= NoneCalled with each top-level entry; returning True leaves that entry in place. The default clears everything.
Returns
Nonefunc_clear_save_subtrees(self, excluded=(), keep=None) -> NoneEmpty every save subtree this session is responsible for.
paramselfparamexcludedtuple[str, ...]= ()Subtrees the whole-card routes carry, which this session must leave alone; the card is laid down before activate runs, so clearing one here would delete what RomM just synced.
paramkeepOptional[Callable[[Path], bool]]= NonePassed through to _clear_subtree for each subtree.
Returns
Nonefuncclear_working_slot(self, excluded=()) -> NoneDrop what an earlier session left in the save tree, before the restore.
Every subclass that carries real save data MUST empty the save tree
here (or in prepare_restore) and declare clears_stale_saves. This
is not an optimisation and not optional. The restore only overwrites
the members the incoming archive happens to carry, so anything the
previous session wrote and this one's archive does not name survives
untouched: on a pooled or shared container that is one player's saves
left readable, and mixed into the dump, in the next player's session. A
clear scoped to a state slot or to one title is not enough on its own
unless nothing else under save_subtrees can hold another player's
data.
Deleting more than the container's own leftovers is the failure in the
other direction, so the clear is scoped to save_subtrees and runs
before the archive is extracted, never after. _clear_save_subtrees
is the usual implementation; an emulator with app data, DLC or
quarantined files sharing a subtree passes a keep predicate.
The default only reports the gap: the base class cannot know which paths are safe to delete, so an emulator with save data that reaches this warns rather than silently starting a session on the last player's files.
paramselfparamexcludedtuple[str, ...]= ()Save subtrees the whole-card routes carry for this session, which the clear must leave alone.
Returns
Nonefuncmemory_card_path(self, platform=None) -> Optional[Path]The directory holding the card the memory-card routes sync, or None.
The broker names the card rather than reading the name out of the
emulator's own config, because RomM lays a card down before the first
launch has written that config. platform is the ROM's platform slug,
for an emulator whose card exists on only one of several platforms it
serves (GameCube vs Wii on Dolphin); most emulators ignore it.
paramselfparamplatformOptional[str]= NoneReturns
typing.OptionalThe card directory, or None for emulators without a memory card.
funcarrange_card(self, heads) -> memcard.PlacementWhere each member of a card pushed on the memory-card route goes inside the card.
The default writes every member where the image put it. An emulator that reads only one layout overrides it to move a member the player packed differently to where the emulator will look for it.
paramselfparamheadsdict[str, bytes]Each member's zip name mapped to its first memcard.HEAD_BYTES bytes.
Returns
webstation_broker.memcard.PlacementEvery member name mapped to its destination relative to the card
funcselect_core(self) -> NoneResolve core for this launch, before anything is cleared.
Default: a launcher that is its own backend takes no core.
Raises
CoreRejectedError: Whencoreis set.
paramselfReturns
Nonefunccore_identity(self) -> dict[str, Any]Extra manifest and activate-response fields describing the running core.
paramselfReturns
dictNothing by default.
funcstate_library(self, path) -> Optional[str]The folder RomM should file the state at path under.
paramselfparampathPathThe state file the state GET is serving.
Returns
typing.OptionalNone by default: only RetroArch sorts states into per-core folders.
funcadopt_archive_identity(self, identity) -> NoneLearn from the restored archive's session identity, before the launch.
Default: nothing. A launcher that fronts many cores reads the previous session's core here.
paramselfparamidentityOptional[Mapping[str, Any]]The restored archive's manifest session, or None.
Returns
Nonefuncarchive_core(self) -> Optional[str]The core or backend actually running the game, or None.
Only meaningful for a launcher that is one shell over many cores; it goes in the archive manifest so the parent can tell a RetroArch PSP archive from a standalone PPSSPP one.
paramselfReturns
typing.OptionalThe core name, or None for emulators that are their own backend.
funccarry_save_across_cores(self, identity, rom_file) -> NoneMove a battery save into the new core's save dir on a core switch.
Default: nothing. A launcher that fronts many cores, each sorting its saves under its own dir, overrides this to carry the file across.
paramselfparamidentityOptional[Mapping[str, Any]]The restored archive's manifest session, or None.
paramrom_fileOptional[Path]The file being booted, or None.
Returns
Nonefuncsave_file_kind(self, rel) -> strWhat an archive member holds, for the manifest the parent reads.
Every emulator lays its save directories out differently, so this is what lets the parent sort an archive without a table of those layouts. The default classifies by subtree; emulators whose states and saves share a directory override it.
paramselfparamrelstrThe member path, relative to save_root and posix-separated.
Returns
strOne of state, state_screenshot, memcard or save.
funcalways_restore(self, rel) -> boolWhether an archive member outranks whatever sits on disk at activate.
The restore skips a member the disk already holds a newer copy of, so it can never roll back progress made since the archive was taken. That reasoning only holds for files whose mtime records this player's own saving. An emulator with a file the container writes on behalf of whoever is using it (a signed-in profile package, an account store) overrides this: the freshest copy of one of those is the last player's, and passing over the incoming one silently runs the session under their identity.
paramselfparamrelstrThe member path, relative to save_root and posix-separated.
Returns
boolTrue to restore the member unconditionally. The default exempts
funcimport_spec(self) -> imports.ImportSpecWhat this emulator accepts as a declared import, on self.platform.
paramselfReturns
webstation_broker.imports.ImportSpecThe spec. The default accepts nothing, so every import member is
funcplace_import(self, member, spec, ctx) -> Union[imports.Placement, imports.ImportRefusal]Place one member that passed hygiene and the kind gate.
Must be pure: nothing is written, nothing is deleted, and the only
reads allowed are member.head() and the rom through ctx.
paramselfparammemberimports.ImportMemberThe member.
paramspecimports.ImportSpecThis emulator's spec, as import_spec answered it.
paramctximports.ImportCtxThe launch context.
Returns
typing.UnionWhere the member lands, or why it cannot. The default refuses.
funcvalidate_import_plan(self, plan, ctx) -> list[imports.ImportRefusal]Check the placements together, after the shared checks.
paramselfparamplanlist[imports.Placement]Every placement.
paramctximports.ImportCtxThe launch context.
Returns
listAny refusals. The default has none.
funcidentity_source(self) -> Optional[imports.IdentitySource]Where this emulator's session identity comes from.
paramselfReturns
typing.OptionalThe source, or None when the emulator checks no identity.
funcstate_target(self, filename) -> Optional[Path]Where a pushed state called filename belongs.
Validating the name against the emulator's own convention is what keeps a caller from dropping arbitrary files into the save tree. The slot in it is not part of that test: RomM holds the library, so a stored state carries whatever slot it was captured in and lands in this broker's own working slot regardless.
paramselfparamfilenamestrThe name the pushed state was stored under.
Returns
typing.OptionalThe path to write it to, or None if the name is not one this
funccheck_state_bytes(self, head) -> boolTell whether a pushed state may be written for this session, going by its first bytes.
The push route calls this once the whole body is in, before the state replaces the slot's. The base takes every state: only an emulator whose states open with the id of the game they belong to can tell.
paramselfparamheadbytesUp to STATE_HEAD_BYTES bytes from the start of the pushed state.
Returns
boolFalse when the state provably belongs to another game.
funcwait_for_state(self, deadline, poll=0.5) -> boolBlock until the working slot holds a state file, or deadline passes.
A resume state can turn up after launch: the state-file routes only answer while a session is up, so RomM pushes its pick once activate has returned and the game is already booting. Waiting for it here is what keeps a deferred resume load from firing on a slot that is still empty and reporting a fresh start.
paramselfparamdeadlinefloatA time.monotonic() value to give up at.
parampollfloat= 0.5Seconds between checks of state_path.
Returns
boolTrue if the slot holds a state file by the time this returns.
funcsave_and_exit(self, slot) -> dict[str, Any]Save state (best effort) and stop.
Default: nothing to save. A slot of None is an exit that writes no
state. The game's own save data is still flushed and shipped: not
writing a state is the whole of "exit without saving", and discarding
an in-game save the player made at a save point would be losing real
progress.
paramselfparamslotOptional[int]The slot to save into before stopping, or None to skip the state save.
Returns
dictA dict with `{"state_saved", "state_slot", "state_file",
funcresolve_rom_file(self, path) -> Optional[Path]File the emulator should boot for path (folder or file).
Raises
NotImplementedError: Always; every subclass overrides this.
paramselfparampathPathThe ROM as RomM delivered it, either a single file or a folder holding the game's files.
Returns
typing.OptionalThe file to hand to launch, or None if nothing bootable is there.
funcswap_disc(self, path) -> boolMount path in place of the running disc.
Only called when supports_disc_swap.
Raises
NotImplementedError: When the emulator has no tray.
paramselfparampathPathThe disc image to mount.
Returns
boolTrue once the new disc is in the tray.
functoggle_menu(self) -> boolOpen the emulator's menu, or close it when it is already open.
Only called when supports_menu.
Raises
NotImplementedError: When the emulator has no menu to reach.
paramselfReturns
boolTrue once the toggle reached the emulator.