# tws-zombies-scripts Combined working repo for the TWS Plutonium Zombies scripts and native plugins across two games. [Changelog](CHANGELOG.md) · [Current component versions](versions.json) ## Layout - [t5/scripts/](t5/scripts/) — T5 GSC modules, [README](t5/scripts/README.md), and [notes](t5/scripts/NOTES.md). - [t5/plugins/](t5/plugins/) — `tws-t5-gsc-utils`, based on [alicealys/t5-gsc-utils](https://github.com/alicealys/t5-gsc-utils), with TWS HTTP changes. - [t6/scripts/](t6/scripts/) — T6 GSC modules, [README](t6/scripts/README.md), [notes](t6/scripts/NOTES.md), and compiler checks. - [t6/plugins/](t6/plugins/) — `tws-t6-gsc-utils`, based on [alicealys/t6-gsc-utils](https://github.com/alicealys/t6-gsc-utils), with the same HTTP changes. The scripts still deploy to the game's existing `storage/t5/scripts/sp/` or `storage/t6/scripts/zm/` directories; the new source layout does not change GSC namespaces. ## Versioning [versions.json](versions.json) records the current source version of each individual script and each native plugin. Every `.gsc` file has its own version, keyed by its exact filename. Changing one script bumps only that script's version; other scripts increase only when their own source changes. | Component | Manifest key | Source | | --- | --- | --- | | Each T5 script | Exact filename, e.g. `tws_t5_afk.gsc` | Corresponding file in `t5/scripts/` | | Each T6 script | Exact filename, e.g. `tws_t6_afk.gsc` | Corresponding file in `t6/scripts/` | | T5 plugin | `t5-plugin` | `t5/plugins/tws-t5-gsc-utils/` | | T6 plugin | `t6-plugin` | `t6/plugins/tws-t6-gsc-utils/` | Every completed script or plugin change includes a version bump for each affected component and an entry in [CHANGELOG.md](CHANGELOG.md). Each entry identifies each changed script or plugin and its new version, compatibility requirements, and deployment steps. Shared helpers such as `tws_t5_common.gsc` have their own versions; document required dependency versions when compatibility changes. A shared helper change alone does not bump its unchanged consumers. Changes to shared build/dependency tooling also bump the plugins they affect. Versions use `MAJOR.MINOR.PATCH`: increase the patch for fixes and internal changes, the minor for compatible features, and the major for breaking changes to commands, configuration, native APIs, or data contracts. Reset lower fields when increasing the minor or major. Bump once per logical change; documentation-only and test-only changes are exempt. All 18 scripts and both plugins advance independently. The initial `1.0.0` versions document the existing source baseline. These TWS versions are separate from upstream revisions in each plugin's `upstream.json` and the existing native loader `plugin_version()` value. Component versions identify source changes; consult the changelog for compatible script/plugin combinations and receiver requirements. Maintenance rules are in [AGENTS.md](AGENTS.md). ## Building plugins Plugin source is vendored as regular tracked files so local modifications travel with this repository. Each plugin's `upstream.json` records its upstream revision and exact Git dependency revisions. Dependencies and build outputs are ignored. The dependency loader replaces upstream's submodule setup; nested upstream `.github` files are retained for reference and do not run as this repo's CI. Use Windows with Visual Studio 2022 C++ build tools, a Windows SDK, Git, and Python 3.9+. From a Developer PowerShell with `msbuild` on PATH, at the repository root: ```powershell .\tools\build_plugins.ps1 -Game all -Configuration Release ``` This restores pinned dependencies, generates both projects, and builds x86 DLLs. Upstream's Premake setup also downloads its MySQL client dependency on first use. Outputs are `t5/plugins/tws-t5-gsc-utils/build/bin/Release/tws-t5-gsc-utils.dll` and `t6/plugins/tws-t6-gsc-utils/build/bin/Release/tws-t6-gsc-utils.dll`. Remove the old `t5-gsc-utils.dll` or `t6-gsc-utils.dll` before installing the corresponding `tws-` DLL in `Plutonium/plugins/`; load only one version per game. **Restart the server process**; restarting only the map cannot load an updated plugin. Use `python tools/fetch_plugin_deps.py all --check` to verify dependency revisions without fetching. Use `python3 tools/test_plugin_http.py` on macOS/Linux with a C++20 compiler and libcurl development files to compile the real HTTP utilities and test local stalled connections, response deadlines, status reporting and recovery. These portable tests do not build the Windows DLLs or exercise Plutonium's VM. Run `python3 tools/test_plugin_component.py` to compile the completion/option handling against host engine shims with address and undefined-behavior sanitizers. Both plugins now default to a 5-second connection timeout and 15-second total transfer timeout. Request options can override these with positive millisecond values. Telemetry explicitly sets those limits and logs native HTTP status and timings. See the [T5](t5/plugins/README.md) and [T6](t6/plugins/README.md) plugin notes. Telemetry retries and durable replay require the updated plugins; see the [delivery and rollout notes](#telemetry-delivery-and-rollout-t5-and-t6). ## Telemetry delivery and rollout (T5 and T6) **Deploy Electric Cherry [#144](https://gitea.thewrightserver.net/josh/electric-cherry/issues/144) before these reporters.** Replay requires the receiver to associate reports with `match` and deduplicate `seq`; the legacy receiver can apply an old end to a newer match. Build and install both updated TWS plugins, restart the server processes, then deploy the corresponding scripts. The new `telemetryNewMatch`, `telemetryLoad`, `telemetryStore`, `telemetryRemove` and `telemetryResponse` functions are required even if telemetry is disabled by dvar. Stat reports now include both `score` (spendable balance) and `score_total` (stock cumulative match score) for each player. Deploy Electric Cherry's `score_total` receiver support before these scripts; older receivers discard the extra field. No additional plugin update is needed for this field. The first stock `end_game` signal is captured without yielding, before intermission replaces the balance with the total. `game_start` remains an identity-only roster. Every captured report includes a `match` key (random native process token plus an increasing match counter, at most 64 characters) and a positive `seq`. `game_start` is sequence 1. Retries and replay send the exact original JSON; sequence numbers are never reassigned. Player snapshots still reflect capture time; no client timestamp has been added and API receipt times retain their existing meaning. A single sender keeps `game_start` and `game_end` at the queue head on transport failure, native timeout, HTTP 429 or 5xx. Delays are 1, 2, 4, 8, 16, then 30 seconds between attempts, capped at 30 seconds without a retry-count limit. Other 4xx (and redirects) are logged and dropped. A 2xx with a JSON boolean `"ok": true` acknowledges delivery; empty, malformed or negative 2xx replies and missing HTTP diagnostics are unacknowledged and retried for boundaries. Accepted reports log Electric Cherry's `telemetry.timing.matchId` as `ec_match_id` when supplied. Heartbeats and round changes remain best effort. Waiting heartbeats are coalesced to the newest snapshot, preserving round reports and any in-flight request. Intentional coalescing can produce gaps in `seq`. The native outbox saves start/end snapshots synchronously before sending, and removes them only after acknowledgment or a terminal HTTP rejection. Files live under `fs_homepath/tws-telemetry/t5/` or `t6/`, scoped to the exact server name and endpoint. Keep this directory on persistent writable storage across process or container restarts; it contains player snapshots but no authorization token. Use a single process per server/endpoint and storage directory. Endpoint/server changes use a separate outbox, so restore the original configuration to replay its backlog. Keep `tws_server_id` stable between maps. On map/process startup, recovered boundaries precede any new `game_start`. An accepted report whose acknowledgment/removal was lost is safely replayed with the same identity. Writes replace the committed file only after a complete temporary write. A storage error blocks the affected boundary before sending and logs an error; unsaved in-memory reports cannot survive a restart. Unreadable/corrupt saved data disables telemetry until storage is repaired and the map restarted, preserving the file for recovery. Each outbox is limited to 4,096 reports / 16 MiB, with individual report bodies below 20 KiB; reaching a limit fails visibly without discarding the saved backlog. Disk removal failures retry cleanup without sending an already resolved report again during the same map. The plugin cannot recover a `game_end` that was never captured (for example, a process crash during play). Run `node tools/check_telemetry.mjs` for both reporters' queue/retry fixtures and `python3 tools/test_plugin_telemetry.py` for native storage, acknowledgment and process-restart checks with sanitizers. The native test uses each restored JSON dependency; alternatively pass `--json-header '/path/{game}-json.hpp'` with the single headers from each plugin's pinned revision. These tests use engine shims. Validate Windows DLL linking and both games in Plutonium before rollout: force a timeout/429/503 at start and end, confirm permanent 400/401 failures do not retry, restart a map and then a process with pending boundaries, and confirm their original match IDs complete before the next start. Also verify a 2xx without `ok: true` stays pending, an outage retains only the latest waiting heartbeat, and a disk failure is visible without advancing past an unsaved boundary. ## Parity T5 and T6 are separate source trees, not a shared library — GSC namespaces, includes, and stock APIs differ enough between the two engines that a shared file is not practical. Feature parity is enforced pairwise: a change landed in `t5/` should be considered for a companion change in `t6/`, adapted to T6 conventions. Modules currently in each tree: | Module | T5 | T6 | |------------|-------------------------------|---------------------------------| | common | `t5/scripts/tws_t5_common.gsc` | `t6/scripts/tws_t6_common.gsc` | | hud | `t5/scripts/tws_t5_hud.gsc` | `t6/scripts/tws_t6_hud.gsc` | | afk | `t5/scripts/tws_t5_afk.gsc` | `t6/scripts/tws_t6_afk.gsc` | | lock | `t5/scripts/tws_t5_lock.gsc` | `t6/scripts/tws_t6_lock.gsc` | | telemetry | `t5/scripts/tws_t5_telemetry.gsc` | `t6/scripts/tws_t6_telemetry.gsc` — queued `game_start` / `heartbeat` / `round_change` / `game_end` matching T5 | | join | `t5/scripts/tws_t5_join.gsc` | `t6/scripts/tws_t6_join.gsc` | | bank | `tws_t5_bank.gsc` | `tws_t6_bank.gsc` — persistent shared GUID bank | | leaderboard | `t5/scripts/tws_t5_leaderboard.gsc` | `t6/scripts/tws_t6_leaderboard.gsc` | ## Lobby lock voting (T5 and T6) Lock scripts `1.1.0` add exact, case-insensitive `.lock` and `.unlock` chat commands, also listed in `.help` when the lock module is installed. `.lock` is available **after round 5 (round 6 onward)**; `.unlock` is available whenever the server is locked. Both require a strict majority of the current connected players: 1 of 1, 2 of 2, 2 of 3, or 3 of 4. Dead, spectating and AFK players count equally. Larger lobbies use `floor(players / 2) + 1`. Typing the command starts a 60-second vote and casts your vote; teammates type the same command to support it. Solo commands apply immediately. Each connection gets one vote per ballot. Repeats neither add votes nor extend the deadline. Joins and disconnects update the majority; disconnected votes are discarded, and reconnecting does not restore a vote. A smaller roster can pass an ongoing vote with the remaining votes. An expired vote needs fresh votes from everyone. Typing the command for the current state only replies privately; it does not withdraw a vote or cancel an ongoing ballot. Automatic locking remains at `tws_lock_round` (default `20`) and happens at most once per match. A successful `.unlock` vote keeps the lobby open for the rest of that match, including when players lock and unlock before round 20. Players can still vote `.lock` again. An `.unlock` request while already open does not disable the automatic lock. Automatic locking clears any unfinished manual lock ballot. Each activation generates a fresh `g_password`; existing players remain connected. Startup and game over clear the password and votes. There is no automatic prompt at round 6. Vote progress and state changes use ordinary message text, with private replies for unavailable/repeated commands; no center-screen popup or recurring reminder is added. Compatibility: no new dependency requirement; keep the matching common and HUD modules installed (common `1.0.0+`, HUD `1.0.0+`). HUD supplies round notifications. Copy each updated lock script to its existing game script directory and restart the map. No native plugin update, full server-process restart or other deployment action is required. Run `node tools/check_lock.mjs` and `node tools/check_hud.mjs` for portable vote, lifecycle and help pagination checks. Compile the T6 script using the [T6 compiler check](t6/scripts/README.md#validation). Before rollout, verify on live T5 and T6 servers: 1. Try `.lock` in rounds 5 and 6, then both commands with 1–4 players. Confirm the exact majority, instant solo behavior, private duplicate replies and ordinary vote text. Check both commands in `.help` and `.commands`. 2. Let a vote expire, then retry; disconnect a voter, disconnect a non-voter, reconnect and join mid-vote. Verify the current roster and fresh votes count. 3. Allow round-20 auto-lock, unlock, advance another round and relock. Also lock/unlock early and cross round 20. Confirm a new client is blocked when locked and can join when unlocked; connected players stay in the game. 4. End the match with a lock or pending vote, then restart the map. Confirm the next lobby is open and old votes cannot change it. Check a custom `tws_lock_round` and confirm it does not change the round-6 manual gate. ## Player bank (T5 and T6) Install `tws_t5_bank.gsc` or `tws_t6_bank.gsc` alongside the game's common and telemetry scripts (`1.0.0` or later). Players can use `.b` to read their balance, `.d ` to deposit spendable points, and `.w ` to withdraw. Configured bank modules register all three commands in `.help`. Balances are shared by GUID across games and survive map/server restarts. Transfers do not change `score_total`; bank-assisted matches still count on existing leaderboards. Deploy [Electric Cherry #146](https://gitea.thewrightserver.net/josh/electric-cherry/issues/146) first, then build/install both native plugins `1.2.0` and **restart the server processes**. The plugins provide `bankLoad`, `bankStore`, `bankRemove` and `bankResponse`. Removing the bank script is necessary to omit these linking requirements; disabling it by dvar does not avoid linking. Electric Cherry generates a unique bank token when it prepares a native server instance and injects it through that instance's private `electric_cherry_bank.cfg`, with `tws_server_id` supplied by the launch configuration. Both bank scripts `2.0.0` always use `https://ec.thewrightserver.net/api/bank`; `tws_bank_url` is no longer read, even if an old configuration still sets it. The host agent's enrolled controller address is independent and continues to serve agent traffic. Agent `1.9.1+` writes only the private `tws_bank_token` in the bank configuration. Rebuilds and host moves keep the instance token; destroying the instance revokes it. Reusing a server name gets a new token. There is no `EC_BANK_TOKENS` map or per-server secret to maintain. Keep secrets outside shared scripts and promoted configs. Set `tws_bank_enabled 0` to disable the module; restart maps after script/config changes. Electric Cherry's `EC_BANK_MAX_BALANCE` defaults to 250000 (range 1–1000000). Each transfer and bank-funded spendable balance is capped at 1000000 points. Transfers require a living player recorded in the server's current telemetry match. Newcomers may wait until the next 30-second heartbeat. The game validates points and player state again after a prepare reply; failed or uncertain prepares leave game points unchanged and cancel the reservation. After points move, a durable commit decision retries settlement without applying them again. A transfer in progress blocks new bank requests on that server. A GUID can have only one prepared transfer across the fleet. Preserve `tws-bank/t5/` and `tws-bank/t6/`, relative to the game process working directory, across process/container restarts, with one process per server/endpoint directory. EC host agents **1.9.1+** settle pending journals before deleting runtime storage on removal, rebuild, migration or launch cancellation. Update the agents before enabling the bank. An outage or invalid journal blocks cleanup and preserves the files. Decisions are scoped to server name and URL and contain no token. Recoveries run before new bank requests; failed cleanup/replies retry with the original identity. Corrupt storage blocks the bank; failed writes prevent points changes. Restore storage or credentials to finish pending work, rather than deleting the journal. Holds have no automatic expiry. Already-applied withdrawals are match points and are not refunded when a match ends or its process crashes. As with any live score, the journal cannot make game memory and the remote database atomic across a machine/storage failure. ### Bank diagnostics — TWS #14 Bank scripts `2.0.2` require their matching TWS plugins `1.2.0+`. Startup logs report the script version, the loaded plugin's TWS version via `bankVersion()`, the effective endpoint, a validated server ID, credential format validation, and the loaded journal count. Credential validation checks the local format; HTTP 401/403 still identifies a credential rejected by EC. A failed journal load logs `pending=unknown` and disables the bank; invalid configuration reports that journals were not loaded. Server IDs outside the bounded log-safe token alphabet are shown as `invalid`/`unavailable` without changing the configured API identity. Each HTTP attempt receives a local `correlation=bank--` for console diagnostics, including `.b`. These IDs are scoped to a map/process log, never sent to EC, and never replace durable transfer IDs. Balance requests still omit the API request ID and require an empty response ID. Transfer logs also include their original request ID, linking prepare, journal and settlement. Outcomes distinguish request creation, transport failure, HTTP rejection, missing diagnostics/response, invalid acknowledgment (with a fixed parser reason), request-ID mismatch, unexpected state, and valid application rejection. Available HTTP status, curl code and cumulative DNS/connect/TLS/total milliseconds are included; absent or malformed fields say `unavailable`. Only allowlisted response codes reach logs; unfamiliar codes say `unknown`. No token, authorization header, raw response/request body, transport exception or private configuration contents are logged. Existing point accounting, journal format, API payloads, retry delays and player-facing messages are unchanged. Settlement lines add the durable decision, attempt count, pending seconds, next retry delay, cleanup failures and eventual resolution. Pending time begins when a new cancel decision is attempted; recovered journals measure from this map's load, so downtime is not included. Counters reset on map/process restart. Repeated identical failures per action are summarized at most every 30 seconds, with a `suppressed` count. Reason/status/code changes and recovery log immediately. Native storage logs similarly identify load/write/remove failures and recovery. Rate-limited attempts retain their correlation internally; only emitted lines appear in the console. No log history or retry metadata is added to the journal. Build/install both updated DLLs and **restart each full server process**, then install the corresponding scripts and restart the map if they were copied after startup. `bankVersion()` is a new linking requirement even when the bank is disabled by dvar. No EC/agent change or new third-party dependency is required; existing bank API, common/telemetry and endpoint-migration requirements still apply. Verify deployed DLL and GSC SHA-256 hashes against the built release artifacts, and confirm startup reports `script=2.0.3 plugin=1.2.0` before interpreting results. An old bank script checksum on Ascension was reported in issue #14; deploying only the DLL does not update GSC. No deployment was performed here. Run `node tools/check_bank.mjs --ec-root /path/to/electric-cherry` and `python3 tools/test_plugin_bank.py`. On **both T5 and T6**, compile/link and test successful `.b`/deposit/withdrawal, HTTP 401/403, timeout, malformed JSON/schema, wrong response ID, unwritable/corrupt journals and settlement recovery after a map/process restart. Check first/change/recovery logs, 30-second suppression, HTTP timings, unchanged player messages/points and one remote ledger entry per transfer. Keep journal files intact during failure tests. Windows DLL/GSC builds, installed-artifact verification and these live game checks remain required. ### Bank identity validation Bank scripts `2.0.1` use T5's `player getGuid()` and T6's persistent `player.guid`, matching each game's telemetry identity source. Both normalize integer identities to strings and preserve existing strings exactly, including large or zero-prefixed GUIDs. Missing or invalid identities show a private "Bank player identity unavailable" message before any request, journal write or score change. GUIDs must meet EC's existing 1–64 character token contract (`A–Z`, `a–z`, `0–9`, `_`, `.`, `:`, `-`). Persisted decisions and their retries retain that original string identity, even after the player disconnects. No new plugin or EC update is required for this fix. Install the updated bank script in each game's script directory and restart the map. This does not repair an older numeric-GUID journal: preserve any such pending decisions and their request IDs for scoped recovery; never delete them to unblock a rebuild. The endpoint upgrade procedure below still applies to legacy URLs. Run `node tools/check_bank.mjs` for both scripts' portable fixtures. To also exercise the actual EC bank service and bank migrations with isolated SQLite: ```sh node tools/check_bank.mjs --ec-root /path/to/electric-cherry python3 tools/test_plugin_bank.py ``` The optional EC check requires that checkout's installed `typescript` and `better-sqlite3` dependencies. It uses no EC configuration, credentials, network or production database. It covers numeric rejection, normalized balances and transfers, zero-balance `.w all`, cancellation, and idempotent commit replay. Native checks require the pinned JSON headers as described above. These tests do not validate game VM linking, HTTP route authentication or deployed artifacts. After deployment, compare the SHA-256 hashes of both installed bank scripts with the source files from this release, and confirm the map restarted. On T5 and T6, check `.b`, a deposit, a withdrawal, and `.w all` with zero bank balance (an insufficient-balance message with no point change). Confirm EC records string GUIDs matching telemetry, one ledger entry per completed transfer, and unchanged `score_total`. On a test server, interrupt settlement and restart the map/process: the original request ID and decision must settle once without reapplying game points. GSC compilation and these live checks remain required. ### Upgrading the bank endpoint Deploy the Electric Cherry agent `1.9.1+` companion to [TWS #13](https://gitea.thewrightserver.net/josh/tws-zombies-scripts/issues/13) before installing bank scripts `2.0.0`. It accepts journals for the public game endpoint and the legacy bank URL derived from that host's enrolled controller. It settles through the existing authenticated, instance-scoped agent API at the controller; it never follows journal URLs and rejects other endpoint identities. The instance token and server identity must stay the same through the upgrade. **If the old `tws_bank_url` differs from the public endpoint, rebuild the server through EC with agent `1.9.1+` and the new scripts.** The agent stops the old process and settles its original commit/cancel decisions before starting the replacement and deleting old storage. A lost reply replays the same request ID; EC deduplicates it. An outage, corrupt journal or failed acknowledgment retains storage and blocks the replacement. Resolve the failure and retry the rebuild. Keep the enrolled controller address unchanged until legacy decisions drain. Do not merely replace scripts and restart the map: native journals are keyed by server and endpoint, so the new scripts cannot see a legacy endpoint's pending decisions. Do not rename, rewrite or delete those journals to force migration. If the previous endpoint already was the exact public URL, install the scripts and restart the map; saved decisions replay under the same journal identity. No new plugin build or full process restart is required for that case. Other legacy endpoints are refused by the agent and require recovery with their original configuration before switching. No deployment has been performed here. Run `node tools/check_bank.mjs` and `python3 tools/test_plugin_bank.py`. Before rollout, build/link Windows DLLs, compile GSC, and verify both games: help, balance and `all` commands, score HUD/persistence, max/overdraw errors, death/respawn/disconnect during HTTP, late telemetry, lost prepare/commit responses, disk failures and map/process restart with pending decisions. For this upgrade also verify stale URL settings are ignored, an empty `.b` shows zero, `.w all` reports insufficient balance, and an EC rebuild/removal settles legacy and public journals without duplicate ledger entries or lost points. ## Core HUD and service window (T5 and T6) The core uses a light, unboxed layout: match time above round time at the upper left, `TWS ZOMBIES | discord.tws.lol` centered along the top, and the zombie count centered near the bottom above a slim health bar. The complete `N ZOMBIES` text uses one centered element at the health track's midpoint, keeping the group centered as the count gains or loses digits. It uses a uniform font scale of `1.25` and shows `-- ZOMBIES` before data is available. Current/maximum HP appears to the right of the bar only while injured, then disappears at full health. The AFK reserve countdown is independently centered on screen. Text uses an outline for contrast; there are no panel backgrounds or timer labels. Fullscreen X/Y anchors bypass safe-area insets. Overlay positions and shader widths use the normalized `640 × 480` viewport directly. Text uses a minimum font scale of `1.0`. Check the upper-left timers against stock map inventory/objectives and performance overlays, and keep the upper-right service area below the Plutonium tag. Physical proportions vary with aspect ratio, so verify text fit and screen-edge clearance in-game. The core is created once per connection and stays in place when services open, across death and respawn, and while newcomers spectate. Unavailable values use placeholders until the first-spawn callback binds the data displays. Creation does not start the match clock. Each engine retains its timer behavior; clocks, health and zombie-count displays freeze at game over. Health display fallbacks never modify gameplay max health. Type `.hud` in chat to hide or show your core, including its branding, timers, zombie count, health bar and damage-only HP. The AFK countdown stays visible. The exact command is case-insensitive. Visibility defaults on and persists across death/respawn within the current connection/map; reconnecting or restarting the map restores the visible default. Data and timers continue updating while hidden, so showing the HUD restores current values or the frozen game-over values. Help, records and the stock HUD remain unaffected. Common owns one unboxed service area at the upper right. `.help` and its `.commands` alias show installed commands in pages of up to nine rows with command and description columns: `.help`, `.hud`, `.afk`, `.back`, `.join`, `.leaders`, `.b`, `.d`, `.w`, plus registered commands such as `.lock` and `.unlock`. Missing modules' commands are omitted. Bank syntax remains `.d ` and `.w `, and `.join` identifies its newcomer restriction. Additional registered commands use pages of nine rows; a page count appears only when more than one page is needed. Each page lasts 12 seconds and the list closes after the final page. Repeating either help command restarts page one. Both engines position rows explicitly, avoiding T5's newline-rendering problem. Records use the same service area, right aligned: `Map record | Round N` followed by up to three bounded holder lines. Single-page records have no counter or footer; ties spanning multiple pages retain a page count and 12 seconds per page. Help pauses a visible record and resumes its remaining page time when help ends. `.leaders` during help replaces the pending record with a fresh replay from page one. Round-start reminders use just the record header and one proximity or milestone line for six seconds. They skip busy services; opening help discards a reminder instead of resuming it later. One service is visible at a time. Services remain private and work when the HUD module is omitted. Both games allocate only the active service view and release it when idle or before switching between help and records. Pages reuse the active view's elements. The core uses 10 archived elements, plus two independent elements for AFK. A nine-row help page uses 19 nonarchived elements, or 20 with a multi-page counter; records use four, or five with a multi-page counter. Hidden elements still occupy slots. Compact reminders use two nonarchived elements. The controller and pending requests are separate from display lifetime, so an idle service holds no HUD elements. HUD `1.4.2` requires matching common `1.4.0+` and [Plutonium r5334+](https://plutonium.pw/docs/changelog/#r5334) on both games. The centered counter uses `SetTextUnlimited` only when the count changes, avoiding dynamic counter strings in the text config-string pool. Numeric HP still uses `SetValue`. On r5334+, deploy the updated HUD script and restart the map; no full server-process restart is required. On older Plutonium revisions, update clients and server and fully restart them before deploying. Leaderboard `1.2.0` requires matching common `1.5.0+` for round-start reminders. AFK `2.0.0` requires plugin `1.4.0+`, common `1.4.0+` and the EC reserve service; see the [AFK guide](docs/afk.md). Existing leaderboard plugin requirements still apply. Common retains its older panel helpers for unchanged consumers; the new layout uses fullscreen overlay helpers with the same integer shader-width handling. ### HUD validation Run `node tools/check_hud.mjs` for portable pagination, service arbitration, reminder rendering, request replacement and cleanup checks, and `node tools/check_leaderboard.mjs` for record data handling and reminder cadence. Run `node tools/check_bank.mjs`, `node tools/check_join.mjs` and `node tools/check_telemetry.mjs` for unchanged command/lifecycle regressions. These checks use engine shims and do not prove GSC VM behavior. Follow the [T6 compiler instructions](t6/scripts/README.md#validation); T5 requires a live Plutonium server because gsc-tool has no T5 backend. Before rollout, validate both engines in Plutonium: 1. Check 16:9 and narrower/wider resolutions, safe-area settings, long match timers, stock map inventory/objectives, performance overlays, player scores, ammo and interaction prompts. Check upper-left timers, top-center branding, bottom-center zombies/health and the upper-right service area. Text must stay legible without panel backgrounds; services must clear the Plutonium tag. Verify the health-bar fill stays within its track and damage-only HP clears stock scores/ammo. Check text fit at other aspect ratios because horizontal geometry scales with viewport width. Check `0`, `6`, `9`, `10`, `99` and `100 ZOMBIES`: the entire text must stay centered over the health track, including after `.hud` hides/shows it and after game over freezes the count. 2. Join before spawning, join mid-match, die/respawn and reconnect. Verify one core per player, spectator placeholders, no early match-clock start, correct health/Juggernog changes, and frozen timers at game over. HP numbers must appear only while injured and disappear on full healing. Toggle AFK and back without overlapping zombies/health or changing existing protection behavior. 3. Open `.help`, `.HELP` and `.commands` with all modules installed. Verify the installed commands including AFK balance/buy, full bank syntax and a page counter and automatic dismissal after 12 seconds. Add custom registered commands to exercise the nine-row page limit, page counter and repeat-to-restart behavior. Remove optional modules and restart the map to confirm filtering, including `.hud` disappearing without HUD, then restore them. 4. Interrupt records with help and check that the interrupted page resumes for its remaining time. Repeat `.leaders` while help is open; only the latest replay should start, at page one. Check long holder names, ties spanning pages, no records, and unavailable records. Repeat without the HUD installed. 5. Disconnect or end the game while help/records are active, reconnect and request them again. Verify no duplicate elements, stale timeout closures or surviving service windows. Confirm `.join`, bank commands and lobby lock retain their existing behavior with their required modules installed. 6. Toggle `.hud` and `.HUD` before spawning, during play, while AFK and after game over. Check private visibility, all core elements and the independently visible AFK countdown, continued updates while hidden, and unchanged help/records/stock HUD. Verify death/respawn preserves the choice and reconnect/map restart restores visibility. 7. On T5, watch the first automatic record announcement and its dismissal, then repeat `.leaders`, help and interrupted/resumed records. Verify the core health bar, damage-only HP, zombie count and timers remain intact while a service is visible and after it closes. Include stock round/connection notifications and other installed HUD mods; portable allocation checks do not establish live T5 SP rendering capacity. ## Map record announcements Install `tws_t5_leaderboard.gsc` for T5 or `tws_t6_leaderboard.gsc` for T6, **along with common `1.5.0+`** for that game when using leaderboard `1.2.0`. Copy both scripts for each installed game and restart the map. This update adds no native-plugin or backend requirement and needs no full server-process restart when existing leaderboard dependencies are already installed. The module privately announces the current map's fleet-wide highest recorded round and every tied holder once per connection, including spectator newcomers. Type `.leaders` to replay the record privately from the latest usable cache. Repeating the command restarts the announcement from its first page. It appears in `.help` when the leaderboard module is enabled. If no usable record is available, the service identifies the record as unavailable; this differs from a valid empty record history. Neither state adds a replay footer. It reads Electric Cherry's public `/api/leaderboard` endpoint without a token; only completed matches contribute. Ties may come from the same run or separate runs. Names are the API's latest player names, with players identified by GUID. Both versions default to `https://ec.thewrightserver.net/api/leaderboard`. T5 discovers the map by engine identifier. T6 selects the exact variant using the active mode/location, so Town Survival, TranZit and Town Grief stay separate. See the game READMEs for plugin requirements and installation paths. | Dvar | Default | Purpose | | --- | --- | --- | | `tws_leaderboard_enabled` | enabled | `0` disables announcements and requests | | `tws_leaderboard_url` | endpoint above | Full HTTPS API endpoint, without query parameters | | `tws_leaderboard_map_key` | automatic | Exact, decoded API map key when automatic selection is unavailable | For example, `set tws_leaderboard_map_key "t6:zm_transit:town"` explicitly selects Town Survival. Normally this setting is unnecessary. An override must exist in the API's map list and match the current game and engine map. Clear it when changing variants; restart the map after changing these settings. The announcement starts five seconds after player readiness and waits for the initial record fetch if necessary. It appears as right-aligned text in the common-owned upper-right service area: `Map record | Round N`, followed by up to three holder lines. There is no background or replay footer, and a page count appears only for multi-page records. Each page lasts 12 seconds. Opening `.help` pauses the record's remaining page time until help closes. A `.leaders` request during help replaces any pending replay and starts from page one after help closes. All holders are included; colour/control codes are removed, and overlong individual names are abbreviated without splitting UTF-8 characters. Bounded lines and additional pages preserve every tied holder. The window closes when the announcement finishes. Both games release their view elements when idle or changing service kinds. Disconnect or game end cleans up any remaining service elements. At round start, reminders become more frequent as the current round approaches the latest usable cached record. The cadence is measured back from that record: | Rounds below the record | Reminder rounds | | --- | --- | | More than 20 | None | | 11–20 | At gaps of 20 and 15 rounds | | 6–10 | At gaps of 10, 8 and 6 rounds | | 1–5 | Every round | | Record reached | Matching-round milestone | | First round above the record | Overtake milestone, then stop for the match | For a round 50 record, reminders appear on rounds 30, 35, 40, 42, 44 and 45–49, with milestones at 50 and 51. Each uses the existing record header and one short proximity or milestone line for six seconds, without holder pages. Milestones describe the round reached; the new result is recorded only after the match ends. The full join announcement and `.leaders` retain all holders and their 12-second pages. `.leaders` remains available after the overtake. Reminders require player readiness and skip players with a pending welcome, help or record view, or a welcome/manual record request in the last 30 seconds. Skipped reminders are not queued. Opening help discards an active reminder. Empty, unavailable and expired snapshots produce no reminders. The existing cache worker supplies all record data; round starts do not trigger HTTP requests. Both games use the same behavior, including with the optional HUD omitted. One level worker fetches the map list and pages of three players, stopping when the highest-round tie ends. Successful snapshots refresh every five minutes. Failures retry after 30 seconds and retain the last complete snapshot for up to 15 minutes. An empty selection says “No recorded high round yet”; unavailable, malformed or ambiguous results are logged and skipped when no usable cache exists. Partial page results never replace the cache. The API has no pagination snapshot token: changed totals/record rounds reject the refresh, but concurrent renames can still move entries between pages. Refreshes deduplicate GUIDs. Requests have a 15-second GSC wait limit. The TWS plugins also enforce native 5-second connection and 15-second transfer limits. The leaderboard waits for native completion before starting another request; player threads never wait directly on HTTP. Responses at the plugins' 20 KiB truncation boundary are rejected. The complete JSON grammar, UTF-8, string escapes and response shape are checked before consuming data. These guards also reject HTML/error bodies. Leaderboard still uses body validation; the new native HTTP status diagnostics are consumed by telemetry. Run the portable data/structure checks with `node tools/check_leaderboard.mjs`. They execute the scripts' data helpers with engine-value shims, not a game VM. Live checks: join while playing and spectating; die/respawn (no repeat); reconnect (one new announcement); compare Town/TranZit/Grief; check one holder, ties across pages, no records, and a slow/unavailable/malformed API. Disconnect or end the game mid-announcement and confirm messages stop. Verify existing HUD, `.help`, `.join` and telemetry still work. Open `.help` during a record page and check that the record resumes afterwards without overlapping it. Runtime linking and client readiness require Plutonium. Try `.leaders` and `.LEADERS` after the announcement ends and during a later page: each should start one private announcement from the first page. Repeat while help is open, then disconnect or end the game during a replay to check cleanup. Disable the leaderboard module and verify `.leaders` is absent from help. For reminders, use a known record on each engine and advance through all cadence boundaries, the matching round and the first higher round. Confirm later rounds remain quiet while `.leaders` still works, including after a cache refresh. Check a record not divisible by five to verify the cadence follows the record. Keep help or a full record open at an eligible round, join immediately before that round, and request `.leaders` within 30 seconds of it; no reminder should replace those views or appear later as a queued message. Open help during a reminder and confirm it does not resume. Verify six-second dismissal, two-element cleanup on disconnect/game end, and the same behavior without HUD installed. Repeat with an empty or expired cache and compare request logs: eligible rounds must add no HTTP traffic. Live T5/T6 rendering and round-transition validation remain required even when portable checks and T6 compilation pass. ## Telemetry authentication (T5 and T6) Both reporters default to `https://ec.thewrightserver.net/api/telemetry` and send `Authorization: Bearer ` plus `Content-Type: application/json` on every POST. Authentication is also used for retried/replayed reports. An explicit `tws_telemetry_url` override must also use HTTPS; remove any old HTTP override. Before upgrading the API or loading these reporters: 1. Set the Electric Cherry service's **environment variable** `EC_TELEMETRY_TOKEN` to a separate shared secret of at least 32 characters. A missing service token disables ingestion. 2. Both telemetry scripts embed the matching reporter secret in `level.telemetry_token`. No game-server token dvar or private config is needed. 3. Keep `tws_server_id` set to the exact registered server name, then restart the map. Both reporters refuse to start with a short embedded token or a non-HTTPS URL. Rotating the service token requires editing the token in both scripts, deploying them to every game server, and restarting the maps. The API also accepts `X-EC-Telemetry-Token`; these reporters use Bearer auth. Cookies, query parameters, and JSON fields cannot authenticate telemetry. Missing or incorrect request tokens are rejected before payload parsing or recording. Reporters that cannot set headers need a trusted relay with an HTTPS reporter-facing endpoint. The plugins used here accept a headers array. After deployment, verify all four event types reach Electric Cherry. On a test server, also verify that a short embedded token and an HTTP URL prevent startup, that a wrong token is rejected without recording an event, and that deploying the correct token restores ingestion. Plugin transport completion alone does not prove API acceptance; inspect the API response or ingestion records. ## Validating T6 ```powershell powershell -File t6/scripts/tools/check.ps1 -CompilerPath C:\path\to\gsc-tool.exe ``` Requires PowerShell and `gsc-tool` (T6 PC). ## Player moderation (T5 and T6) See [Moderation runtime](docs/moderation.md) for the instance credential contract, policy persistence, command-result limits and deployment validation. New `tws_t5_moderation.gsc` / `tws_t6_moderation.gsc` 1.0.0 require the corresponding TWS plugin 1.3.0 and common script 1.2.0. Moderation is optional; without private configuration it stays disabled. Existing bank and telemetry credentials are never reused. Native plugin updates require a full game-server process restart. ## Persistent AFK reserves (T5 and T6) See [AFK reserves](docs/afk.md) for commands, gameplay rewards, countdown behavior, recovery, coordinated EC deployment and the required live validation. AFK `2.1.0` requires matching TWS plugin `1.4.0+`, common `1.4.0+`, telemetry `1.0.0+`, Plutonium r5334+, EC #151 and host agent `1.12.0+`. Full server-process restart is required when installing the updated plugins. Solo and last-active-player AFK are allowed, including all players being AFK together. Upgrading only the AFK scripts from `2.0.0` requires a map restart, with no new dependencies or full server-process restart.