603 lines
38 KiB
Markdown
603 lines
38 KiB
Markdown
# 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` |
|
||
|
||
## 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 <amount|all>` to deposit spendable points, and `.w <amount|all>` 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-<map milliseconds>-<counter>`
|
||
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. Current/maximum HP
|
||
appears to the right of the bar only while injured, then disappears at full
|
||
health. The AFK indicator sits above the zombie count. 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, damage-only HP and AFK indicator. 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 all nine installed commands in one list with command
|
||
and description columns: `.help`, `.hud`, `.afk`, `.back`, `.join`, `.leaders`,
|
||
`.b`, `.d`, `.w`. Missing modules' commands are omitted. Bank syntax remains
|
||
`.d <amount|all>` and `.w <amount|all>`, 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. 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 11 archived elements, plus one for AFK. The standard
|
||
nine-command help view 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. The controller and pending requests are separate from
|
||
display lifetime, so an idle service holds no HUD elements.
|
||
|
||
HUD `1.4.0` and AFK `1.2.0` require matching common `1.4.0+` on both games.
|
||
Update common, HUD and AFK together where installed, then restart the map.
|
||
Leaderboard `1.1.0` remains compatible. There is no new Plutonium, native-plugin
|
||
or backend requirement, and this change does not require a full server-process
|
||
restart. 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,
|
||
request replacement and cleanup checks, and `node tools/check_leaderboard.mjs`
|
||
for record data handling. 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.
|
||
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
|
||
one list of nine commands, full bank syntax, no single-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 AFK indicator,
|
||
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.2.0+`** for that game when using leaderboard `1.1.0`.
|
||
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.
|
||
|
||
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.
|
||
|
||
## Telemetry authentication (T5 and T6)
|
||
|
||
Both reporters default to `https://ec.thewrightserver.net/api/telemetry` and send
|
||
`Authorization: Bearer <token>` 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.
|