A Streamer.bot-native tournament scorebug for OBS, Streamlabs Desktop, XSplit — any streaming app with a browser source. Player names, scores, and nationality flags on stream — driven from Twitch chat, a Stream Deck, hotkeys, or a small browser control panel. No dedicated server: Streamer.bot serves the overlays and holds the state.
Type !sb p1flag japan in chat and the flag appears. Import a start.gg or Challonge
bracket once and every entrant's name autocompletes in the control panel — with their
flag. Made for FGC / esports streams that run fast matches and don't want to alt-tab.
| Overlay | the bundled Primetime theme — an ESPN-style title plate + one combined strip per player (score · flag · name), with auto-fit type and score-bump/name-swap animation |
| Teams | 2 by default, up to 4 (!sb teams 4 — see below); or use a single strip as a plain on-stream counter (deaths, attempts, …) |
| State | a handful of Streamer.bot global variables (persist across restarts) |
| Control | chat commands, Stream Deck, hotkeys, and tally-shared/control.html |
| Roster | import a bracket (start.gg, Challonge, TourneyBot, Matcherino, RoundOne) → name autocomplete + flag autofill |
| Needs | Streamer.bot (tested on 1.0.4) + a streaming app with a browser source (see below). Node.js 18+ only for the roster import and the local no-SB mock — the scorebug itself runs on Streamer.bot alone |
Works with any streaming software whose browser source is Chromium-based (≥ 80, i.e.
anything from 2020 on): the panels are plain web pages loaded from
http://127.0.0.1:7474 that open a WebSocket to Streamer.bot — nothing OBS-specific.
OBS and Streamlabs Desktop use the same CEF browser source; current XSplit Broadcaster
works too (old XSplit builds shipped an ancient engine — update if panels stay blank).
Where this README says "Browser Source", use your app's equivalent (XSplit: Webpage).
First, get the code onto the machine that runs Streamer.bot — either
git clone https://github.com/FlashGalatine/tally-scorebug.git
or click <> Code → Download ZIP on the repo page and extract it. The
<repo> paths below refer to that folder.
-
WebSocket Server — Streamer.bot → Servers/Clients → WebSocket Server: enable,
127.0.0.1:8080, authentication off. (8080is only Tally's default, not a requirement — if the port is taken, run SB's WS server anywhere and append?sbport=<port>to every panel/control-panel URL, e.g.…/title-309x49.html?sbport=9090. For a non-localhost host, setwindow.__SB_WS_URL = 'ws://host:port/'in a<script>tag before thepanel-core.jsinclude instead. Auth off is a requirement.) -
HTTP Server — Servers/Clients → HTTP Server: enable,
127.0.0.1:7474, add two Path → Folder mappings (folders from wherever you cloned this repo):Path Folder tally-themes<repo>\tally-themestally-shared<repo>\tally-shared(The
tally-prefix keeps these paths from colliding with other Streamer.bot add-ons that serve genericthemes/sharedfolders. Upgrading from an older Tally that usedthemes/shared? Update both mappings and re-point your browser-source URLs below — the old/themes/…and/shared/…URLs will 404. Themes ported from StreamScoreboard need their panels'panel-core.jssrc repointed too — see docs/THEMING.md.) -
Two actions — Actions → new action named exactly
Scoreboard Push; add a sub-action Core → C# → Execute C# Code; pasteactions/scoreboard-push.cs; Compile. Repeat forScoreboard Commandwithactions/scoreboard-command.cs. Both compile with no added references. (The names matter: panels requestScoreboard Pushon connect, andScoreboard Commandruns it by name.) -
Your streaming app — add a Browser Source (OBS/Streamlabs) or Webpage source (XSplit) per panel, at its native size:
http://127.0.0.1:7474/tally-themes/primetime/panels/title-309x49.html (309×49) http://127.0.0.1:7474/tally-themes/primetime/panels/player1-strip-545x63.html (545×63) http://127.0.0.1:7474/tally-themes/primetime/panels/player2-strip-545x63.html (545×63) -
Drive it. Create a chat command
!sb(Commands tab), then — the step that's easy to miss — open theScoreboard Commandaction and add the command as a Trigger on it (Triggers box → search the command's name). Then, in chat:!sb header SOLUTION TO SUNDAY !sb p1name FGC | Vamp Fatale !sb subheader Grand Finals · FT10 !sb p1flag japan !sb p1+ !sb swap
A panel added mid-match paints immediately: on connect it asks Streamer.bot to re-broadcast current state (there is no "blank until the next update").
Everything runs through the one parametric Scoreboard Command action:
command |
value |
Effect |
|---|---|---|
p1+ p1- p2+ p2- |
— | score ±1 (clamped 0–99) |
p1score p2score |
number | set a score directly |
reset |
— | all scores → 0 |
swap |
— | swap players 1 and 2 (name + score + flag) |
p1name p2name |
text | set a name — Sponsor | Player renders as a sponsor plate |
p1flag p2flag |
nation | set the nationality flag (see below) |
header subheader |
text | match title / subtitle |
teams |
2–4 | enable/disable the optional 3rd and 4th slots (see below) |
Every p1/p2 command also exists as p3/p4 (p3+, p4name, p3flag, …) — they
drive the extra slots of teams mode.
There is one optional extra argument: alongside command=pNname you may pass flag,
and that player's name and nation are applied in a single invocation. The control panel
uses it so one Set click sends exactly one DoAction — Streamer.bot 1.0.4 bleeds the
arguments of two DoActions that reach the same action within a few milliseconds, so a
burst silently loses one of them. Chat and Stream Deck never need it.
For the same reason there is a batched command, setmany: each sibling argument
present among p1name…p4name, p1flag…p4flag, header, subheader is applied in
one invocation (absent ones untouched; a bad flag warns and skips just that field). The
control panel uses it to flush every edited-but-unset field on any Set click — edit
Player 1 and Player 2, click Set once, both land. Chat and Stream Deck never need it.
The scorebug is two slots out of the box. For 3- and 4-way formats (crew battles,
free-for-alls, team leagues), send teams 3 or teams 4 (chat: !sb teams 4; the
control panel has a Teams selector). From then on every broadcast includes
player3/player4, the control panel grows a Player 3 / Player 4 card, and the
p3*/p4* commands (and their dedicated-command variants !p3+, !p4name, … — add
them as triggers like the others) drive them. Add the extra Primetime strips as sources:
http://127.0.0.1:7474/tally-themes/primetime/panels/player3-strip-545x63.html (545×63, green)
http://127.0.0.1:7474/tally-themes/primetime/panels/player4-strip-545x63.html (545×63, gold)
The P3/P4 strips render blank until teams mode reaches them, so you can leave the
sources in your scene permanently. teams 2 returns to the classic scorebug without
erasing the extra slots' names/scores (they come back on the next teams 3/4), and
two-team broadcasts keep the exact original payload shape, so custom themes that only
know player1/player2 are unaffected. swap always swaps slots 1↔2 — with more
teams, re-set names directly.
Nothing says a slot has to be a player. A strip is just a name + a number driven from chat, a Stream Deck, or a hotkey, which makes it a ready-made on-stream tally for anything you'd otherwise count on a sticky note:
- Souls-like death counter —
!sb p1name Deaths, hide the P2 strip (or run a one-slot layout), and bindp1+to a Stream Deck key.!sb p1score 57corrects it. - Attempt counter — "Attempts at this jump":
p1+per try,reseton a new obstacle,header/subheaderas the challenge title. - Running gags — times the streamer noticed the same background NPC, rage quits,
"that's the third time today" oddities. With
teams 3/4you can track several counters at once (Deaths / Rage quits / Chat was right).
Tips for counter use: scores clamp at 0–99; leave the flag unset and the flag cell collapses; mod-only counting works by restricting the chat command's permission in Streamer.bot (or skipping chat triggers entirely and using the deck/control panel).
Flags & nation aliases. A nation can be an ISO-3166 code (jp, gb, fr), one of
~260 country names/aliases (japan, uk, britain, great britain, united kingdom,
usa, america, holland, …), a flag emoji pasted directly (covers 🏴☠️ and friends), or
none/clear to remove. Values resolve to the flag emoji internally and the theme renders
real SVG flags (flag-icons). Unknown nations are rejected with a warning in SB → Logs, so a
typo can't blank the panel. The alias table lives in tally-shared/nations.js
with a mechanical copy in the C# — npm run verify diffs the two so they can't drift.
Chat styles. Either one dispatch command (!sb p1+ — a single trigger handles
everything) or a dedicated command per action (!p1+, !p1flag, …) — create each command
and add each as a trigger on Scoreboard Command. The command text must include the !
you actually type. Watch SB → Logs for [Scoreboard Command] token='…' to see what
each message parsed to.
Stream Deck / hotkeys. Make a wrapper action per button: Set Argument command =
p1+ → Run Action Scoreboard Command; bind the key to the wrapper (Stream Deck plugin
→ Do Action). Best for the no-typing commands: scores, reset, swap.
Open http://127.0.0.1:7474/tally-shared/control.html in any browser tab — or, in OBS,
add it as a Custom Browser Dock (View → Docks → Custom Browser Docks). Name fields with roster
autocomplete, flag fields with alias resolution + live preview, score buttons,
titles, reset/swap, and a Teams selector (the Player 3/4 cards appear when you
pick 3 or 4). It drives the same Scoreboard Command over SB's WebSocket and
live-reflects state, so it never fights chat or the deck. Edited-but-unset fields are
protected from that live reflection, and any Set click applies every edited field at
once (one batched setmany message) — so you can type both players' names and click a
single Set.
Roster import (names + flags). Paste a tournament URL in the Roster card and click Import: every entrant autocompletes, and picking a player auto-fills their flag (from their start.gg profile location — editable before you Set; that's the override). Setting a name sends the flag with it in the same message, so one click applies both.
The Import button talks to a tiny local helper (it does the scraping — needed only at import time, never mid-match). Start it any of three ways, no terminal required:
- From Streamer.bot (automatic): third action
Roster Helperwithactions/streamerbot-roster-helper.cs— editBUNDLE, addSystem.dllin the C# editor's References tab (required — see the file header), Compile, and give it your SB's application-started trigger. - Double-click
start-roster-helper.bat. - Terminal:
npm run roster(alias fornode roster-helper.mjs).
API keys are optional — public Challonge brackets and start.gg's public path work keyless
(a live start.gg test returned 12/12 entrants with flags). If a bracket imports flagless,
copy config.example.json → config.json and set startggApiKey. CLI alternative:
npm run import -- <tournament-url>.
The Primetime theme is one look — the wire format is deliberately tiny and any HTML page can be a panel. docs/THEMING.md is a full tutorial with two worked approaches: a single-file scorebug (fewest OBS sources — one combined strip per player, or one full-scene overlay) and per-field components (one small panel per name/score/ flag, maximum layout freedom in OBS), plus the copy-paste flag renderer and the dev loop.
npm install
npm start # mock Streamer.bot: http://127.0.0.1:7474/ lists the panels
npm run verify # protocol + HTTP + nation-table parity → ALL GREEN
npm start runs a small mock of SB's two servers so you can develop themes and poke the
panels with zero setup: drive state with the console keys (q/a P1 ±, w/s P2 ±, r
reset, x swap) or GET /mock/cmd?command=p1+. npm run verify:render (needs
npm i --no-save playwright-core) renders the real theme headless and checks pixels;
npm run shots regenerates the screenshots above.
- Chat command does nothing → the command isn't a trigger on
Scoreboard Command(the action shows Triggers: 0), or its text doesn't match what you type (p1+configured vs!p1+typed). SB → Logs shows[Scoreboard Command] token='…'for every hit. - Panels blank → state never arrived over the WS. Check the WS Server is on
:8080with auth off (or that every panel URL carries a matching?sbport=, if you moved it), and that the action is named exactlyScoreboard Pushand compiles (DoActionreturning ok only means the action started — a compile error broadcasts nothing). On error the actions broadcast{ type:'scoreboard:error', message }. Uri/Process/Newtonsoft"does not exist" when writing your own actions → SB (a .NET Framework 4.7.2 app) resolves a minimal default reference set. AddSystem.dll(or SB's ownNewtonsoft.Json.dll) in the C# editor's References tab. Tally's two scoreboard actions need no references on purpose.- Control panel: clicking Set on a name snaps the box back to the old name (while the
same edit works from chat) → you're on an older
control.html+Scoreboard Commandpair. It sent the name and the flag as two back-to-backDoActions, and SB 1.0.4 bleeds the arguments of same-action calls that land within a few milliseconds together: both ran asp<N>flag, the name was never written, and the next broadcast reflected the old name back into the input. SB → Logs shows the tell — twotoken='p1flag'lines and notoken='p1name'. Fix: updatetally-shared/control.htmland re-pasteactions/scoreboard-command.cs; together they now send name + flag in one call. Updating only the panel still fixes the name (the flag just stops riding along until the C# is re-pasted). - Import fails with "helper not running" → start the roster helper (see above).
- Flags show as letters (GB) in the control panel → Windows has no color flag-emoji glyphs; cosmetic and panel-only. The theme renders real SVG flags.
npm startfails withEADDRINUSE/EACCES→ real Streamer.bot already owns:7474/:8080. You don't need the mock when SB is serving; or relocate it:SB_HTTP_PORT=7480 SB_WS_PORT=8090 npm start.- SB says "Unable to start websocket server" on
:8080, but Task Manager shows no culprit → a zombie socket. If SB previously exited uncleanly (crash/force-kill) while the Roster Helper action'snode roster-helper.mjschild was running, an older version of that action let node inherit SB's listen sockets, keeping:8080bound under a PID that no longer exists. Fix: kill the orphaned node (taskkill /im node.exe /f, or find it viaGet-NetTCPConnection -LocalPort 8080), restart SB's WS server, and re-paste the currentactions/streamerbot-roster-helper.cs— it now launches node withUseShellExecute = true, which doesn't pass SB's handles to the child, so this can't recur. - Subscribe case gotcha (for integrators): the shim subscribes with lowercase
events: { general: ['Custom'] }even though delivered events carryGeneral.Custom— capitalGeneralin the Subscribe silently receives nothing.
Built by Ashe "Flash" Galatine.
- Email — AsheJunius@gmail.com
- X — @AsheJunius · BlueSky — @projectgalatine.com
- Twitch — FlashGalatine · Discord — Project Galatine
- Support — Patreon ProjectGalatine · CashApp $ProjectGalatine
MIT — see LICENSE. The Primetime theme and the roster platform scrapers are vendored from StreamScoreboard (same author, MIT) — if you want a full web dashboard, more themes, and per-player add-in fields, that's the bigger sibling this project is the Streamer.bot-native distillation of. Flag SVGs by flag-icons (MIT, via jsDelivr); Quantico typeface via Google Fonts (OFL). See THIRD_PARTY_NOTICES.md.



