- TypeScript 59.7%
- Python 37%
- HTML 3.3%
|
|
||
|---|---|---|
| overlay | ||
| .gitignore | ||
| adapter.py | ||
| pack.toml | ||
| README.md | ||
pack-doom
Doom Chaos — a showcase Pack that lets viewers wreak havoc on a live
Zandronum (Doom) session. Where pack-hello-world is the smallest possible
adapter, this Pack demonstrates a real integration: an external game
controlled over its own remote protocol, multiple queues, timed effects,
and failure reporting when the game is unreachable.
Events
| Event | Queue | Effect |
|---|---|---|
say(message) |
chat |
Viewer message in game chat |
gravity(level: moon|heavy, seconds) |
timed |
sv_gravity for seconds, then reverts |
fast_monsters(seconds) |
timed |
Nightmare-speed monsters for seconds |
add_bot(count: 1-4) |
effects |
Adds hostile bots |
State & config
- Pack state (adapter → platform):
effect_count(lifetime chaos tally,persist = trueso it survives adapter restarts via thehello_ackpersisted[]replay),active_effect(currently-running timed effect,""when none),last_effect({name, seq, by?}written per applied effect;seqmakes consecutive identical effects distinguishable,byis the redeeming viewer when known — drives the overlay toast), anddeaths(session death count, parsed from the obituary lines the server pushes to RCON clients — heuristic; matches stock English obituaries, exact counts need an ACS hook). - Pack config (broadcaster → adapter,
[[pack.config.section]], delivered as latchedconfigframes):max_bots(cap per redeem regardless of viewer count),chat_prefix(prepended tosaymessages). Adapter defaults match the schema defaults and apply until the first frame lands. - Per-reward duration: the timed events take a
secondsparam (5–120, default 30) without viewer override — the broadcaster sets it per reward in the Cue→Event mapping, so "Moon Gravity (30s)" and "Moon Gravity (60s)" can be two rewards at different prices.
Queues
Three queues, demonstrating both queue policies:
chat/effects(ready_after = "applied") — instant effects; a chat message never waits behind a 30-second gravity change.timed(ready_after = "done") — the adapter applies the effect, sendsappliedimmediately, but holdsdoneuntil the effect's per-reward duration elapses and it reverts. The bridge therefore serializes timed effects — one active at a time, queued redeems wait their turn — with zero adapter-side overlap bookkeeping.
Game setup
The Pack does not ship the game. Install separately:
- Zandronum — https://zandronum.com/download (the engine; client and
zandronum-server). Not bundled deliberately: parts of its codebase carry legacy non-commercial license terms, so we point at upstream instead of redistributing. - Freedoom — https://freedoom.github.io/ (free BSD-licensed game data;
or use a commercial
DOOM2.WADyou own — it is never distributed).
Start a local server with RCON enabled, then connect your client to it:
zandronum-server -host -iwad freedoom2.wad \
+sv_rconpassword 'pick-a-password' +sv_cheats 1
zandronum -iwad freedoom2.wad -connect 127.0.0.1:10666
Run the adapter
BRIDGE_TOKEN=<token> DOOM_RCON_PASSWORD='pick-a-password' python3 adapter.py
The adapter connects to the bridge at $BRIDGE_HOST:$BRIDGE_PORT (defaults
127.0.0.1:7777) and to Zandronum RCON at $DOOM_RCON_HOST:$DOOM_RCON_PORT
(defaults 127.0.0.1:10666, set in [adapter.env]). The RCON password is
read from the environment only — never from pack.toml, whose contents are
hashed and visible.
If the game server is down, invocations fail with a failed frame (the
viewer-facing refund path) and the adapter reconnects lazily on the next
redeem — the game crashing must not kill the Pack.
Wire
Zandronum RCON, protocol v4 (see src/sv_rcon.{h,cpp} in the Zandronum
source): UDP datagrams Huffman-coded with the fixed Skulltag tree. The
adapter always sends unencoded packets (the protocol's 0xFF escape) and
only decodes server replies, so it needs no encoder. Handshake:
CLRC_BEGINCONNECTION → SVRC_SALT → CLRC_PASSWORD with
md5(salt + password) → SVRC_LOGGEDIN; keepalive CLRC_PONG every 5 s.
Viewer text routed into say is sanitized before being embedded in a
console command: the Zandronum console treats ; as a command separator and
" as a string delimiter, so both (plus \ and non-printable-ASCII) are
stripped to prevent console-command injection.
Status
The RCON client is verified against a protocol-faithful mock server (handshake, auth, command framing, Huffman decode round-trip). Not yet verified against a real Zandronum binary — do one manual end-to-end run before demoing live.
Overlay
overlay/ ships two Doom-themed widgets (Vite + Solid lib, same shape as
pack-hello-world/overlay/; dist/index.js committed):
effectToast— subscribes tolast_effect; flashes "GRAVITY SHIFTED — doomguy_2026" for 4 s per applied effect (viewer credit when the invocation carried a username), then renders nothing. The initial snapshot only arms the watcher (no stale toast on OBS refresh).saydeliberately doesn't toast — it's already visible in the game chat.deathCounter— compact 💀 n badge fed bydeaths.
Stream-safety: the game is the content. No fullscreen elements, single-line plates, translucent backgrounds; suggested placement is toast top-center, death badge bottom-left. The dev harness renders both over a fake game frame so footprint is easy to judge:
cd overlay
npm install # first time only
npm run dev # mock data plane streams scripted diffs for ~20 s
npm run build # rebuild the committed dist/index.js
Ideas / escalation path
In order — each step unlocks the next:
- Manual e2e run against a real Zandronum (see Status above) — gate for everything below; ACS work can't be developed blind.
- Ship a
mobrule.pk3with ACS scripts, triggered via RCONpuke <script>— enables effects raw cvars can't do (spawn a specific monster at the player, item rain, screen effects). - Chat-Plays via
[input_vote]— the platform half is nearly free (bridge tallies votes, deliverscp_active/cp_pad_maskover theconfigframes the adapter already handles); the game half rides the pk3 from step 2. Note the fidelity ceiling: RCON cannot press buttons, so ACS approximates inputs (ThrustThingshoves,SetActorAngleturns, projectile spawns for "fire") — "Twitch shoves Doom guy", not true input control. Deliberately kept out of the tutorial: Chat-Plays' honest home is a Pack that feeds real pad input (e.g. into an emulator); this Pack would teach a worse example of the same feature. active_effectbanner widget — third overlay widget, persistent "MOON GRAVITY ACTIVE" plate while a timed effect runs.