diff --git a/CHANGELOG.md b/CHANGELOG.md index 63eb5a1..1129cdb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,81 @@ Changes are listed newest first, with an independent version for every individua T5/T6 script and each native plugin. Current versions are recorded in [versions.json](versions.json); see the [version policy](README.md#versioning). +## 2026-10-19 + +### Move the initial help hint to the private message area + +| Component | Version | Affected source | +| --- | --- | --- | +| `tws_t5_common.gsc` | `1.7.1` | `t5/scripts/tws_t5_common.gsc` | +| `tws_t6_common.gsc` | `1.7.1` | `t6/scripts/tws_t6_common.gsc` | +| `tws_t5_leaderboard.gsc` | `1.4.1` | `t5/scripts/tws_t5_leaderboard.gsc` | +| `tws_t6_leaderboard.gsc` | `1.4.1` | `t6/scripts/tws_t6_leaderboard.gsc` | + +- Send `Type .help for commands` once through the player's `iPrintLn`, matching + the location and formatting of the unknown-command reply. Keep each engine's + existing welcome readiness checks and five-second delay; print before waiting + for record data so a slow or failed fetch does not hold up the hint. +- Remove the welcome hint's HUD element, rendering state and optional argument + from common's record service. Manual replays and round reminders do not repeat + the message. Both T5 and T6 receive equivalent changes with their existing + namespaces and readiness differences preserved. +- Compatibility: no new dependency requirement. Leaderboard retains its common + `1.7.0+` requirement for command aliases; deploy common `1.7.1` with leaderboard + `1.4.1` for the updated source pair. Native plugins, backend and upstream pins + are unchanged. +- Validation: 179 leaderboard, 76 service HUD, 14 core HUD and 80 command discovery + checks passed. Fixtures verify readiness/delay, sender-specific native output, + a pending/failed record fetch and duplicate welcome dispatch. Changed T6 + scripts compiled and assembled with the local upstream gsc-tool libraries. + Live Windows/Plutonium message placement and rendering remain unverified; + T5 has no gsc-tool backend. JSON/manifest coverage, matching versions/changelog, + local documentation links and `git diff --check` passed. No build of native + plugins or deployment was performed. +- Deployment: copy updated common and leaderboard scripts together for each + installed game and restart the map. No full server-process restart or other + deployment action is required. + +### Make chat commands discoverable — issue #4 + +| Component | Version | Affected source | +| --- | --- | --- | +| `tws_t5_common.gsc` | `1.7.0` | `t5/scripts/tws_t5_common.gsc` | +| `tws_t6_common.gsc` | `1.7.0` | `t6/scripts/tws_t6_common.gsc` | +| `tws_t5_leaderboard.gsc` | `1.4.0` | `t5/scripts/tws_t5_leaderboard.gsc` | +| `tws_t6_leaderboard.gsc` | `1.4.0` | `t6/scripts/tws_t6_leaderboard.gsc` | + +- Reply only to the sender of an unknown `.` or `!` command with + `Unknown command. Type .help for the list`. Compare the first word against + installed help registrations, including labels with arguments, so `.w 2000` + is recognized. Ordinary chat is unaffected; unregistered optional-module commands + receive the same fallback. +- Add case-insensitive `.info` and `.command` aliases for `.help`, and `.record` + for `.leaders`, without duplicate help entries. Keep existing exact-message + dispatch and module argument handling. Recognize the record alias only when + the leaderboard registers its command. +- Add `Type .help for commands` below the personal best on welcome, using one + private nonarchived HUD element. Retain help pause/resume and release the hint + on expiry, disconnect, game end or replacement by a manual replay. Manual + replays and compact round reminders retain their existing layouts. +- Apply the same changes to both engines, preserving their namespaces and + welcome readiness behavior. Extend portable checks and both games' live + validation instructions for unknown commands, `.w 2000`, every alias and + welcome-hint lifecycle behavior. +- Compatibility: leaderboard `1.4.0` requires matching common `1.7.0+` for the + optional welcome argument and alias recognition. Older common consumers + remain supported. No new native-plugin, backend or upstream dependency + requirement; unchanged scripts and plugins retain their versions. +- Validation: 80 command discovery, 78 service HUD, 14 core HUD, 177 leaderboard, + 42 bank, 17 join, 23 telemetry, 76 lock and 47 AFK checks passed, plus both + moderation checks. Changed T6 scripts compiled and assembled using the local + upstream gsc-tool libraries. These are engine-shim/compiler checks; live + Windows/Plutonium chat delivery, privacy and rendering remain unverified. + T5 has no gsc-tool backend. No plugin build or deployment was performed. +- Deployment: copy updated common and leaderboard scripts together for each + installed game and restart the map. No full server-process restart or other + deployment action is required. See [command discovery](README.md#chat-command-discovery-t5-and-t6). + ## 2026-10-18 ### Show each player's personal map record on join and replay diff --git a/README.md b/README.md index 9b9a6ed..619dd4d 100644 --- a/README.md +++ b/README.md @@ -462,7 +462,9 @@ Records use the same service area, right aligned: `Map Record | Round N`, followed by every tied holder on a separate line. After a blank row, `Your Best | Round N` uses the same heading style as the map record, distinct from the holder names. The entire announcement is visible at once for 12 seconds; -records have no pages, counter or footer. Help pauses a visible record and +records have no pages, counter or footer. The initial `Type .help for commands` +hint uses the same private native message area and style as unknown-command +replies. Help pauses a visible record and resumes its remaining time when help ends. `.leaders` during help replaces the pending record with a fresh replay. Round-start reminders use just the record header and one proximity or milestone line for six seconds. They skip busy @@ -486,7 +488,7 @@ avoiding dynamic counter strings in the text config-string pool. Numeric HP still uses `SetValue`. On r5334+, deploy the updated HUD script and restart the map; no full server-process restart is required. On older Plutonium revisions, update clients and server and fully restart them before deploying. -Leaderboard `1.3.1` requires matching common `1.6.1+` for personal records +Leaderboard `1.4.1` requires matching common `1.7.0+` for personal records and round-start reminders. AFK `2.0.0` requires plugin `1.4.0+`, common `1.4.0+` and the EC reserve service; see the [AFK guide](docs/afk.md). Existing @@ -547,10 +549,37 @@ Before rollout, validate both engines in Plutonium: notifications and other installed HUD mods; portable allocation checks do not establish live T5 SP rendering capacity. +## Chat command discovery (T5 and T6) + +Common `1.7.0` replies privately to unknown messages starting with `.` or `!`: +`Unknown command. Type .help for the list`. It compares the first word with +installed modules' registered commands, so `.w 2000` and `.afk buy 10` are +recognized without changing their existing argument handling. Ordinary chat +gets no fallback reply. Bang commands are recognized only if registered with `!`. + +`.info` and `.command` are case-insensitive aliases for `.help`; `.commands` +continues to work. Leaderboard `1.4.1` adds `.record` as an alias for `.leaders`. +Aliases use the same exact-message handling as their original commands and +add no duplicate help rows. Without the enabled leaderboard module, `.record` +and `.leaders` receive the unknown-command reply. + +On welcome, `Type .help for commands` appears once through the same private +`iPrintLn` message area and style as unknown-command replies, after readiness +and the five-second welcome delay. It does not wait for record data, occupy a +record HUD row, or repeat on manual replay. Install matching common `1.7.1` and +leaderboard `1.4.1` scripts together and restart the map. There is +no new native-plugin or backend dependency and no full process restart needed. + +Run `node tools/check_commands.mjs`, `node tools/check_hud.mjs` and +`node tools/check_leaderboard.mjs` for portable command and display checks. +Live chat/privacy and rendering checks are in the +[T5 module checks](t5/scripts/README.md#tws_t5_commongsc) and +[T6 notes](t6/scripts/NOTES.md#checks). + ## Map record announcements Install `tws_t5_leaderboard.gsc` for T5 or `tws_t6_leaderboard.gsc` for T6, -**along with common `1.6.1+`** for that game when using leaderboard `1.3.1`. +**along with common `1.7.0+`** for that game when using leaderboard `1.4.1`. Copy both scripts for each installed game and restart the map. This update adds no native-plugin or backend requirement and needs no full server-process restart when existing leaderboard dependencies are already installed. diff --git a/t5/scripts/README.md b/t5/scripts/README.md index dda04f4..ac54a4d 100644 --- a/t5/scripts/README.md +++ b/t5/scripts/README.md @@ -85,7 +85,9 @@ and the stock HUD are unaffected. `.hud` requires the HUD module. Common provides one unboxed upper-right service area below the Plutonium tag. Records are right aligned: `Map Record | Round N`, every tied holder on its own line, then a blank row and a separate `Your Best | Round N` heading. The whole -record is shown at once for 12 seconds, with no pages, counter or footer. Help displays +record is shown at once for 12 seconds, with no pages, counter or footer. The +initial `Type .help for commands` hint uses the same private native message +area and style as unknown-command replies. Help displays the installed command list in pages of nine rows in aligned rows. Services remain private, leave the core in place and work without the HUD module installed. See the [shared layout and validation guide](../../README.md#core-hud-and-service-window-t5-and-t6). @@ -101,15 +103,15 @@ HUD `1.4.2` requires common `1.4.0+` and `SetTextUnlimited` counter, updated only when the count changes. Deploy the updated HUD script and restart the map on r5334+; no full server-process restart is required. Older Plutonium revisions need client/server updates and -full restarts first. Leaderboard `1.3.1` requires common `1.6.1+` for its +full restarts first. Leaderboard `1.4.1` requires common `1.7.0+` for its personal records and round-start reminders. AFK `2.0.0` requires the matching plugin `1.4.0+` and EC reserve service; see the [AFK guide](../../docs/afk.md). ## Map record announcements Copy `tws_t5_leaderboard.gsc` and the updated `tws_t5_common.gsc` to -`storage/t5/scripts/sp/`, then restart the map. Leaderboard `1.3.1` requires -common `1.6.1+`; this script update adds no native-plugin or backend dependency +`storage/t5/scripts/sp/`, then restart the map. Leaderboard `1.4.1` requires +common `1.7.0+`; this script update adds no native-plugin or backend dependency and requires no full server-process restart. Leaderboard requires a [t5-gsc-utils](https://github.com/alicealys/t5-gsc-utils) build exposing `http::request`, `jsonParse`, `typeof` and `formatString`. A plugin upgrade @@ -150,12 +152,12 @@ explicit row positions so T5 does not concatenate newline-separated text. | Command | Effect | |---|---| -| `.help`, `.commands` | List available commands | +| `.help`, `.commands`, `.info`, `.command` | List available commands | | `.hud` | Hide or show your core HUD (AFK countdown stays visible) | | `.afk` | Enter protected AFK using your time reserve | | `.back` | Return from AFK | | `.join` | Join mid-round (newcomers only) | -| `.leaders` | Show map and personal records (leaderboard module required) | +| `.leaders`, `.record` | Show map and personal records (leaderboard module required) | | `.b` | Check bank balance (bank module required) | | `.d ` | Deposit points (bank module required) | | `.w ` | Withdraw points (bank module required) | @@ -167,6 +169,15 @@ the order above; additional entries follow registration order. Removing a module also removes its entries from `.help` after a map restart. Lock and extended AFK commands appear on additional pages. Bank entries retain the full `.d ` / `.w ` syntax. Removing HUD removes `.hud` from the list. + +Unknown messages starting with `.` or `!` receive a private +`Unknown command. Type .help for the list` reply. Recognition compares the first +word with installed help registrations, including argument labels, so `.w 2000` +gets no false unknown-command reply. Ordinary chat is unaffected. `.info` and +`.command` alias `.help`; `.record` aliases `.leaders` only with the enabled +leaderboard module. Aliases are case-insensitive exact messages and add no help +rows. Run `node tools/check_commands.mjs` from the repository root. + The enabled leaderboard module registers `.leaders`, which replays the latest map and personal records privately. Repeating it restarts the full 12-second announcement; help pauses the record's remaining time and resumes it after the @@ -260,7 +271,7 @@ connect listener anywhere else crashes the T5 SP handshake. | `make_core_shader(p, x, y, w, h, c, alpha, sort)` | Retained legacy panel shader with `.hud` visibility | | `set_core_alpha(p, elem, alpha)` | Preserve the element's intended alpha while applying the player's visibility choice | | `toggle_core_hud()` | Toggle the player's registered core elements without stopping updates | -| `show_record(snapshot)` | Replace the player's record service request; an undefined snapshot shows unavailable | +| `show_record(snapshot, personal)` | Replace the player's record service request; an undefined snapshot shows unavailable | | `destroy_on_disconnect(elems)` | Waits on `disconnect`, destroys the array | | `int_dvar_or(name, fallback)` | Parse an integer dvar with default | | `format_mmss(elapsed_ms)` | Format milliseconds as `M:SS` | @@ -274,6 +285,13 @@ connect listener anywhere else crashes the T5 SP handshake. the requesting player. Check explicit rows, full bank syntax and the absence of a single-page counter. HP should appear beside the bottom health bar only while injured. + Also try `.info`, `.INFO` and `.command`; each must open the same private help. + Send `.unknown` and `!unknown`: only the sender sees + `Unknown command. Type .help for the list`. Ordinary chat gets no reply. + With bank enabled, `.w 2000` reaches the bank handler with no unknown reply. + Try `.record` and `.RECORD`: both replay records like `.leaders`, including + while help is open. Disable leaderboard and restart: `.record` and `.leaders` + now get the private unknown reply. Restore the module afterward. 2. Remove AFK or join, restart the map, and verify its entries disappear while `.help` and the remaining commands still work. Restore the scripts afterward. 3. Verify help closes after 12 seconds. Add custom registered commands to test @@ -283,7 +301,11 @@ connect listener anywhere else crashes the T5 SP handshake. 4. Reopen after expiry, die/respawn and reconnect; verify no duplicate text or leftover elements. Repeat without the HUD module. Follow the complete [HUD validation guide](../../README.md#hud-validation) before rollout. -5. Watch the first automatic record announcement and its dismissal. Repeat +5. Watch the first automatic record announcement and its dismissal. Verify + `Type .help for commands` appears once in the same private message area and + style as the unknown-command reply, after the welcome delay, including when + record data is empty or still loading. It must not appear in the record HUD + or repeat on `.leaders`/`.record` replay or round reminders. Repeat records, help and interrupted/resumed records alongside stock notifications. Check that branding, the health bar, damage-only HP, zombie count and timers remain correct during each view and after it closes. diff --git a/t5/scripts/tws_t5_common.gsc b/t5/scripts/tws_t5_common.gsc index 4ba3ed6..f46215a 100644 --- a/t5/scripts/tws_t5_common.gsc +++ b/t5/scripts/tws_t5_common.gsc @@ -157,11 +157,46 @@ help_chat_listener() continue; command = toLower(message); - if (command == ".help" || command == ".commands") + if (command == ".help" || command == ".commands" || command == ".info" || command == ".command") player show_help(); + else if (help_unknown_command(command)) + player iPrintLn("Unknown command. Type .help for the list"); } } +// Compare command tokens, not help labels containing argument syntax. +help_first_word(message) +{ + words = strTok(toLower(message), " \t\r\n"); + if (words.size == 0) + return ""; + return words[0]; +} + +help_unknown_command(message) +{ + if (message.size == 0) + return false; + prefix = getSubStr(message, 0, 1); + if (prefix != "." && prefix != "!") + return false; + + command = help_first_word(message); + if (command == ".help" || command == ".commands" || command == ".info" || command == ".command") + return false; + if (isDefined(level.tws_help_commands)) + { + for (i = 0; i < level.tws_help_commands.size; i++) + { + registered = help_first_word(level.tws_help_commands[i].command); + // The record alias is available when the leaderboard registers itself. + if (command == registered || (command == ".record" && registered == ".leaders")) + return false; + } + } + return true; +} + // A single per-player controller owns service priority, pages and expiry. show_help() { diff --git a/t5/scripts/tws_t5_leaderboard.gsc b/t5/scripts/tws_t5_leaderboard.gsc index 5c8a366..d0887cf 100644 --- a/t5/scripts/tws_t5_leaderboard.gsc +++ b/t5/scripts/tws_t5_leaderboard.gsc @@ -168,6 +168,8 @@ lb_welcome() wait 0.25; wait 5; + self iPrintLn("Type .help for commands"); + while (!isDefined(level.tws_lb_cache) && level.tws_lb_attempt == 0) wait 0.25; @@ -185,7 +187,7 @@ lb_chat_listener() level waittill("say", message, player); if (!isDefined(player) || !isDefined(message)) continue; - if (toLower(message) == ".leaders") + if (toLower(message) == ".leaders" || toLower(message) == ".record") player thread lb_request(true); } } diff --git a/t6/scripts/NOTES.md b/t6/scripts/NOTES.md index 65d3974..aff767d 100644 --- a/t6/scripts/NOTES.md +++ b/t6/scripts/NOTES.md @@ -217,6 +217,17 @@ Live checks after deploying: registered commands to exercise the nine-row page limit and page counter. Remove optional modules and restart the map to confirm their commands are absent. Restore them and confirm command behavior is unchanged. + Try `.info`, `.INFO` and `.command`: each opens the same private help. + Send `.unknown` and `!unknown`: only the sender sees + `Unknown command. Type .help for the list`; ordinary chat gets no reply. + With bank enabled, `.w 2000` reaches its handler without an unknown reply. + `.record` and `.RECORD` replay records like `.leaders`, including during help. + Disable leaderboard and restart: both `.record` and `.leaders` get the private + unknown reply. Restore the module afterward. + Check `Type .help for commands` appears once after the welcome delay in the + same private message area and style as the unknown-command reply, including + empty history or record data still loading. There must be no hint in the record + HUD and no repeated hint on manual replay or compact round reminders. 7. Interrupt a record with help and check that its remaining page time resumes. Repeat `.leaders` during help; only the latest replay should start from page one after help closes. Check long names across pages, no records and diff --git a/t6/scripts/README.md b/t6/scripts/README.md index dd615dc..50dac6b 100644 --- a/t6/scripts/README.md +++ b/t6/scripts/README.md @@ -13,7 +13,7 @@ private server token. | `tws_t6_lock.gsc` | Auto-lock at round 20 plus majority `.lock` / `.unlock` votes; clear on game over | Common, HUD | | `tws_t6_telemetry.gsc` | Async queued game reports (game_start/heartbeat/round_change/game_end) | tws-t6-gsc-utils | | `tws_t6_join.gsc` | `.join` chat command lets a spectator newcomer enter mid-match once | Common | -| `tws_t6_leaderboard.gsc` | Private map and personal records on connection, `.leaders` replay and adaptive round-start reminders | Common 1.6.1+, tws-t6-gsc-utils | +| `tws_t6_leaderboard.gsc` | Private map and personal records on connection, `.leaders` replay and adaptive round-start reminders | Common 1.7.0+, tws-t6-gsc-utils | | `tws_t6_bank.gsc` | Persistent shared GUID bank | Common, telemetry, TWS plugin 1.2.0 | | `tws_t6_moderation.gsc` | Optional controller-managed bans, mutes and live moderation | Common 1.2.0+, TWS plugin 1.3.0+ | @@ -98,7 +98,9 @@ and the stock HUD are unaffected. `.hud` requires the HUD module. Common provides one unboxed upper-right service area below the Plutonium tag. Records are right aligned: `Map Record | Round N`, every tied holder on its own line, then a blank row and a separate `Your Best | Round N` heading. The whole -record is shown at once for 12 seconds, with no pages, counter or footer. Help displays +record is shown at once for 12 seconds, with no pages, counter or footer. The +initial `Type .help for commands` hint uses the same private native message +area and style as unknown-command replies. Help displays the installed command list in pages of nine rows in aligned rows. Services remain private, leave the core in place and work without the HUD module installed. See the [shared layout and validation guide](../../README.md#core-hud-and-service-window-t5-and-t6). @@ -114,7 +116,7 @@ HUD `1.4.2` requires common `1.4.0+` and `SetTextUnlimited` counter, updated only when the count changes. Deploy the updated HUD script and restart the map on r5334+; no full server-process restart is required. Older Plutonium revisions need client/server updates and -full restarts first. Leaderboard `1.3.1` requires common `1.6.1+` for its +full restarts first. Leaderboard `1.4.1` requires common `1.7.0+` for its personal records and round-start reminders. AFK `2.0.0` requires the matching plugin `1.4.0+` and EC reserve service; see the [AFK guide](../../docs/afk.md). @@ -124,7 +126,7 @@ plugin `1.4.0+` and EC reserve service; see the [AFK guide](../../docs/afk.md). It works independently of telemetry and HUD, and privately announces records once per connection, including newcomers waiting in spectator mode. -Leaderboard `1.3.1` requires common `1.6.1+`. Copy both scripts to +Leaderboard `1.4.1` requires common `1.7.0+`. Copy both scripts to `Plutonium/storage/t6/scripts/zm/` and restart the map. This script update adds no native-plugin or backend dependency and requires no full server-process restart when the existing plugin is installed. @@ -169,12 +171,12 @@ explicit row positions so T5 does not concatenate newline-separated text. | Command | Description | | --- | --- | -| `.help`, `.commands` | List available commands | +| `.help`, `.commands`, `.info`, `.command` | List available commands | | `.hud` | Hide or show your core HUD (AFK countdown stays visible) | | `.afk` | Enter protected AFK using your time reserve | | `.back` | Return from AFK | | `.join` | Join mid-round (newcomers only) | -| `.leaders` | Show map and personal records (leaderboard module required) | +| `.leaders`, `.record` | Show map and personal records (leaderboard module required) | | `.b` | Check bank balance (bank module required) | | `.d ` | Deposit points (bank module required) | | `.w ` | Withdraw points (bank module required) | @@ -189,6 +191,15 @@ commands alongside their module's initialization so the list stays current. Lock and extended AFK commands appear on additional pages. Bank entries retain the full `.d ` / `.w ` syntax. Removing HUD removes `.hud` from the list. + +Unknown messages starting with `.` or `!` receive a private +`Unknown command. Type .help for the list` reply. Recognition compares the first +word with installed help registrations, including argument labels, so `.w 2000` +gets no false unknown-command reply. Ordinary chat is unaffected. `.info` and +`.command` alias `.help`; `.record` aliases `.leaders` only with the enabled +leaderboard module. Aliases are case-insensitive exact messages and add no help +rows. Run `node tools/check_commands.mjs` from the repository root. + The enabled leaderboard module registers `.leaders`, which replays the latest map and personal records privately. Repeating it restarts the full 12-second announcement; help pauses the record's remaining time and resumes it after the diff --git a/t6/scripts/tws_t6_common.gsc b/t6/scripts/tws_t6_common.gsc index 01c7a71..2f7bf6b 100644 --- a/t6/scripts/tws_t6_common.gsc +++ b/t6/scripts/tws_t6_common.gsc @@ -153,11 +153,46 @@ help_chat_listener() continue; command = toLower(message); - if (command == ".help" || command == ".commands") + if (command == ".help" || command == ".commands" || command == ".info" || command == ".command") player show_help(); + else if (help_unknown_command(command)) + player iPrintLn("Unknown command. Type .help for the list"); } } +// Compare command tokens, not help labels containing argument syntax. +help_first_word(message) +{ + words = strTok(toLower(message), " \t\r\n"); + if (words.size == 0) + return ""; + return words[0]; +} + +help_unknown_command(message) +{ + if (message.size == 0) + return false; + prefix = getSubStr(message, 0, 1); + if (prefix != "." && prefix != "!") + return false; + + command = help_first_word(message); + if (command == ".help" || command == ".commands" || command == ".info" || command == ".command") + return false; + if (isDefined(level.tws_help_commands)) + { + for (i = 0; i < level.tws_help_commands.size; i++) + { + registered = help_first_word(level.tws_help_commands[i].command); + // The record alias is available when the leaderboard registers itself. + if (command == registered || (command == ".record" && registered == ".leaders")) + return false; + } + } + return true; +} + // A single per-player controller owns service priority, pages and expiry. show_help() { diff --git a/t6/scripts/tws_t6_leaderboard.gsc b/t6/scripts/tws_t6_leaderboard.gsc index f4d8f6d..ef3bfd0 100644 --- a/t6/scripts/tws_t6_leaderboard.gsc +++ b/t6/scripts/tws_t6_leaderboard.gsc @@ -171,6 +171,8 @@ lb_welcome() wait 0.25; wait 5; + self iPrintLn("Type .help for commands"); + while (!isDefined(level.tws_lb_cache) && level.tws_lb_attempt == 0) wait 0.25; @@ -188,7 +190,7 @@ lb_chat_listener() level waittill("say", message, player); if (!isDefined(player) || !isDefined(message)) continue; - if (toLower(message) == ".leaders") + if (toLower(message) == ".leaders" || toLower(message) == ".record") player thread lb_request(true); } } diff --git a/tools/check_commands.mjs b/tools/check_commands.mjs new file mode 100644 index 0000000..c34b8fe --- /dev/null +++ b/tools/check_commands.mjs @@ -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(/(? 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.`); diff --git a/tools/check_leaderboard.mjs b/tools/check_leaderboard.mjs index 9bb41e1..2eb542e 100644 --- a/tools/check_leaderboard.mjs +++ b/tools/check_leaderboard.mjs @@ -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(/(? 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'] }; diff --git a/versions.json b/versions.json index f7cb4bb..7173a69 100644 --- a/versions.json +++ b/versions.json @@ -1,18 +1,18 @@ { "tws_t5_afk.gsc": "2.1.0", "tws_t5_bank.gsc": "2.0.3", - "tws_t5_common.gsc": "1.6.1", + "tws_t5_common.gsc": "1.7.1", "tws_t5_hud.gsc": "1.4.2", "tws_t5_join.gsc": "1.0.0", - "tws_t5_leaderboard.gsc": "1.3.1", + "tws_t5_leaderboard.gsc": "1.4.1", "tws_t5_lock.gsc": "1.1.0", "tws_t5_telemetry.gsc": "1.0.0", "tws_t6_afk.gsc": "2.1.0", "tws_t6_bank.gsc": "2.0.3", - "tws_t6_common.gsc": "1.6.1", + "tws_t6_common.gsc": "1.7.1", "tws_t6_hud.gsc": "1.4.2", "tws_t6_join.gsc": "1.0.1", - "tws_t6_leaderboard.gsc": "1.3.1", + "tws_t6_leaderboard.gsc": "1.4.1", "tws_t6_lock.gsc": "1.1.0", "tws_t6_telemetry.gsc": "1.0.0", "t5-plugin": "1.4.0",