Report cumulative player scores alongside balances

This commit is contained in:
2026-10-12 13:57:36 -04:00
parent 158916b664
commit b508b6739b
6 changed files with 121 additions and 18 deletions
+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']) {