Improve command discovery and welcome messages

This commit is contained in:
2026-10-19 14:37:24 -04:00
parent e93182cd02
commit a457cbe539
12 changed files with 378 additions and 26 deletions
+75
View File
@@ -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 T5/T6 script and each native plugin. Current versions are recorded in
[versions.json](versions.json); see the [version policy](README.md#versioning). [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 ## 2026-10-18
### Show each player's personal map record on join and replay ### Show each player's personal map record on join and replay
+32 -3
View File
@@ -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, 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 `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; 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 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 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 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 still uses `SetValue`. On r5334+, deploy the updated HUD script and restart
the map; no full server-process restart is required. On older Plutonium the map; no full server-process restart is required. On older Plutonium
revisions, update clients and server and fully restart them before deploying. 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. and round-start reminders.
AFK `2.0.0` requires plugin `1.4.0+`, 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 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 notifications and other installed HUD mods; portable allocation checks do
not establish live T5 SP rendering capacity. 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 ## Map record announcements
Install `tws_t5_leaderboard.gsc` for T5 or `tws_t6_leaderboard.gsc` for T6, 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 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 adds no native-plugin or backend requirement and needs no full server-process
restart when existing leaderboard dependencies are already installed. restart when existing leaderboard dependencies are already installed.
+30 -8
View File
@@ -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. 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 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 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 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 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). [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 `SetTextUnlimited` counter, updated only when the count changes. Deploy the
updated HUD script and restart the map on r5334+; no full server-process updated HUD script and restart the map on r5334+; no full server-process
restart is required. Older Plutonium revisions need client/server updates and 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 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). plugin `1.4.0+` and EC reserve service; see the [AFK guide](../../docs/afk.md).
## Map record announcements ## Map record announcements
Copy `tws_t5_leaderboard.gsc` and the updated `tws_t5_common.gsc` to 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 `storage/t5/scripts/sp/`, then restart the map. Leaderboard `1.4.1` requires
common `1.6.1+`; this script update adds no native-plugin or backend dependency common `1.7.0+`; this script update adds no native-plugin or backend dependency
and requires no full server-process restart. Leaderboard requires a and requires no full server-process restart. Leaderboard requires a
[t5-gsc-utils](https://github.com/alicealys/t5-gsc-utils) build exposing [t5-gsc-utils](https://github.com/alicealys/t5-gsc-utils) build exposing
`http::request`, `jsonParse`, `typeof` and `formatString`. A plugin upgrade `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 | | 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) | | `.hud` | Hide or show your core HUD (AFK countdown stays visible) |
| `.afk` | Enter protected AFK using your time reserve | | `.afk` | Enter protected AFK using your time reserve |
| `.back` | Return from AFK | | `.back` | Return from AFK |
| `.join` | Join mid-round (newcomers only) | | `.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) | | `.b` | Check bank balance (bank module required) |
| `.d <amount\|all>` | Deposit points (bank module required) | | `.d <amount\|all>` | Deposit points (bank module required) |
| `.w <amount\|all>` | Withdraw points (bank module required) | | `.w <amount\|all>` | 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 also removes its entries from `.help` after a map restart. Lock and extended AFK
commands appear on additional pages. Bank entries retain the full commands appear on additional pages. Bank entries retain the full
`.d <amount|all>` / `.w <amount|all>` syntax. Removing HUD removes `.hud` from the list. `.d <amount|all>` / `.w <amount|all>` 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 The enabled leaderboard module registers `.leaders`, which replays the latest
map and personal records privately. Repeating it restarts the full 12-second 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 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 | | `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 | | `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 | | `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 | | `destroy_on_disconnect(elems)` | Waits on `disconnect`, destroys the array |
| `int_dvar_or(name, fallback)` | Parse an integer dvar with default | | `int_dvar_or(name, fallback)` | Parse an integer dvar with default |
| `format_mmss(elapsed_ms)` | Format milliseconds as `M:SS` | | `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 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 of a single-page counter. HP should appear beside the bottom health bar only
while injured. 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 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. `.help` and the remaining commands still work. Restore the scripts afterward.
3. Verify help closes after 12 seconds. Add custom registered commands to test 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 4. Reopen after expiry, die/respawn and reconnect; verify no duplicate text or
leftover elements. Repeat without the HUD module. Follow the complete leftover elements. Repeat without the HUD module. Follow the complete
[HUD validation guide](../../README.md#hud-validation) before rollout. [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. records, help and interrupted/resumed records alongside stock notifications.
Check that branding, the health bar, damage-only HP, zombie count and timers Check that branding, the health bar, damage-only HP, zombie count and timers
remain correct during each view and after it closes. remain correct during each view and after it closes.
+36 -1
View File
@@ -157,11 +157,46 @@ help_chat_listener()
continue; continue;
command = toLower(message); command = toLower(message);
if (command == ".help" || command == ".commands") if (command == ".help" || command == ".commands" || command == ".info" || command == ".command")
player show_help(); 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. // A single per-player controller owns service priority, pages and expiry.
show_help() show_help()
{ {
+3 -1
View File
@@ -168,6 +168,8 @@ lb_welcome()
wait 0.25; wait 0.25;
wait 5; wait 5;
self iPrintLn("Type .help for commands");
while (!isDefined(level.tws_lb_cache) && level.tws_lb_attempt == 0) while (!isDefined(level.tws_lb_cache) && level.tws_lb_attempt == 0)
wait 0.25; wait 0.25;
@@ -185,7 +187,7 @@ lb_chat_listener()
level waittill("say", message, player); level waittill("say", message, player);
if (!isDefined(player) || !isDefined(message)) if (!isDefined(player) || !isDefined(message))
continue; continue;
if (toLower(message) == ".leaders") if (toLower(message) == ".leaders" || toLower(message) == ".record")
player thread lb_request(true); player thread lb_request(true);
} }
} }
+11
View File
@@ -217,6 +217,17 @@ Live checks after deploying:
registered commands to exercise the nine-row page limit and page counter. registered commands to exercise the nine-row page limit and page counter.
Remove optional modules and restart the map to confirm their commands are Remove optional modules and restart the map to confirm their commands are
absent. Restore them and confirm command behavior is unchanged. 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. 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 Repeat `.leaders` during help; only the latest replay should start from page
one after help closes. Check long names across pages, no records and one after help closes. Check long names across pages, no records and
+17 -6
View File
@@ -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_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_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_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_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+ | | `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. 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 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 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 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 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). [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 `SetTextUnlimited` counter, updated only when the count changes. Deploy the
updated HUD script and restart the map on r5334+; no full server-process updated HUD script and restart the map on r5334+; no full server-process
restart is required. Older Plutonium revisions need client/server updates and 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 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). 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 It works independently of telemetry and HUD, and privately announces records
once per connection, including newcomers waiting in spectator mode. 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 `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 no native-plugin or backend dependency and requires no full server-process
restart when the existing plugin is installed. restart when the existing plugin is installed.
@@ -169,12 +171,12 @@ explicit row positions so T5 does not concatenate newline-separated text.
| Command | Description | | 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) | | `.hud` | Hide or show your core HUD (AFK countdown stays visible) |
| `.afk` | Enter protected AFK using your time reserve | | `.afk` | Enter protected AFK using your time reserve |
| `.back` | Return from AFK | | `.back` | Return from AFK |
| `.join` | Join mid-round (newcomers only) | | `.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) | | `.b` | Check bank balance (bank module required) |
| `.d <amount\|all>` | Deposit points (bank module required) | | `.d <amount\|all>` | Deposit points (bank module required) |
| `.w <amount\|all>` | Withdraw points (bank module required) | | `.w <amount\|all>` | 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 Lock and extended AFK commands appear on additional pages. Bank entries retain
the full `.d <amount|all>` / `.w <amount|all>` syntax. Removing HUD removes `.hud` the full `.d <amount|all>` / `.w <amount|all>` syntax. Removing HUD removes `.hud`
from the list. 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 The enabled leaderboard module registers `.leaders`, which replays the latest
map and personal records privately. Repeating it restarts the full 12-second 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 announcement; help pauses the record's remaining time and resumes it after the
+36 -1
View File
@@ -153,11 +153,46 @@ help_chat_listener()
continue; continue;
command = toLower(message); command = toLower(message);
if (command == ".help" || command == ".commands") if (command == ".help" || command == ".commands" || command == ".info" || command == ".command")
player show_help(); 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. // A single per-player controller owns service priority, pages and expiry.
show_help() show_help()
{ {
+3 -1
View File
@@ -171,6 +171,8 @@ lb_welcome()
wait 0.25; wait 0.25;
wait 5; wait 5;
self iPrintLn("Type .help for commands");
while (!isDefined(level.tws_lb_cache) && level.tws_lb_attempt == 0) while (!isDefined(level.tws_lb_cache) && level.tws_lb_attempt == 0)
wait 0.25; wait 0.25;
@@ -188,7 +190,7 @@ lb_chat_listener()
level waittill("say", message, player); level waittill("say", message, player);
if (!isDefined(player) || !isDefined(message)) if (!isDefined(player) || !isDefined(message))
continue; continue;
if (toLower(message) == ".leaders") if (toLower(message) == ".leaders" || toLower(message) == ".record")
player thread lb_request(true); player thread lb_request(true);
} }
} }
+100
View File
@@ -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.`);
+31 -1
View File
@@ -34,7 +34,7 @@ function functions(source) {
let body = protectedSource.slice(headers.lastIndex, end - 1); let body = protectedSource.slice(headers.lastIndex, end - 1);
// Each GSC function has its own implicit locals, including recursion. // 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 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') 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 };' : ';'); .replace(/\bwait\s+([\d.]+);/g, coroutine ? 'yield { milliseconds: $1 * 1000 };' : ';');
if (coroutine) if (coroutine)
@@ -172,6 +172,36 @@ for (const game of ['t5', 't6']) {
if (game === 't6') assert(!all.get('lb_welcome').includes('self.hasSpawned')); if (game === 't6') assert(!all.get('lb_welcome').includes('self.hasSpawned'));
assert(source.includes('register_connect_handler(::lb_welcome)')); 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`, () => { check(`${game} record replay uses only a fresh complete cache`, () => {
assert.equal(c.lb_cached_record(), undefined); assert.equal(c.lb_cached_record(), undefined);
const snapshot = { saved_at: 1000000, round: 52, lines: ['Alice'] }; const snapshot = { saved_at: 1000000, round: 52, lines: ['Alice'] };
+4 -4
View File
@@ -1,18 +1,18 @@
{ {
"tws_t5_afk.gsc": "2.1.0", "tws_t5_afk.gsc": "2.1.0",
"tws_t5_bank.gsc": "2.0.3", "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_hud.gsc": "1.4.2",
"tws_t5_join.gsc": "1.0.0", "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_lock.gsc": "1.1.0",
"tws_t5_telemetry.gsc": "1.0.0", "tws_t5_telemetry.gsc": "1.0.0",
"tws_t6_afk.gsc": "2.1.0", "tws_t6_afk.gsc": "2.1.0",
"tws_t6_bank.gsc": "2.0.3", "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_hud.gsc": "1.4.2",
"tws_t6_join.gsc": "1.0.1", "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_lock.gsc": "1.1.0",
"tws_t6_telemetry.gsc": "1.0.0", "tws_t6_telemetry.gsc": "1.0.0",
"t5-plugin": "1.4.0", "t5-plugin": "1.4.0",