340 lines
21 KiB
Markdown
340 lines
21 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 16 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.1.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`. It sets `tws_server_id`, `tws_bank_token` and
|
||
`tws_bank_url` automatically. 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. The URL must use HTTPS.
|
||
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.0+** 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.
|
||
|
||
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.
|
||
|
||
## Map record announcements
|
||
|
||
Install `tws_t5_leaderboard.gsc` for T5 or `tws_t6_leaderboard.gsc` for T6,
|
||
**along with the updated common module** for that game.
|
||
The new 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 command asks you to try again shortly.
|
||
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 in the same HUD area as `.help`,
|
||
below the health bar, with a heading such as `Record: Round 23` and holder names
|
||
underneath. Each page shows up to three holder lines for 12 seconds. Opening
|
||
`.help` hides the record and pauses its display time until help closes.
|
||
All holders are included; colour/control codes are removed, and unusually long
|
||
names are shortened to keep messages readable. HUD elements are removed when
|
||
the announcement finishes, the player disconnects, or the game ends.
|
||
|
||
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).
|