Add fixed core HUD and shared service window for T5 and T6

This commit is contained in:
2026-10-14 15:55:22 -04:00
parent d15bc50b7d
commit 929bf0354b
18 changed files with 1973 additions and 804 deletions
+61 -3
View File
@@ -44,7 +44,7 @@ function functions(source) {
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 excluded = new Set(['lb_worker', 'lb_welcome', 'lb_get', 'lb_collect', 'lb_log', 'lb_chat_listener', 'lb_request']);
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' };
@@ -111,6 +111,42 @@ for (const game of ['t5', 't6']) {
c.level.tws_lb_cache = undefined;
assert.equal(queries.length, 0);
});
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('lb_show_hud'));
assert(!source.includes('lb_hud_'));
assert(!source.includes('tws_help_elems'));
assert(!source.includes('iPrintLnBold'));
assert(!source.includes('make_text_elem'));
assert(!source.includes('tws_t5_hud') && !source.includes('tws_t6_hud'));
});
check(`${game} manual missing/stale requests cancel welcome and reach the service`, () => {
const calls = [];
c.self = { 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, '')
.replace(/\bself notify\(/g, 'self.notify(')
.replace(/\bself scripts\\(?:sp|zm)\\tws_t[56]_common::show_record\(/g, 'recordService(');
vm.runInContext(request, c);
for (const cached of [undefined, { saved_at: 1, round: 52, lines: ['Alice'] }]) {
c.level.tws_lb_cache = cached;
c.lb_request(false);
assert.deepEqual(calls, []);
c.lb_request(true);
assert.deepEqual(calls, [['notify', 'tws_lb_requested'], ['record', undefined]]);
calls.length = 0;
}
const snapshot = { saved_at: 1000000, round: 0, lines: [] };
c.level.tws_lb_cache = snapshot;
c.lb_request(false);
assert.deepEqual(calls, [['record', snapshot]]);
calls.length = 0;
c.lb_request(true);
assert.deepEqual(calls, [['notify', 'tws_lb_requested'], ['record', snapshot]]);
c.level.tws_lb_cache = undefined;
});
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é 🧟');
@@ -193,11 +229,33 @@ for (const game of ['t5', 't6']) {
assert.equal(c.lb_refresh(), undefined);
c.level.tws_lb_override = '';
});
check(`${game} all holders fit bounded messages`, () => {
check(`${game} all holders fit bounded service rows`, () => {
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(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']);
});
check(`${game} holder width counts glyphs and clips without breaking UTF-8`, () => {
const samples = ['M'.repeat(64), 'é'.repeat(32), '🧟'.repeat(16), '玩家名字'.repeat(5),
'abcdefghijklééé', '^1Alice\n^7 René 🧟', '^1\n\t'];
for (const sample of samples) {
const value = c.lb_holder_name(bytes(sample));
assert(c.lb_holder_width(value) <= 16, sample);
assert(c.lb_utf8_valid(value), sample);
assert(!text(value).includes('\uFFFD'), sample);
assert(!/[\x00-\x1f\x7f^]/.test(value), sample);
assert(value.length > 0, sample);
}
assert.equal(c.lb_holder_name('M'.repeat(64)), 'M'.repeat(13) + '...');
assert.equal(text(c.lb_holder_name(bytes('🧟'.repeat(16)))), '🧟'.repeat(6) + '...');
assert.equal(c.lb_holder_name('^1\n\t'), 'Unknown player');
assert.equal(c.lb_holder_width(bytes('René 🧟')), 8);
const names = Array(100).fill(bytes('同じ名前'.repeat(5)));
const rows = Array.from(c.lb_holder_lines(names));
assert.equal(rows.length, names.length, 'identical long names still represent every holder');
assert(rows.every((line) => c.lb_holder_width(line) <= 16 && c.lb_utf8_valid(line)));
});
if (game === 't6') {
const variants = [