Show personal map records on join and replay

This commit is contained in:
2026-10-19 14:11:54 -04:00
parent 9ef5932d36
commit e93182cd02
11 changed files with 620 additions and 200 deletions
+90 -8
View File
@@ -34,8 +34,8 @@ function functions(source) {
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]));
const coroutine = match[1] === 'lb_round_watcher';
body = body.replace(/\btypeof\(/g, 'gscType(').replace(/\.size\b/g, '.length')
const coroutine = ['lb_round_watcher', 'lb_get'].includes(match[1]);
body = body.replace(/player getGuid\(/g, 'player.getGuid(').replace(/\btypeof\(/g, 'gscType(').replace(/\.size\b/g, '.length')
.replace(/\bwait\s+([\d.]+);/g, coroutine ? 'yield { milliseconds: $1 * 1000 };' : ';');
if (coroutine)
body = body.replace(/level waittill\(([^;]+)\);/g, 'yield { entity: level, event: $1 };');
@@ -127,7 +127,7 @@ function roundRuntime(game) {
return { ...e, players, attempts, shown, records, notifications, endEvents,
get now() { return e.now; },
readyPlayer(fields = {}) {
const player = { sessionstate: 'playing', tws_lb_last_request: e.now - 30000, ...fields };
const player = { getGuid: () => undefined, sessionstate: 'playing', tws_lb_last_request: e.now - 30000, ...fields };
players.push(player);
return player;
},
@@ -188,7 +188,7 @@ for (const game of ['t5', 't6']) {
});
check(`${game} common owns the record service window`, () => {
const commonPath = game === 't5' ? 'scripts\\sp\\tws_t5_common' : 'scripts\\zm\\tws_t6_common';
assert(source.includes(`self ${commonPath}::show_record(snapshot);`));
assert(source.includes(`self ${commonPath}::show_record(snapshot, personal);`));
assert(!source.includes('lb_show_hud'));
assert(!source.includes('lb_hud_'));
assert(!source.includes('tws_help_elems'));
@@ -198,7 +198,7 @@ for (const game of ['t5', 't6']) {
});
check(`${game} manual missing/stale requests cancel welcome and reach the service`, () => {
const calls = [];
c.self = { notify: (event) => calls.push(['notify', event]) };
c.self = { getGuid: () => undefined, notify: (event) => calls.push(['notify', event]) };
c.recordService = (snapshot) => calls.push(['record', snapshot]);
const request = all.get('lb_request')
.replace(/\b(?:self|level) endon\([^;]*\);/g, '')
@@ -406,6 +406,88 @@ for (const game of ['t5', 't6']) {
assert.equal(c.lb_page_valid(value, 0, 1), false);
}
});
check(`${game} HTTP callers serialize and native timeouts retain the slot until collection`, () => {
const e = engine(game), c = e.context;
const started = [], pending = [];
c.httpRequest = (url) => { started.push(url); return {}; };
c.collect = (request, job) => pending.push(job);
c.jsonParse = JSON.parse;
vm.runInContext(e.all.get('lb_get')
.replace(/http::request|httpPost/g, 'httpRequest')
.replace(/level thread lb_collect\(/g, 'collect('), c);
const first = c.lb_get('&first');
assert.equal(first.next().done, false);
const second = c.lb_get('&second');
assert.equal(second.next().done, false);
assert.equal(started.length, 1);
e.setTime(e.now + 15000);
assert.equal(first.next().done, true);
assert.equal(second.next().done, true);
assert.equal(started.length, 1, 'timed-out queue wait must not start another native request');
assert.equal(c.level.tws_lb_pending, pending[0]);
const third = c.lb_get('&third');
assert.equal(third.next().done, false);
Object.assign(pending[0], { done: true, success: true, body: '{}' });
c.level.tws_lb_pending = undefined;
assert.equal(third.next().done, false);
assert.equal(started.length, 2);
Object.assign(pending[1], { done: true, success: true, body: '{"ok":true}' });
c.level.tws_lb_pending = undefined;
const result = third.next();
assert.equal(result.done, true);
assert.equal(result.value.ok, true);
});
check(`${game} personal best finds exact GUID below the top rank across search pages`, () => {
const guid = '00012345678901234567890';
responses.push(page(option, [entry(option, 'prefix' + guid), entry(option, 'other', guid), entry(option, 'third')], 4),
page(option, [entry(option, guid, 'Me', 27)], 4, 3));
assert.equal(c.lb_personal_fetch(option.key, guid), 27);
assert(queries.at(-1).includes(`q=${guid}&limit=3&offset=3`));
assert(queries.at(-1).includes(`map=${encodeURIComponent(option.key)}`));
});
check(`${game} personal absence requires a complete search, failures stay unavailable`, () => {
responses.push(page(option, [entry(option, 'not-me', 'me')], 1));
assert.equal(c.lb_personal_fetch(option.key, 'me'), 0);
responses.push(page(option, [], 0));
assert.equal(c.lb_personal_fetch(option.key, 'me'), 0);
for (const response of [undefined, { ok: false }, page(option, [], 1),
page(option, [entry(option, 'me')], 0),
page(option, [{ ...entry(option, 'me'), guid: 123 }], 1),
page(option, [entry({ ...option, key: 'wrong' }, 'me')], 1),
page(option, [entry({ ...option, game: 'wrong' }, 'me')], 1),
page(option, [{ ...entry(option, 'me'), highestRound: 27 }], 1)]) {
responses.push(response);
assert.equal(c.lb_personal_fetch(option.key, 'me'), undefined);
}
responses.push(page(option, [entry(option, 'a'), entry(option, 'b'), entry(option, 'c')], 4),
page(option, [entry(option, 'me')], 5, 3));
assert.equal(c.lb_personal_fetch(option.key, 'me'), undefined);
});
check(`${game} personal identity, per-player caching, expiry and unavailable retries`, () => {
const player = { guid: '00012345678901234567890', getGuid() { return this.guid; } };
const other = { guid: 'other', getGuid() { return this.guid; } };
const snapshot = { key: option.key, round: 52 };
assert.equal(c.lb_player_guid(player), player.guid);
player.guid = 123;
assert.equal(c.lb_player_guid(player), '123');
player.guid = '00012345678901234567890';
responses.push(page(option, [entry(option, player.guid, 'Me', 27)], 1));
assert.equal(c.lb_personal_record(player, snapshot), 'Your Best | Round 27');
assert.equal(c.lb_personal_record(player, snapshot), 'Your Best | Round 27');
responses.push(page(option, [], 0));
assert.equal(c.lb_personal_record(other, snapshot), 'Your Best | None yet');
player.tws_lb_personal.saved_at -= 300000;
responses.push(undefined);
assert.equal(c.lb_personal_record(player, snapshot), 'Your Best | Unavailable');
assert.equal(c.lb_personal_record(player, snapshot), 'Your Best | Unavailable');
player.tws_lb_personal.saved_at -= 30000;
responses.push(page(option, [entry(option, player.guid, 'Me', 30)], 1));
assert.equal(c.lb_personal_record(player, snapshot), 'Your Best | Round 30');
assert.equal(c.lb_personal_record(player, undefined), 'Your Best | Unavailable');
assert.equal(c.lb_personal_record(player, { round: 0 }), 'Your Best | None yet');
player.guid = undefined;
assert.equal(c.lb_personal_record(player, snapshot), 'Your Best | Unavailable');
});
check(`${game} one holder`, () => {
responses.push(discovery(option), page(option, [entry(option, '000123', 'Alice')], 1));
const result = c.lb_refresh();
@@ -455,13 +537,13 @@ for (const game of ['t5', 't6']) {
assert.equal(c.lb_refresh(), undefined);
c.level.tws_lb_override = '';
});
check(`${game} all holders fit bounded service rows`, () => {
check(`${game} each holder gets a separate bounded service row`, () => {
const names = Array.from({ length: 100 }, (_, i) => `Holder ${i}`);
const lines = Array.from(c.lb_holder_lines(names));
assert(lines.every((line) => c.lb_holder_width(line) <= 16));
assert.deepEqual(lines.join(', ').split(', '), names);
assert.deepEqual(Array.from(c.lb_holder_lines([])), []);
assert.deepEqual(Array.from(c.lb_holder_lines(['Alex', 'Sam', 'Jamie'])), ['Alex, Sam, Jamie']);
assert.deepEqual(Array.from(c.lb_holder_lines(['Alex', 'Sam', 'Jamie'])), ['Alex', 'Sam', 'Jamie']);
});
check(`${game} holder width counts glyphs and clips without breaking UTF-8`, () => {
const samples = ['M'.repeat(64), 'é'.repeat(32), '🧟'.repeat(16), '玩家名字'.repeat(5),
@@ -514,7 +596,7 @@ 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', 'lb_player_ready'].includes(name)) {
if (!['lb_selection', 'lb_welcome', 'lb_get', 'lb_log', 'lb_player_ready', 'lb_player_guid'].includes(name)) {
const normalized = t6.get(name).replaceAll('scripts\\zm\\tws_t6_common::', 'scripts\\sp\\tws_t5_common::');
assert.equal(normalized, fn, name);
}