598 lines
33 KiB
Markdown
598 lines
33 KiB
Markdown
# 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 eight `.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) | `.afk` / `.back` chat toggle + `[AFK]` indicator |
|
||
| [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) | Lobby `g_password` lock at a milestone round |
|
||
| [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 high-round record and all tied holders on connection |
|
||
| [tws_t5_bank.gsc](tws_t5_bank.gsc) | Persistent shared GUID bank and private balance/deposit/withdraw commands |
|
||
|
||
## Install / upgrade
|
||
|
||
Copy `tws_t5_*.gsc` to `storage/t5/scripts/sp/`, then restart the map.
|
||
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. Current/maximum HP appears to the right
|
||
of the bar only while injured; it disappears at full health. The AFK indicator
|
||
sits above the zombie count. 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 and AFK indicator.
|
||
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 full nine-command list 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), or four for records (five with a multi-page
|
||
counter). 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 11 + 1 elements.
|
||
|
||
HUD `1.4.0` and AFK `1.2.0` require common `1.4.0+`. Update common, HUD and AFK
|
||
together where installed, then restart the map. Leaderboard `1.1.0` remains
|
||
compatible. This change requires no new Plutonium/plugin/backend dependency or
|
||
full server-process restart.
|
||
|
||
## 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 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.
|
||
|
||
## 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. All nine installed commands
|
||
fit in one list with command and description columns. Additional 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 and AFK indicator (HUD module required) |
|
||
| `.afk` | Pause with protection |
|
||
| `.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) |
|
||
|
||
Each module registers its own help entries. Commands follow the order above,
|
||
independent of initialization order; removing a module also removes its entries
|
||
from `.help` after a map restart.
|
||
With all command modules installed, help shows one page of nine commands,
|
||
including 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.
|
||
|
||
---
|
||
|
||
## 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 and AFK indicator; data updates continue while hidden
|
||
- Top-center `TWS ZOMBIES · discord.tws.lol` branding
|
||
- Bottom-center zombies-remaining counter, with the AFK indicator above it
|
||
- 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`
|
||
- 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
|
||
|
||
Self-invoked pause mode. Owns the `[AFK]` indicator element, centered above the
|
||
zombie count through common layout values. It does not require HUD.
|
||
|
||
**Features**
|
||
- Chat commands `.afk` and `.back` (via `level waittill("say")` — not
|
||
`callbackPlayerSay`, which never fires on T5 SP dedicated)
|
||
- On enter: `enableInvulnerability`, `freezeControls(true)`,
|
||
`self.ignoreme = true` (drops out of zombie targeting)
|
||
- On exit: reverse, then a 2s grace tail keeps invuln + ignoreme through
|
||
the target-reacquisition window
|
||
- `[AFK]` HUD element created per-player via spawn handler; alpha-toggled
|
||
by enter / exit through common's core visibility helpers, respecting `.hud`
|
||
|
||
**Provides**
|
||
- Per-player: `self.hud_afk_on` (bool), `self.hud_afk_elem` (HUD element),
|
||
writes `self.ignoreme` (stock BO1 targeting flag)
|
||
- Level var: `level.afk_grace_seconds` (default 2)
|
||
|
||
**Depends on**
|
||
- `scripts\sp\tws_t5_common::` — `ensure_config`, `make_core_overlay_text`,
|
||
`set_core_alpha`, `destroy_on_disconnect`, `register_spawn_handler`, `register_help_command`
|
||
- Shared config: overlay AFK position, `level.hud_font_afk`, `level.hud_c_afk`
|
||
- Stock BO1: `enableInvulnerability`, `disableInvulnerability`,
|
||
`freezeControls`, `.ignoreme`, `level waittill("say", message, player)`
|
||
|
||
Spawn handler registered: `::afk_spawn`.
|
||
|
||
---
|
||
|
||
## 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)
|
||
- Existing players are unaffected; `g_password` is only checked at connect
|
||
- Clears `g_password` on `hud_game_over` so the next lobby opens unlocked
|
||
- 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 |
|
||
|
||
**Depends on**
|
||
- `scripts\sp\tws_t5_common::` — `ensure_config`, `int_dvar_or`
|
||
- `tws_t5_hud` — waits on `hud_round_changed` and `hud_game_over`
|
||
- Stock BO1: `level.round_number`
|
||
- Dvar: `tws_lock_round` (int, default `20`)
|
||
|
||
---
|
||
|
||
## 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.
|