Files
tws-zombies-scripts/tools/check_leaderboard.mjs
T
josh 1121b44857 Organize scripts and add TWS plugins with HTTP timeouts
Vendor pinned T5 and T6 plugin sources under the new plugins layout. Add native HTTP deadlines, status and timing diagnostics, telemetry integration, and build/test tooling.
2026-10-09 11:02:20 -04:00

240 lines
14 KiB
JavaScript

// Execute the actual data-only GSC functions with small engine-value shims.
// This is a fixture check, not a GSC VM, linker or gameplay test.
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import vm from 'node:vm';
const root = new URL('../', import.meta.url);
const bytes = (text) => Buffer.from(text, 'utf8').toString('latin1');
const text = (value) => Buffer.from(value, 'latin1').toString('utf8');
let checks = 0;
function check(name, run) {
try { run(); checks++; }
catch (error) { throw new Error(name, { cause: error }); }
}
function functions(source) {
// Protect literals/comments before rewriting the small shared language subset.
const literals = [];
const protectedSource = source.replace(/"(?:\\.|[^"\\])*"|\/\/[^\n]*|\/\*[\s\S]*?\*\//g, (token) => {
if (token.startsWith('/')) return '';
literals.push(token);
return `__literal_${literals.length - 1}__`;
});
const found = new Map();
const headers = /^(lb_\w+)\(([^\n]*)\)\s*\n\{/gm;
for (let match; (match = headers.exec(protectedSource));) {
let end = headers.lastIndex, depth = 1;
while (depth && end < protectedSource.length) {
const ch = protectedSource[end++];
if (ch === '{') depth++;
if (ch === '}') depth--;
}
assert.equal(depth, 0);
let body = protectedSource.slice(headers.lastIndex, end - 1);
// Each GSC function has its own implicit locals, including recursion.
const locals = new Set([...body.matchAll(/(?<![\w.])(\w+)\s*(?:[+*/%-]?=(?!=)|\+\+|--)/g)].map((x) => x[1]));
body = body.replace(/\btypeof\(/g, 'gscType(').replace(/\.size\b/g, '.length').replace(/\bwait\s+[\d.]+;/g, ';');
body = body.replace(/__literal_(\d+)__/g, (_, index) => literals[Number(index)]);
found.set(match[1], `function ${match[1]}(${match[2]}) { var ${[...locals].join(', ') || 'unused'}; ${body} }`);
}
return found;
}
function engine(game) {
const source = readFileSync(new URL(`${game}/scripts/tws_${game}_leaderboard.gsc`, root), 'utf8');
const all = functions(source);
const excluded = new Set(['lb_worker', 'lb_welcome', 'lb_get', 'lb_collect', 'lb_log', 'lb_show_hud', 'lb_hud_help_visible', 'lb_hud_end_game', 'lb_hud_destroy', 'lb_chat_listener', 'lb_request', 'lb_hud_disconnect']);
const table = [];
for (let i = 1; i < 256; i++) table[String.fromCharCode(i)] = i;
const dvars = { mapname: game === 't5' ? 'zombie_theater' : 'zm_transit', ui_gametype: 'zstandard', ui_zm_mapstartlocation: 'town' };
const logs = [], queries = [], responses = [];
const context = vm.createContext({
level: { tws_lb_game: game, tws_lb_override: '', tws_lb_bytes: table },
GetDvar: (key) => dvars[key] ?? '',
getTime: () => 1000000,
spawnStruct: () => ({}),
isDefined: (value) => value !== undefined && value !== null,
gscType: (value) => value === undefined || value === null ? 'undefined' : typeof value === 'object' ? 'array'
: typeof value === 'number' ? (Number.isInteger(value) ? 'int' : 'float') : typeof value === 'boolean' ? 'int' : typeof value,
toLower: (value) => value.toLowerCase(),
getSubStr: (value, start, end) => {
assert.equal(typeof value, 'string');
assert(start >= 0 && start <= value.length, `substring start ${start} exceeds ${value.length}`);
return value.slice(start, end);
},
strTok: (value, delimiter) => value.split(delimiter),
int: (value) => Math.trunc(Number(value)),
lb_log: (message) => logs.push(message),
lb_get: (query) => { queries.push(query); assert(responses.length, 'unexpected request'); return responses.shift(); },
});
vm.runInContext([...all].filter(([name]) => !excluded.has(name)).map(([, code]) => code).join('\n'), context);
return { context, all, source, dvars, responses, queries, logs };
}
function mapOption(game) {
return game === 't5'
? { game, map: 'zombie_theater', key: 't5:zombie_theater', name: 'Kino Der Toten' }
: { game, map: 'zm_transit', key: 't6:zm_transit:town', name: 'Town Survival' };
}
function entry(option, guid, name = `Player ${guid}`, round = 52) {
return { guid, name: bytes(name), rank: round === 52 ? 1 : 10, highestRound: round,
records: { highestRound: { game: option.game, mapKey: option.key, value: round } } };
}
function page(option, entries, total, offset = 0, limit = 3, round = 52) {
return { entries, total, offset, limit, metric: 'highestRound', maps: [option], summary: { highestRound: round } };
}
function discovery(option) { return page(option, [], 0, 0, 1, 0); }
for (const game of ['t5', 't6']) {
const { context: c, source, all, dvars, responses, queries } = engine(game);
const option = mapOption(game);
check(`${game} exactly one connect listener across modules`, () => {
const common = readFileSync(new URL(`${game}/scripts/tws_${game}_common.gsc`, root), 'utf8');
assert.equal((common.match(/waittill\("connected"/g) ?? []).length, 1);
assert(!source.includes('waittill("connected"'));
assert(!all.get('lb_welcome').includes('spawned_player'));
if (game === 't6') assert(!all.get('lb_welcome').includes('self.hasSpawned'));
assert(source.includes('register_connect_handler(::lb_welcome)'));
});
check(`${game} record replay uses only a fresh complete cache`, () => {
assert.equal(c.lb_cached_record(), undefined);
const snapshot = { saved_at: 1000000, round: 52, lines: ['Alice'] };
c.level.tws_lb_cache = snapshot;
assert.equal(c.lb_cached_record(), snapshot);
snapshot.saved_at = 100000;
assert.equal(c.lb_cached_record(), snapshot);
snapshot.saved_at--;
assert.equal(c.lb_cached_record(), undefined);
c.level.tws_lb_cache = { saved_at: 1000000, round: 0, lines: [] };
assert.equal(c.lb_cached_record().round, 0);
c.level.tws_lb_cache = undefined;
assert.equal(queries.length, 0);
});
check(`${game} unicode and URL encoding`, () => {
assert.equal(c.lb_url_encode(bytes('t6:zm_transit:town /?&=#é')), 't6%3Azm_transit%3Atown%20%2F%3F%26%3D%23%C3%A9');
assert.equal(text(c.lb_display_text(bytes('^1Alice\n^7 René 🧟'), 64)), 'Alice René 🧟');
assert.equal(text(c.lb_display_text(bytes('abc🧟'), 6)), 'abc...');
assert.equal(text(c.lb_display_text(bytes('abc🧟'), 7)), 'abc🧟');
});
const valid = [discovery(option), { text: 'René 🧟 " \\ /\n\r\t\b\f', n: -1.23e-4, yes: true, no: false, empty: null },
[], {}, 0, -0.2, 'hello', true, null];
for (const value of valid) check(`${game} valid JSON ${JSON.stringify(value).slice(0, 60)}`, () => {
assert.equal(c.lb_json_valid(bytes(JSON.stringify(value))), true);
});
check(`${game} escaped unicode pair and whitespace`, () => {
assert.equal(c.lb_json_valid(' \n{"s":"\\u00e9 \\uD83E\\uDDDF"}\t'), true);
});
const invalid = ['', ' ', '<html>Error</html>', '{', '{"a":}', '{"a":1,}', '[1,]', '[,1]', '{a:1}', 'true false',
'{"a":"\\q"}', '{"a":"raw\nline"}', '"\\ud800"', '"\\udc00"', '"\\ud800\\u0041"', '"\\u0000"',
'"\\uX001"', '"unterminated', '01', '-01', '+1', '.5', '1.', '1e', '1e+', '1e999', '1'.repeat(33),
'NaN', 'Infinity', '['.repeat(18) + '0' + ']'.repeat(18), '"' + 'a'.repeat(20480) + '"',
'"\xc0\xaf"', '"\xed\xa0\x80"', '"\xf4\x90\x80\x80"', '"\xff"', '"\xc2"'];
for (const value of invalid) check(`${game} malformed JSON ${JSON.stringify(value).slice(0, 50)}`, () => {
assert.equal(c.lb_json_valid(value), false);
});
check(`${game} every truncated prefix of a valid API response`, () => {
const body = JSON.stringify(page(option, [entry(option, '000123')], 1));
for (let i = 0; i < body.length; i++) assert.equal(c.lb_json_valid(body.slice(0, i)), false, `accepted prefix ${i}`);
});
check(`${game} success schema rejects API errors and bad field types`, () => {
for (const value of [{ ok: false, error: 'bad request' }, [], null, 'oops',
{ ...discovery(option), entries: 'no' }, { ...discovery(option), metric: undefined },
{ ...discovery(option), limit: "1" }, { ...discovery(option), total: -1 }]) {
assert.equal(c.lb_page_valid(value, 0, 1), false);
}
});
check(`${game} one holder`, () => {
responses.push(discovery(option), page(option, [entry(option, '000123', 'Alice')], 1));
const result = c.lb_refresh();
assert.equal(result.round, 52);
assert.deepEqual(Array.from(result.names), ['Alice']);
assert(queries.at(-1).includes(`map=${encodeURIComponent(option.key)}`));
});
check(`${game} every tie across pages, with repeated GUID and identical names`, () => {
const tied = Array.from({ length: 8 }, (_, i) => entry(option, `${i}`, i < 2 ? 'Same name' : `Holder ${i}`));
tied.splice(3, 0, tied[2]);
responses.push(discovery(option), page(option, tied.slice(0, 3), 10), page(option, tied.slice(3, 6), 10, 3),
page(option, tied.slice(6, 9), 10, 6), page(option, [entry(option, 'lower', 'Lower', 51)], 10, 9));
const result = c.lb_refresh();
assert.equal(result.names.length, 8);
assert.equal(result.names.filter((x) => x === 'Same name').length, 2);
assert(!result.names.includes('Lower'));
assert.equal(queries.at(-1).includes('offset=9'), true);
});
check(`${game} no records versus missing response`, () => {
responses.push(discovery(option), page(option, [], 0, 0, 3, 0));
assert.equal(c.lb_refresh().round, 0);
responses.push({ ...discovery(option), maps: [] });
assert.equal(c.lb_refresh().round, 0);
responses.push(undefined);
assert.equal(c.lb_refresh(), undefined);
});
check(`${game} failed later page never returns partial holders`, () => {
responses.push(discovery(option), page(option, [entry(option, '1'), entry(option, '2'), entry(option, '3')], 4), undefined);
assert.equal(c.lb_refresh(), undefined);
});
check(`${game} changing top record / total rejects mixed pages`, () => {
for (const [total, round] of [[4, 53], [5, 52]]) {
responses.push(discovery(option), page(option, [entry(option, '1'), entry(option, '2'), entry(option, '3')], 4),
page(option, [entry(option, '4')], total, 3, 3, round));
assert.equal(c.lb_refresh(), undefined);
}
});
check(`${game} wrong map evidence rejects response`, () => {
const wrong = entry({ ...option, key: 'wrong' }, '1');
responses.push(discovery(option), page(option, [wrong], 1));
assert.equal(c.lb_refresh(), undefined);
});
check(`${game} explicit key must match current game and map`, () => {
c.level.tws_lb_override = 't6:zm_transit:town';
const wrong = { ...option, game: 'wrong', key: c.level.tws_lb_override };
responses.push(discovery(wrong));
assert.equal(c.lb_refresh(), undefined);
c.level.tws_lb_override = '';
});
check(`${game} all holders fit bounded messages`, () => {
const names = Array.from({ length: 100 }, (_, i) => `Holder ${i}`);
const lines = Array.from(c.lb_holder_lines(names));
assert(lines.every((line) => line.length <= 100));
assert.deepEqual(lines.join(', ').split(', '), names);
});
if (game === 't6') {
const variants = [
['zm_transit', 'zclassic', 'transit', 'transit'], ['zm_transit', 'zstandard', 'town', 'town'],
['zm_transit', 'zstandard', 'farm', 'farm'], ['zm_transit', 'zstandard', 'transit', 'busdepot'],
['zm_nuked', 'zstandard', 'nuked', 'nuked'], ['zm_highrise', 'zclassic', 'rooftop', 'highrise'],
['zm_prison', 'zclassic', 'prison', 'prison'], ['zm_buried', 'zclassic', 'processing', 'buried'],
['zm_tomb', 'zclassic', 'tomb', 'tomb'], ['zm_transit', 'zgrief', 'town', 'towngrief'],
['zm_transit', 'zgrief', 'farm', 'farmgrief'], ['zm_transit', 'zgrief', 'transit', 'busdepotgrief'],
['zm_prison', 'zgrief', 'cellblock', 'cellblock'], ['zm_buried', 'zgrief', 'street', 'buriedgrief'],
['zm_transit_dr', 'zcleansed', 'diner', 'diner'], ['zm_buried', 'zcleansed', 'street', 'buriedturned'],
];
for (const [map, mode, location, slug] of variants) check(`T6 ${slug}`, () => {
assert.equal(c.lb_t6_selection({ map }, mode, location).key, `t6:${map}:${slug}`);
});
check('T6 unknown variant does not fall back to available Town', () => {
dvars.ui_gametype = 'unknown';
assert.equal(c.lb_selection(), undefined);
dvars.ui_gametype = 'zstandard';
});
check('T6 live stock settings take precedence over changed dvars', () => {
c.level.scr_zm_ui_gametype = 'zclassic';
c.level.scr_zm_map_start_location = 'transit';
assert.equal(c.lb_selection().key, 't6:zm_transit:transit');
});
}
assert.equal(responses.length, 0, `${game} unused responses`);
}
check('paired data helpers stay identical', () => {
const t5 = functions(readFileSync(new URL('t5/scripts/tws_t5_leaderboard.gsc', root), 'utf8'));
const t6 = functions(readFileSync(new URL('t6/scripts/tws_t6_leaderboard.gsc', root), 'utf8'));
for (const [name, fn] of t5) {
if (!['lb_selection', 'lb_welcome', 'lb_get', 'lb_log'].includes(name)) {
const normalized = t6.get(name).replaceAll('scripts\\zm\\tws_t6_common::', 'scripts\\sp\\tws_t5_common::');
assert.equal(normalized, fn, name);
}
}
});
console.log(`PASS ${checks} leaderboard fixture/static checks (both games). Engine linking and gameplay still require Plutonium.`);