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= TrueOn; activate empties both save subtrees.
attributesave_root= PSP_DIRThe 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= TrueTrue, states are saved and loaded over the bracket hotkeys.
attributestate_slot= STATE_SLOTThe one slot the broker works in, echoed back as the effective slot.
attributestate_dir= STATE_DIRWhere PPSSPP writes .ppst files.
attributelog_path= PPSSPP_LOG_PATHThe PPSSPP log the broker exposes.
attribute_launch_seq= 0attributerom_extensionstuple[str, ...]Bootable ROM formats, best first, then the archive formats while the cache is on.
Functions
func__init__(self) -> NoneSet up the process state and the launch sequence counter that fences deferred loads.
paramselfReturns
Nonefuncresolve_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.
paramselfparampathPathThe ROM file or folder RomM handed over.
Returns
typing.OptionalThe ROM or archive to pass to launch, or None when there is
func_archive_bootable(self, archive) -> boolWhether an archive can be booted: the cache is on and a member is a PSP image.
paramselfparamarchivePathThe archive, already checked to sit under the ROM root.
Returns
boolTrue 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.
paramselfparamcandidateslist[Path]The paths found under base.
parambasePathThe ROM folder.
Returns
typing.OptionalThe 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.
paramselfparamargsstr= ()Returns
typing.OptionalIts 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.
paramselfparamlog_missingbool= TrueWhether 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.OptionalThe X window id as xdotool prints it, or None when this launch has no game window up.
func_wait_for_game_window(self, deadline) -> boolPoll until this launch has a game window up, or deadline passes.
paramselfparamdeadlinefloattime.monotonic value to give up at.
Returns
boolTrue once _game_window names a window, False on timeout.
func_send_key(self, key) -> boolFocus 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.
paramselfparamkeystrThe key name in xdotool's syntax, for example bracketleft.
Returns
boolTrue when the window was found, activated and the key sent, False otherwise.
funclaunch(self, rom_path, resume_slot) -> NoneStop 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.
paramselfparamrom_pathPathThe ROM or archive to boot.
paramresume_slotOptional[int]Slot to resume from, or None to boot clean.
Returns
Nonefunc_deferred_load_state(self, seq) -> NoneWait 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.
paramselfparamseqintThe launch sequence number this load belongs to.
Returns
Nonefuncsave_state(self, slot) -> boolSave 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.
paramselfparamslotintThe slot RomM requested; not used.
Returns
boolTrue once the state file has been written and settled within STATE_WAIT, False if
funcload_state(self, slot) -> boolLoad 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.
paramselfparamslotintThe slot RomM requested; the broker's STATE_SLOT is what gets loaded.
Returns
boolTrue 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.
paramselfReturns
typing.Optional[pathlib.Path]funcclear_working_slot(self, excluded=()) -> NoneEmpty 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.
paramselfparamexcludedtuple[str, ...]= ()Subtrees carried by the whole-card routes. PPSSPP names no memory card subtree, so this is always empty.
Returns
Nonefuncstate_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.
paramselfparamfilenamestrThe basename RomM is pushing.
Returns
typing.OptionalThe path to write to, or None when the name is not a plain, printable basename, is not
funcimport_spec(self) -> imports.ImportSpecDeclare 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.
paramselfReturns
webstation_broker.imports.ImportSpecThe 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.
paramselfparammemberimports.ImportMemberThe member, already past the kind gate.
paramspecimports.ImportSpecThis emulator's spec.
paramctximports.ImportCtxThe launch context.
Returns
typing.UnionThe placement, or a refusal.
funcvalidate_import_plan(self, plan, ctx) -> list[imports.ImportRefusal]Refuse a screenshot whose state is not in the plan.
paramselfparamplanlist[imports.Placement]The placements that passed every per-member check.
paramctximports.ImportCtxThe launch context.
Returns
listOne 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.
paramselfReturns
typing.OptionalThe source.
funcsave_and_exit(self, slot) -> dict[str, Any]Save a state if asked, then stop the emulator.
paramselfparamslotOptional[int]Slot RomM asked to save into (resolved to STATE_SLOT), or None to exit
without saving a state.
Returns
dictA dict with state_saved (bool), state_slot (the effective slot, or None when no
funcstop(self) -> NoneInvalidate any in-flight deferred state load before the kill.
paramselfReturns
None