webstation-broker
DeveloperPython referenceemulatorsemulators.retroarch

emulators.retroarch.Retroarch

RetroArch driven over its stdin command interface.

RetroArch driven over its stdin command interface.

One launcher for every platform in PLATFORMS: platform is set from the activate payload before launch, and picks the core, the ROM extensions and the save scope. Saves, loads and quit go over stdin; replies come back on a stdout pipe drained by a reader thread, and saves are confirmed on disk.

Declared imports take one .srm save per archive, renamed to the booted content's stem and placed in saves/<library_name>/, where the core loads SRAM. Only a platform whose core loads a .srm (save_ram in PLATFORMS) takes one, and where the core reads a device's .sav the same way (sav_is_srm), a .sav is placed as the .srm. The Wii and 3DS platforms take the files of their core's own tree instead, a NAND title folder or a path in Azahar's SD card and NAND, and GameCube and every other platform whose core keeps its saves elsewhere take nothing. States are never imported; they are pushed after activate.

Attributes

attributename
= 'retroarch'

Registry key, retroarch.

attributedisplay_name
= 'RetroArch'

Shown as "RetroArch".

attributesave_root
= RA_DATA_DIR

RA_DATA_DIR, the broker-managed data root.

attributestate_subtrees
= ('states',)

states, where savestates land.

attributelog_path
= RA_LOG_PATH

RA_LOG_PATH, where RetroArch's stderr goes.

attributesupports_disc_swap
= True

On; discs are swapped through the virtual tray.

attributesupports_menu
= True

On; the Quick Menu is toggled over stdin.

attributestate_slot
= STATE_SLOT

STATE_SLOT, the one slot the broker works in.

attributestate_dir
= STATE_DIR

STATE_DIR, the broker-managed savestate directory.

attributeclears_stale_saves
= True

On; activate empties the platform's save subtrees.

attributeterm_timeout
= float(os.environ.get('RETROARCH_STOP_WAIT', '15'))

Seconds QUIT gets before SIGTERM, from RETROARCH_STOP_WAIT (default 15).

attributeplatformOptional[str]
= None

The RomM platform slug, set before launch.

attribute_rom_basestr
= ''

The loaded content's basename, which RetroArch names its state and SRAM files after.

attribute_slot_homed
= False

Whether the current slot has been parked on STATE_SLOT since launch.

attribute_playing_monotonicOptional[float]
= None

time.monotonic() when _track_first_playing first saw PLAYING; None until then.

Anchors _wait_for_first_save_settle. Reset to None on every launch.

attribute_launch_seq
= 0

Launch generation, bumped on every launch and stop so stale background waits bail out.

attribute_resume_settle
= RESUME_LOAD_SETTLE

Seconds between PLAYING and a deferred resume load; set from the platform table at launch.

attribute_state_confirm_wait
= STATE_CONFIRM_WAIT

Seconds a save gets to land on disk; set from the platform table at launch.

attribute_stdout_buf
= bytearray()

Replies read off RetroArch's stdout and not yet consumed.

attribute_stdout_lock
= threading.Lock()

Guards _stdout_buf between the reader thread and the callers waiting on replies.

attribute_reply_lock
= threading.Lock()

Serializes command/reply exchanges.

Replies carry no request id, so two threads waiting at once would each be free to consume the other's line.

attribute_readerOptional[threading.Thread]
= None

The thread draining RetroArch's stdout into _stdout_buf.

attribute_playlistOptional[Path]
= None

The playlist this session booted, or None when the content was not an .m3u.

attribute_disc_indexint
= 0

Where the tray currently sits.

RetroArch has no way to read the mounted disc back, so the broker remembers what it did and steps relative to that.

attribute_disc_lock
= threading.Lock()

Serializes swap_disc, save_state and one resume load attempt.

