From bbafebe208f6759552f7d8b44775bb22ab462a84 Mon Sep 17 00:00:00 2001 From: Josh Wright Date: Sun, 4 Oct 2026 14:51:58 -0400 Subject: [PATCH] Add modular chat help for T5 and T6 --- t5/NOTES.md | 2 +- t5/README.md | 37 ++++++++++++++++++-- t5/tws_afk.gsc | 2 ++ t5/tws_common.gsc | 82 +++++++++++++++++++++++++++++++++++++++++++- t5/tws_join.gsc | 1 + t6/NOTES.md | 26 ++++++++++++-- t6/README.md | 24 +++++++++++-- t6/tws_t6_afk.gsc | 2 ++ t6/tws_t6_common.gsc | 81 ++++++++++++++++++++++++++++++++++++++++++- t6/tws_t6_join.gsc | 1 + 10 files changed, 248 insertions(+), 10 deletions(-) diff --git a/t5/NOTES.md b/t5/NOTES.md index 3e662a3..cfc14f7 100644 --- a/t5/NOTES.md +++ b/t5/NOTES.md @@ -76,7 +76,7 @@ Copy all `tws_*.gsc` to `storage/t5/scripts/sp/` on the Plutonium T5 server | File | Role | Fires notifies | Reads notifies | |---|---|---|---| -| `tws_common` | Shared config, HUD helpers, spawn-handler registry, single connect listener + dispatcher | — | — | +| `tws_common` | Shared config, HUD helpers, `.help` registry, spawn-handler registry, single connect listener + dispatcher | — | `say` | | `tws_db` | MariaDB pool config + reachability probe | `db_probe_done` | — | | `tws_hud` | Timer row, zombie count, health bar | `hud_round_changed`, `hud_match_started`, `hud_game_over` | (internal) | | `tws_bank` | Persistent bank + `bank` / `deposit` / `withdraw` commands | — | `db_probe_done` | diff --git a/t5/README.md b/t5/README.md index 56a0cbb..83f6c6d 100644 --- a/t5/README.md +++ b/t5/README.md @@ -11,7 +11,7 @@ Platform quirks and deploy commands: [NOTES.md](NOTES.md). | File | Role | |---|---| -| [tws_common.gsc](tws_common.gsc) | Shared config, HUD helpers, spawn-handler registry, single `connected` listener + dispatcher | +| [tws_common.gsc](tws_common.gsc) | Shared config, HUD helpers, `.help`, spawn-handler registry, single `connected` listener + dispatcher | | [tws_db.gsc](tws_db.gsc) | MariaDB pool config + reachability probe | | [tws_hud.gsc](tws_hud.gsc) | Bottom-left HUD stack: timers, zombie count, health bar | | [tws_bank.gsc](tws_bank.gsc) | Persistent per-player bank + console commands | @@ -20,6 +20,25 @@ Platform quirks and deploy commands: [NOTES.md](NOTES.md). | [tws_lock.gsc](tws_lock.gsc) | Lobby `g_password` lock at a milestone round | | [tws_telemetry.gsc](tws_telemetry.gsc) | Direct HTTP game start, round, heartbeat, and game end reports | +## Player commands + +Type `.help` in chat to see available player commands and a short description. +The response is visible only to the requesting player and works while spectating, +before the first spawn. Commands are case-insensitive. +Help appears below the health bar for 12 seconds, clear of the timers, zombie +counter and health display. Typing `.help` again restarts that display time. + +| Command | Effect | +|---|---| +| `.help` | List available commands | +| `.afk` | Pause with protection | +| `.back` | Return from AFK | +| `.join` | Join mid-round (newcomers only) | + +Each module registers its own help entries; removing AFK or join also removes +those entries from `.help` after a map restart. The bank's `bank`, `deposit`, and +`withdraw` commands are server-console commands and are not listed in chat help. + ## Dependency graph Arrows read "depends on" — solid means the source names the target with @@ -80,6 +99,7 @@ connect listener anywhere else crashes the T5 SP handshake. - Shared HUD constants: `level.hud_x`, `hud_y`, `hud_row_*`, `hud_col_*`, `hud_font_*`, `hud_bar_*`, `hud_c_*`, `hud_rate`, `hud_health_threshold` - Spawn-handler registry +- `.help` listener and help registry populated by installed command modules - Connect listener + per-player dispatch: iterates `level.tws_spawn_handlers` after `spawned_player` and threads each on the player @@ -90,6 +110,7 @@ connect listener anywhere else crashes the T5 SP handshake. | `common_config()` | Populate every `level.hud_*` (idempotent) | | `ensure_config()` | No-op if config already set; called by every module's `init` | | `register_spawn_handler(fn)` | Appends `::fn` to `level.tws_spawn_handlers` | +| `register_help_command(command, description)` | Adds a player command to `.help`; call from the owning module's `init()` | | `make_text_elem(p, x, y, ax, s, c)`| `NewClientHudElem` text element with black-glow outline | | `make_shader_elem(p, x, y)` | `NewClientHudElem` shader element | | `destroy_on_disconnect(elems)` | Waits on `disconnect`, destroys the array | @@ -98,6 +119,18 @@ connect listener anywhere else crashes the T5 SP handshake. **Depends on**: nothing (leaf module). +**Live validation (requires a T5 server)** + +1. Type `.help` and `.HELP` while playing and while spectating before the first + spawn. Verify all four descriptions appear below the health bar, only for the + requesting player. Take damage so health numbers are visible and check that + they do not cover the help text. +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. Repeat `.help` before 12 seconds elapse; verify one list stays visible for + 12 seconds after the latest request, then hides. Reopen it after expiry, + die/respawn and reconnect; verify no duplicate text or leftover help elements. + --- ## tws_db.gsc @@ -219,7 +252,7 @@ module has zero knowledge of AFK. **Depends on** - `scripts\sp\tws_common::` — `ensure_config`, `make_text_elem`, - `destroy_on_disconnect`, `register_spawn_handler` + `destroy_on_disconnect`, `register_spawn_handler`, `register_help_command` - Shared config: `level.hud_x`, `level.hud_row_match`, `level.hud_font_small`, `level.hud_c_afk` - Stock BO1: `enableInvulnerability`, `disableInvulnerability`, diff --git a/t5/tws_afk.gsc b/t5/tws_afk.gsc index 6ab93a9..5b81dd3 100644 --- a/t5/tws_afk.gsc +++ b/t5/tws_afk.gsc @@ -13,6 +13,8 @@ init() return; scripts\sp\tws_common::ensure_config(); + scripts\sp\tws_common::register_help_command(".afk", "Pause with protection"); + scripts\sp\tws_common::register_help_command(".back", "Return from AFK"); afk_init(); diff --git a/t5/tws_common.gsc b/t5/tws_common.gsc index b100d76..6a70d7f 100644 --- a/t5/tws_common.gsc +++ b/t5/tws_common.gsc @@ -1,4 +1,4 @@ -/* tws_common.gsc — shared config, HUD helpers, spawn-handler registry, connect dispatcher. */ +/* tws_common.gsc — shared config, HUD helpers, spawn/help registries, connect dispatcher. */ #include maps\_utility; #include common_scripts\utility; @@ -10,6 +10,8 @@ init() common_config(); + register_help_command(".help", "List available commands"); + level thread help_chat_listener(); level thread on_player_connect_dispatch(); } @@ -72,6 +74,84 @@ register_spawn_handler(fn) level.tws_spawn_handlers[level.tws_spawn_handlers.size] = fn; } +// Modules register their own entries so help only lists installed commands. +register_help_command(command, description) +{ + if (!isDefined(level.tws_help_commands)) + level.tws_help_commands = []; + + entry = spawnStruct(); + entry.command = command; + entry.description = description; + level.tws_help_commands[level.tws_help_commands.size] = entry; +} + +// Listen at level scope so newcomers can ask for help before their first spawn. +help_chat_listener() +{ + for (;;) + { + level waittill("say", message, player); + + if (!isDefined(player) || !isDefined(message)) + continue; + + if (toLower(message) == ".help") + player show_help(); + } +} + +show_help() +{ + // Cache the text per connection; repeated SetText calls consume config strings. + if (!isDefined(self.tws_help_elems)) + { + elems = []; + // Use the same anchors as the HUD, with room below the health numbers. + y = level.hud_row_bar + level.hud_bar_height + 20; + title = make_text_elem(self, level.hud_x, y, + "left", level.hud_font_mid, level.hud_c_afk); + title SetText("Commands"); + elems[elems.size] = title; + + for (i = 0; i < level.tws_help_commands.size; i++) + { + entry = level.tws_help_commands[i]; + elem = make_text_elem(self, level.hud_x, y + 22 + i * 18, + "left", level.hud_font_small, level.hud_c_count); + elem SetText("^3" + entry.command + "^7 - " + entry.description); + elems[elems.size] = elem; + } + + self.tws_help_elems = elems; + self thread destroy_on_disconnect(elems); + } + + for (i = 0; i < self.tws_help_elems.size; i++) + { + elem = self.tws_help_elems[i]; + elem.alpha = 1; + } + + // Only the expiry timer is replaced; the say listener and cleanup stay alive. + self notify("tws_help_refresh"); + self thread hide_help_after_delay(); +} + +hide_help_after_delay() +{ + self endon("disconnect"); + self endon("tws_help_refresh"); + + wait 12; + + for (i = 0; i < self.tws_help_elems.size; i++) + { + elem = self.tws_help_elems[i]; + elem.alpha = 0; + } +} + // only connect listener in the fleet; a second one crashes the T5 SP handshake on_player_connect_dispatch() { diff --git a/t5/tws_join.gsc b/t5/tws_join.gsc index 9ef6fe4..815cd5e 100644 --- a/t5/tws_join.gsc +++ b/t5/tws_join.gsc @@ -9,6 +9,7 @@ init() return; scripts\sp\tws_common::ensure_config(); + scripts\sp\tws_common::register_help_command(".join", "Join mid-round (newcomers only)"); scripts\sp\tws_common::register_spawn_handler(::join_spawn); level thread join_chat_listener(); diff --git a/t6/NOTES.md b/t6/NOTES.md index 10c7279..3d1b75e 100644 --- a/t6/NOTES.md +++ b/t6/NOTES.md @@ -23,6 +23,16 @@ HUD again owns the `hud_game_over` notification fan-out. Lock again waits on HUD notifications, applies T5's random password, and clears it on startup/game over. The added password-preservation dvars and independent lock watcher are gone. +Common also listens for `.help` through the native `say` notification, independent +of first-spawn initialization so newcomers can view commands while spectating. +Each loaded command module registers its command and description with +`register_help_command()` during `init()`. Lazy registry initialization preserves +registrations regardless of module initialization order. Help uses client HUD +elements below the health bar, sharing the HUD's anchors to avoid overlap with +timers, zombie count and health numbers. Text is created once per connection and +reused; each request restarts a 12-second visibility timer. Disconnect cleanup +is independent of that timer. The command handlers retain their existing listeners. + AFK again listens to the native `say` notification, so it no longer needs plugin chat registration. The additional T6 controls are only held while AFK; `.back` releases them before the protection grace period. Restoration is deferred during @@ -82,9 +92,10 @@ at link time. The other four TWS modules can run without telemetry. ## Checks -Local: all five files pass gsc-tool 1.4.10 T6 PC compilation; shared references and -entry points are checked by `tools/check.ps1`. No live server was run for this -baseline simplification. Compiler success does not certify runtime linking. +Local: `tools/check.ps1` compiles all six files for T6 PC and checks shared +references and entry points. It requires PowerShell and gsc-tool. The `.help` +change passed the static entry-point/reference checks, but compilation and live +gameplay were not run. Compiler success does not certify runtime linking. Live checks after deploying: @@ -97,3 +108,12 @@ Live checks after deploying: 4. Telemetry sends the initial occupied round and each new round to the existing endpoint. Check server ID/player count, same-round joins and an unreachable API. 5. Repeat on the actual server maps, especially afterlife and special-enemy rounds. +6. Type `.help` and `.HELP` before first spawn, during play and while AFK: see + `.help`, `.afk`, `.back` and `.join` with descriptions, only on the requesting + player's screen below the health bar. Take damage so health numbers appear + and verify they do not overlap help. Repeat the command before 12 seconds + elapse: one list should remain visible until 12 seconds after the latest + request. Reopen after expiry and reconnect; verify no duplicates or leftovers. + Remove the AFK or Join module, restart the map and confirm its commands are + absent. Restore the modules and confirm `.afk`, `.back` and `.join` still + behave as before. diff --git a/t6/README.md b/t6/README.md index 77e6495..ed3b641 100644 --- a/t6/README.md +++ b/t6/README.md @@ -5,7 +5,7 @@ just like T5. There is no separate TWS config file or database setup. | File | Role | Dependencies | | --- | --- | --- | -| `tws_t6_common.gsc` | Shared HUD values, helpers and first-spawn dispatcher | Stock T6 | +| `tws_t6_common.gsc` | Shared HUD values, helpers, `.help` and first-spawn dispatcher | Stock T6 | | `tws_t6_hud.gsc` | Match/round timers, name, zombie count and health bar | Common | | `tws_t6_afk.gsc` | `.afk` / `.back`, protection, movement and weapon blocking | Common | | `tws_t6_lock.gsc` | Lock at round 20; clear password on game over | Common, HUD | @@ -31,6 +31,26 @@ Keep the filenames and `scripts/zm` location so shared references resolve. To omit telemetry, remove its script; disabling it at runtime does not remove its plugin dependency during linking. +## Chat commands + +Type `.help` in chat to see the commands supplied by the loaded TWS modules and +a short description of each. The list is shown only to the requesting player, +including spectators who have not spawned. Commands are case-insensitive. +Help appears below the health bar for 12 seconds, clear of the timers, zombie +counter and health display. Typing `.help` again restarts that display time. + +| Command | Description | +| --- | --- | +| `.help` | List available commands | +| `.afk` | Pause with protection | +| `.back` | Return from AFK | +| `.join` | Join mid-round (newcomers only) | + +AFK and Join register their commands during `init()` through +`tws_t6_common::register_help_command(command, description)`. If a module is +omitted, its commands are omitted from `.help`. Register new commands alongside +their module's initialization so the list stays current. + ## Defaults, matching T5 - HUD values live in `tws_t6_common::common_config()`. That function and @@ -95,7 +115,7 @@ another mod installed one first. ## Validation -All six scripts compile for T6 PC with gsc-tool 1.4.10. To repeat: +To compile all six scripts for T6 PC with gsc-tool 1.4.10: ```powershell .\tools\check.ps1 -CompilerPath C:\path\to\gsc-tool.exe diff --git a/t6/tws_t6_afk.gsc b/t6/tws_t6_afk.gsc index 6574abd..44a946d 100644 --- a/t6/tws_t6_afk.gsc +++ b/t6/tws_t6_afk.gsc @@ -10,6 +10,8 @@ main() init() { scripts\zm\tws_t6_common::ensure_config(); + scripts\zm\tws_t6_common::register_help_command(".afk", "Pause with protection"); + scripts\zm\tws_t6_common::register_help_command(".back", "Return from AFK"); afk_init(); scripts\zm\tws_t6_common::register_spawn_handler(::afk_spawn); } diff --git a/t6/tws_t6_common.gsc b/t6/tws_t6_common.gsc index e1f4b25..162e58e 100644 --- a/t6/tws_t6_common.gsc +++ b/t6/tws_t6_common.gsc @@ -1,4 +1,4 @@ -/* tws_t6_common.gsc — shared config, HUD helpers, spawn-handler registry, connect dispatcher. */ +/* tws_t6_common.gsc — shared config, HUD helpers, help and spawn-handler registries. */ #include maps\mp\_utility; #include common_scripts\utility; @@ -11,7 +11,9 @@ main() init() { common_config(); + register_help_command(".help", "List available commands"); + level thread help_chat_listener(); level thread on_player_connect_dispatch(); } @@ -74,6 +76,83 @@ register_spawn_handler(fn) level.tws_spawn_handlers[level.tws_spawn_handlers.size] = fn; } +// Modules register their own commands, regardless of init order. +register_help_command(command, description) +{ + if (!isDefined(level.tws_help_commands)) + level.tws_help_commands = []; + + entry = spawnStruct(); + entry.command = command; + entry.description = description; + level.tws_help_commands[level.tws_help_commands.size] = entry; +} + +help_chat_listener() +{ + for (;;) + { + level waittill("say", message, player); + + if (!isDefined(player) || !isDefined(message)) + continue; + + if (toLower(message) == ".help") + player show_help(); + } +} + +show_help() +{ + // Cache the text per connection; repeated SetText calls consume config strings. + if (!isDefined(self.tws_help_elems)) + { + elems = []; + // Use the same anchors as the HUD, with room below the health numbers. + y = level.hud_row_bar + level.hud_bar_height + 20; + title = make_text_elem(self, level.hud_x, y, + "left", level.hud_font_mid, level.hud_c_afk); + title SetText("Commands"); + elems[elems.size] = title; + + for (i = 0; i < level.tws_help_commands.size; i++) + { + entry = level.tws_help_commands[i]; + elem = make_text_elem(self, level.hud_x, y + 22 + i * 18, + "left", level.hud_font_small, level.hud_c_count); + elem SetText("^3" + entry.command + "^7 - " + entry.description); + elems[elems.size] = elem; + } + + self.tws_help_elems = elems; + self thread destroy_on_disconnect(elems); + } + + for (i = 0; i < self.tws_help_elems.size; i++) + { + elem = self.tws_help_elems[i]; + elem.alpha = 1; + } + + // Only the expiry timer is replaced; the say listener and cleanup stay alive. + self notify("tws_help_refresh"); + self thread hide_help_after_delay(); +} + +hide_help_after_delay() +{ + self endon("disconnect"); + self endon("tws_help_refresh"); + + wait 12; + + for (i = 0; i < self.tws_help_elems.size; i++) + { + elem = self.tws_help_elems[i]; + elem.alpha = 0; + } +} + // Shared dispatcher: one HUD instance and account load per connection. on_player_connect_dispatch() { diff --git a/t6/tws_t6_join.gsc b/t6/tws_t6_join.gsc index 218e565..d164db1 100644 --- a/t6/tws_t6_join.gsc +++ b/t6/tws_t6_join.gsc @@ -14,6 +14,7 @@ init() println("[tws_t6_join] init reached"); scripts\zm\tws_t6_common::ensure_config(); + scripts\zm\tws_t6_common::register_help_command(".join", "Join mid-round (newcomers only)"); scripts\zm\tws_t6_common::register_spawn_handler(::join_spawn); level thread join_chat_listener();