asyncapi: 3.0.0
info:
  title: APIExpose Real-time Events
  version: 1.3.3
  description: |
    WebSocket event surface of the APIExpose plugin for RetroBat / EmulationStation.
    Local only (127.0.0.1). Connect to `/ws` for the unfiltered firehose or to
    `/ws/{stream}` for a filtered feed. Some streams replay their last snapshot
    as soon as a client connects (retained messages).

    Live discovery endpoint: `GET http://127.0.0.1:12345/api/v1/ws/streams`
    (always in sync with the running build — this document mirrors it).

    Contract policy: additive JSON only — fields are added, never removed nor renamed.
defaultContentType: application/json

servers:
  local:
    host: 127.0.0.1:12345
    protocol: ws
    description: Local machine only. Nothing leaves the PC.

channels:
  firehose:
    address: /ws
    description: Every event except esevent.* (raw EmulationStation hook traffic).
    messages:
      envelope:
        $ref: '#/components/messages/envelope'
  frontend:
    address: /ws/frontend
    description: >
      Prefixes `ui.` — EmulationStation navigation and lifecycle:
      ui.system.selected, ui.game.selected, ui.game.started, ui.game.ended,
      ui.event, ui.frontend.started, ui.frontend.stopped (frontend process
      lifecycle — replaces polling the emulationstation process).
    messages:
      envelope:
        $ref: '#/components/messages/envelope'
  esevent:
    address: /ws/esevent
    description: >
      Prefixes `esevent.` — raw EmulationStation hook refreshes (esevent.events_refresh).
      Aliases: es-event, es_event, esevents, es-events.
    messages:
      envelope:
        $ref: '#/components/messages/envelope'
  marquee:
    address: /ws/marquee
    description: >
      Prefixes `marquee.` — marquee media snapshots for the selected game/system:
      marquee.snapshot, marquee.snapshot.updated.
      Payload carries `Media` (legacy named fields, kept as-is) plus `Assets` and,
      in game scope, `SystemAssets`: tables keyed by canonical media kind holding
      only what THAT scope really owns. A key is present only when the file is.
    messages:
      envelope:
        $ref: '#/components/messages/envelope'
  topper:
    address: /ws/topper
    description: >
      Prefixes `topper.` — topper media snapshots (topper.snapshot). Payload carries
      `Media.Topper` plus `Assets`/`SystemAssets`: the game identity and printed matter
      a topper can show (flyer, box art, wheel, fanart, screenshot), and `Text`:
      description, genre, players, rating in the language EmulationStation is set to.
    messages:
      envelope:
        $ref: '#/components/messages/envelope'
  instruction-card:
    address: /ws/instruction-card
    description: >
      Prefixes `instruction-card.` — instruction card snapshots
      (instruction-card.snapshot). Aliases: instructioncard, instruction_card, instructions.
      Payload carries `Cards`, `Assets`/`SystemAssets` (wheel, box art, flyer, fanart),
      `Text` (description, genre, players, rating in the EmulationStation language) and
      `Controls`: what each button DOES and its colour, from resources/dynpanels —
      enough to compose a card, not only to show one. The full panel geometry stays on
      /ws/panel.
    messages:
      envelope:
        $ref: '#/components/messages/envelope'
  screen:
    address: /ws/screen
    description: >
      Prefixes `screen.` — secondary screen snapshots (screen.snapshot). Payload carries
      `Media` plus `Assets`/`SystemAssets`: screen-shaped media, with the title screen
      (`image`) and the in-game capture (`thumbnail`) kept apart.
    messages:
      envelope:
        $ref: '#/components/messages/envelope'
  panel:
    address: /ws/panel
    description: >
      Prefixes `panel.`, `theme.` — resolved control panel of the current
      context: panel.state (retained, replayed on connect),
      cpo.panel.config.selected, panel.settings.changed.


      Also carries, on the SAME channel because it is the same subject:


      `panel.config.changed` (retained) — the CABINET's own description: how many
      player panels, how many buttons each, which peripherals, and the rows they
      sit in. Static configuration: it only moves when the user reconfigures, so
      it is retained rather than repeated on every selection.


      `panel.input.pressed` / `panel.input.released` — a physical press, already
      resolved to a panel slot (player, slot, function) and never a raw button
      index. This is what makes a wiring check possible without any LED
      hardware: press a button, see that slot light up. START and SELECT are
      reported as system inputs rather than as "no slot" — they are wired on
      their own pins, outside the numbered slots. These are EVENTS, not state:
      a consumer that coalesces snapshots must not coalesce them, or a press and
      its release cancel each other out.


      `panel.state` also carries `Svg`: the two drawn views of this panel (top
      and 3D front), each with its file path, its size and WHERE each button
      landed inside the drawing. The geometry travels with the file so a
      consumer can light a button exactly where the artwork drew it, without
      recomputing this renderer's layout on its side.
    messages:
      panelState:
        $ref: '#/components/messages/panelState'
  ingame:
    address: /ws/ingame
    description: >
      Prefixes `ingame.`, `retroarch.`, `wrapper.` — in-game memory events:
      ingame.memory.changed, retroarch.memory.changed, ingame.mame.session.started/stopped.
    messages:
      envelope:
        $ref: '#/components/messages/envelope'
  arcade:
    address: /ws/arcade
    description: >
      Prefixes `arcade.`, `mame.`, `fbneo.`, `outputs.` — arcade outputs (lamps)
      and sessions: mame.session.started, mame.output.changed.
    messages:
      envelope:
        $ref: '#/components/messages/envelope'
  score:
    address: /ws/score
    description: >
      Prefix `score.live.` — normalized live score, all sources
      (score.live.changed, retained). Alias: scores.
    messages:
      envelope:
        $ref: '#/components/messages/envelope'
  timer:
    address: /ws/timer
    description: >
      Prefix `timer.live.` — normalized live timer (timer.live.changed, retained).
      Alias: timers.
    messages:
      envelope:
        $ref: '#/components/messages/envelope'
  retroachievements:
    address: /ws/retroachievements
    description: >
      Prefixes `retroachievements.` — session, catalog (retained), challenges,
      leaderboards, unlocks, rich presence, runtime.* inference events.
      Aliases: retroachievement, cheevos.
    messages:
      envelope:
        $ref: '#/components/messages/envelope'
  hiscore:
    address: /ws/hiscore
    description: >
      Prefixes `hiscore.` — high score captures and updates: hiscore.updated,
      hiscore.console.score.captured. Aliases: highscore, highscores, hiscores.
    messages:
      envelope:
        $ref: '#/components/messages/envelope'
  media:
    address: /ws/media
    description: Prefixes `media.` — media store changes.
    messages:
      envelope:
        $ref: '#/components/messages/envelope'
  roms:
    address: /ws/roms
    description: >
      Prefixes `roms.`, `rom-pack.`, `romset.` — Roms Manager and pack installer events.
    messages:
      envelope:
        $ref: '#/components/messages/envelope'
  system:
    address: /ws/system
    description: >
      Prefixes `startup.`, `health.`, `version.`, `hub.`, `config.`,
      `maintenance.`, `rules.`, `notifications.` — service-level events.
    messages:
      envelope:
        $ref: '#/components/messages/envelope'
  control:
    address: /ws/control
    description: >
      Prefixes `commands.`, `intent.`, `es-control.` — command and navigation
      acknowledgements.
    messages:
      envelope:
        $ref: '#/components/messages/envelope'

