webstation-broker
DeveloperPython referenceemulatorsemulators.rpcs3

emulators.rpcs3.Rpcs3

RPCS3 (PS3) launcher: exit-only savestates via PINE, plus archive boot support.

RPCS3 (PS3) launcher: exit-only savestates via PINE, plus archive boot support.

Declared imports take PS3 save folders (save data under home/00000001/savedata, game data under game/) and one savestate, which rides the archive and is filed under the boot target's title id. See import_spec.

Attributes

attributename
= 'rpcs3'
attributedisplay_name
= 'RPCS3'
attributesave_root
= DEV_HDD0
attributestate_subtrees
= ('savestates',)

States land under the symlinked savestates dir; see _ensure_sstate_link.

attributeclears_stale_saves
= True

On: clear_working_slot empties every cellSaveData and cellGameData dir.

Savestates are the one part of the tree cleared per title rather than whole, since a title the session never boots can hold states no dump has carried off yet; the ones a session cannot scope by title id are handled by _clear_leftover_states once PINE names the title.

attributelog_path
= RPCS3_LOG_PATH
attributeterm_timeout
= float(os.environ.get('RPCS3_STOP_WAIT', '2'))
attributerom_extensionstuple[str, ...]

Bootable formats, minus the archives needing the disabled extraction cache.

An archive only reaches a boot target by way of CACHE_DIR, so with the cache off it is not bootable and must not be advertised as accepted. .pkg is unaffected: it installs through _install_pkgs into GAME_DIR regardless of the cache flag.

attributesave_subtreestuple[str, ...]

CellSaveData saves, save states, plus the cellGameData save dirs under game/.

game/ mixes save data with installed PKG titles and RPCS3's lock folders, so the dump enumerates only the dirs without a bootable EBOOT.BIN. A restore inverts the problem: the archive holds nothing but previously dumped save dirs, and those dirs don't exist on disk yet, so the whole game/ prefix is declared to let them through.

attributerestore_subtreestuple[str, ...]

The restore set, whatever _restoring says.

Preflight reads this before clear_working_slot has flipped _restoring, and a restore needs the whole game/ prefix: the dirs it brings back do not exist on disk yet.

attributelink_rootstuple[Path, ...]

The savestate tree dev_hdd0/savestates links to.

Read at call time so a test that patches SSTATE_ROOT is honoured.

Functions

func__init__(self) -> None

Initialize per-session launch tracking state.

paramself

Returns

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

Create the savestates symlink and drop the last session's saves and states.

Save data goes first and goes whole (_clear_stale_save_data): a save dir is not scoped to the incoming title the way a savestate dir is, and the exit dump ships a session save dir whole, so a previous player's untouched files left sitting in one would leave in this player's archive.

RPCS3 has no fixed slot to clear, but activate() calls this before it ever reads save_subtrees to restore an archive, which makes it the one place that is safe to create the savestates symlink: it runs on every activate, but never during a bare construction (registry sweeps build every emulator against real, unredirected paths).

It also drops the incoming title's own savestates dir and flips _restoring on early. Clearing first means a leftover local state from a previous session never outranks (by mtime) whatever this session's archive restores. Only that one title goes: every other title's dir holds states that may not have been archived yet (a crashed session, a title mid-restore for a different game), and none of them can outrank this session's states anyway because every lookup is already scoped to SSTATE_ROOT/<title id>. A boot target whose title id the path alone cannot supply (a bare .iso, an archive) therefore clears nothing rather than guessing. Flipping _restoring here, not just in prepare_restore(), matters because api.py reads save_subtrees for the restore extract before it ever calls prepare_restore().

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

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

Returns

None
funcimport_spec(self) -> imports.ImportSpec

Declare what RPCS3 takes: PS3 save folders, and one savestate.