All three act on the live process after polling for PLAYING: a LOAD_STATE landing inside a tray-settle window, or a SAVE_STATE landing inside a resume's settle window, is the collision. The waiters differ: a swap fails fast, a save queues. The resume's retry loop holds this only for an attempt at a time, never across the gaps between them.

attribute_state_handout_atfloat
= float('-inf')

time.monotonic() of the last time the state-file GET route read the file.

Set by note_state_handout. load_state's confirmation watches the same access time that read moves, so a handout landing mid-load reads as a false confirmation unless the wait knows to discount it.

attribute_resolvedOptional[tuple[Optional[str], Optional[str], retroarch_cores.Profile]]
= None

The (platform, core) the profile was resolved for, and the profile; see _profile.

attribute_observed_libOptional[str]
= None

An untested core's library_name as confirmed on disk.

Unlike _seeded_lib, this is never overridden once set: it came from watching RetroArch actually write the dir, not from a guess.

attribute_seeded_libOptional[str]
= None

An untested core's library_name as recorded in a restored archive's manifest.

Only a seed, not a confirmation: adopt_archive_identity sets this, but _observe_library_name keeps running and its result overrides it (the manifest only seeds the name; observation may still correct it).

attribute_launch_wallfloat
= 0.0

time.time() set right before the launch spawn; anchors _observe_library_name.

attribute_prelaunch_mtimesdict[Path, float]
= {}

Every sorted-dir file's mtime just before the launch spawn.

_observe_library_name skips a file still at its recorded mtime: the restore and the save carry stamp old mtimes, so an untouched file proves nothing about where RetroArch writes.

attribute_broker_written_statesset[Path]
= set()

Paths this session's broker wrote itself: a mid-session state push or an import placement.

_observe_library_name excludes these: a file the broker itself put there cannot be evidence of where RetroArch writes, and would otherwise falsely confirm whatever dir the push happened to target.

attributesave_subtreestuple[str, ...]

Per-platform dump scope.

Cores whose savefile dir is also their app-data dir (dolphin) narrow the archive to real save files.

attributesupports_statesbool

Whether the loaded platform's core can save states.

attributerom_cacheablebool

Whether, with the ROM cache enabled, launch boots a local copy of the ROM; off for the arcade cores that load romsets from beside it.

attributerom_extensionstuple[str, ...]

The loaded platform's ROM extensions, or none when no platform is mapped.

Functions

func__init__(self) -> None

Set up the reply buffer, reader thread slot and tray tracking for a session.

paramself

Returns

None
func_experimental(self) -> bool

Whether a blocked core may run: RomM's flag or RETROARCH_EXPERIMENTAL_CORES.

paramself

Returns

bool

True when either is on.

func_profile(self) -> Optional[retroarch_cores.Profile]

The profile for the current platform and core, resolved once per pair.

Callers that only set platform (import-spec, most tests) get the default profile. select_core is where a bad core raises; here an unmapped platform with no core is None.

paramself

Returns

typing.Optional

The profile, or None when the platform is unset or unmapped.

funcselect_core(self) -> None

Resolve the platform's profile now, and warn about an untested or blocked core.

Raises

  • CoreRejectedError: For a core that is not offered, or blocked without the opt-in.
paramself

Returns

None
funclibrary_name(self) -> Optional[str]

The sorted-dir name in effect: observed, then seeded, then the profile's.

An observed name (confirmed on disk) always wins. Absent that, a manifest seed from adopt_archive_identity is used until observation either confirms it or replaces it with something else.

paramself

Returns

typing.Optional

The name, or None when unmapped.

funccore_identity(self) -> dict[str, Any]

The tier and library name, for the manifest and the activate response.

paramself

Returns

dict

core_tier and library_name, or nothing when unmapped.

funcstate_library(self, path) -> Optional[str]

The sorted dir path sits in, which can differ from library_name.

An unconfirmed core's state sits where RetroArch put it, not where the catalog guessed; a legacy state at the unsorted root is the default core's.

paramself
parampathPath

The state file the state GET is serving.

