Skip to content

Latest commit

 

History

History
205 lines (169 loc) · 10.4 KB

PROTOCOL.md

File metadata and controls

205 lines (169 loc) · 10.4 KB

Zipper wire protocol

Everything rides Streamer.bot's WebSocket bus as General.Custom broadcasts (CPH.WebsocketBroadcastJson). Three payload types, all carrying a type key.

ticker:update

Broadcast by the Ticker Push action — either because the sidecar pushed a fresh payload (as the tickerPayload action argument), or because an overlay (re)connected and asked for the persisted replay. The C# action stores the last payload in the persisted global ticker.payload, so a freshly-added OBS source paints immediately.

{
  "type": "ticker:update",
  "tournament": {
    "name": "BLKO Weekly #42",
    "platform": "challonge",
    "url": "https://challonge.com/blko42",
    "state": "in_progress"
  },
  "generatedAt": "2026-07-11T20:15:00Z",
  "matches": [
    { "id": "9001", "round": "Winners Semis", "state": "completed",
      "p1": { "name": "Flash", "score": 2 }, "p2": { "name": "Newbie", "score": 0 },
      "winner": 1, "order": 1 }
  ],
  "standings": [ { "placement": 1, "name": "Flash" } ],
  "caps": { "text": "@FlashGalatine", "logo": "/zipper-overlay/assets/logo.png" },
  "message": { "lines": ["Bracket starts at 8pm ET", "!discord for the lobby code"] }
}

Rules:

  • matches[].state ∈ completed | in_progress | upcoming.
  • winner ∈ 0 | 1 | 2 (0 = undecided). Scores are integers or null (null renders as "vs").
  • Matches arrive pre-sorted: newest completed first, then in-progress, then upcoming — capped at the sidecar's maxItems (default 20). Upcoming TBD-vs-TBD matches are dropped before the cap.
  • standings is populated when a platform can't provide matches (Matcherino); the overlay renders the standings crawl only when matches is empty.
  • caps travels inside the payload so the overlay needs no separate config fetch; empty string hides that cap.
  • tournament.state ∈ upcoming | in_progress | completed.
  • message is present only while persistent message mode is on and its text is non-blank; the overlay then crawls message.lines (one item per line) instead of the results. See below.

Persistent message mode

A configured message that holds the strip until you switch it off — for "bracket starts at 8pm", a Discord plug, a rules line. Set it from the control page's Persistent message box (textarea + a Show this instead of results checkbox), or from chat / a Stream Deck via the single-field commands.

"message": { "enabled": true, "text": "Bracket starts at 8pm ET\n!discord for the lobby code" }

Rules:

  • One non-blank line per ticker item, trimmed, capped at 20 lines / 1000 characters. The marquee repeats them to fill the strip, exactly as it does for a short results set.
  • Enabled with blank text is the same as off — the payload's message key is omitted and results show. That's the fallback for any malformed line list too.
  • Results keep polling underneath: the same payload still carries matches and standings, so switching the message off restores live results on the spot with no re-poll.
  • It needs no tournament at all. Message edits push immediately rather than riding the next poll (which, with polling off, would never come), and the sidecar re-pushes a configured message when it connects to Streamer.bot, so a message-only setup survives a restart. For the same reason end-cap changes now push immediately when polling is off.
  • ticker:announce still interrupts it — announcements cover the strip for their duration and the message resumes.
  • Persisted in config.json (message), so it survives a sidecar restart.

ticker:status

Ephemeral sidecar status for the control page. It must not clobber the persisted update payload, so it rides the Ticker Command relay as the reserved command __status with the status JSON as value:

{ "type": "ticker:command", "command": "__status",
  "value": "{\"type\":\"ticker:status\",\"polling\":true,\"url\":\"…\",\"platform\":\"challonge\",\"lastPollAt\":\"…\",\"lastError\":\"\",\"intervalMs\":30000,\"maxItems\":20,\"caps\":{…}}" }

The sidecar ignores its own __status echo; the control page unwraps value. Sent after every poll and after every command.

The status carries an auto block describing the auto-scheduler (see setAutoSchedule below):

"auto": { "enabled": true, "eventState": "upcoming",
  "startAt": "2026-08-01T00:00:00.000Z", "waitingUntil": "2026-08-01T00:00:00.000Z",
  "stopped": null }
  • eventState / startAt — what the last successful poll said about the event (startAt is null when the platform has no date; Matcherino never reports either).
  • waitingUntil — set while polling is on but sleeping for the start time; the next poll fires at that instant.
  • stopped — "completed" after the scheduler stopped polling itself, null otherwise.

