webstation-broker
DeveloperPython referenceemulators

emulators.flycast

Flycast (Sega Dreamcast) launcher.

Flycast (Sega Dreamcast) launcher.

Handles ROM resolution, save state via a graceful close request, and boot-time resume.

Flycast has no control socket and, unlike DuckStation, installs no SIGTERM/SIGINT handler at all: a bare SIGTERM is a hard kill. Its only graceful-shutdown path is SDL_QUIT (a window close request), which dc_exit() turns into unloadGame(), and that is also the only place Dreamcast.AutoSaveState gets written.

Getting a real SDL_QUIT out of it takes more than an xdotool window-close command: windowclose is a raw XDestroyWindow, which never reaches SDL's own event loop, and windowquit's _NET_CLOSE_WINDOW is sent to the root window for a window manager to pick up, but this container's compositor (labwc, on wlroots) has no handler for that message at all, only for its own internal Close action. That action is what actually sends a real WM_DELETE_WINDOW to the window (wlroots' wlr_xwayland_surface_close()), and the only way to reach it externally is the same keybind a player would use: Alt+F4, which labwc's default config binds to Close. stop() therefore activates the window and sends that keypress, exactly the hotkey-injection shape dolphin.py/ppsspp.py already use for their own hotkeys, before falling back to the base SIGTERM->SIGKILL, the same graceful-trigger-then-escalate shape shadPS4 uses for its IPC STOP command.

Everything the broker needs (auto-load, auto-save, slot, fullscreen) rides -config section:key=value transient overrides on the command line rather than a patched ini, so nothing here touches emu.cfg. The section for the typed Dreamcast.* options is "config" (that's the whole key, dot included; see core/cfg/option.cpp), not "Dreamcast" as the key's own name might suggest.

Save state is a single deterministic file, <rom-basename>.state, in the flat XDG data dir ($XDG_DATA_HOME/flycast or ~/.local/share/flycast, same resolution flycast itself does). With slot pinned to 0 the broker can compute that path itself instead of guessing at the newest or most-recently-changed file in the directory. clear_working_slot() still has to sweep the directory though: the name is only unambiguous once the rom is known, and that happens after clear_working_slot() runs (activate resolves the rom, then clears the slot, then launches), so a leftover from an earlier local session for the same title would otherwise sit there with a newer mtime than the incoming archive member and get skipped by extract_save_archive's newer-file guard, same as DuckStation's problem.

VMU saves are loose files in the same flat data dir (vmu_save_A1.bin etc., shared across games by default). The whole directory ships as one save subtree rather than named files, since nothing here otherwise separates VMU saves from savestates from a first-run onboarding artifact.

attributelog
= logging.getLogger(__name__)
attributeROM_ROOT
= Path(os.environ.get('ROM_ROOT', '/romm'))
attributeFLYCAST_BIN
= os.environ.get('FLYCAST_BIN', '/opt/flycast/AppRun')
attributeDATA_DIR
= Path(os.environ.get('FLYCAST_DATA_DIR', _default_data_dir()))
attributeFLYCAST_LOG_PATH
= Path(os.environ.get('FLYCAST_LOG_PATH', '/config/flycast.log'))
attributeROM_EXTENSIONS
= ('.chd', '.gdi', '.cdi', '.cue', '.elf')