Returns

typing.Optional

The file's first dir under STATE_DIR, else library_name().

func_known_libs(self) -> frozenset[str]

Sorted-dir names that belong to cores other than the running one.

paramself

Returns

frozenset

Every library_name in the platform table (defaults and alternates)

func_state_scope(self) -> tuple[Optional[str], frozenset[str], bool]

Where this core's states may be looked up.

paramself

Returns

typing.Optional

(only, skip, include_root) for _newest_state. The default

func_observe_library_name(self) -> None

Adopt the sorted dir RetroArch actually wrote for an untested core.

Takes the newest subdirectory of saves/ or states/ holding a file genuinely written since launch. Called after PLAYING and after each confirmed save; a no-op once a name is confirmed (_observed_lib set), and for default and vetted cores. Keeps running even after a manifest seed (_seeded_lib) is in place: the manifest only seeds the name, and this observer's result, once it has one, overrides that seed.

A candidate file's mtime has to fall in [launch - OBSERVE_WINDOW_BEFORE_SLACK, now + OBSERVE_WINDOW_AFTER_SLACK]: the lower bound is "since launch" from the spec, loosened by OBSERVE_WINDOW_BEFORE_SLACK for a mount that only tracks mtime to a coarse unit; the upper bound discards a clock-skewed or otherwise bogus future timestamp, widened by OBSERVE_WINDOW_AFTER_SLACK to tolerate the share and the broker's clocks disagreeing slightly. Files this session's own broker wrote (_broker_written_states), and files still at their pre-launch mtime (_prelaunch_mtimes), are skipped outright: they prove nothing about where RetroArch writes.

paramself

Returns

None
funcadopt_archive_identity(self, identity) -> None

Seed the library name from the restored archive's manifest.

Only a seed: _observe_library_name keeps running afterward, and its result overrides this one once it has confirmed a real dir.

paramself
paramidentityOptional[Mapping[str, Any]]

The restored archive's manifest session, or None.

Returns

None
funccarry_save_across_cores(self, identity, rom_file) -> None

Carry the battery save from the archive's core's dir into this core's.

Runs after the archive is restored and before the baseline, so the carry, which keeps the file's mtime, does not count as this session's write. A default or vetted core's dir is known, so the save moves; an untested or blocked core's dir is a guess, so the save is copied and the original stays where the old core reads it. An existing target is replaced only by a strictly newer source, and kept beside it as .srm.bak. States are never carried. A carry that fails, or a target dir name that is not a single safe path component, is logged and skipped; it never fails activate.

paramself
paramidentityOptional[Mapping[str, Any]]

The restored archive's manifest session, or None.

paramrom_fileOptional[Path]

The file being booted; its stem names the .srm.

Returns

None
funcnote_broker_state_write(self, path) -> None

Track path so _observe_library_name does not mistake it for a core's own write.

Called for a mid-session state push and for every file a declared import places (and its sidecars).

paramself
parampathPath

The file's absolute path: a pushed state as returned by state_target, or save_root joined with a placement's dest.

Returns

None
funcarchive_core(self) -> Optional[str]

The libretro core booting the loaded platform, or None when unmapped.

paramself

Returns

typing.Optional[str]
funcresolve_rom_file(self, path) -> Optional[Path]

File the core should boot for path.

A file is taken as is. A folder is searched one level deep and ranked by _pick_rom_file against the platform's extensions; when that finds nothing, a lone archive holding content the core takes is booted.

paramself
parampathPath

The ROM as RomM delivered it, a file or a folder.

Returns

typing.Optional

The file to boot, or None when the platform is unmapped, the path

func_spawn_ra(self, cmd, env) -> None

Spawn with a real stdout pipe (stderr to the log).

stdout carries the command replies, so it must stay clean. A reader thread drains it into _stdout_buf. The pid is recorded the way the shared _spawn records it, so a broker that restarts mid-session can still reap the RetroArch it no longer has a handle on.

