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= TrueOn; 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= TrueOn; activate empties the save subtrees this session owns.
attributesave_root= USER_DIRDolphin'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_EXTENSIONSBootable disc formats, best first.
attributesupports_states= TrueTrue, states are saved over the hotkey and loaded at boot or by hotkey.
attributestate_slot= STATE_SLOTThe one slot the broker works in, echoed back as the effective slot.
attributestate_dir= STATE_DIRWhere Dolphin writes .sNN files.
attributelog_path= DOLPHIN_LOG_PATHThe Dolphin log the broker exposes.
attribute_launch_seq= 0attributememory_card_subtreeOptional[str]GC on a GameCube session, None on any other.
Functions
func__init__(self) -> NoneSet up the process state and the launch sequence counter that fences deferred loads.
paramselfReturns
Nonefuncmemory_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.
paramselfparamplatformOptional[str]= NoneReturns
typing.Optional[pathlib.Path]funcarrange_card(self, heads) -> memcard.PlacementMove 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.
paramselfparamheadsdict[str, bytes]Each member's zip name mapped to its first bytes.
Returns
webstation_broker.memcard.PlacementEach 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.
paramselfparampathPathThe ROM file or folder RomM handed over.
Returns
typing.OptionalThe 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.
paramselfparamargsstr= ()Returns
typing.OptionalIts 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.
paramselfReturns
typing.OptionalThe X window id as xdotool prints it, or None when this process has no render
func_send_key(self, key) -> boolFocus 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.
paramselfparamkeystrThe key name in xdotool's syntax, for example shift+F1.
Returns
boolTrue when the window was found, activated and the key sent, False otherwise.
funclaunch(self, rom_path, resume_slot) -> NoneStop 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.
paramselfparamrom_pathPathThe disc image to boot.
paramresume_slotOptional[int]Slot to resume from, or None to boot clean.
Returns
Nonefunc_deferred_load_state(self, seq, rom_path) -> NoneWait 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.
paramselfparamseqintThe launch sequence number this load belongs to.
paramrom_pathPathThe image that booted, whose game id the state must carry.
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 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.
paramselfparamslotintThe slot RomM requested; the broker's STATE_SLOT is what gets loaded.
Returns
boolTrue 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.
paramselfReturns
typing.Optional[pathlib.Path]funcclear_working_slot(self, excluded=()) -> NoneEmpty 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.
paramselfparamexcludedtuple[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
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>.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.
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 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.
paramselfReturns
webstation_broker.imports.ImportSpecThe 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.
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 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.
paramselfparamplanlist[imports.Placement]The placements that passed every per-member check.
paramctximports.ImportCtxThe launch context; its archive_paths are the archive's ordinary members.
Returns
listOne destination_conflict per imported GCI, or none.
funccheck_state_bytes(self, head) -> boolRefuse 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.
paramselfparamheadbytesUp to STATE_HEAD_BYTES bytes from the start of the pushed state.
Returns
boolFalse 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.
paramselfReturns
typing.OptionalA GameCube/Wii disc-id source that reads the rom through _rom_game_id.
func_drop_undo_buffer(self) -> NoneDelete 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.
paramselfReturns
Nonefuncsave_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.
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