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

35 KiB
Raw Blame History

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.

Files

File Role
tws_t5_common.gsc Shared config, HUD helpers, paginated .help, service window, connect/spawn registries, single connected listener + dispatcher
tws_t5_hud.gsc Unboxed timers, branding, zombies/health and per-player .hud visibility toggle
tws_t5_afk.gsc Persistent AFK reserve, contribution rewards, purchases and central countdown
tws_t5_join.gsc .join lets newcomers spawn before the current round ends
tws_t5_lock.gsc Lobby g_password lock at a milestone round
tws_t5_telemetry.gsc Direct HTTP game start, round, heartbeat, and game end reports
tws_t5_leaderboard.gsc Private map record on connection, .leaders replay and adaptive round-start reminders
tws_t5_bank.gsc Persistent shared GUID bank and private balance/deposit/withdraw commands
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. 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.

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, 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: 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.

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. 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+ 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.

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 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 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. 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 (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)

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.

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 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 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. 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 and spawn entry point.

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 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):

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.

{"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 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. The upstream baseline contains http::request and jsonSerialize. 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 and score tracking.

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 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 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 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.