Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e95ebe3117 | ||
|
|
b508b6739b |
@@ -0,0 +1,58 @@
|
|||||||
|
# Repository instructions
|
||||||
|
|
||||||
|
## Versioning and changelog
|
||||||
|
|
||||||
|
- Follow the component boundaries and version policy in
|
||||||
|
[README.md](README.md#versioning). [versions.json](versions.json) is the
|
||||||
|
authoritative source for current TWS component versions.
|
||||||
|
- Each `.gsc` file has its own version in `versions.json`, keyed by its exact
|
||||||
|
filename. Every completed edit to a game script must bump that script's
|
||||||
|
version and add a matching entry to [CHANGELOG.md](CHANGELOG.md) in the same
|
||||||
|
change. This includes refactors and comment-only edits to script source.
|
||||||
|
Bump only scripts whose source changes; an unchanged script does not inherit
|
||||||
|
a bump from its game, a shared helper, or its T5/T6 counterpart. Document
|
||||||
|
required dependency versions when a shared helper or native API change affects
|
||||||
|
compatibility.
|
||||||
|
- Give each new script its own `1.0.0` entry. For a rename, carry forward and
|
||||||
|
bump its version under the new filename, recording both filenames in the
|
||||||
|
changelog. For removal, record the next version as the final removal entry in
|
||||||
|
the changelog and remove the filename from the active manifest.
|
||||||
|
- Every completed change to a plugin's source, pinned dependencies, or build
|
||||||
|
behavior must bump that plugin's version and add a matching changelog entry.
|
||||||
|
Changes to shared build/dependency tooling that affect both plugins require
|
||||||
|
both versions to increase.
|
||||||
|
- Bump each affected script or plugin once per logical change, not once per
|
||||||
|
editing operation. Use the highest applicable bump: patch for fixes or
|
||||||
|
internal changes, minor for compatible features, major for breaking changes.
|
||||||
|
Reset lower version fields when increasing the minor or major version.
|
||||||
|
- Documentation-only and test-only changes do not require a component version
|
||||||
|
bump. A documentation or test edit accompanying a script/plugin change does
|
||||||
|
not exempt that component from a bump. Ignored build outputs and dependency
|
||||||
|
checkouts do not count as source changes.
|
||||||
|
- Add entries newest first under the actual change date (`YYYY-MM-DD`), with a
|
||||||
|
heading or table row identifying each affected script or plugin and its new
|
||||||
|
version. Record what changed,
|
||||||
|
affected source files, compatibility requirements, and deployment steps,
|
||||||
|
including whether a map restart or full server-process restart is required.
|
||||||
|
State explicitly when there is no new dependency requirement or deployment
|
||||||
|
action. Preserve previous entries; extend the current entry while completing
|
||||||
|
the same logical change.
|
||||||
|
- Consider the matching T5/T6 implementation for every game-specific change.
|
||||||
|
Adapt companion changes to that engine and bump each component actually
|
||||||
|
changed. If the companion needs no change, briefly explain why in the
|
||||||
|
changelog entry. Version numbers do not need to match across components.
|
||||||
|
- Keep TWS component versions separate from upstream revision pins and the
|
||||||
|
native loader's existing `plugin_version()` value. A TWS version bump alone
|
||||||
|
must not rewrite either. Never invent historical releases or describe an
|
||||||
|
unperformed build, test, or deployment as completed.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- Before finishing, confirm that every affected component has a version bump
|
||||||
|
and a changelog entry with the same version (or its initial/removal entry).
|
||||||
|
Ensure every existing game script has exactly one filename entry in
|
||||||
|
`versions.json`, with no stale entries for removed scripts. Check JSON validity,
|
||||||
|
local documentation links, and `git diff --check` after metadata changes.
|
||||||
|
- Run the checks relevant to the change using the root and component READMEs.
|
||||||
|
Report checks performed and any remaining Windows/Plutonium validation.
|
||||||
|
Documentation or version-metadata changes alone do not require game builds.
|
||||||
+100
@@ -0,0 +1,100 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
Changes are listed newest first, with an independent version for every individual
|
||||||
|
T5/T6 script and each native plugin. Current versions are recorded in
|
||||||
|
[versions.json](versions.json); see the [version policy](README.md#versioning).
|
||||||
|
|
||||||
|
## 2026-10-12
|
||||||
|
|
||||||
|
### Initial versioning baseline
|
||||||
|
|
||||||
|
Established independent version `1.0.0` entries for each of the 14 scripts and
|
||||||
|
both plugins from source commit
|
||||||
|
`b508b6739b185d5048e8a8f96ccb16e429f1d37a`. This records the existing source state;
|
||||||
|
earlier changes remain in Git history without retroactively assigned versions.
|
||||||
|
The baseline introduces `versions.json`, this changelog, the README links and
|
||||||
|
version policy, and the maintenance rules in `AGENTS.md`. It changes no game
|
||||||
|
script or plugin behavior and requires no deployment by itself.
|
||||||
|
|
||||||
|
The component entries below summarize the existing functionality and deployment
|
||||||
|
requirements for servers adopting this baseline. Version assignment does not
|
||||||
|
indicate that a Windows build, live-game validation, or deployment was performed.
|
||||||
|
|
||||||
|
### T5 script baselines
|
||||||
|
|
||||||
|
Each row is a separately versioned script under `t5/scripts/`.
|
||||||
|
|
||||||
|
| Script | Version | Existing functionality |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `tws_t5_afk.gsc` | `1.0.0` | AFK toggle, protection, and indicator |
|
||||||
|
| `tws_t5_common.gsc` | `1.0.0` | Shared configuration, helpers, commands, and player dispatchers |
|
||||||
|
| `tws_t5_hud.gsc` | `1.0.0` | Timers, zombie count, and health display |
|
||||||
|
| `tws_t5_join.gsc` | `1.0.0` | Immediate join for newcomers |
|
||||||
|
| `tws_t5_leaderboard.gsc` | `1.0.0` | Map record announcements and `.leaders` command |
|
||||||
|
| `tws_t5_lock.gsc` | `1.0.0` | Lobby lock at the configured round milestone |
|
||||||
|
| `tws_t5_telemetry.gsc` | `1.0.0` | Game reports, durable boundary retries, balance and cumulative score |
|
||||||
|
|
||||||
|
- Compatibility: these initial script versions use `tws_t5_common.gsc` `1.0.0`;
|
||||||
|
lobby lock also needs `tws_t5_hud.gsc` `1.0.0`. Telemetry and leaderboard use
|
||||||
|
TWS T5 plugin `1.0.0`. Telemetry additionally requires
|
||||||
|
Electric Cherry's match/sequence deduplication and `score_total` support;
|
||||||
|
follow the [receiver and plugin rollout order](README.md#telemetry-delivery-and-rollout-t5-and-t6).
|
||||||
|
Disabling telemetry by dvar does not remove its native linking requirements.
|
||||||
|
- Deployment: follow the [T5 installation/upgrade instructions](t5/scripts/README.md#install--upgrade),
|
||||||
|
copy the desired scripts and their required dependencies to
|
||||||
|
`storage/t5/scripts/sp/`, and restart the map.
|
||||||
|
Installing or updating the required plugin needs a full server-process restart.
|
||||||
|
|
||||||
|
### T6 script baselines
|
||||||
|
|
||||||
|
Each row is a separately versioned script under `t6/scripts/`, adapted to T6 APIs
|
||||||
|
and map variants.
|
||||||
|
|
||||||
|
| Script | Version | Existing functionality |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `tws_t6_afk.gsc` | `1.0.0` | AFK toggle, protection, and indicator |
|
||||||
|
| `tws_t6_common.gsc` | `1.0.0` | Shared configuration, helpers, commands, and player dispatchers |
|
||||||
|
| `tws_t6_hud.gsc` | `1.0.0` | Timers, zombie count, and health display |
|
||||||
|
| `tws_t6_join.gsc` | `1.0.0` | Immediate join for newcomers |
|
||||||
|
| `tws_t6_leaderboard.gsc` | `1.0.0` | Map record announcements and `.leaders` command |
|
||||||
|
| `tws_t6_lock.gsc` | `1.0.0` | Lobby lock at the configured round milestone |
|
||||||
|
| `tws_t6_telemetry.gsc` | `1.0.0` | Game reports, durable boundary retries, balance and cumulative score |
|
||||||
|
|
||||||
|
- Compatibility: these initial script versions use `tws_t6_common.gsc` `1.0.0`;
|
||||||
|
lobby lock also needs `tws_t6_hud.gsc` `1.0.0`. Telemetry and leaderboard use
|
||||||
|
TWS T6 plugin `1.0.0`. Telemetry additionally requires
|
||||||
|
Electric Cherry's match/sequence deduplication and `score_total` support;
|
||||||
|
follow the [receiver and plugin rollout order](README.md#telemetry-delivery-and-rollout-t5-and-t6).
|
||||||
|
Disabling telemetry by dvar does not remove its native linking requirements.
|
||||||
|
- Deployment: follow the [T6 installation instructions](t6/scripts/README.md#install),
|
||||||
|
copy the desired scripts and their required dependencies to
|
||||||
|
`storage/t6/scripts/zm/`, and restart the map.
|
||||||
|
Installing or updating the required plugin needs a full server-process restart.
|
||||||
|
|
||||||
|
### T5 plugin — 1.0.0
|
||||||
|
|
||||||
|
- Baseline: the TWS-maintained plugin, including configurable HTTP deadlines,
|
||||||
|
status/timing diagnostics, and durable telemetry storage and response helpers.
|
||||||
|
- Source covered: `t5/plugins/tws-t5-gsc-utils/` and the existing shared plugin
|
||||||
|
build/dependency tooling under `tools/`.
|
||||||
|
- Compatibility: provides the native functions used by `tws_t5_telemetry.gsc`
|
||||||
|
`1.0.0` and `tws_t5_leaderboard.gsc` `1.0.0`.
|
||||||
|
Upstream source and dependency pins remain in the plugin's `upstream.json`.
|
||||||
|
Upstream binary downloads do not include these TWS changes.
|
||||||
|
- Deployment: [build the x86 DLL on Windows](README.md#building-plugins), install
|
||||||
|
`tws-t5-gsc-utils.dll` in `Plutonium/plugins/`, remove the old upstream DLL if
|
||||||
|
present, and restart the full server process before loading dependent scripts.
|
||||||
|
|
||||||
|
### T6 plugin — 1.0.0
|
||||||
|
|
||||||
|
- Baseline: the matching TWS T6 plugin with configurable HTTP deadlines,
|
||||||
|
status/timing diagnostics, and durable telemetry storage and response helpers.
|
||||||
|
- Source covered: `t6/plugins/tws-t6-gsc-utils/` and the existing shared plugin
|
||||||
|
build/dependency tooling under `tools/`.
|
||||||
|
- Compatibility: provides the native functions used by `tws_t6_telemetry.gsc`
|
||||||
|
`1.0.0` and `tws_t6_leaderboard.gsc` `1.0.0`.
|
||||||
|
Upstream source and dependency pins remain in the plugin's `upstream.json`.
|
||||||
|
Upstream binary downloads do not include these TWS changes.
|
||||||
|
- Deployment: [build the x86 DLL on Windows](README.md#building-plugins), install
|
||||||
|
`tws-t6-gsc-utils.dll` in `Plutonium/plugins/`, remove the old upstream DLL if
|
||||||
|
present, and restart the full server process before loading dependent scripts.
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
Combined working repo for the TWS Plutonium Zombies scripts and native plugins across two games.
|
Combined working repo for the TWS Plutonium Zombies scripts and native plugins across two games.
|
||||||
|
|
||||||
|
[Changelog](CHANGELOG.md) · [Current component versions](versions.json)
|
||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
|
|
||||||
- [t5/scripts/](t5/scripts/) — T5 GSC modules, [README](t5/scripts/README.md), and [notes](t5/scripts/NOTES.md).
|
- [t5/scripts/](t5/scripts/) — T5 GSC modules, [README](t5/scripts/README.md), and [notes](t5/scripts/NOTES.md).
|
||||||
@@ -12,6 +14,40 @@ Combined working repo for the TWS Plutonium Zombies scripts and native plugins a
|
|||||||
The scripts still deploy to the game's existing `storage/t5/scripts/sp/` or
|
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.
|
`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 14 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
|
## Building plugins
|
||||||
|
|
||||||
Plugin source is vendored as regular tracked files so local modifications travel
|
Plugin source is vendored as regular tracked files so local modifications travel
|
||||||
@@ -61,6 +97,14 @@ then deploy the corresponding scripts. The new `telemetryNewMatch`,
|
|||||||
`telemetryLoad`, `telemetryStore`, `telemetryRemove` and `telemetryResponse`
|
`telemetryLoad`, `telemetryStore`, `telemetryRemove` and `telemetryResponse`
|
||||||
functions are required even if telemetry is disabled by dvar.
|
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
|
Every captured report includes a `match` key (random native process token plus
|
||||||
an increasing match counter, at most 64 characters) and a positive `seq`.
|
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;
|
`game_start` is sequence 1. Retries and replay send the exact original JSON;
|
||||||
|
|||||||
+23
-8
@@ -336,11 +336,23 @@ The reporter emits exactly four event types:
|
|||||||
All four events also include `match` and `seq` (see delivery below).
|
All four events also include `match` and `seq` (see delivery below).
|
||||||
|
|
||||||
Each player in round/heartbeat/end reports has `name`, `guid`, `kills`, `headshots`,
|
Each player in round/heartbeat/end reports has `name`, `guid`, `kills`, `headshots`,
|
||||||
`downs`, `revives`, and `score`. Names and GUIDs remain strings; GUIDs never
|
`downs`, `revives`, `score`, and `score_total`. Names and GUIDs remain strings; GUIDs never
|
||||||
pass through numeric conversion. Round and stats are nonnegative integers,
|
pass through numeric conversion. Round and stats are nonnegative integers,
|
||||||
with missing or negative stat values reported as zero. The four counters
|
with missing or negative stat values reported as zero. The four counters
|
||||||
come from T5's `player.stats`; `score` is the player's current spendable
|
come from T5's `player.stats`; `score` is the player's current spendable
|
||||||
`player.score`, rather than cumulative points earned. `map` is `mapname`.
|
`player.score`. `score_total` reads the stock `player.score_total` cumulative
|
||||||
|
match score used by the final scoreboard; spending points does not reduce it.
|
||||||
|
It follows stock scoring rules, including grants excluded from the total.
|
||||||
|
Missing or negative totals report as zero, independently of the balance.
|
||||||
|
`game_start` includes only player identity. `map` is `mapname`.
|
||||||
|
|
||||||
|
Stock `end_game` yields before `player_intermission` replaces `player.score`
|
||||||
|
with `player.score_total`. The reporter captures both immediately at the first
|
||||||
|
end signal without yielding; retries and replay retain that exact snapshot.
|
||||||
|
Custom scripts that overwrite the score before emitting any supported end
|
||||||
|
signal must preserve this ordering to retain a spendable final balance.
|
||||||
|
Deploy receiver support for `score_total` before the reporters. Old payloads
|
||||||
|
remain valid; the receiver treats their missing totals as unknown, not zero.
|
||||||
|
|
||||||
Every report includes everyone returned by `getPlayers()`, including
|
Every report includes everyone returned by `getPlayers()`, including
|
||||||
spectators, downed players, and AFK players. Empty rosters are sent as `[]`.
|
spectators, downed players, and AFK players. Empty rosters are sent as `[]`.
|
||||||
@@ -353,9 +365,9 @@ use `jsonSerialize` for escaping.
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{"event":"game_start","match":"process-token-1","seq":1,"server":"t5_zombie_prototype_1","map":"zombie_cod5_prototype","players":[{"name":"Josh","guid":"12345678901234567"}]}
|
{"event":"game_start","match":"process-token-1","seq":1,"server":"t5_zombie_prototype_1","map":"zombie_cod5_prototype","players":[{"name":"Josh","guid":"12345678901234567"}]}
|
||||||
{"event":"round_change","match":"process-token-1","seq":2,"server":"t5_zombie_prototype_1","map":"zombie_cod5_prototype","round":17,"players":[{"name":"Josh","guid":"12345678901234567","kills":420,"headshots":160,"downs":1,"revives":3,"score":32000}]}
|
{"event":"round_change","match":"process-token-1","seq":2,"server":"t5_zombie_prototype_1","map":"zombie_cod5_prototype","round":17,"players":[{"name":"Josh","guid":"12345678901234567","kills":420,"headshots":160,"downs":1,"revives":3,"score":32000,"score_total":62000}]}
|
||||||
{"event":"heartbeat","match":"process-token-1","seq":3,"server":"t5_zombie_prototype_1","map":"zombie_cod5_prototype","round":17,"players":[{"name":"Josh","guid":"12345678901234567","kills":430,"headshots":165,"downs":1,"revives":3,"score":32500}]}
|
{"event":"heartbeat","match":"process-token-1","seq":3,"server":"t5_zombie_prototype_1","map":"zombie_cod5_prototype","round":17,"players":[{"name":"Josh","guid":"12345678901234567","kills":430,"headshots":165,"downs":1,"revives":3,"score":32500,"score_total":62500}]}
|
||||||
{"event":"game_end","match":"process-token-1","seq":4,"server":"t5_zombie_prototype_1","map":"zombie_cod5_prototype","round":18,"players":[{"name":"Josh","guid":"12345678901234567","kills":445,"headshots":170,"downs":2,"revives":3,"score":33500}]}
|
{"event":"game_end","match":"process-token-1","seq":4,"server":"t5_zombie_prototype_1","map":"zombie_cod5_prototype","round":18,"players":[{"name":"Josh","guid":"12345678901234567","kills":445,"headshots":170,"downs":2,"revives":3,"score":33500,"score_total":65000}]}
|
||||||
```
|
```
|
||||||
|
|
||||||
Events are captured immediately into an in-memory queue. One HTTP worker
|
Events are captured immediately into an in-memory queue. One HTTP worker
|
||||||
@@ -439,11 +451,14 @@ and [score tracking](https://github.com/plutoniummod/t5-scripts/blob/main/ZM/Com
|
|||||||
|
|
||||||
1. Start a match and verify one `game_start`, then an initial `round_change`
|
1. Start a match and verify one `game_start`, then an initial `round_change`
|
||||||
and one per new round. Check the exact server name, map, string GUIDs,
|
and one per new round. Check the exact server name, map, string GUIDs,
|
||||||
all player identities, and all five stats against the game.
|
all player identities, and all six stats against the game. Earn points, then
|
||||||
|
spend some: the balance should decrease while the total stays unchanged.
|
||||||
2. Join again in the same round, go AFK, become downed, and spectate. Verify
|
2. Join again in the same round, go AFK, become downed, and spectate. Verify
|
||||||
the next report includes the full roster without another start event.
|
the next report includes the full roster without another start event.
|
||||||
3. End a match and verify one `game_end` with final stats after all round
|
3. End a match and verify one `game_end` with both scores captured before the
|
||||||
reports, including if more than one stock end notification fires. Check
|
scoreboard overwrite; compare `score_total` to the final scoreboard. Verify
|
||||||
|
the final stats arrive after all round reports, including if more than one
|
||||||
|
stock end notification fires. Check
|
||||||
a round-0 intro and an empty roster, including last-player disconnect.
|
a round-0 intro and an empty roster, including last-player disconnect.
|
||||||
4. Delay HTTP completion while rounds advance and the match ends. Verify
|
4. Delay HTTP completion while rounds advance and the match ends. Verify
|
||||||
requests remain ordered and retain the stats captured at each event.
|
requests remain ordered and retain the stats captured at each event.
|
||||||
|
|||||||
@@ -158,6 +158,8 @@ telemetry_report_round()
|
|||||||
telemetry_game_end_watcher()
|
telemetry_game_end_watcher()
|
||||||
{
|
{
|
||||||
// One waiter consumes the first end signal, even if a map fires all three.
|
// One waiter consumes the first end signal, even if a map fires all three.
|
||||||
|
// Stock end_game yields before player_intermission replaces score with
|
||||||
|
// score_total. Serialize here without waiting so score stays spendable.
|
||||||
level waittill_any("end_game", "game_ended", "intermission");
|
level waittill_any("end_game", "game_ended", "intermission");
|
||||||
telemetry_log(1, "Game-end signal received; stopping producers and draining queue");
|
telemetry_log(1, "Game-end signal received; stopping producers and draining queue");
|
||||||
|
|
||||||
@@ -227,8 +229,9 @@ telemetry_players_json(include_stats)
|
|||||||
data["headshots"] = telemetry_stat(player, "headshots");
|
data["headshots"] = telemetry_stat(player, "headshots");
|
||||||
data["downs"] = telemetry_stat(player, "downs");
|
data["downs"] = telemetry_stat(player, "downs");
|
||||||
data["revives"] = telemetry_stat(player, "revives");
|
data["revives"] = telemetry_stat(player, "revives");
|
||||||
// Current spendable points, not the lifetime points-earned stat.
|
// Stock match total is independent of the spendable balance.
|
||||||
data["score"] = telemetry_nonnegative_int(player.score);
|
data["score"] = telemetry_nonnegative_int(player.score);
|
||||||
|
data["score_total"] = telemetry_nonnegative_int(player.score_total);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (body != "[")
|
if (body != "[")
|
||||||
|
|||||||
+12
-1
@@ -131,11 +131,22 @@ It sends four event types over the life of a match, mirroring the T5 script:
|
|||||||
|
|
||||||
Each event payload includes `event`, `server`, `map`, `match`, `seq`, `round` (except `game_start`)
|
Each event payload includes `event`, `server`, `map`, `match`, `seq`, `round` (except `game_start`)
|
||||||
and a `players` array with `name`, `guid`, `kills`, `headshots`, `downs`, `revives`
|
and a `players` array with `name`, `guid`, `kills`, `headshots`, `downs`, `revives`
|
||||||
and `score` per player. `guid` reads Plutonium's `player.guid` engine property
|
and `score` plus `score_total` per player in stat reports. `game_start` includes
|
||||||
|
only player identity. `guid` reads Plutonium's `player.guid` engine property
|
||||||
(T5 uses the `player getGuid()` method — T6 has no such method, but the field is
|
(T5 uses the `player getGuid()` method — T6 has no such method, but the field is
|
||||||
equivalent for identity purposes). `kills`/`headshots`/`downs`/`revives` read
|
equivalent for identity purposes). `kills`/`headshots`/`downs`/`revives` read
|
||||||
from T6's stock match counters in `player.pers[]`; missing or negative values
|
from T6's stock match counters in `player.pers[]`; missing or negative values
|
||||||
are reported as `0`. `score` is the player's current spendable `player.score`.
|
are reported as `0`. `score` is the player's current spendable `player.score`.
|
||||||
|
`score_total` reads `player.score_total`, the stock cumulative match score used
|
||||||
|
by the final scoreboard. Spending does not reduce it; stock grants can opt out
|
||||||
|
of adding to the total. Missing or negative totals report as `0`, independently
|
||||||
|
of the balance. Deploy Electric Cherry's receiver support before these scripts;
|
||||||
|
older payloads without totals remain valid and their totals are unknown.
|
||||||
|
|
||||||
|
Stock `end_game` yields before `player_intermission` replaces the balance with
|
||||||
|
the total. The reporter serializes both at the first end signal without yielding,
|
||||||
|
so retries and replay keep the original balance and total. Custom end sequences
|
||||||
|
must emit a supported end signal before overwriting the balance.
|
||||||
|
|
||||||
Delivery uses one in-flight request and a FIFO queue. Every match receives a
|
Delivery uses one in-flight request and a FIFO queue. Every match receives a
|
||||||
native-generated key; `seq` starts at 1 for `game_start` and increases at capture.
|
native-generated key; `seq` starts at 1 for `game_start` and increases at capture.
|
||||||
|
|||||||
@@ -165,6 +165,8 @@ telemetry_report_round()
|
|||||||
telemetry_game_end_watcher()
|
telemetry_game_end_watcher()
|
||||||
{
|
{
|
||||||
// One waiter consumes the first end signal, even if a map fires all three.
|
// One waiter consumes the first end signal, even if a map fires all three.
|
||||||
|
// Stock end_game yields before player_intermission replaces score with
|
||||||
|
// score_total. Serialize here without waiting so score stays spendable.
|
||||||
level waittill_any("end_game", "game_ended", "intermission");
|
level waittill_any("end_game", "game_ended", "intermission");
|
||||||
telemetry_log(1, "Game-end signal received; stopping producers and draining queue");
|
telemetry_log(1, "Game-end signal received; stopping producers and draining queue");
|
||||||
|
|
||||||
@@ -240,8 +242,9 @@ telemetry_players_json(include_stats)
|
|||||||
data["headshots"] = telemetry_stat(player, "headshots");
|
data["headshots"] = telemetry_stat(player, "headshots");
|
||||||
data["downs"] = telemetry_stat(player, "downs");
|
data["downs"] = telemetry_stat(player, "downs");
|
||||||
data["revives"] = telemetry_stat(player, "revives");
|
data["revives"] = telemetry_stat(player, "revives");
|
||||||
// Current spendable points, not the lifetime points-earned stat.
|
// Stock match total is independent of the spendable balance.
|
||||||
data["score"] = telemetry_nonnegative_int(player.score);
|
data["score"] = telemetry_nonnegative_int(player.score);
|
||||||
|
data["score_total"] = telemetry_nonnegative_int(player.score_total);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (body != "[")
|
if (body != "[")
|
||||||
|
|||||||
@@ -31,12 +31,15 @@ function functions(source) {
|
|||||||
let body = protectedSource.slice(headers.lastIndex, end - 1);
|
let body = protectedSource.slice(headers.lastIndex, end - 1);
|
||||||
const locals = new Set([...body.matchAll(/(?<![\w.])(\w+)\s*(?:[+*/%-]?=(?!=)|\+\+|--)/g)].map(x => x[1]));
|
const locals = new Set([...body.matchAll(/(?<![\w.])(\w+)\s*(?:[+*/%-]?=(?!=)|\+\+|--)/g)].map(x => x[1]));
|
||||||
body = body.replace(/\.size\b/g, '.length').replace(/http::request/g, 'httpPost');
|
body = body.replace(/\.size\b/g, '.length').replace(/http::request/g, 'httpPost');
|
||||||
|
body = body.replace(/player getGuid\(\)/g, 'getGuid(player)');
|
||||||
body = body.replace(/level thread \w+\(\);/g, '').replace(/level notify\(/g, 'notify(');
|
body = body.replace(/level thread \w+\(\);/g, '').replace(/level notify\(/g, 'notify(');
|
||||||
|
body = body.replace(/level waittill_any\(([^;]+)\);/g, 'yield [$1];');
|
||||||
body = body.replace(/level waittill\(([^;]+)\);/g, 'yield $1;');
|
body = body.replace(/level waittill\(([^;]+)\);/g, 'yield $1;');
|
||||||
body = body.replace(/wait \(([^;]+)\);/g, 'yield $1;');
|
body = body.replace(/wait \(([^;]+)\);/g, 'yield $1;');
|
||||||
body = body.replace(/request waittill\([^;]+\);/g, 'var [response, success, error, diagnostics] = httpComplete(request);');
|
body = body.replace(/request waittill\([^;]+\);/g, 'var [response, success, error, diagnostics] = httpComplete(request);');
|
||||||
body = body.replace(/__literal_(\d+)__/g, (_, index) => literals[Number(index)]);
|
body = body.replace(/__literal_(\d+)__/g, (_, index) => literals[Number(index)]);
|
||||||
found.set(match[1], `function${match[1] === 'telemetry_sender' ? '*' : ''} ${match[1]}(${match[2]}) { var ${[...locals].join(', ') || 'unused'}; ${body} }`);
|
const generator = ['telemetry_sender', 'telemetry_game_end_watcher'].includes(match[1]);
|
||||||
|
found.set(match[1], `function${generator ? '*' : ''} ${match[1]}(${match[2]}) { var ${[...locals].join(', ') || 'unused'}; ${body} }`);
|
||||||
}
|
}
|
||||||
return found;
|
return found;
|
||||||
}
|
}
|
||||||
@@ -46,11 +49,11 @@ function disk() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let nextMatch = 0;
|
let nextMatch = 0;
|
||||||
function engine(game, storage = disk()) {
|
function engine(game, storage = disk(), players = []) {
|
||||||
const source = readFileSync(new URL(`${game}/scripts/tws_${game}_telemetry.gsc`, root), 'utf8');
|
const source = readFileSync(new URL(`${game}/scripts/tws_${game}_telemetry.gsc`, root), 'utf8');
|
||||||
const all = functions(source);
|
const all = functions(source);
|
||||||
const excluded = new Set(['telemetry_players_json', 'telemetry_stat', 'telemetry_heartbeat_watcher',
|
const excluded = new Set(['telemetry_heartbeat_watcher',
|
||||||
'telemetry_round_watcher', 'telemetry_game_end_watcher', 'telemetry_watchdog', 'telemetry_log']);
|
'telemetry_round_watcher', 'telemetry_watchdog', 'telemetry_log']);
|
||||||
const sent = [], responses = [], logs = [], notifications = [];
|
const sent = [], responses = [], logs = [], notifications = [];
|
||||||
const queue = storage.entries.map(e => ({ ...e, stored: true, captured: 1000 }));
|
const queue = storage.entries.map(e => ({ ...e, stored: true, captured: 1000 }));
|
||||||
const c = vm.createContext({
|
const c = vm.createContext({
|
||||||
@@ -63,10 +66,14 @@ function engine(game, storage = disk()) {
|
|||||||
getTime: () => 1000,
|
getTime: () => 1000,
|
||||||
isDefined: value => value !== undefined && value !== null,
|
isDefined: value => value !== undefined && value !== null,
|
||||||
int: Math.trunc,
|
int: Math.trunc,
|
||||||
jsonSerialize: JSON.stringify,
|
// GSC associative arrays serialize as objects in the native plugin.
|
||||||
|
jsonSerialize: value => JSON.stringify(Array.isArray(value) &&
|
||||||
|
Object.keys(value).some(key => !/^\d+$/.test(key))
|
||||||
|
? Object.fromEntries(Object.entries(value)) : value),
|
||||||
getSubStr: (value, start, end) => value.slice(start, end),
|
getSubStr: (value, start, end) => value.slice(start, end),
|
||||||
strTok: (value, delimiter) => value.split(delimiter),
|
strTok: (value, delimiter) => value.split(delimiter),
|
||||||
telemetry_players_json: () => '[]',
|
getPlayers: () => players,
|
||||||
|
getGuid: player => player.guid,
|
||||||
telemetry_log: (verbosity, text) => logs.push(text),
|
telemetry_log: (verbosity, text) => logs.push(text),
|
||||||
notify: event => notifications.push(event),
|
notify: event => notifications.push(event),
|
||||||
telemetryNewMatch: () => `boot-${++nextMatch}`,
|
telemetryNewMatch: () => `boot-${++nextMatch}`,
|
||||||
@@ -110,6 +117,62 @@ function engine(game, storage = disk()) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const game of ['t5', 't6']) {
|
for (const game of ['t5', 't6']) {
|
||||||
|
const player = () => ({ name: 'Josh', guid: '12345678901234567', score: 32000, score_total: 62000,
|
||||||
|
stats: { kills: 420, headshots: 160, downs: 1, revives: 0 },
|
||||||
|
pers: { kills: 420, headshots: 160, downs: 1, revives: 0, score: 32000 } });
|
||||||
|
|
||||||
|
check(`${game}: reports balance and match total separately, with an identity-only start`, () => {
|
||||||
|
const players = [player()];
|
||||||
|
const { c } = engine(game, disk(), players);
|
||||||
|
c.telemetry_game_start();
|
||||||
|
assert.deepEqual(JSON.parse(c.level.telemetry_queue[0].body).players,
|
||||||
|
[{ name: 'Josh', guid: '12345678901234567' }]);
|
||||||
|
c.level.round_number = 17;
|
||||||
|
c.telemetry_report_round();
|
||||||
|
const initial = JSON.parse(c.level.telemetry_queue[1].body).players[0];
|
||||||
|
assert.deepEqual(initial, { name: 'Josh', guid: '12345678901234567', kills: 420,
|
||||||
|
headshots: 160, downs: 1, revives: 0, score: 32000, score_total: 62000 });
|
||||||
|
// Earn 1,000 and spend 5,000 between snapshots.
|
||||||
|
players[0].score = 28000;
|
||||||
|
players[0].score_total = 63000;
|
||||||
|
c.telemetry_enqueue('heartbeat');
|
||||||
|
const later = JSON.parse(c.level.telemetry_queue[2].body).players[0];
|
||||||
|
assert.equal(later.score, 28000);
|
||||||
|
assert.equal(later.score_total, 63000);
|
||||||
|
for (const total of [undefined, -10, 0, 65000.9]) {
|
||||||
|
players[0].score_total = total;
|
||||||
|
const stats = JSON.parse(c.telemetry_players_json(true))[0];
|
||||||
|
assert.equal(stats.score_total, total > 0 ? 65000 : 0);
|
||||||
|
assert.equal(stats.score, 28000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
check(`${game}: end captures before scoreboard overwrite and replays both scores unchanged`, () => {
|
||||||
|
const players = [player()];
|
||||||
|
const first = engine(game, disk(), players);
|
||||||
|
first.c.telemetry_game_start();
|
||||||
|
first.responses.push({});
|
||||||
|
first.c.telemetry_sender().next();
|
||||||
|
first.c.level.round_number = 18;
|
||||||
|
const watcher = first.c.telemetry_game_end_watcher();
|
||||||
|
assert.deepEqual(Array.from(watcher.next().value), ['end_game', 'game_ended', 'intermission']);
|
||||||
|
// Resuming at stock end_game must finish capture without another yield.
|
||||||
|
assert.equal(watcher.next().done, true);
|
||||||
|
players[0].score = players[0].score_total;
|
||||||
|
const saved = first.storage.entries[0].body;
|
||||||
|
const final = JSON.parse(saved);
|
||||||
|
assert.equal(final.event, 'game_end');
|
||||||
|
assert.equal(final.players[0].score, 32000);
|
||||||
|
assert.equal(final.players[0].score_total, 62000);
|
||||||
|
// A fresh VM/process cannot re-read the player; replay the saved JSON.
|
||||||
|
const replay = engine(game, first.storage);
|
||||||
|
replay.responses.push({ status: 503 }, {});
|
||||||
|
const sender = replay.c.telemetry_sender();
|
||||||
|
assert.equal(sender.next().value, 1);
|
||||||
|
assert.equal(sender.next().value, 'telemetry_queued');
|
||||||
|
assert.deepEqual(replay.sent.map(request => request.body), [saved, saved]);
|
||||||
|
});
|
||||||
|
|
||||||
check(`${game}: match identity, sequence and capture before send`, () => {
|
check(`${game}: match identity, sequence and capture before send`, () => {
|
||||||
const { c, storage, sent, responses } = engine(game);
|
const { c, storage, sent, responses } = engine(game);
|
||||||
c.telemetry_game_start(); c.telemetry_game_start();
|
c.telemetry_game_start(); c.telemetry_game_start();
|
||||||
@@ -272,7 +335,7 @@ for (const game of ['t5', 't6']) {
|
|||||||
|
|
||||||
check('paired queue and delivery implementations match', () => {
|
check('paired queue and delivery implementations match', () => {
|
||||||
const t5 = engine('t5').all, t6 = engine('t6').all;
|
const t5 = engine('t5').all, t6 = engine('t6').all;
|
||||||
for (const name of ['telemetry_enqueue', 'telemetry_queue_push', 'telemetry_critical', 'telemetry_persist_pending',
|
for (const name of ['telemetry_game_end_watcher', 'telemetry_enqueue', 'telemetry_queue_push', 'telemetry_critical', 'telemetry_persist_pending',
|
||||||
'telemetry_sender', 'telemetry_process', 'telemetry_retry_delay', 'telemetry_send', 'telemetry_response_outcome'])
|
'telemetry_sender', 'telemetry_process', 'telemetry_retry_delay', 'telemetry_send', 'telemetry_response_outcome'])
|
||||||
assert.equal(t5.get(name), t6.get(name), name);
|
assert.equal(t5.get(name), t6.get(name), name);
|
||||||
for (const file of ['telemetry.hpp', 'telemetry.cpp']) {
|
for (const file of ['telemetry.hpp', 'telemetry.cpp']) {
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"tws_t5_afk.gsc": "1.0.0",
|
||||||
|
"tws_t5_common.gsc": "1.0.0",
|
||||||
|
"tws_t5_hud.gsc": "1.0.0",
|
||||||
|
"tws_t5_join.gsc": "1.0.0",
|
||||||
|
"tws_t5_leaderboard.gsc": "1.0.0",
|
||||||
|
"tws_t5_lock.gsc": "1.0.0",
|
||||||
|
"tws_t5_telemetry.gsc": "1.0.0",
|
||||||
|
"tws_t6_afk.gsc": "1.0.0",
|
||||||
|
"tws_t6_common.gsc": "1.0.0",
|
||||||
|
"tws_t6_hud.gsc": "1.0.0",
|
||||||
|
"tws_t6_join.gsc": "1.0.0",
|
||||||
|
"tws_t6_leaderboard.gsc": "1.0.0",
|
||||||
|
"tws_t6_lock.gsc": "1.0.0",
|
||||||
|
"tws_t6_telemetry.gsc": "1.0.0",
|
||||||
|
"t5-plugin": "1.0.0",
|
||||||
|
"t6-plugin": "1.0.0"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user