// 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(/(? 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 ', '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.`);