paramself
paramcmdlist[str]

The argv to run.

paramenvdict[str, str]

The environment to run it in.

Returns

None
func_read_stdout(self) -> None

Reader thread body: copy RetroArch's stdout into _stdout_buf until it closes.

paramself

Returns

None
func_wait_for_reply(self, prefixes, timeout) -> Optional[str]

Poll the reply buffer for a line beginning with one of prefixes.

Only _send calls this, and only while holding _reply_lock, so the line consumed here belongs to the command that exchange just sent.

paramself
paramprefixestuple[str, ...]

Reply prefixes the caller is waiting for.

paramtimeoutfloat

Seconds to keep polling the buffer.

Returns

typing.Optional

The matched line, with everything up to and including it dropped

func_write_cmd(self, cmd) -> bool

Write one command line to RetroArch's stdin.

paramself
paramcmdstr

The command line to send; the newline is added here.

Returns

bool

True once the line is flushed. False when there is no running

func_send(self, cmd, wait_prefix, timeout=5.0) -> Optional[str]

Send one command and return the reply it answers with.

A reply carries nothing naming the command that produced it, so one exchange runs at a time and the buffer is emptied before the command goes out. Without that, a background GET_STATUS poll consumes the OK or the LOAD_STATE_SLOT echo another thread is waiting on, and a reply left over from an earlier command answers this one.

paramself
paramcmdstr

The command line to send; the newline is added here.

paramwait_prefixUnion[str, tuple[str, ...]]

One reply prefix, or several of which the earliest matching line wins.

paramtimeoutfloat
= 5.0

Seconds to wait for the reply.

Returns

typing.Optional

The reply line, or None when the command could not be sent or no

funclaunch(self, rom_path, resume_slot) -> None

Start RetroArch on rom_path with the platform's core.

Any running session is stopped first. The core is downloaded if missing, its assets linked, its fixed-path saves linked into the archive, its core options pinned or seeded, and the broker config written; then RetroArch starts fullscreen with that config appended. A resume is deferred to a background thread that waits for the game to be up.

Raises

  • RuntimeError: When platform has no core mapped, or the RetroArch binary (RETROARCH_BIN) is not on PATH.
paramself
paramrom_pathPath

The file to boot, as returned by resolve_rom_file.

paramresume_slotOptional[int]

The slot to load once the game is running, or None.

Returns

None
func_msu1_boot_path(self, rom_path) -> Path

The file to boot for rom_path: an MSU-1 extraction for a zipped MSU-1 game, else itself.

Snes9x finds MSU-1 audio and video beside the ROM on disk, so a zip RetroArch opens itself boots silent. Its extraction keeps the zip's stem, so saves and states keep their names. A game that cannot be extracted still boots from the zip, without the MSU-1 data.

paramself
paramrom_pathPath

The file launch was handed.

Returns

pathlib.Path

The extracted ROM, or rom_path unchanged.

func_deferred_load_state(self, slot, seq) -> None

Load slot once RetroArch reports the content PLAYING.

