No description
  • TypeScript 59.7%
  • Python 37%
  • HTML 3.3%
Find a file
aiko b777f8a164 feat: default redeems and full README (stage 9)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 16:26:20 +02:00
overlay feat: overlay - effect toasts with viewer credit, death counter (stage 8) 2026-06-12 16:26:20 +02:00
.gitignore feat: overlay - effect toasts with viewer credit, death counter (stage 8) 2026-06-12 16:26:20 +02:00
adapter.py feat: overlay - effect toasts with viewer credit, death counter (stage 8) 2026-06-12 16:26:20 +02:00
pack.toml feat: default redeems and full README (stage 9) 2026-06-12 16:26:20 +02:00
README.md feat: default redeems and full README (stage 9) 2026-06-12 16:26:20 +02:00

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 = true so it survives adapter restarts via the hello_ack persisted[] replay), active_effect (currently-running timed effect, "" when none), last_effect ({name, seq, by?} written per applied effect; seq makes consecutive identical effects distinguishable, by is the redeeming viewer when known — drives the overlay toast), and deaths (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 latched config frames): max_bots (cap per redeem regardless of viewer count), chat_prefix (prepended to say messages). Adapter defaults match the schema defaults and apply until the first frame lands.
  • Per-reward duration: the timed events take a seconds param (5120, 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, sends applied immediately, but holds done until 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:

  1. Zandronumhttps://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.
  2. Freedoomhttps://freedoom.github.io/ (free BSD-licensed game data; or use a commercial DOOM2.WAD you 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_BEGINCONNECTIONSVRC_SALTCLRC_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 to last_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). say deliberately doesn't toast — it's already visible in the game chat.
  • deathCounter — compact 💀 n badge fed by deaths.

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:

  1. Manual e2e run against a real Zandronum (see Status above) — gate for everything below; ACS work can't be developed blind.
  2. Ship a mobrule.pk3 with ACS scripts, triggered via RCON puke <script> — enables effects raw cvars can't do (spawn a specific monster at the player, item rain, screen effects).
  3. Chat-Plays via [input_vote] — the platform half is nearly free (bridge tallies votes, delivers cp_active/cp_pad_mask over the config frames 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 (ThrustThing shoves, SetActorAngle turns, 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.
  4. active_effect banner widget — third overlay widget, persistent "MOON GRAVITY ACTIVE" plate while a timed effect runs.