webstation-broker
DeveloperPython referenceemulatorsemulators.ppsspp

emulators.ppsspp.Ppsspp

Sony PSP sessions on PPSSPPQt.

Sony PSP sessions on PPSSPPQt.

The broker launches PPSSPPQt --fullscreen -- <rom> after patching both of PPSSPP's inis: ppsspp.ini so a fresh container never shows the setup wizard or an update toast and so the working state slot is pinned, and controls.ini so Save State and Load State are bound to the bracket keys, the only hotkeys that reach PPSSPP through this container's input stack. Both files carry a UTF-8 BOM that the patcher strips and restores. Save and load are hotkey only: the game window is activated, the key is sent through XTEST, and because the hotkey gives no acknowledgement, a save polls the state directory until the file settles and a load watches the state's access time until PPSSPP reads it back. There is no boot-time state-load flag, so a resume always goes through a deferred thread that waits for the state file, polls for the game window, and only then sends the load hotkey.

Save data (SAVEDATA) and states (PPSSPP_STATE) both ride the save archive. A state is named for the game id and version, so pushed names are restamped into the broker's slot and the working slot is cleared before a boot. PPSSPP writes a .jpg of its own beside every state, and clearing a state drops that too.

Declared imports take PSP save folders and one state with its screenshot; see import_spec.

Attributes

attributename
= 'ppsspp'

RomM platform key, ppsspp.

attributedisplay_name
= 'PPSSPP'

Human-readable name shown in the UI.

attributeclears_stale_saves
= True

On; activate empties both save subtrees.

attributesave_root
= PSP_DIR

The emulated memory stick root, which the save subtrees hang off.

attributesave_subtrees
= ('SAVEDATA', 'PPSSPP_STATE')

SAVEDATA and PPSSPP_STATE, the directories the save archive carries.

attributestate_subtrees
= ('PPSSPP_STATE',)
attributesupports_states
= True

True, states are saved and loaded over the bracket hotkeys.

attributestate_slot
= STATE_SLOT

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

attributestate_dir
= STATE_DIR

Where PPSSPP writes .ppst files.

attributelog_path
= PPSSPP_LOG_PATH

The PPSSPP log the broker exposes.

attribute_launch_seq
= 0
attributerom_extensionstuple[str, ...]

Bootable ROM formats, best first, then the archive formats while the cache is on.

Functions

func__init__(self) -> None

Set up the process state and the launch sequence counter that fences deferred loads.

paramself

Returns

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

Resolve a RomM path to the ROM to boot.

A file is taken as is, an archive once its members show a PSP image. A directory is searched one level deep for the best candidate by _pick_rom_file, and when it holds none, a lone archive holding one is taken instead. A pattern that cannot be walked (an unreadable subdirectory, a broken mount) costs only what that pattern would have contributed: the candidates already found still rank, since reporting a title unbootable over one bad directory is worse than booting the best of what could be read.

paramself
parampathPath

The ROM file or folder RomM handed over.

Returns

typing.Optional

The ROM or archive to pass to launch, or None when there is

func_archive_bootable(self, archive) -> bool

Whether an archive can be booted: the cache is on and a member is a PSP image.

paramself
paramarchivePath

The archive, already checked to sit under the ROM root.

Returns

bool

True when launch can extract and boot it; a refusal is logged.

func_folder_archive(self, candidates, base) -> Optional[Path]

A ROM folder's lone archive, for a folder holding no PSP image of its own.

Only the shallowest archives count, so one in an extras subfolder never makes the game's own archive ambiguous. Several at that depth are refused rather than guessed between.

paramself
paramcandidateslist[Path]

The paths found under base.

parambasePath

The ROM folder.

Returns

typing.Optional

The resolved archive, or None when there is none, more than one

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

Run one xdotool command against the session display.

paramself
paramargsstr
= ()

Returns

typing.Optional

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

func_game_window(self, log_missing=True) -> Optional[str]

Find the window this launch is running the game in.

Two things have to hold. The window must belong to the process this broker spawned, since a window left behind by the previous emulator process carries the same class and title shape and would swallow the save hotkey, and its title must carry _GAME_TITLE_MARK, which PPSSPP only appends once a game is loaded: before that the window is a menu a hotkey does nothing useful to.

paramself
paramlog_missingbool
= True

Whether a miss is worth a warning. Off while polling a booting emulator, where a miss is the expected answer until the game comes up.

Returns

typing.Optional

The X window id as xdotool prints it, or None when this launch has no game window up.

func_wait_for_game_window(self, deadline) -> bool

Poll until this launch has a game window up, or deadline passes.

paramself
paramdeadlinefloat

time.monotonic value to give up at.

Returns

