Show personal map records on join and replay
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user