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_screenshot_path,state_target,clear_working_slotandwait_for_stateare the save-state hooks. The broker only calls the first two whensupports_statesis on; the defaults report an empty slot.swap_discis only called whensupports_disc_swapis on.memory_card_pathpairs withmemory_card_subtreefor emulators whose whole memory card travels on its own routes.prepare_restoreruns before a save archive is extracted.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 state,prepare_restoreruns, 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.
Attributes
attributenamestr= 'base'Registry key and log label for the emulator.
attributedisplay_namestr= 'Webstation'Human-readable name the UI shows.
attributerequires_rombool= TrueWhether a launch needs a ROM; the desktop session does not.
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.
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.
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= 5.0Seconds SIGTERM gets before escalating to SIGKILL.
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.
Functions
func__init__(self) -> NoneStart with no process handle and no boot failure flagged.
paramselfReturns
Nonefunc_spawn(self, cmd, env, stdin_pipe=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.
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.
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 handle and pid record are dropped first, then the process group gets
SIGTERM, escalating to SIGKILL once term_timeout passes. A process
that is already gone is a no-op.
paramselfReturns
Nonefuncprepare_restore(self) -> NoneHook run before a save archive is extracted into save_root.
Default: nothing. Override to clear anything that would block 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.
funcstate_screenshot_path(self) -> Optional[Path]The frame captured alongside the working slot's state, or None.
Only for emulators that write the thumbnail as a separate file. The ones that embed it in the state itself return None and let RomM pull it out of the state it already fetched.
paramselfReturns
typing.OptionalThe screenshot's path, or None. The default reports none.
funcclear_working_slot(self) -> NoneDrop whatever the working slot holds from an earlier session.
Called at activate, before the incoming save archive is restored, so only the container's own leftovers go. Emulators that name a state after the loaded content can tell a stale one apart on sight and leave this alone; the override exists for the ones that cannot.
paramselfReturns
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.
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
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.