webstation-broker
DeveloperPython reference

imports

Declared imports: save data RomM names, placed by each emulator's own rules.

Declared imports: save data RomM names, placed by each emulator's own rules.

A restored dump puts every member back at its own path. An import cannot work that way: a player's save from another emulator, a card from real hardware or an EmulatorJS export has no path this broker wrote. So RomM declares what each import member is (a save, a state or a memory card), under the .import/<kind>/ prefix and in a version 2 manifest, and the emulator either places it or refuses it with one of REASONS.

Everything here runs in preflight, before the working slot is cleared, so a refusal always leaves the player's slot as it was. The emulator side is four hooks on Emulator (import_spec, place_import, validate_import_plan and identity_source); this module holds the shared machinery they lean on.

attributelog
= logging.getLogger(__name__)
attributeImportKind
= Literal['save', 'state', 'memcard']

What RomM declares an import member to be.

attributeOrigin
= Literal['emulatorjs', 'standalone', 'hardware', 'unknown']

Where RomM says a member came from. Advisory: placement never reads it.

attributeStateChannel
= Literal['archive', 'push', 'none']

How an emulator takes states: in the archive, pushed after activate, or not at all.

attributeIdFamily
= Literal['ps_serial_dashed', 'ps2_card_dir', 'ps_serial_nodash', 'hex8', 'xbox', 'gc_wii_disc', 'hex16', 'wiiu_title', 'dc_product', 'scummvm_target']

The game-id notations NORMALISERS can bring to one canonical form.

attributeREASONSfrozenset[str]
= frozenset({'manifest_invalid', 'unsafe_path', 'unreadable_member', 'kind_not_accepted', 'state_uses_push', 'resume_slot_required', 'memcard_synced_separately', 'source_incompatible', 'needs_conversion', 'unrecognised_layout', 'shape_unverified', 'incomplete_unit', 'destination_unresolvable', 'identity_unknown', 'identity_mismatch', 'protected_destination', 'destination_conflict', 'too_large'})

Every refusal code. The set is closed: RomM branches on these.

attributeREFUSAL_CAP
= 200

Most refusals one 422 lists; the rest are counted in truncated.

attributeHEAD_MAX_BYTES
= 64 * 1024

Most bytes ImportMember.head reads, however many are asked for.

attributeREADABLE_EXPECTED
= 'an unencrypted, intact member, stored or compressed with deflate'

The expected text on every unreadable_member refusal.

attributeLIBRETRO_STATE_RE
= re.compile('^.+\\.state(\\d+|\\.auto)$', re.I | re.ASCII)

A RetroArch numbered or auto state name (.state3, .state.auto), which no standalone loads.

A bare .state is not matched: flycast takes that name as its own.

attributeOWNER_MARKER_SUFFIX
= '.rom'

What flycast and duckstation append to a resume state's name to name its owner marker.

attributeNORMALISERSdict[str, Callable[[str], Optional[str]]]
= {'ps_serial_dashed': _ps_serial_dashed, 'ps2_card_dir': lambda raw: raw.strip().upper() or None, 'ps_serial_nodash': _ps_serial_nodash, 'hex8': _hex8, 'xbox': _xbox, 'gc_wii_disc': _gc_wii_disc, 'hex16': _hex16, 'wiiu_title': _wiiu_title, 'dc_product': _dc_product, 'scummvm_target': lambda raw: raw.strip().casefold() or None}

One normaliser per IdFamily: each brings every notation of an id to one form.

attributeIdentityPolicy
= Literal['none', 'advisory', 'strict', 'required']

How hard a hook holds a member to the session's id.

attributePSP_SAVEDATA_DIR
= re.compile('[A-Z]{4}[0-9]{5}[A-Za-z0-9_\\-]{0,23}', re.ASCII)

A PSP SAVEDATA folder name: a product code, then up to 23 characters the game picks.