bool

True once _game_window names a window, False on timeout.

func_send_key(self, key) -> bool

Focus the game 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 PPSSPP goes to the desktop instead.

paramself
paramkeystr

The key name in xdotool's syntax, for example bracketleft.

Returns

bool

True when the window was found, activated and the key sent, False otherwise.

funclaunch(self, rom_path, resume_slot) -> None

Stop any running instance, patch the inis, and start PPSSPPQt.

The binary comes from env PPSSPP_BIN (default PPSSPPQt). With resume_slot set, a deferred thread waits for the state file and loads it over the hotkey once the window is up.

An archive is extracted into the cache first (or its earlier extraction reused), and the PSP image inside it is what boots.

Raises

  • RuntimeError: When either ini could not be patched or an archive could not be extracted, so nothing is spawned; see _patch_ini_file.
paramself
paramrom_pathPath

The ROM or archive to boot.

paramresume_slotOptional[int]

Slot to resume from, or None to boot clean.

Returns

None
func_deferred_load_state(self, seq) -> None

Wait for the resume state and a booted game, then load it over the hotkey.

Both waits share RESUME_LOAD_WAIT. The window wait is the load-bearing one: a resume state restored from the save archive is already on disk when the thread starts, so waiting only on the file would fire the one hotkey this path ever sends seconds into a boot with no window to take it, losing the resume silently. Once the game window is up, RESUME_LOAD_SETTLE covers the rest of PPSSPP's own boot. Abandons itself whenever 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 a state into the broker's slot over the hotkey and wait for it to land.

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.

paramself
paramslotint

The slot RomM requested; not used.

Returns

bool

True once the state file has been written and settled within STATE_WAIT, False if

funcload_state(self, slot) -> bool

Load the broker's slot over the hotkey and confirm PPSSPP read the state back.

The hotkey is silent on an empty slot, so an absent file has to be caught here or the caller reads a no-op as success. A hotkey that was sent is no proof either: PPSSPP swallows one that lands on an unfocused window or a core still registering HLE modules, so the state's access time is backdated first and the load only counts once a read has moved it. The deserialize that read starts is unobservable, and LOAD_SETTLE covers it rather than handing the caller a session mid-restore.

paramself
paramslotint

The slot RomM requested; the broker's STATE_SLOT is what gets loaded.

Returns

bool

True once the state has been read back and settled, False when the slot is empty,

funcstate_path(self) -> Optional[Path]

Return the newest state file in the broker's slot, or None when it holds nothing.

paramself

Returns

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

Empty the state tree and the memory stick saves before the restore.

A state is named for the game it was taken from, and the game id only comes off the running disc, so a leftover cannot be told apart from the state of the game about to boot. Anything still here belongs to a session that has already exited and whose states RomM holds.

The staging files go with them. A session killed mid-save leaves one behind with no state to pair it against, and the save archive sweeps up whatever sits in the state tree, so it would ship to RomM as a state of its own. So would a .jpg whose state is already gone.

SAVEDATA is cleared for the stronger reason: it is the memory stick, the player's actual in-game progress, keyed by game id with nothing in the path naming whose session wrote it. PPSSPP has no whole-card route to move it on, so the archive is the only thing that should ever put a save there, and a directory the last player left would otherwise be loaded by this one and ship back out in their dump.

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

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

Returns

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

Map a pushed state's filename to where it may be written.

With the slot already holding a state, a pushed name has to match it; otherwise the game id is taken on trust, bounded to a <game>_<slot>.ppst basename in the state dir.

paramself
paramfilenamestr

The basename RomM is pushing.

Returns

typing.Optional

The path to write to, or None when the name is not a plain, printable basename, is not

funcimport_spec(self) -> imports.ImportSpec

Declare what PPSSPP takes: PSP save folders, and one state with its screenshot.

Every folder under SAVEDATA is a unit that must hold PARAM.SFO, the file PPSSPP lists a save by. 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 under SAVEDATA, a state in the slot.

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 screenshot whose state is not in the plan.

paramself
paramplanlist[imports.Placement]

The placements that passed every per-member check.

paramctximports.ImportCtx

The launch context.

Returns

list

One incomplete_unit per orphaned screenshot.

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

Take the session's product code from RomM; a PSP image is not read for one.

paramself

Returns

typing.Optional

The source.

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

Save a state if asked, then stop the emulator.

paramself
paramslotOptional[int]

Slot RomM asked to save into (resolved to STATE_SLOT), or None to exit without saving a state.

Returns

dict

A dict with state_saved (bool), state_slot (the effective slot, or None when no

funcstop(self) -> None

Invalidate any in-flight deferred state load before the kill.

paramself

Returns

None

On this page