Improve command discovery and welcome messages
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
// Exercise the actual common/leaderboard chat listeners with private reply shims.
|
||||
// This does not emulate Plutonium's VM or native chat delivery.
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import vm from 'node:vm';
|
||||
|
||||
const root = new URL('../', import.meta.url);
|
||||
let checks = 0;
|
||||
function translated(source, name) {
|
||||
const match = new RegExp(`^${name}\\(([^\\n]*)\\)\\s*\\n\\{`, 'm').exec(source);
|
||||
assert(match, name);
|
||||
const start = match.index + match[0].length;
|
||||
let end = start, depth = 1;
|
||||
while (depth) {
|
||||
const ch = source[end++];
|
||||
if (ch === '{') depth++;
|
||||
if (ch === '}') depth--;
|
||||
}
|
||||
let body = source.slice(start, end - 1);
|
||||
const locals = new Set([...body.matchAll(/(?<![\w.])(\w+)\s*=(?!=)/g)].map(m => m[1]));
|
||||
const listener = name.endsWith('_listener');
|
||||
if (listener) { locals.add('message'); locals.add('player'); }
|
||||
for (const param of match[1].split(',').map(s => s.trim())) locals.delete(param);
|
||||
body = body.replace(/level endon\([^;]*\);/g, '')
|
||||
.replace(/level waittill\("say", message, player\);/g, '[message, player] = yield "say";')
|
||||
.replace(/player show_help\(\);/g, 'help.push(player);')
|
||||
.replace(/player thread lb_request\(true\);/g, 'records.push(player);')
|
||||
.replace(/player iPrintLn\(/g, 'player.replies.push(')
|
||||
.replace(/\.size\b/g, '.length');
|
||||
return `function${listener ? '*' : ''} ${name}(${match[1]}) { var ${[...locals].join(', ') || 'unused'}; ${body} }`;
|
||||
}
|
||||
|
||||
for (const game of ['t5', 't6']) {
|
||||
const common = readFileSync(new URL(`${game}/scripts/tws_${game}_common.gsc`, root), 'utf8');
|
||||
const leaderboard = readFileSync(new URL(`${game}/scripts/tws_${game}_leaderboard.gsc`, root), 'utf8');
|
||||
const help = [], records = [], sender = { replies: [] }, other = { replies: [] };
|
||||
const c = vm.createContext({
|
||||
level: {}, help, records,
|
||||
isDefined: value => value !== undefined && value !== null,
|
||||
toLower: value => value.toLowerCase(),
|
||||
strTok: value => value.split(/[ \t\r\n]+/).filter(Boolean),
|
||||
getSubStr: (value, start, end) => value.slice(start, end),
|
||||
spawnStruct: () => ({}),
|
||||
});
|
||||
for (const name of ['register_help_command', 'help_first_word', 'help_unknown_command', 'help_chat_listener'])
|
||||
vm.runInContext(translated(common, name), c);
|
||||
vm.runInContext(translated(leaderboard, 'lb_chat_listener'), c);
|
||||
// Missing/disabled optional modules must not suppress unknown-command replies.
|
||||
assert.equal(c.help_unknown_command('.record'), true);
|
||||
assert.equal(c.help_unknown_command('.w 2000'), true);
|
||||
c.register_help_command('.record', 'Custom record command');
|
||||
assert.equal(c.help_unknown_command('.record'), false);
|
||||
c.level.tws_help_commands = [];
|
||||
checks += 3;
|
||||
const chat = c.help_chat_listener(), lb = c.lb_chat_listener();
|
||||
assert.equal(chat.next().value, 'say');
|
||||
assert.equal(lb.next().value, 'say');
|
||||
for (const file of ['common', 'afk', 'bank', 'hud', 'join', 'lock', 'leaderboard']) {
|
||||
const source = readFileSync(new URL(`${game}/scripts/tws_${game}_${file}.gsc`, root), 'utf8');
|
||||
for (const [, command, description] of source.matchAll(/register_help_command\("([^"]+)", "([^"]+)"\)/g))
|
||||
c.register_help_command(command, description);
|
||||
}
|
||||
c.register_help_command('!custom <value>', 'Custom bang command');
|
||||
const send = (message, player = sender) => {
|
||||
sender.replies.length = 0; other.replies.length = 0; help.length = 0; records.length = 0;
|
||||
assert.equal(chat.next([message, player]).value, 'say');
|
||||
assert.equal(lb.next([message, player]).value, 'say');
|
||||
};
|
||||
for (const message of ['.unknown', '!unknown', '.', '!', '.return', '.records', '.helpful', '!help']) {
|
||||
send(message);
|
||||
assert.deepEqual(sender.replies, ['Unknown command. Type .help for the list'], `${game} ${message}`);
|
||||
assert.deepEqual(other.replies, []);
|
||||
assert.deepEqual(help, []); assert.deepEqual(records, []);
|
||||
checks++;
|
||||
}
|
||||
for (const message of ['', 'hello', 'info', 'hello .unknown', ' .unknown', undefined,
|
||||
'.w 2000', '.W ALL', '.d 500', '.w\t2000', '.afk', '.afk buy 10', '.lock', '.unlock', '!custom 5',
|
||||
'.help extra', '.record extra']) {
|
||||
send(message);
|
||||
assert.deepEqual(sender.replies, [], `${game} ${message}`);
|
||||
assert.deepEqual(help, []); assert.deepEqual(records, []);
|
||||
checks++;
|
||||
}
|
||||
for (const message of ['.help', '.HELP', '.commands', '.info', '.INFO', '.command', '.COMMAND']) {
|
||||
send(message, other);
|
||||
assert.deepEqual(help, [other]); assert.deepEqual(records, []);
|
||||
assert.deepEqual(sender.replies, []); assert.deepEqual(other.replies, []);
|
||||
checks++;
|
||||
}
|
||||
for (const message of ['.leaders', '.LEADERS', '.record', '.RECORD']) {
|
||||
send(message, other);
|
||||
assert.deepEqual(records, [other]); assert.deepEqual(help, []);
|
||||
assert.deepEqual(sender.replies, []); assert.deepEqual(other.replies, []);
|
||||
checks++;
|
||||
}
|
||||
send('.unknown', null);
|
||||
assert.deepEqual(sender.replies, []); assert.deepEqual(help, []); assert.deepEqual(records, []);
|
||||
checks++;
|
||||
}
|
||||
console.log(`PASS ${checks} command discovery checks (both games). Native chat delivery still requires Plutonium.`);
|
||||
@@ -34,7 +34,7 @@ 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 = ['lb_round_watcher', 'lb_get'].includes(match[1]);
|
||||
const coroutine = ['lb_round_watcher', 'lb_get', 'lb_welcome'].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)
|
||||
@@ -172,6 +172,36 @@ for (const game of ['t5', 't6']) {
|
||||
if (game === 't6') assert(!all.get('lb_welcome').includes('self.hasSpawned'));
|
||||
assert(source.includes('register_connect_handler(::lb_welcome)'));
|
||||
});
|
||||
check(`${game} welcome sends one private native hint before waiting for record data`, () => {
|
||||
const e = engine(game), c = e.context, replies = [], requests = [];
|
||||
const other = { replies: [] };
|
||||
c.self = { iPrintLn: message => replies.push(message) };
|
||||
c.level.tws_lb_attempt = 0;
|
||||
c.welcomeRecord = manual => requests.push(manual);
|
||||
const code = e.all.get('lb_welcome')
|
||||
.replace(/\b(?:self|level) endon\([^;]*\);/g, '')
|
||||
.replace(/self iPrintLn\(/g, 'self.iPrintLn(')
|
||||
.replace(/self lb_request\(/g, 'welcomeRecord(');
|
||||
vm.runInContext(code, c);
|
||||
const welcome = c.lb_welcome();
|
||||
assert.equal(welcome.next().value.milliseconds, 250, 'wait for readiness');
|
||||
assert.deepEqual(replies, []);
|
||||
c.self.sessionstate = 'spectator';
|
||||
assert.equal(welcome.next().value.milliseconds, 5000, 'retain the welcome delay');
|
||||
assert.deepEqual(replies, []);
|
||||
assert.equal(welcome.next().value.milliseconds, 250, 'record fetch can still be pending');
|
||||
assert.deepEqual(replies, ['Type .help for commands']);
|
||||
assert.deepEqual(other.replies, []);
|
||||
assert.deepEqual(requests, []);
|
||||
assert.equal(welcome.next().value.milliseconds, 250);
|
||||
assert.equal(replies.length, 1, 'waiting does not repeat the hint');
|
||||
c.level.tws_lb_attempt = 1;
|
||||
assert.equal(welcome.next().done, true);
|
||||
assert.deepEqual(requests, [false]);
|
||||
assert.equal(c.lb_welcome().next().done, true, 'duplicate connection dispatch stays silent');
|
||||
assert.equal(replies.length, 1);
|
||||
assert(!e.all.get('lb_request').includes('iPrintLn'), 'manual replay does not print the hint');
|
||||
});
|
||||
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'] };
|
||||
|
||||
Reference in New Issue
Block a user