webstation-broker
DeveloperPython referenceemulatorsemulators.dolphin

emulators.dolphin.Dolphin

GameCube and Wii sessions on dolphin-emu.

GameCube and Wii sessions on dolphin-emu.

The broker launches dolphin-emu -b, with XDG_DATA_HOME/XDG_CONFIG_HOME pinning the user and config directories, and a run of -C overrides for fullscreen, no stop confirmation, no panic dialogs, analytics consent already answered, and slot A pinned to a GCI folder card. The video backend is left to whatever Dolphin's own layered config already has, so a player's choice in the UI is never clobbered on the next launch. Qt is forced onto xcb so the window lives on Xwayland, where xdotool can reach it. A resume whose state is already on disk, and whose game id matches the disc booting, loads at boot with -s, which is both more reliable than the hotkey and invisible to the player; a resume whose state RomM pushes after activate returns is delivered by a deferred thread over the load hotkey instead. Saving is hotkey only: this launch's own render window is activated, SAVE_KEY is sent through XTEST, and the state directory is polled until the file settles, since the hotkey gives no acknowledgement. A hotkey load is confirmed the other way round, off the access time of the state Dolphin has to read to deserialize it, or off the undo buffer it rewrites on every load where the mount records no access times.

Save data rides the save archive: GC holds the memory cards as loose .gci files, Wii the NAND, so nothing here needs the whole-card routes. A state is named for the game id, so pushed names are restamped into the broker's slot, the working slot is cleared before a boot, and Dolphin's undo-load buffer is dropped on exit so the archive does not carry a second full-size copy of a state RomM already stores.

Declared imports take GameCube GCIs, Wii NAND title saves and one state; see import_spec.

Attributes

attributename
= 'dolphin'

RomM platform key, dolphin.

attributerom_cacheable
= True

On; with the ROM cache enabled, launch boots a local copy of the ROM.

attributedisplay_name
= 'Dolphin'

Human-readable name shown in the UI.

attributeclears_stale_saves
= True

On; activate empties the save subtrees this session owns.

attributesave_root
= USER_DIR

Dolphin's user directory, which the save subtrees hang off.

attributesave_subtrees
= ('StateSaves', 'GC', 'Wii')

StateSaves, GC and Wii, the directories the save archive carries.

attributestate_subtrees
= ('StateSaves',)
attributerom_extensions
= ROM_EXTENSIONS

Bootable disc formats, best first.

attributesupports_states
= True

True, states are saved over the hotkey and loaded at boot or by hotkey.

attributestate_slot
= STATE_SLOT

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

attributestate_dir
= STATE_DIR

Where Dolphin writes .sNN files.

attributelog_path
= DOLPHIN_LOG_PATH

The Dolphin log the broker exposes.

attribute_launch_seq
= 0
attributememory_card_subtreeOptional[str]

GC on a GameCube session, None on any other.

Functions

func__init__(self) -> None

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

paramself

Returns

None
funcmemory_card_path(self, platform=None) -> Optional[Path]

The whole GC/ tree, or None for Wii (NAND, no physical card).

Returns the tree rather than one region's Card A folder: Dolphin buckets a GCI folder card by the disc's region (GC/<region>/Card A), and a library can mix regions, so syncing has to carry all of them rather than guessing one.

paramself
paramplatformOptional[str]
= None

Returns

typing.Optional[pathlib.Path]
funcarrange_card(self, heads) -> memcard.Placement

Move each GCI of a pushed card into <region>/Card A, the only place the folder card reads.

Players pack a card however their own Dolphin keeps it: loose GCIs, a bare Card A folder, or the whole GC folder with its name on top. All of those used to land a level off and boot to an empty card. A wrapper such as GC/ is dropped, and a misplaced GCI goes to the region its game code names. The extension is matched without regard to case, as Dolphin's own folder scan does.

Every member may always stay at the name it was packed under, so a move is taken only when its destination is free of every other member, packed or moved, and a member losing out stays put. Two members can then never land on one file, which matters because anything refused here aborts RomM's claim and locks the player out of the game rather than starting it on an empty card. A GCI or card image left where Dolphin will not read it is logged and reported back.

paramself
paramheadsdict[str, bytes]

Each member's zip name mapped to its first bytes.

Returns

webstation_broker.memcard.Placement

Each member name mapped to its destination under GC, and the

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

Resolve a RomM path to the disc image to boot.

A file is taken as is. A directory is searched one level deep for the best candidate by _pick_rom_file.

paramself
parampathPath

The ROM file or folder RomM handed over.

Returns

typing.Optional