Save data and game data are both folders that must hold PARAM.SFO. RPCS3 groups the units itself in validate_import_plan, because a unit sits two or four components deep depending on the tree. The state rides the archive and resumes through save.resume_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 folder's file, 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 save folder that has no PARAM.SFO at its root.

paramself
paramplanlist[imports.Placement]

The placements that passed every per-member check.

paramctximports.ImportCtx

The launch context.

Returns

list

One incomplete_unit per member of each folder that lacks one.

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

Read the session's title id off the boot target, and take RomM's when it has none.

paramself

Returns

typing.Optional

The source.

funcprepare_restore(self) -> None

Stop any running instance and mark the session as archive-restoring.

paramself

Returns

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

Resolve a RomM path to a single bootable PKG, disc, or ISO.

A file is taken as is. A directory is searched for the game's installer/disc layout via _ROM_SEARCH_GLOBS. The chosen target is also recorded in _pending_rom for clear_working_slot, which runs next and has no other way to name the title it is clearing for.

paramself
parampathPath

RomM's resolved rom path, file or directory.

Returns

typing.Optional

The path to boot, or None if nothing bootable was found, or path

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

Run xdotool, returning its stdout, or None if it failed.

paramself
paramargsstr
= ()

Returns

typing.Optional[str]
func_game_window(self) -> Optional[str]

The render window belonging to this session's rpcs3, or None.

The class is shared by every rpcs3 on the display, so a zombie left by a session that failed to die answers the search too, and the first match is as likely to be that one as this one: a save hotkey sent there saves nothing and never returns a state. xdotool search --pid cannot make the distinction either, because AppRun runs the emulator as a child and the window's pid is the child's, not the handle's, so the launch process group is what names it.

paramself

Returns

typing.Optional

The matching window id. A window whose pid cannot be read at all

func_send_key(self, key) -> bool

Focus the render window and send key through XTEST.

Activating first is what makes this survive the player clicking back into the page: XTEST delivers to whatever holds focus, so a key sent at an unfocused RPCS3 goes to the desktop instead.

paramself
paramkeystr

Returns

bool
funclaunch(self, rom_path, resume_slot) -> None

Stop any running instance and boot rpcs3 for the given ROM.

Installs PKGs and extracts archives as needed, then resolves a savestate to boot into when a resume slot is requested and one exists for the title.

Raises

  • RuntimeError: If the PKG install or the archive extraction fails, or the boot target it produced resolves outside the ROM library and the emulator's own data dirs.
paramself
paramrom_pathPath

RomM's resolved rom path (file or directory).

paramresume_slotOptional[int]

Slot to resume from, or None for a fresh boot.

Returns

None
func_boot_watchdog(self, seq) -> None

Confirm the launched game reaches a running state via PINE.

Also opportunistically resolves the title id via MsgID for boots that had no serial from the file path alone (a bare .iso).

A process still alive when the deadline passes without ever reporting running is the boot-error-dialog case: RPCS3 does not exit on its own, so nothing else in the broker would ever notice.

paramself
paramseqint

Returns

None
funcsave_and_exit(self, slot) -> dict

Send the save-state hotkey, then stop the emulator.

paramself
paramslotOptional[int]

Slot to save to, or None to exit without saving.

Returns

dict

A dict with state_saved, state_slot, and state_file (path,

funcstate_path(self) -> Optional[Path]

Return the state the last saving exit confirmed, or None.

Only a confirmed exit state is served, never the newest file in the title's savestates dir: until the exit's write settles, the newest file there is an older one the archive brought in.

paramself

Returns

typing.Optional

The state file's path, or None when no saving exit has confirmed one

funcstop(self) -> None

Kill the process, invalidating any in-flight boot watchdog first.

paramself

Returns

None
func_session_save_dirs(self) -> list[Path]

This title's save dirs.

A dir qualifies by name (prefixed with the session serial) or by containing a file written while the session ran.

paramself

Returns

list

The save dirs belonging to this session. Empty when nothing was

On this page