Improve command discovery and welcome messages
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
+30
-8
@@ -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 <amount\|all>` | Deposit 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
|
||||
commands appear on additional pages. Bank entries retain the full
|
||||
`.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
|
||||
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.
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
+17
-6
@@ -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 <amount\|all>` | Deposit 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
|
||||
the full `.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
|
||||
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
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'] };
|
||||
|
||||
+4
-4
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user