The image to pass to dolphin-emu, or None when there is nothing bootable.

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_render_window(self) -> Optional[str]

Find the window this launch's Dolphin renders the game into.

Picked by title rather than by taking the first match, because the main window and any dialog carry the same class, and a hotkey sent at either of those does nothing. Then confirmed by pid: a window left behind by the previous emulator process carries the same class and the same kind of title, and would swallow the save hotkey for a session that has already moved on.

paramself

Returns

typing.Optional

The X window id as xdotool prints it, or None when this process has no render

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 Dolphin goes to the desktop instead.

paramself
paramkeystr

The key name in xdotool's syntax, for example shift+F1.

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, seed the pad bindings, and start dolphin-emu.

The binary comes from env DOLPHIN_BIN (default dolphin-emu). With resume_slot set and the working slot holding a state for this disc, the state is loaded at boot with -s; with the slot empty or holding another game's state, a deferred thread waits for RomM's push and loads it over the hotkey.

paramself
paramrom_pathPath

The disc image to boot.

paramresume_slotOptional[int]

Slot to resume from, or None to boot clean.

Returns

None
func_deferred_load_state(self, seq, rom_path) -> None

Wait for a pushed state to arrive, then load it over the hotkey.

Gives the file RESUME_LOAD_WAIT to appear, then RESUME_LOAD_SETTLE for the window to be ready. The state has to belong to the ROM that booted, the same test the -s resume makes, since this path also runs for a launch whose slot held another game's state. 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.

paramrom_pathPath

The image that booted, whose game id the state must carry.

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 Dolphin 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: Dolphin drops one that lands before the core is running and says nothing about it, so the state's access time is backdated first and the load only counts once a read has moved it. Where that read leaves no trace, on a noatime mount or when the marker could not be stamped, the undo buffer Dolphin rewrites as part of a load is what confirms it; an unconfirmable load is reported as a failure rather than taken on trust, since the caller would otherwise hand the player a session still sitting where it was. The deserialize the load 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 load has been confirmed 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 save subtrees this session owns before the archive 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 GC cards and the Wii NAND go the same way, and for a stronger reason: they are the player's actual progress, nothing in a .gci or a NAND title directory names whose session wrote it, and the restore only writes the members the incoming archive carries. A card or a NAND save the last player left would otherwise be readable by this one and ship back out in their dump.

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

Subtrees carried by the whole-card routes. GC lands here when a GameCube session syncs its card separately, and the card is written before activate runs, so clearing it here would delete what RomM just laid down.

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>.s<slot> basename in the state dir.

The push route also holds the state's header to the session's game, through check_state_bytes.

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 Dolphin takes: GCIs on GameCube, NAND title saves on Wii, and one state.

The platform picks the spec, as it does memory_card_subtree; any other platform takes nothing. A GCI is taken as a save or a memcard: players think of one as a save. 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 state in the slot, a Wii save in the NAND, a GCI in its card.

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 an imported GCI when the archive already carries a GameCube card.

A folder card holds every title's saves, and an archive carries one card: its own GC members, or imported GCIs, never both. RomM is to strip the archived card, and warn the player, before it sends a replacement.

A GCI that clashes with another member is left to the shared one-member-per-destination check, which has already refused it, so it is not refused twice.

paramself
paramplanlist[imports.Placement]

The placements that passed every per-member check.

paramctximports.ImportCtx

The launch context; its archive_paths are the archive's ordinary members.

Returns

list

One destination_conflict per imported GCI, or none.

funccheck_state_bytes(self, head) -> bool

Refuse a pushed state whose header names another game than the session's.

A Dolphin state opens with the game id of the disc it was taken from. A header with no id, or a session with none, is taken on trust.

paramself
paramheadbytes

Up to STATE_HEAD_BYTES bytes from the start of the pushed state.

Returns

bool

False when the header's game code is not the session's.

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

Read the session's game id off the disc, or take RomM's when the format hides it.

paramself

Returns

typing.Optional

A GameCube/Wii disc-id source that reads the rom through _rom_game_id.

func_drop_undo_buffer(self) -> None

Delete the undo-load-state buffer before the save archive is built.

Dolphin rewrites this on every state load, so it lands in the dump as a second full-size copy of a state RomM is already storing, for the sake of an undo hotkey a streaming session has no way to press. A failure to remove it is logged, not raised.

paramself

Returns

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

Save a state if asked, stop the emulator, and clear what the archive must not carry.

The undo buffer and any stranded access-time probe both live in the state directory the exit archive is built from, and neither is save data, so both go before the dump runs.

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