webstation-broker
DeveloperPython referenceemulatorsemulators.flycast

emulators.flycast.Flycast

Exit-only Flycast (Sega Dreamcast) launcher.

Exit-only Flycast (Sega Dreamcast) launcher.

Save state is triggered by a graceful window-close request (Alt+F4) rather than a control socket, since Flycast has no other clean shutdown path; see the module docstring for the full hardware/protocol rationale.

Attributes

attributename
= 'flycast'
attributerom_cacheable
= True
attributedisplay_name
= 'Flycast'
attributesave_root
= DATA_DIR.parent
attributesave_subtrees
= (DATA_DIR.name,)
attributeclears_stale_saves
= True
attributerom_extensions
= ROM_EXTENSIONS
attributelog_path
= FLYCAST_LOG_PATH
attributeterm_timeout
= float(os.environ.get('FLYCAST_STOP_WAIT', '20'))

Functions

func__init__(self) -> None

Initialize the emulator with no ROM loaded yet.

paramself

Returns

None
funcsave_file_kind(self, rel) -> str

Classify an archive member for the manifest.

Flycast keeps the savestate loose beside the VMU images, so the subtree-based default cannot split them; the .state suffix can.

paramself
paramrelstr

The member path, relative to save_root and posix-separated.

Returns

str

state for the savestate, save for everything else.

funcimport_spec(self) -> imports.ImportSpec

Declare what Flycast takes: VMU and flash saves, raw VMU cards, and one resume state.

The state rides the archive and resumes through save.resume_slot, like the one the broker saves on exit. There is still no mid-session save or load, so supports_states stays False.

paramself

Returns

webstation_broker.imports.ImportSpec

The spec. Its paths follow DATA_DIR, so it is built on every call.

funcplace_import(self, member, spec, ctx) -> Union[imports.Placement, imports.ImportRefusal]

Place one declared member in the flat data dir.

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.

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

Resolve a ROM path to a single playable disc image or file.

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

A direct file path, or a folder searched up to one level deep for the best candidate, ranked by disc number, extension preference, and path depth.

Returns

typing.Optional

The resolved file path, or None if no suitable candidate was

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

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

paramself
paramargsstr
= ()

Returns

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

The window id belonging to this launch's own process, or None.

Matched by title then confirmed by pid rather than trusting the first title hit, since Alt+F4 acts on whatever id comes back and a stray match would close the wrong thing instead of just failing a hotkey.

paramself

Returns

typing.Optional[str]
func_close_request(self, win_id) -> bool

Activate the emulator's own window and send it Alt+F4.

The keystroke goes through XTEST (no key --window) on purpose: Alt+F4 is a labwc keybind, and the compositor only acts on input that reaches the seat, so a synthetic event delivered straight to the client window would never fire its Close action and flycast would never see SDL_QUIT. XTEST lands on whatever holds focus, so focus is read back and the request is abandoned when a different window owns it, rather than closing something the player did not ask to close.

paramself
paramwin_idstr

Window id already confirmed to belong to this launch.

Returns

bool

True when the keystroke was sent, False when the window could not

funclaunch(self, rom_path, resume_slot) -> None

Stop any running instance and launch Flycast for the given ROM.

paramself
paramrom_pathPath

The resolved disc image or file to boot.

paramresume_slotOptional[int]

If not None and a loadable resume state claimed by this rom exists, enable auto-load on boot; otherwise boot fresh.

Returns

None
funcstop(self) -> None

Request a graceful exit via Alt+F4, then escalate if it does not take.

Activates the emulator's own window and sends Alt+F4 to trigger Flycast's only clean-shutdown path (SDL_QUIT). Falls back to the base class's SIGTERM/SIGKILL escalation if no window can be found, activated or focused, or if the process has not exited within term_timeout.

paramself

Returns

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

Drop every session-owned file left in DATA_DIR before a restore.

Resume states, their owner markers, VMU images, arcade nvmem and eeprom all sit loose in one flat directory, none of them namespaced by title or by player, and this runs before the rom is even known (activate resolves the rom, clears the slot, then launches). A leftover would either outrank its incoming archive member by mtime and get skipped by extract_save_archive's newer-file guard, or, with no member to replace it at all, be handed to the next player and swept into their exit archive as their own progress. Anything still here belongs to a session that already exited, so dropping it all is the same trade-off DuckStation's clear_working_slot makes.

CONTAINER_FILES and subdirectories (flycast's controller mappings) are left: they are container setup, not one session's data. So are states set aside under UNTRUSTED_SUFFIX, which can be the only copy of that progress and which no resume can pick up anyway.

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

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

Returns

None
func_set_aside_untrusted_state(self, path) -> None

Rename a resume state that a force-killed exit may have torn, marker and all.

Size and mtime are all the broker has to judge a state by, and that is enough to refuse to resume from one but not enough to destroy the only copy of a player's progress, so the file is moved to a UNTRUSTED_SUFFIX sidecar instead of unlinked. A previous sidecar for the same rom is replaced, which bounds the leftovers at one per title. The owner marker travels with it: left behind it would claim the next state flycast writes under the same basename.

paramself
parampathPath

The resume state to move aside.

Returns

None
funcsave_and_exit(self, slot) -> dict

Stop the emulator and report whether this exit actually saved state.

A state the exit may have torn is not reported and not resumed from, but it is kept: _set_aside_untrusted_state renames it rather than deleting it. That quarantine does not depend on slot, since AutoSaveState is on for every launch and the caller dumps DATA_DIR by mtime whether or not a slot was asked for.

paramself
paramslotOptional[int]

The resume slot to report on. If None, a torn state is still caught and set aside, but a good one is not waited for or reported.

Returns

dict

A dict with "state_saved" (bool), "state_slot" (the given

On this page