Files
tws-zombies-scripts/t5/scripts/README.md
T

638 lines
36 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# TWS T5 Zombies scripts
Custom HUD + AFK toggle + immediate join + lobby lock + game telemetry + map records for Plutonium T5
(Black Ops 1) zombies dedicated servers. Split across nine `.gsc` files,
with `tws_t5_common` as the shared hub. Lobby lock also requires the HUD module.
Platform quirks and deploy commands: [NOTES.md](NOTES.md).
## Files
| File | Role |
|---|---|
| [tws_t5_common.gsc](tws_t5_common.gsc) | Shared config, HUD helpers, paginated `.help`, service window, connect/spawn registries, single `connected` listener + dispatcher |
| [tws_t5_hud.gsc](tws_t5_hud.gsc) | Unboxed timers, branding, zombies/health and per-player `.hud` visibility toggle |
| [tws_t5_afk.gsc](tws_t5_afk.gsc) | Persistent AFK reserve, contribution rewards, purchases and central countdown |
| [tws_t5_join.gsc](tws_t5_join.gsc) | `.join` lets newcomers spawn before the current round ends |
| [tws_t5_lock.gsc](tws_t5_lock.gsc) | Automatic lobby lock plus majority `.lock` / `.unlock` votes |
| [tws_t5_telemetry.gsc](tws_t5_telemetry.gsc) | Direct HTTP game start, round, heartbeat, and game end reports |
| [tws_t5_leaderboard.gsc](tws_t5_leaderboard.gsc) | Private map record on connection, `.leaders` replay and adaptive round-start reminders |
| [tws_t5_bank.gsc](tws_t5_bank.gsc) | Persistent shared GUID bank and private balance/deposit/withdraw commands |
| [tws_t5_moderation.gsc](tws_t5_moderation.gsc) | Optional controller-managed bans, mutes and live moderation; requires common 1.2.0+ and native plugin 1.3.0+ |
## Install / upgrade
Copy the desired `tws_t5_*.gsc` modules to `storage/t5/scripts/sp/`, then restart the map.
Before adding the optional moderation module, install native plugin 1.3.0+ and
restart the full game-server process; see [moderation setup](../../docs/moderation.md).
When upgrading an existing installation, first remove these old scripts from
that directory:
- `tws_bank.gsc` and `tws_db.gsc` (removed modules)
- `tws_common.gsc`, `tws_hud.gsc`, `tws_afk.gsc`, `tws_join.gsc`,
`tws_lock.gsc`, `tws_telemetry.gsc` and `tws_leaderboard.gsc` (renamed modules)
Do not leave both common scripts installed: duplicate connection listeners
break T5's connect handshake. Existing dvar names are unchanged.
## Player bank
Bank `2.0.2` requires plugin `1.2.0+` for startup version reporting and bounded
parser diagnostics. Install the DLL and restart the full server process before
loading these scripts. Follow the [diagnostic rollout and live checks](../../README.md#bank-diagnostics--tws-14).
The optional `tws_t5_bank.gsc` adds `.b`, `.d <amount|all>` and
`.w <amount|all>` and registers them in help. It requires common and telemetry
`1.0.0` or newer, the TWS t5 plugin `1.2.0`, Electric Cherry's bank API and
a private per-server credential. Follow [bank setup and recovery](../../README.md#player-bank-t5-and-t6),
including a full process restart for the plugin. Bank `2.0.0+` uses only
`https://ec.thewrightserver.net/api/bank` and ignores `tws_bank_url`.
Update EC agents to `1.9.1+` first. If the old endpoint differs, follow the
[endpoint upgrade procedure](../../README.md#upgrading-the-bank-endpoint):
an EC rebuild must settle legacy journals before the new scripts start.
When the endpoint already matches, only a map restart is required.
Bank `2.0.1` converts T5's `player getGuid()` result to a string before
requests or journal writes, preserving existing strings exactly. Missing or
invalid identities fail privately without changing points. Follow the shared
[identity and deployment checks](../../README.md#bank-identity-validation).
## Core HUD and service window
The unboxed core puts match time above round time at the upper left,
`TWS ZOMBIES | discord.tws.lol` at the top center, and zombies remaining above a
slim health bar at the bottom center. `N ZOMBIES` is one centered element at
the health track's midpoint, using uniform font scale `1.25` so the whole group
stays centered as the digit count changes. Its initial placeholder is
`-- ZOMBIES`. Current/maximum HP appears to the right of the bar only while
injured; it disappears at full health. The AFK reserve countdown is independently centered on screen. There are no panel backgrounds or timer labels.
Fullscreen X/Y anchors use the normalized `640 × 480` viewport directly and
bypass safe-area insets. Font scale is at least `1.0`; verify text fit, stock
HUD overlap and Plutonium tag clearance at each aspect ratio in-game.
The core is created once per connection, including for spectator newcomers
with placeholders until values are available. Creation does not seed the match
clock. It persists across death and respawn. Clocks, health and zombie-count
displays freeze at game over. Health fallbacks only affect the display and
never modify gameplay max health.
The exact, case-insensitive `.hud` command toggles your core. The AFK countdown stays visible.
It defaults visible, keeps your choice across death/respawn on the current
connection/map, and resets visible on reconnect or map restart. Timers and data
continue updating while hidden; game-over values remain frozen. Help, records
and the stock HUD are unaffected. `.hud` requires the HUD module.
Common provides one unboxed upper-right service area below the Plutonium tag.
Records are right aligned, with `Map record | Round N` followed by up to three
holder lines; there is no replay footer or single-page counter. Help displays
the installed command list in pages of nine rows in aligned rows. Services remain private, leave the
core in place and work without the HUD module installed. See the
[shared layout and validation guide](../../README.md#core-hud-and-service-window-t5-and-t6).
Only the active service view holds HUD elements: 19 for nine-command help
(20 with a multi-page counter; the standard AFK command set uses multiple pages), four for records (five with a multi-page
counter), or two for compact round-start reminders. Pages reuse elements; switching views or closing the service releases
them. Idle services hold no HUD elements, while the controller retains pending
requests and interruption/resume behavior. Core + AFK use 10 + 2 elements.
HUD `1.4.2` requires common `1.4.0+` and
[Plutonium r5334+](https://plutonium.pw/docs/changelog/#r5334) for its
`SetTextUnlimited` counter, updated only when the count changes. Deploy the
updated HUD script and restart the map on r5334+; no full server-process
restart is required. Older Plutonium revisions need client/server updates and
full restarts first. Leaderboard `1.2.0` requires common `1.5.0+` for its
round-start reminders. AFK `2.0.0` requires the matching
plugin `1.4.0+` and EC reserve service; see the [AFK guide](../../docs/afk.md).
## Map record announcements
Copy `tws_t5_leaderboard.gsc` and the updated `tws_t5_common.gsc` to
`storage/t5/scripts/sp/`, then restart the map. Leaderboard `1.2.0` requires
common `1.5.0+`; this script update adds no native-plugin or backend dependency
and requires no full server-process restart. Leaderboard requires a
[t5-gsc-utils](https://github.com/alicealys/t5-gsc-utils) build exposing
`http::request`, `jsonParse`, `typeof` and `formatString`. A plugin upgrade
requires a full server-process restart. To omit leaderboard, remove its script;
the enable dvar does not bypass native linking dependencies.
Records are read from the public Electric Cherry API with no credentials and
work independently of telemetry and HUD. Spectator newcomers receive
the same private announcement as playable joins. T5 matches its engine map to
the API's map list. [Shared configuration, caching and validation](../../README.md#map-record-announcements)
apply to both games.
Round-start reminders use the latest usable cache at gaps of 20, 15, 10, 8 and
6 rounds, then every round for the final five, followed by matching/overtake
milestones. After the first overtake, reminders stop for that match; `.leaders`
still works. A reminder shows the record and one short status line for six
seconds. Join and manual announcements keep all holders and 12-second pages.
Reminders skip busy help/record views, unready players, recent welcome/manual
requests and empty/unavailable/expired records. Help discards a reminder instead
of pausing it. No extra HTTP requests are made. Follow the shared live checks
for cadence, interruption, cleanup and operation without the optional HUD.
## Player commands
Type `.help` or `.commands` in chat to see available player commands and a short description.
The response is visible only to the requesting player and works while spectating,
before the first spawn. Commands are case-insensitive.
Help uses the unboxed upper-right service area with command and description
columns. Registered commands use pages of nine rows, with a counter only for
multi-page lists.
Pages advance every 12 seconds and close after the last page. Typing either
command again returns to page one with a fresh timer. Both engines use
explicit row positions so T5 does not concatenate newline-separated text.
| Command | Effect |
|---|---|
| `.help`, `.commands` | List available commands |
| `.hud` | Hide or show your core HUD (AFK countdown stays visible) |
| `.afk` | Enter protected AFK using your time reserve |
| `.back` | Return from AFK |
| `.join` | Join mid-round (newcomers only) |
| `.leaders` | Show the map record again (leaderboard module required) |
| `.b` | Check bank balance (bank module required) |
| `.d <amount\|all>` | Deposit points (bank module required) |
| `.w <amount\|all>` | Withdraw points (bank module required) |
| `.lock` | Vote to lock the lobby from round 6 (lock module required) |
| `.unlock` | Vote to unlock the lobby (lock module required) |
Each module registers its own help entries. The first nine commands follow
the order above; additional entries follow registration order. Removing a module
also removes its entries from `.help` after a map restart. Lock and extended AFK
commands appear on additional pages. Bank entries retain the full
`.d <amount|all>` / `.w <amount|all>` syntax. Removing HUD removes `.hud` from the list.
The enabled leaderboard module registers `.leaders`, which replays the latest
cached map record privately. Repeating it restarts the record from the first page;
help pauses the record's remaining page time and resumes it after the final
help page. `.leaders` during help replaces the pending record replay from page one.
## Dependency graph
Arrows read "depends on" — solid means the source names the target with
`scripts\sp\tws_t5_common::...`, dashed means the source waits on a level
notify fired by the target.
```mermaid
graph BT
common[tws_t5_common<br/><i>config + helpers + dispatcher</i>]
hud[tws_t5_hud]
afk[tws_t5_afk]
join[tws_t5_join]
lock[tws_t5_lock]
telemetry[tws_t5_telemetry]
leaderboard[tws_t5_leaderboard]
hud --> common
afk --> common
join --> common
lock --> common
telemetry --> common
leaderboard --> common
lock -. hud_round_changed<br/>hud_game_over .-> hud
```
- `tws_t5_common` is the only module named by other files at compile time.
- All other cross-module coupling flows through level notifies
(`hud_round_changed`, `hud_match_started`,
`hud_game_over`) or through spawn handlers registered with
`tws_t5_common::register_spawn_handler` / `register_connect_handler`.
- Any file except `tws_t5_common` can be removed; the rest still load.
`tws_t5_afk` has no downstream consumers — pull it independently.
`tws_t5_lock` needs `tws_t5_hud` to receive round and game-over notifications.
## Runtime notifies
| Notify | Fired by | Read by |
|---|---|---|
| `hud_round_changed` | `tws_t5_hud` | `tws_t5_hud` (round-timer reset), `tws_t5_lock` |
| `hud_match_started` | `tws_t5_hud` | `tws_t5_hud` (match-timer reveal) |
| `hud_game_over` | `tws_t5_hud` | `tws_t5_hud` (freeze timers), `tws_t5_lock` (clear password) |
---
## tws_t5_common.gsc
Hub file. Shared config, HUD helpers, service window, spawn-handler registry, and the
single `level waittill("connected")` in the fleet. Adding a second
connect listener anywhere else crashes the T5 SP handshake.
**Features**
- Shared HUD constants for overlay positions, rows/columns, fonts, health bar,
colors and update rate
- Spawn-handler registry
- Connection-handler registry for optional modules that serve spectator newcomers
- `.help` / `.commands` listener and ordered help registry populated by installed modules
- One service controller per player, with help/record pagination,
interruption/resume, request replacement and disconnect/game-end cleanup;
display elements exist only for the active view and are released when idle
- Core element registry with per-element alpha state and a per-player visibility mask
- Connect listener + per-player dispatch: iterates
`level.tws_spawn_handlers` after `spawned_player` and threads each on
the player
**Provides**
| Function | Purpose |
|---|---|
| `common_config()` | Populate every `level.hud_*` (idempotent) |
| `ensure_config()` | No-op if config already set; called by every module's `init` |
| `register_spawn_handler(fn)` | Appends `::fn` to `level.tws_spawn_handlers` |
| `register_connect_handler(fn)` | Threads `::fn` after connection dispatch yields, without waiting for playable spawn |
| `register_help_command(command, description)` | Adds a player command to `.help`; call from the owning module's `init()` |
| `make_text_elem(p, x, y, ax, s, c)`| `NewClientHudElem` text element with black-glow outline |
| `make_shader_elem(p, x, y)` | `NewClientHudElem` shader element |
| `make_overlay_text(p, x, y, ax, s, c)` | Text at absolute fullscreen coordinates in the normalized `640 × 480` frame |
| `make_overlay_shader(p, x, y, w, h, c, alpha, sort)` | Shader at absolute fullscreen coordinates |
| `make_overlay_width(width)` | Clamp a normalized width and return the integer required by `SetShader` |
| `make_core_overlay_text(p, x, y, ax, s, c)` | Create overlay text registered for `.hud` visibility |
| `make_core_overlay_shader(p, x, y, w, h, c, alpha, sort)` | Create an overlay shader registered for `.hud` visibility |
| `make_panel_text(p, x, y, ax, s, c)` | Legacy text positioned relative to the former upper-right panel origin |
| `make_panel_shader(p, x, y, w, h, c, alpha, sort)` | Legacy shader positioned relative to that panel origin |
| `make_panel_width(width)` | Retained legacy design-width conversion for existing consumers |
| `make_core_text(p, x, y, ax, s, c)` | Retained legacy panel text with `.hud` visibility |
| `make_core_shader(p, x, y, w, h, c, alpha, sort)` | Retained legacy panel shader with `.hud` visibility |
| `set_core_alpha(p, elem, alpha)` | Preserve the element's intended alpha while applying the player's visibility choice |
| `toggle_core_hud()` | Toggle the player's registered core elements without stopping updates |
| `show_record(snapshot)` | Replace the player's record service request; an undefined snapshot shows unavailable |
| `destroy_on_disconnect(elems)` | Waits on `disconnect`, destroys the array |
| `int_dvar_or(name, fallback)` | Parse an integer dvar with default |
| `format_mmss(elapsed_ms)` | Format milliseconds as `M:SS` |
**Depends on**: nothing (leaf module).
**Live validation (requires a T5 server)**
1. Type `.help`, `.HELP` and `.commands` while playing and spectating before the
first spawn. Verify all nine commands appear in one unboxed list, private to
the requesting player. Check explicit rows, full bank syntax and the absence
of a single-page counter. HP should appear beside the bottom health bar only
while injured.
2. Remove AFK or join, restart the map, and verify its entries disappear while
`.help` and the remaining commands still work. Restore the scripts afterward.
3. Verify help closes after 12 seconds. Add custom registered commands to test
the nine-row page limit, counter and repeat-to-restart behavior. Interrupt a record
with help and verify remaining page time resumes; repeat `.leaders` during
help and verify only the latest replay starts from page one.
4. Reopen after expiry, die/respawn and reconnect; verify no duplicate text or
leftover elements. Repeat without the HUD module. Follow the complete
[HUD validation guide](../../README.md#hud-validation) before rollout.
5. Watch the first automatic record announcement and its dismissal. Repeat
records, help and interrupted/resumed records alongside stock notifications.
Check that branding, the health bar, damage-only HP, zombie count and timers
remain correct during each view and after it closes.
Include other installed HUD mods; the live T5 SP element limit is unverified.
6. Check `0`, `6`, `9`, `10`, `99` and `100 ZOMBIES` at different resolutions:
the complete text must stay centered above the health track. Repeat after
toggling `.hud` and after game over freezes the count.
---
## tws_t5_hud.gsc
Unboxed core overlays. Per-player elements are created on connection;
gameplay updates start through the existing spawn lifecycle.
**Features**
- Upper-left match timer above round timer (round resets each round, both freeze on game over)
- `.hud` toggles this player's core; the AFK countdown stays visible and data updates continue
- Top-center `TWS ZOMBIES | discord.tws.lol` branding
- One `N ZOMBIES` element centered above the health track, separate from the central AFK countdown
- Slim health bar with green / amber / red ramp; current/maximum HP appears
beside it only while injured
- Spectator placeholders without starting the match clock on connection
- Round-transition and game-over notify machinery
**Provides**
| Notify / level var | Meaning |
|---|---|
| notify `hud_round_changed` | Fires each time `level.round_number` transitions |
| notify `hud_match_started` | Fires once when the match clock seeds |
| notify `hud_game_over` | Fires on the first `end_game` / `game_ended` / `intermission` notify |
| `level.round_start_time` | Anchored at first observed round transition |
| `level.match_start_time` | Anchored at first observed round-1 (or on first player spawn) |
Spawn handler registered: `::hud_start` — threads `hud_timer_row`,
`hud_zombie_counter`, `hud_health_display`.
Connection handler registered: `::hud_connect` — creates the core with placeholders
without seeding clocks; `hud_start` calls it as a fallback if needed.
**Depends on**
- `scripts\sp\tws_t5_common::` — `ensure_config`, `make_core_overlay_text`,
`make_core_overlay_shader`, `make_overlay_width`, `set_core_alpha`, `toggle_core_hud`,
`destroy_on_disconnect`, `format_mmss`, `register_help_command`,
`register_spawn_handler`, `register_connect_handler`
- Every `level.hud_*` config value from `tws_t5_common`
- Plutonium r5334+ for the count's `SetTextUnlimited` updates
- Stock BO1 state: `level.round_number`, `level.zombie_total`,
`level.players`, `self.health`, `self.maxhealth`, AI entities' `.animname`
and `.ignore_enemy_count`
---
## tws_t5_afk.gsc
Persistent protected breaks using the shared GUID reserve. See the
[AFK guide](../../docs/afk.md) for commands, policy, ownership, native APIs,
crash recovery and live validation. Requires common `1.4.0+`, telemetry `1.0.0+`,
matching plugin `1.4.0+`, Plutonium r5334+ and EC #151.
Chat uses `level waittill("say")`; the independent central countdown stays visible
with `.hud` hidden. Activation rejects laststand before changing state. A stock
damage callback owns only AFK protection; expiration immediately relinquishes it
and AFK-owned controls/targeting, while manual `.back` keeps its separate two-second
grace. AFK `2.1.0` permits solo and last-active-player entry, including all players
being AFK together; teammate loss does not force a match end. Upgrade from `2.0.0`
by copying the script and restarting the map; no new dependencies or full
server-process restart are required. Spawn handler `::afk_spawn` binds contribution
and lifecycle watchers.
---
## tws_t5_join.gsc
Type `.join` in chat while spectating after joining a match in progress to
spawn immediately. Install with `tws_t5_common.gsc`; no HUD dependency.
**Eligibility**
- Only before the player's first playable spawn on the current connection.
- A normal spawn or successful `.join` permanently closes eligibility for
that connection. Getting downed, bleeding out, or changing rounds never
restores it. Dead players must wait for the normal round-end respawn.
- Requests during connection setup, without a living teammate, or after
game over cannot spawn a player. Repeated `.join` requests cannot create
overlapping spawns.
- This is connection-local tracking; reconnecting creates a new player and
resets eligibility. It is not an account-level reconnect restriction.
Uses the stock `level.spawnPlayer` entry point and preserves map-specific
`level.custom_spawnPlayer` callbacks. If absent, installs the stock zombies
spectator callback, as the round-end routine does. Missing spectator spawn
locations are seeded from the map's `initial_spawn_points`.
Spawn handler registered: `::join_spawn`. Stock `self.initialized` provides
an additional first-spawn guard; `hasSpawned` cannot distinguish spectators.
See the [stock zombies lifecycle](https://github.com/plutoniummod/t5-scripts/blob/main/ZM/Common/maps/_zombiemode.gsc)
and [spawn entry point](https://github.com/plutoniummod/t5-scripts/blob/main/ZM/Common/maps/_callbackglobal.gsc).
**Live validation (requires a T5 server)**
1. Join during an active round, type `.join`, and verify one playable spawn
with normal weapons, controls, zombie targeting, and HUD initialization.
Check both round 1 and a later round, including each deployed map.
2. Repeat `.join` rapidly; verify no duplicate spawns. Try it alive, while
downed, and after bleeding out; verify no revive or respawn.
3. Join and wait for the normal round-end spawn instead; after dying, verify
`.join` remains blocked, including on subsequent rounds.
4. Try during round transition and game over; verify no duplicate spawn or
restart of an ended match. Check that `.afk` / `.back` still work.
---
## tws_t5_lock.gsc
**Features**
- Sets `g_password` to a random per-lobby value when the round crosses a
milestone (default: round 20), once per match
- Adds `.lock` from round 6 and `.unlock` whenever locked, using strict-majority
votes (1/1, 2/2, 2/3, 3/4) of all connected players
- Votes expire after 60 seconds; duplicates do not count, and roster changes
update the tally without retaining disconnected votes
- A successful unlock disables automatic locking for the rest of that match;
players can vote to relock at any time from round 6
- No automatic round-6 prompt; progress/results use ordinary message text
- Existing players are unaffected; `g_password` is only checked at connect
- Clears `g_password` and pending votes at startup and on HUD/stock game-over events
- Trigger round is dvar-driven — retune without redeploying
**Provides**
| Level var | Meaning |
|---|---|
| `level.lock_round` | Round at which the lobby locks (read at init) |
| `level.lock_active` | `true` after the password has been set |
| `level.lock_auto_done` | Automatic locking consumed or overridden by a successful unlock |
| `level.lock_vote` | Current ballot, or undefined when idle |
**Depends on**
- `scripts\sp\tws_t5_common::` — `ensure_config`, `int_dvar_or`, `register_help_command`
- `tws_t5_hud` — waits on `hud_round_changed` and `hud_game_over`
- Stock BO1: `level.round_number`
- Dvar: `tws_lock_round` (int, default `20`)
Lock `1.1.0` has no new dependency requirement. Copy the updated script and
restart the map; no full server-process restart is required. See the shared
[voting behavior and validation](../../README.md#lobby-lock-voting-t5-and-t6).
---
## Dvars
| Dvar | Consumer | Default | Purpose |
|---|---|---|---|
| `zombiemode` | every module | — | Gate: every `init()` returns early unless `"1"` |
| `tws_lock_round` | `tws_t5_lock` | `20` | Round at which the lobby locks |
| `tws_server_id` | `tws_t5_telemetry` | Required for telemetry | Exact registered Electric Cherry server name |
| `tws_telemetry_enabled` | `tws_t5_telemetry` | `1` | Set to `0` to disable telemetry at init |
| `tws_telemetry_url` | `tws_t5_telemetry` | `https://ec.thewrightserver.net/api/telemetry` | Electric Cherry telemetry endpoint for JSON POSTs |
| `tws_telemetry_log_level` | `tws_t5_telemetry` | `1` | Read live: `0` warnings/errors/recovery only, `1` lifecycle/request/health logs, `2` also capture/queue logs and response previews |
## tws_t5_telemetry.gsc
Uses `tws-t5-gsc-utils` HTTP/JSON components to POST directly to Electric Cherry's
`/api/telemetry`. Install alongside `tws_t5_common.gsc` in `storage/t5/scripts/sp/`.
No HUD dependency. Reports run whether or not a beta is active.
Follow the shared [authentication setup](../../README.md#telemetry-authentication-t5-and-t6)
to configure the service before upgrading. The reporter token is embedded in
`level.telemetry_token`; it must match the service's `EC_TELEMETRY_TOKEN`. Short
embedded tokens and non-HTTPS URLs disable reporting at init.
Example server config (except log level, read at script init; restart the map after changes):
```cfg
set tws_server_id "t5_zombie_prototype_1"
set tws_telemetry_enabled "1"
set tws_telemetry_url "https://ec.thewrightserver.net/api/telemetry"
set tws_telemetry_log_level "1"
```
`tws_server_id` must match the registered server name exactly. If it is
unset, the script logs a configuration error and does not start reporting.
An unknown or destroyed server is rejected by Electric Cherry with HTTP 404.
The reporter emits exactly four event types:
| Event | When | Payload |
|---|---|---|
| `game_start` | First playable spawn, with the stock `begin_spawning` flag as a fallback | `server`, `map`, `players` with `name` and `guid` |
| `round_change` | First observed round after start, then each change (checked every 0.5 seconds) | `server`, `map`, `round`, full player stats |
| `heartbeat` | Every 30 seconds after `game_start`, until game end | Same fields as `round_change`, with current round and player stats |
| `game_end` | First stock `end_game`, `game_ended`, or `intermission` notification | `server`, `map`, final `round`, full player stats |
All four events also include `match` and `seq` (see delivery below).
Each player in round/heartbeat/end reports has `name`, `guid`, `kills`, `headshots`,
`downs`, `revives`, `score`, and `score_total`. Names and GUIDs remain strings; GUIDs never
pass through numeric conversion. Round and stats are nonnegative integers,
with missing or negative stat values reported as zero. The four counters
come from T5's `player.stats`; `score` is the player's current spendable
`player.score`. `score_total` reads the stock `player.score_total` cumulative
match score used by the final scoreboard; spending points does not reduce it.
It follows stock scoring rules, including grants excluded from the total.
Missing or negative totals report as zero, independently of the balance.
`game_start` includes only player identity. `map` is `mapname`.
Stock `end_game` yields before `player_intermission` replaces `player.score`
with `player.score_total`. The reporter captures both immediately at the first
end signal without yielding; retries and replay retain that exact snapshot.
Custom scripts that overwrite the score before emitting any supported end
signal must preserve this ordering to retain a spendable final balance.
Deploy receiver support for `score_total` before the reporters. Old payloads
remain valid; the receiver treats their missing totals as unknown, not zero.
Every report includes everyone returned by `getPlayers()`, including
spectators, downed players, and AFK players. Empty rosters are sent as `[]`.
Undefined players and players with missing/empty names or GUIDs are skipped
with a warning containing their roster index; a later snapshot can include
them once their identity is available.
The script builds the JSON array delimiters explicitly because the plugin
serializes an empty GSC array as `null`; player objects and strings still
use `jsonSerialize` for escaping.
```json
{"event":"game_start","match":"process-token-1","seq":1,"server":"t5_zombie_prototype_1","map":"zombie_cod5_prototype","players":[{"name":"Josh","guid":"12345678901234567"}]}
{"event":"round_change","match":"process-token-1","seq":2,"server":"t5_zombie_prototype_1","map":"zombie_cod5_prototype","round":17,"players":[{"name":"Josh","guid":"12345678901234567","kills":420,"headshots":160,"downs":1,"revives":3,"score":32000,"score_total":62000}]}
{"event":"heartbeat","match":"process-token-1","seq":3,"server":"t5_zombie_prototype_1","map":"zombie_cod5_prototype","round":17,"players":[{"name":"Josh","guid":"12345678901234567","kills":430,"headshots":165,"downs":1,"revives":3,"score":32500,"score_total":62500}]}
{"event":"game_end","match":"process-token-1","seq":4,"server":"t5_zombie_prototype_1","map":"zombie_cod5_prototype","round":18,"players":[{"name":"Josh","guid":"12345678901234567","kills":445,"headshots":170,"downs":2,"revives":3,"score":33500,"score_total":65000}]}
```
Events are captured immediately into an in-memory queue. One HTTP worker
POSTs snapshots in gameplay order, with `Content-Type:
application/json` and `Authorization: Bearer <token>`, waiting for completion
before sending the next. Gameplay and round detection continue while requests
are pending. Repeated joins or
end notifications do not duplicate start/end reports. At game over, the
reporter captures any round change since the last poll, stops detection,
and lets the sender drain through `game_end`.
The first heartbeat is captured 30 seconds after match start, then every
30 seconds even when the round is unchanged or the roster is empty. Round
changes and later spawns do not reset the interval or add heartbeat workers.
Heartbeats use the same snapshot builder and ordered sender as round reports;
pending HTTP requests can delay transmission. Game end stops the heartbeat
worker. Only the newest waiting heartbeat is retained; round reports and the
in-flight request retain their order before `game_end`.
Each report includes a native-generated `match` key and a positive `seq`, starting
at 1 for `game_start`. Start/end snapshots are written to disk before sending and
replayed before a new match after map/process restarts. Retryable failures retain
the queue head with 1–30 second backoff; other 4xx are terminal. Acceptance requires
a 2xx response containing the JSON boolean `ok: true`, and logs include Electric
Cherry's `telemetry.timing.matchId`. The native 5-second connect / 15-second total
transfer limits still apply. Ordinary round/heartbeat failures are best effort.
**Deploy Electric Cherry #144 and the updated TWS plugin before these scripts.**
See the shared [delivery and rollout notes](../../README.md#telemetry-delivery-and-rollout-t5-and-t6)
for outbox location, persistence requirements, capacity/error handling and tests.
**Diagnosing a lobby that stops reporting**
Default logging includes initialization, match start/end, each request's
event/round and local ID, queue wait and request duration in milliseconds,
transport failures/recovery, HTTP rejection/status/timings, and the final queue drain.
Each log identifies the original match key and payload sequence, including replays. Every log includes
the server, map, and game time in milliseconds.
An independent watchdog prints health every 30 seconds, even before match
start and while HTTP is pending or the game-end queue is draining. It shows
the sender stage, current snapshot capture, pending queue size (including
the retained in-flight head), total captured, accepted reports, completed transports, transport failures,
HTTP rejections, and
time since the last completion (`-1` means none). Warnings remain enabled
at log level `0`:
- `HTTP stalled`: a request has been pending for at least 30 seconds. The
warning repeats on watchdog ticks with its ID, event, age, and queue size.
- `sender is not consuming`: the oldest queued report has waited at least
30 seconds with no in-flight request outside an intentional retry backoff.
- `round watcher has stopped advancing`: no polling progress for over
5 seconds, detected on the next watchdog tick.
- `no snapshot captured`: an active match has gone at least 65 seconds
without a new snapshot, despite its 30-second heartbeat.
Set `tws_telemetry_log_level "2"` in the server console while investigating;
no map restart is needed. This adds capture/queue stages and API response
previews, escaped onto one line and limited to the first 256 characters.
Look for `ok: false` and the rejection reason in those previews; HTML may
indicate a proxy error. Previews can include player identities echoed by
the API. Restore `1` afterward, or use `0` to retain only warnings, errors,
and transport recovery messages.
Missing handles, transport errors, empty/invalid acknowledgments and retryable
HTTP responses keep start/end pending. The watchdog never launches overlapping
requests. A request pending beyond 30 seconds warrants checking the installed
plugin version, worker-queue delay and completion delivery. Native deadlines,
diagnostics and the telemetry outbox/acknowledgment helpers require the updated
[TWS plugin build](../plugins/README.md).
The upstream baseline contains
[`http::request`](https://github.com/alicealys/t5-gsc-utils/blob/main/src/component/http.cpp)
and [`jsonSerialize`](https://github.com/alicealys/t5-gsc-utils/blob/main/src/component/json.cpp).
Disabling the dvar does not remove that compile-time dependency; remove the
script when using a plugin build without those functions.
Stat and lifecycle references: [stock T5 zombies](https://github.com/plutoniummod/t5-scripts/blob/main/ZM/Common/maps/_zombiemode.gsc)
and [score tracking](https://github.com/plutoniummod/t5-scripts/blob/main/ZM/Common/maps/_zombiemode_score.gsc).
**Live validation (requires a T5 server)**
1. Start a match and verify one `game_start`, then an initial `round_change`
and one per new round. Check the exact server name, map, string GUIDs,
all player identities, and all six stats against the game. Earn points, then
spend some: the balance should decrease while the total stays unchanged.
2. Join again in the same round, go AFK, become downed, and spectate. Verify
the next report includes the full roster without another start event.
3. End a match and verify one `game_end` with both scores captured before the
scoreboard overwrite; compare `score_total` to the final scoreboard. Verify
the final stats arrive after all round reports, including if more than one
stock end notification fires. Check
a round-0 intro and an empty roster, including last-player disconnect.
4. Delay HTTP completion while rounds advance and the match ends. Verify
requests remain ordered and retain the stats captured at each event.
With the API unreachable, verify transport-error logging and continued
gameplay. Repeat without an active beta and with the HUD module removed.
5. Stay in one round for over a minute. Verify heartbeats at 30 and 60 seconds
after start, with current stats and the same fields as `round_change`.
Check that round changes and joins do not reset or duplicate the interval,
and no heartbeat follows `game_end`, including with requests pending.
6. Change log level between `0`, `1`, and `2` without a map restart. Check
request IDs pair sends with completions and level `2` shows bounded,
escaped response previews. Use an unregistered server name on a test
server and confirm the API rejection is visible in the response even
though transport completes. Also check an empty or HTML response.
7. Point a test server at an endpoint that accepts a connection but does
not reply. With the TWS plugin, verify a native timeout near 15 seconds and
a retained start/end retry with bounded backoff while gameplay continues.
Restore the endpoint and verify the queue resumes in order and
the watchdog stops after the final drain. Test a refused connection
separately to check failure counts and the recovery log once restored.
## External dependencies
[t5-gsc-utils](https://github.com/alicealys/t5-gsc-utils) is required for telemetry
and leaderboard HTTP, JSON, type and string helpers. Install a build exposing
the natives listed in each module's documentation, then restart the server
process. No database is required.
## Moderation
Optional `tws_t5_moderation.gsc` 1.0.0 requires common 1.2.0 and the matching
plugin 1.3.0. See the [moderation contract](../../docs/moderation.md) for private
configuration, persistence, action results and deployment checks.
## Persistent AFK reserve
AFK `2.0.0` uses EC authorization and a local central countdown. See the
[shared AFK guide](../../docs/afk.md) for exact commands, dependencies, recovery
and live checks. Install plugin `1.4.0+` and fully restart the server process.
The countdown remains visible with `.hud` hidden; expiration has no grace period.