ticker:command

Control surfaces (control page, chat trigger, Stream Deck) call the Ticker Command action with command/value arguments; the C# side re-broadcasts

{ "type": "ticker:command", "command": "setUrl", "value": "https://challonge.com/blko42" }

which the sidecar consumes. Commands:

command value effect
setUrl tournament URL switch tournaments (forces a re-push on next poll)
start — start polling
stop — stop polling
pollNow — immediate poll (starts polling if stopped)
setInterval milliseconds (min 15000) poll cadence
setCaps JSON {text,logo} batched — sets both end caps in one command (the control page uses this)
setOptions JSON {intervalMs,maxItems,topN} batched — sets all three options in one command
setMessage JSON {text,enabled} batched — persistent message text + on/off in one command (the control page uses this)
setMessageText string (≤1000 chars, \n-separated lines) message text (single-field; for chat / Stream Deck)
setMessageEnabled on/off (also true/false, 1/0, yes) show the message instead of results
setCapsText string (≤120 chars) right-cap static text (single-field; for chat / Stream Deck)
setCapsLogo URL/path (≤500 chars) left-cap logo image (single-field)
setInterval above, setMaxItems, setTopN see below single-field variants
setMaxItems 1–100 ticker item cap
setTopN 0–100 (0 = off) only show matches involving the top-N placed participants (needs platform standings — start.gg always; Challonge via final_rank on the keyed API for your own tournaments, or derived from elimination order on finished brackets when scraping; also trims the standings crawl to N)
setAutoSchedule on/off (default on) auto start/stop: a poll that returns completed pushes the final payload then stops polling; one that returns upcoming sleeps until the platform's start time (re-checking every 5 min without one, and at most every 6 h for far-off dates) and resumes normal polling when the event goes live. start/pollNow always force a fresh poll regardless.
status — request a status re-broadcast

Why the batched setCaps / setOptions exist. Streamer.bot bleeds arguments between action invocations that arrive in a burst — firing two DoActions to the same action within a few milliseconds shuffles their args together (verified on SB 1.0.4). A control page that set caps text and logo as two back-to-back setCapsText + setCapsLogo commands would have the second's value land on the first, silently blanking the text. So each control button sends exactly one command carrying a JSON value, and panel-core additionally serializes outbound DoActions (one in flight at a time). The single-field commands remain for chat / Stream Deck, where one action fires at a time.

ticker:announce

Interrupts the strip: the overlay pauses the results crawl in place, shows the announcement (kind pill + text) for duration seconds, then resumes. Announcements queue (up to 10) and play back-to-back. Ephemeral — never persisted, so reconnecting overlays don't replay them.

{ "type": "ticker:announce", "kind": "raid", "text": "Flash is raiding with 42 viewers!", "duration": 8 }
  • kind ∈ sub | raid | milestone | custom (styles the pill; anything else renders as custom). duration is clamped 2–30 s (default 8).
  • Broadcast by the Ticker Announce action. Sources:
    • SB Twitch triggers (subs, gift subs, raids): add the action to the trigger; with no text arg it auto-composes from the trigger's user / viewers arguments. Pass explicit text (Set Argument) to customize.
    • Control page: the Announcement box DoActions it directly.
    • Sidecar milestones: when a StreamElements session counter crosses a configured step (see below), the sidecar fires it with kind milestone.

StreamElements milestones (sidecar)

config.json:

"streamElements": { "jwtToken": "…", "pollSeconds": 60 },
"milestones": { "followers": 100, "subs": 25, "tips": 50, "cheers": 0 }

The sidecar decodes the channel id from the JWT and polls api.streamelements.com/kappa/v2/sessions/<channel> (the same recipe as the Midnight Velvet goal meter). Each milestones entry is a step size — e.g. followers: 100 announces at 300, 400, 500 followers ("300 followers — thank you!"). 0 disables a metric. The first successful poll only records the current thresholds (no announcement blast when you first configure it), and announced thresholds persist in config.json (milestoneState) so restarts don't repeat them. The JWT never travels the bus — ticker:status carries only se: { configured, lastPollAt, lastError }.

ticker:error

Broadcast by either C# action when it throws, so failures are visible instead of silent: { "type": "ticker:error", "message": "…" }.

Test hook

A tournament URL of the form stub:<path-relative-to-repo> makes the sidecar load a normalized platform result ({tournamentName, state, matches, standings}) from that JSON file instead of the network — used by verify.mjs (stub:platforms/fixtures/stub-result.json) and handy for offline styling.