Compare commits

..
2 Commits
9 changed files with 333 additions and 18 deletions
+58
View File
@@ -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
View File
@@ -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.
+44
View File
@@ -2,6 +2,8 @@
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).
@@ -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
`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
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`
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;
+23 -8
View File
@@ -336,11 +336,23 @@ The reporter emits exactly four event types:
All four events also include `match` and `seq` (see delivery below).
Each player in round/heartbeat/end reports has `name`, `guid`, `kills`, `headshots`,
`downs`, `revives`, 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,
with missing or negative stat values reported as zero. The four counters
come from T5's `player.stats`; `score` is the player's current spendable
`player.score`, 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
spectators, downed players, and AFK players. Empty rosters are sent as `[]`.
@@ -353,9 +365,9 @@ use `jsonSerialize` for escaping.
```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":"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":"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":"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":"round_change","match":"process-token-1","seq":2,"server":"t5_zombie_prototype_1","map":"zombie_cod5_prototype","round":17,"players":[{"name":"Josh","guid":"12345678901234567","kills":420,"headshots":160,"downs":1,"revives":3,"score":32000,"score_total":62000}]}
{"event":"heartbeat","match":"process-token-1","seq":3,"server":"t5_zombie_prototype_1","map":"zombie_cod5_prototype","round":17,"players":[{"name":"Josh","guid":"12345678901234567","kills":430,"headshots":165,"downs":1,"revives":3,"score":32500,"score_total":62500}]}
{"event":"game_end","match":"process-token-1","seq":4,"server":"t5_zombie_prototype_1","map":"zombie_cod5_prototype","round":18,"players":[{"name":"Josh","guid":"12345678901234567","kills":445,"headshots":170,"downs":2,"revives":3,"score":33500,"score_total":65000}]}
```
Events are captured immediately into an in-memory queue. One HTTP worker
@@ -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`
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
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
reports, including if more than one stock end notification fires. Check
3. End a match and verify one `game_end` with both scores captured before the
scoreboard overwrite; compare `score_total` to the final scoreboard. Verify
the final stats arrive after all round reports, including if more than one
stock end notification fires. Check
a round-0 intro and an empty roster, including last-player disconnect.
4. Delay HTTP completion while rounds advance and the match ends. Verify
requests remain ordered and retain the stats captured at each event.
+4 -1
View File
@@ -158,6 +158,8 @@ telemetry_report_round()
telemetry_game_end_watcher()
{
// 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");
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["downs"] = telemetry_stat(player, "downs");
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_total"] = telemetry_nonnegative_int(player.score_total);
}
if (body != "[")
+12 -1
View File
@@ -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`)
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
equivalent for identity purposes). `kills`/`headshots`/`downs`/`revives` read
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`.
`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
native-generated key; `seq` starts at 1 for `game_start` and increases at capture.
+4 -1
View File
@@ -165,6 +165,8 @@ telemetry_report_round()
telemetry_game_end_watcher()
{
// 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");
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["downs"] = telemetry_stat(player, "downs");
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_total"] = telemetry_nonnegative_int(player.score_total);
}
if (body != "[")
+70 -7
View File
@@ -31,12 +31,15 @@ function functions(source) {
let body = protectedSource.slice(headers.lastIndex, end - 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(/player getGuid\(\)/g, 'getGuid(player)');
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(/wait \(([^;]+)\);/g, 'yield $1;');
body = body.replace(/request waittill\([^;]+\);/g, 'var [response, success, error, diagnostics] = httpComplete(request);');
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;
}
@@ -46,11 +49,11 @@ function disk() {
}
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 all = functions(source);
const excluded = new Set(['telemetry_players_json', 'telemetry_stat', 'telemetry_heartbeat_watcher',
'telemetry_round_watcher', 'telemetry_game_end_watcher', 'telemetry_watchdog', 'telemetry_log']);
const excluded = new Set(['telemetry_heartbeat_watcher',
'telemetry_round_watcher', 'telemetry_watchdog', 'telemetry_log']);
const sent = [], responses = [], logs = [], notifications = [];
const queue = storage.entries.map(e => ({ ...e, stored: true, captured: 1000 }));
const c = vm.createContext({
@@ -63,10 +66,14 @@ function engine(game, storage = disk()) {
getTime: () => 1000,
isDefined: value => value !== undefined && value !== null,
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),
strTok: (value, delimiter) => value.split(delimiter),
telemetry_players_json: () => '[]',
getPlayers: () => players,
getGuid: player => player.guid,
telemetry_log: (verbosity, text) => logs.push(text),
notify: event => notifications.push(event),
telemetryNewMatch: () => `boot-${++nextMatch}`,
@@ -110,6 +117,62 @@ function engine(game, storage = disk()) {
}
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`, () => {
const { c, storage, sent, responses } = engine(game);
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', () => {
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'])
assert.equal(t5.get(name), t6.get(name), name);
for (const file of ['telemetry.hpp', 'telemetry.cpp']) {
+18
View File
@@ -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"
}