Cores with no game running yet (Dolphin boot screen) reject loads, so this polls GET_STATUS first and does nothing else itself. Everything the load needs (the wait for the slot's state file, _resume_settle, the load and its retries) belongs to _load_until_confirmed, which runs all of it under the tray lock for what is left of RESUME_LOAD_WAIT.

The status poll is the one part that stays outside the tray: it sends no tray command and reads nothing off disk, so a save or disc swap running alongside it cannot collide with anything.

paramself
paramslotint

The slot to load.

paramseqint

The launch generation this load belongs to; a relaunch or stop bumps _launch_seq and ends the wait.

Returns

None
func_load_until_confirmed(self, slot, deadline, seq) -> bool

Wait for the slot's state file, settle, then load it, retrying until deadline.

A load RetroArch echoes but never reads leaves the player in a fresh boot with the resume silently lost, and one attempt is too little to call it: the core can still be finishing its own startup past the settle. Attempts repeat every RESUME_LOAD_RETRY_GAP while the resume budget holds. Nothing on the status route carries a resume result, so an exhausted budget is escalated to an error log rather than setting boot_failed, which means a game that never started at all.

Each attempt takes _disc_lock on its own and drops it again, rather than the whole retry run holding it: an attempt is a few seconds long while the run can span the entire resume budget, and a save queued behind that would time out and be lost. The first attempt keeps the wait for the state file and _resume_settle inside its own hold, since a save landing in either window writes exactly the unrestored state the resume is about to read back. Waiting outside the tray is not enough: that save creates the very file the wait is looking for. Later attempts already know the file is there and only re-send the load, but the tray is free between attempts, so a save can still land in the retry gap; the file is fingerprinted the moment it is found ready and re-checked before every later attempt, and a mismatch ends the run rather than loading whatever is there now back as if it were the resume.

The state-file wait gets no longer than RESUME_ATTEMPT_LOCK_WAIT of any one hold; a slot still empty at that point drops the tray and comes back on the next attempt, so a state RomM pushes late is still caught without a save waiting out the whole resume budget behind it.

paramself
paramslotint

The slot to load.

paramdeadlinefloat

A time.monotonic() value to stop retrying at.

paramseqint

The launch generation this load belongs to.

Returns

bool

True once a load is confirmed; False when the budget runs out, the

func_wait_until_playing(self, deadline, seq) -> bool

Block until the core reports a running game, or deadline passes.

A tray command sent before the content is up is dropped, and there is no error to catch when it is. seq is the launch generation this wait belongs to, checked the same way _deferred_load_state checks _launch_seq, so a relaunch during the wait ends it rather than letting a stale wait later act on the new session.

paramself
paramdeadlinefloat

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

paramseqint

The launch generation this wait belongs to.

Returns

bool

True once GET_STATUS reports PLAYING; False on timeout, relaunch

funcswap_disc(self, path) -> bool

Mount the playlist entry at path without restarting the core.

Stepping is relative and wraps, because the command protocol has DISK_NEXT but no way to set an index or read the current one back. The tray is opened, stepped the needed number of times, and closed; the tracked index only moves once the session is confirmed to have outlived the sequence. An open menu would drop those commands, so _close_menu_before_tray closes one first.

paramself
parampathPath

The disc image to mount; it must be listed in the session's playlist.

Returns

bool

True once the new disc is in the tray (or was already mounted);

func_track_first_playing(self, seq) -> None

Record the moment GET_STATUS first reports PLAYING, for _wait_for_first_save_settle.

Runs from launch in the background, polling tightly so the recorded timestamp tracks the real transition closely rather than lagging it by a coarse poll interval. _send is safe to call concurrently with the rest of the session (see _reply_lock/_stdout_lock), so this can run alongside a deferred resume load's own PLAYING poll without colliding.

paramself
paramseqint

The launch generation this belongs to; a relaunch or stop bumps _launch_seq and ends the wait.

Returns

None
func_wait_for_first_save_settle(self) -> None

Sleep off whatever is left of FIRST_SAVE_SETTLE since PLAYING.

Called once, right before a session's first _home_state_slot(). A fast-booting core can already be running well past its own boot sequence by the time the player's first save request arrives, in which case this is a no-op; only a save requested within FIRST_SAVE_SETTLE of PLAYING actually waits.

If _track_first_playing has not observed PLAYING yet (a very fast first save, or a slow-booting core), this waits up to FIRST_SAVE_SETTLE_WAIT for it before giving up and proceeding unsettled.

paramself

Returns

None
func_home_state_slot(self) -> bool

Park RetroArch's current state slot on STATE_SLOT.

Absolute, not relative: MINUS runs the slot down onto its -1 floor first, so this holds wherever the slot was, as long as it was no higher than SLOT_HOME_STEPS presses above the floor. Nothing in the protocol reads the slot back, so a player who cycled past that is only caught by the save confirmation missing the slot afterwards.

paramself

Returns

bool

True when every press was delivered. A dropped press leaves the

func_states(self) -> dict[Path, tuple[int, float]]

Snapshot the loaded game's state files.

paramself

Returns

dict

_state_snapshot of STATE_DIR for the loaded content.

func_toggle_menu_settled(self) -> bool

Send MENU_TOGGLE and give the menu MENU_SETTLE to open or close.

paramself

Returns

bool

False when the command could not be written.

func_try_save(self) -> bool

Send SAVE_STATE once and confirm the file landed in STATE_SLOT.

paramself

Returns

bool

True when the slot's state file changed on disk, is non-empty,

funcsave_state(self, slot) -> bool

Save the running game into STATE_SLOT.

slot is what RomM asked for and is ignored: this saves into STATE_SLOT and the caller reads the effective slot back off state_slot.

Homing costs a couple of seconds, so it runs once per launch and then only to recover: a save landing on another slot means the player moved it with their own hotkeys, and re-homing puts the next one back.

The save takes _disc_lock, so it cannot land inside a deferred resume load's settle window: a save written there captures the game before the resume was restored, and the resume then loads that back over what the player had. Unlike swap_disc this waits (up to SAVE_LOCK_WAIT) instead of failing fast, because after the resume finishes the save is exactly the one the caller asked for.

paramself
paramslotint

The slot RomM asked for; ignored.

Returns

bool

True once the state file is confirmed on disk; False when the core

func_save_into_slot(self, lock_wait) -> bool

Take the tray within lock_wait and write the running game into STATE_SLOT.

The wait is the caller's to pick because the cost of waiting is not the same everywhere: an ordinary save only delays itself, while the exit save holds the API's process-wide session lock for as long as it sits here.

A miss is retried once, and which retry depends on what the miss left behind: a state written to another slot means the player moved the slot, so it is re-homed; nothing written anywhere means a menu is swallowing the command, so _save_past_menu closes it first.

paramself
paramlock_waitfloat

Seconds to wait for _disc_lock before giving up.

Returns

bool

True once the state file is confirmed on disk; False when the core

func_save_past_menu(self) -> bool

Close the menu a save could not get past, retry, and reopen it if that missed too.

Called when a SAVE_STATE wrote nothing to any slot. That is what an open menu does: it swallows the command, and nothing over stdin reports whether the menu is up. A save that landed on another slot means the player moved the slot instead, which _save_into_slot re-homes without coming here. The slot is re-homed after the toggle because the menu swallows slot presses too, and its own State Slot entry can move it.

paramself

Returns

bool

True once the retried save is confirmed on disk. False when the

func_close_menu_before_tray(self) -> bool

Make sure no menu is up to drop a swap's tray commands, closing one that is.

The menu drops DISK_* the way it drops SAVE_STATE, and GET_STATUS still says PLAYING with it open, so a save is the only probe: one that writes nothing means a menu took it. The probe's writes are undone, so the exit dump never ships them. Called with _disc_lock held.

paramself

Returns

bool

True when the menu is closed (or was closed here), and when the

func_probe_save(self) -> bool

Send SAVE_STATE and report whether any state file changed.

paramself

Returns

bool

True once a write to any slot is seen within _state_confirm_wait.

functoggle_menu(self) -> bool

Send MENU_TOGGLE, which opens the Quick Menu or closes it.

Fails fast while a save, resume load or disc swap holds _disc_lock: a toggle landing inside a save's retry would reopen the menu the retry just closed, and the save would be swallowed again.

paramself

Returns

bool

True once the command reached RetroArch's stdin; False when the

funcload_state(self, slot) -> bool

Take _disc_lock and load STATE_SLOT, confirming the state was read.

The lock keeps this load's access-time backdate-and-poll from overlapping a deferred resume attempt's: both watch the same state file to confirm their own read, and without serializing them one can mistake the other's read for its own and report a load that never happened.

paramself
paramslotint

The slot RomM asked for; ignored in favour of STATE_SLOT.

Returns

bool

True once the load is confirmed by _load_state_locked; False

func_load_state_locked(self, slot, *, resume=False) -> bool

Load STATE_SLOT into the running game and confirm the state was read.

Assumes _disc_lock is already held; called by load_state and by the resume retry loop, which holds the lock across its own attempt.

LOAD_STATE_SLOT is absolute and does not move the current slot, so this needs no homing. Its echo is a bare repeat of the command with no success bit, and a load writes nothing to disk, so the echo alone cannot tell a restored game from a refused load (a core that has no content up yet rejects one). What does separate them is the read: a load that happens opens the state file, so the file's access time is stamped behind its own mtime beforehand and polled for afterwards.

On a noatime mount nothing records that read, and there the echo is all there is; the load is then reported as delivered and the warning says the result is unconfirmed. That charity is for the mount, where no load could ever be confirmed. A mount that does track access times but whose state file could not be stamped is a fault on that one file, so it is reported as a failed load and left for the caller to retry. A probe that could not run at all proved nothing about the mount, so it is treated as tracking rather than earning the same charity: guessing noatime there would pass every load through unchecked.

The state is fingerprinted alongside the stamp, so a state pushed in over the slot while the load is in flight is not mistaken for the load having read the one that was there. The state-file GET route reads the same file and moves the same access time; an access-time move seen within STATE_READ_HANDOUT_GRACE of one of those reads taints that attempt's read for good (see _wait_for_state_read), so a tainted attempt is retried with a fresh marker up to LOAD_TAINT_RETRY_LIMIT times rather than being reported as a failed load RetroArch actually performed.

A resume load an untested or blocked core never reads is logged as a warning naming the likely cause: a state pushed before the core's sorted dir was known may sit where that core does not look.

paramself
paramslotint

The slot RomM asked for; ignored in favour of STATE_SLOT.

paramresumebool
= False

Whether this is a resume attempt rather than a manual load.

Returns

bool

True once RetroArch has read the slot's state file (or echoed the

funcstate_path(self) -> Optional[Path]

The newest state file for the loaded content in STATE_SLOT, or None.

Scoped to the running core's own sorted dir once it is known (default, vetted, or an untested core whose dir has been observed), plus the legacy unsorted root for the default core only; otherwise everywhere except another known core's dir.

paramself

Returns

typing.Optional

The file found by _newest_state, or None before a launch has set

funcnote_state_handout(self) -> None

Record now as the last time the state-file GET route read our state.

See _state_handout_at.

paramself

Returns

None
funclock_for_state_write(self) -> bool

Take _disc_lock before a pushed state overwrites the working slot.

Without this, a push racing an in-flight resume attempt or manual load would replace the file those are backdating and polling for their own confirmation, which reads as a save landing in the retry gap even when it is the resume's own seed pushed twice.

paramself

Returns

bool

True once _disc_lock is held; False after LOAD_LOCK_WAIT with

funcunlock_state_write(self) -> None

Release _disc_lock taken by lock_for_state_write.

paramself

Returns

None
funcclear_working_slot(self, excluded=()) -> None

Empty every save subtree the incoming archive restores into.

The states alone are not enough. RetroArch names both a state and an SRAM file after the loaded content and nothing in either name says whose session wrote it, so in a shared container the previous player's .srm sits there with a fresh mtime when the next player activates the same title. The restore then skips its own member as older than what is on disk, and the exit dump ships the leftover back as the new player's save. Clearing the whole tree first is what leaves the restore the only thing that puts save data in place.

Scoped to save_subtrees, so the platforms that share a savefile dir with their core's app data (dolphin, azahar) lose their saves and keep the rest of that dir.

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

Subtrees carried by the whole-card routes. RetroArch names no memory card subtree, so this is always empty.

Returns

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

Where a pushed state called filename belongs.

RetroArch looks a state up by the name it derives from the loaded content, so the target is that name in this broker's slot and a pushed one only has to say which content it belongs to.

paramself
paramfilenamestr

The name the pushed state was stored under.

Returns

typing.Optional

The existing slot file when there is one, else the slot's path in

funcimport_spec(self) -> imports.ImportSpec

Take one .srm (or .sav), NAND or SD files, or nothing; take states through the push routes.

A platform whose core reads a .sav as its .srm (sav_is_srm) names both shapes, still one member at most.

A platform whose core loads no .srm and has no layout of its own (see _srm_dir and _layout), and an unmapped platform, declare the save kind with no shapes: discovery tells RomM the save is refused there, and place_import says why. Wii and 3DS declare the shape of their core's tree and the globs no import may write. A state placed in the archive would race the resume's fingerprint check, so a mapped platform whose core has states takes them through PUT /api/session/state-file after activate.

paramself

Returns

webstation_broker.imports.ImportSpec

The spec for the loaded platform.

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

Name the game id a Wii or 3DS save has to match: RomM's, as neither is read off the rom.

The Dolphin module reads a disc's id in _rom_game_id, but RetroArch may not import another emulator's module, so the session's id is RomM's title_id alone, and a session without one places any title.

paramself

Returns

typing.Optional

A GameCube/Wii disc-id source on the Wii, a sixteen-digit title-id

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

Place a Wii or 3DS file in its core's tree, or one .srm named for the booted content.

Only saves reach here: the kind gate refuses a state, which is pushed, and a memory card, which is resent as the core's .srm save. On Wii and 3DS a member that is not a .srm is placed by the layout; a .srm there, and every member on any other platform, is answered by _srm_dir. Where the core reads a .sav as its .srm (sav_is_srm), a .sav is placed as that .srm.

paramself
parammemberimports.ImportMember

The save 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 saying why this platform takes no such file.

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

Work out place_import's answer, before the placed paths are noted.

paramself
parammemberimports.ImportMember

The save member, already past the kind gate.

paramctximports.ImportCtx

The launch context.

Returns

typing.Union

The placement, or a refusal saying why this platform takes no such file.

func_flush_sram(self) -> bool

Ask RetroArch to write the game's SRAM out before the archive is dumped.

SAVE_FILES answers OK or NO, and a refusal has to be reported: the dump that follows ships whatever is on disk either way, so an unflushed session ships the previous save as if it were current.

paramself

Returns

bool

True when RetroArch answered OK; False when it refused or never

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

Save state when asked, flush SRAM, and quit RetroArch.

The state save is skipped when slot is None or the platform entry opts out of savestate. The SRAM flush always runs so the save dump ships current save data, then the process is quit through _quit. A flush RetroArch refuses is logged by _flush_sram; the exit still goes ahead, since holding the session open would not make the refusal any more likely to succeed.

The save here waits only EXIT_SAVE_LOCK_WAIT for the tray, not the SAVE_LOCK_WAIT an ordinary save gets: this runs under the API's process-wide session lock, so the wait is paid by every other session route as well.

paramself
paramslotOptional[int]

The slot to save into, or None to exit without writing a state.

Returns

dict

A dict with `{"state_saved", "state_slot", "state_file",

func_quit(self) -> None

Quit RetroArch gracefully over stdin, escalating to stop if it stays up.

QUIT is sent, then sent again after QUIT_CONFIRM_GAP if the process is still running, and given QUIT_WAIT to exit. A graceful exit forgets the handle; anything else falls through to self.stop (not the base class's) and its SIGTERM, so an in-flight deferred resume load is invalidated the same as any other exit path even when the process outlives that SIGTERM/SIGKILL escalation.

paramself

Returns

None
funcstop(self) -> None

Stop RetroArch, invalidating any in-flight deferred state load before the kill.

paramself

Returns

None

On this page