components:
  messages:
    envelope:
      name: EventEnvelope
      title: Generic event envelope
      payload:
        $ref: '#/components/schemas/Envelope'
    panelState:
      name: PanelState
      title: panel.state (retained snapshot)
      payload:
        $ref: '#/components/schemas/Envelope'
      examples:
        - name: real-capture
          summary: Real frame captured from a running install (truncated payload)
          payload:
            Type: panel.state
            Ts: '2026-07-18T21:11:40.0533714Z'
            NodeId: cab-01
            CorrelationId: 1c513dc1-3e27-4fc1-98cc-99e0f17beb32
            Payload:
              SnapshotVersion: 2
              Source: resources/dynpanels
              Trigger: ui.game.selected.raw
              SelectionKey: mame|flagrall
              Emulator: mame64
              SystemId: mame
              Rom: flagrall
              Scope: game
  schemas:
    Envelope:
      type: object
      description: >
        Every event shares this envelope. `Payload` is event-type specific and
        only ever grows (additive contract).
      required: [Type, Ts, Payload]
      properties:
        Type:
          type: string
          description: Dotted event type (routing key), e.g. ui.game.selected.
          examples: [ui.game.selected, panel.state, mame.output.changed]
        Ts:
          type: string
          format: date-time
          description: Server-side timestamp (UTC).
        NodeId:
          type: string
          description: Emitting node identity.
          examples: [cab-01]
        CorrelationId:
          type: string
          description: Correlates related events of one interaction.
        Payload:
          type: object
          description: Event-type specific body.
