Add .leaders command to replay map records in T5 and T6

This commit is contained in:
2026-10-07 22:49:55 -04:00
parent dec51a7032
commit 58399d2191
6 changed files with 156 additions and 11 deletions
+8
View File
@@ -29,6 +29,10 @@ Install `tws_t5_leaderboard.gsc` for T5 or `tws_t6_leaderboard.gsc` for T6,
**along with the updated common module** for that game.
The new module privately announces the current map's fleet-wide highest recorded
round and every tied holder once per connection, including spectator newcomers.
Type `.leaders` to replay the record privately from the latest usable cache.
Repeating the command restarts the announcement from its first page. It appears
in `.help` when the leaderboard module is enabled. If no usable record is
available, the command asks you to try again shortly.
It reads Electric Cherry's public `/api/leaderboard` endpoint without a token;
only completed matches contribute. Ties may come from the same run or separate
runs. Names are the API's latest player names, with players identified by GUID.
@@ -83,6 +87,10 @@ mid-announcement and confirm messages stop. Verify existing HUD, `.help`, `.join
and telemetry still work. Open `.help` during a record page and check that the
record resumes afterwards without overlapping it. Runtime linking and client
readiness require Plutonium.
Try `.leaders` and `.LEADERS` after the announcement ends and during a later
page: each should start one private announcement from the first page. Repeat
while help is open, then disconnect or end the game during a replay to check
cleanup. Disable the leaderboard module and verify `.leaders` is absent from help.
## Telemetry authentication (T5 and T6)
+4
View File
@@ -60,9 +60,13 @@ counter and health display. Typing either command again restarts that display ti
| `.afk` | Pause with protection |
| `.back` | Return from AFK |
| `.join` | Join mid-round (newcomers only) |
| `.leaders` | Show the map record again (leaderboard module required) |
Each module registers its own help entries; removing AFK or join also removes
those entries from `.help` after a map restart.
The enabled leaderboard module registers `.leaders`, which replays the latest
cached map record privately. Repeating it restarts the record from the first page;
help temporarily hides the record until its own display ends.
## Dependency graph
+62 -5
View File
@@ -1,4 +1,4 @@
/* tws_t5_leaderboard.gsc - private map record announcement, once per connection. */
/* tws_t5_leaderboard.gsc - private map record announcements and .leaders replay. */
#include maps\_utility;
#include common_scripts\utility;
@@ -24,7 +24,9 @@ init()
level.tws_lb_pending = undefined;
level.tws_lb_cache = undefined;
level.tws_lb_attempt = 0;
scripts\sp\tws_t5_common::register_help_command(".leaders", "Show the map record again");
scripts\sp\tws_t5_common::register_connect_handler(::lb_welcome);
level thread lb_chat_listener();
level thread lb_worker();
}
@@ -65,6 +67,7 @@ lb_worker()
lb_welcome()
{
self endon("disconnect");
self endon("tws_lb_requested");
level endon("end_game");
level endon("game_ended");
level endon("intermission");
@@ -84,18 +87,62 @@ lb_welcome()
while (!isDefined(level.tws_lb_cache) && level.tws_lb_attempt == 0)
wait 0.25;
self lb_request(false);
}
lb_chat_listener()
{
level endon("end_game");
level endon("game_ended");
level endon("intermission");
for (;;)
{
level waittill("say", message, player);
if (!isDefined(player) || !isDefined(message))
continue;
if (toLower(message) == ".leaders")
player thread lb_request(true);
}
}
lb_cached_record()
{
if (!isDefined(level.tws_lb_cache))
return;
return undefined;
snapshot = level.tws_lb_cache;
// Do not advertise an indefinitely stale record during an outage.
if (getTime() - snapshot.saved_at > 900000)
return;
return undefined;
return snapshot;
}
lb_request(manual)
{
self endon("disconnect");
level endon("end_game");
level endon("game_ended");
level endon("intermission");
snapshot = lb_cached_record();
if (!isDefined(snapshot))
{
if (manual)
self iPrintLnBold("^3Map record unavailable; try .leaders again shortly");
return;
}
// A manual replay replaces any welcome announcement still waiting to run.
if (manual)
self notify("tws_lb_requested");
self lb_show_hud(snapshot);
}
lb_show_hud(snapshot)
{
// Stop the previous display and its cleanup threads before replacing it.
self notify("tws_lb_hud_done");
if (isDefined(self.tws_lb_elems))
lb_hud_destroy(self.tws_lb_elems);
self endon("disconnect");
self endon("tws_lb_hud_done");
@@ -121,8 +168,9 @@ lb_show_hud(snapshot)
elems[elems.size] = row;
}
// Cleanup runs independently of the welcome thread's endon handlers.
self thread scripts\sp\tws_t5_common::destroy_on_disconnect(elems);
// Cleanup runs independently of the display and ends when it is replaced.
self.tws_lb_elems = elems;
self thread lb_hud_disconnect(elems);
self thread lb_hud_end_game(elems, "end_game");
self thread lb_hud_end_game(elems, "game_ended");
self thread lb_hud_end_game(elems, "intermission");
@@ -156,6 +204,7 @@ lb_show_hud(snapshot)
break;
}
lb_hud_destroy(elems);
self.tws_lb_elems = undefined;
self notify("tws_lb_hud_done");
}
@@ -165,12 +214,20 @@ lb_hud_help_visible()
isDefined(self.tws_help_elems[0]) && self.tws_help_elems[0].alpha > 0;
}
lb_hud_disconnect(elems)
{
self endon("tws_lb_hud_done");
self waittill("disconnect");
lb_hud_destroy(elems);
}
lb_hud_end_game(elems, event)
{
self endon("disconnect");
self endon("tws_lb_hud_done");
level waittill(event);
lb_hud_destroy(elems);
self.tws_lb_elems = undefined;
self notify("tws_lb_hud_done");
}
+4
View File
@@ -72,11 +72,15 @@ counter and health display. Typing either command again restarts that display ti
| `.afk` | Pause with protection |
| `.back` | Return from AFK |
| `.join` | Join mid-round (newcomers only) |
| `.leaders` | Show the map record again (leaderboard module required) |
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.
The enabled leaderboard module registers `.leaders`, which replays the latest
cached map record privately. Repeating it restarts the record from the first page;
help temporarily hides the record until its own display ends.
## Defaults, matching T5
+62 -5
View File
@@ -1,4 +1,4 @@
/* tws_t6_leaderboard.gsc - private map record announcement, once per connection. */
/* tws_t6_leaderboard.gsc - private map record announcements and .leaders replay. */
#include maps\mp\_utility;
#include common_scripts\utility;
@@ -26,7 +26,9 @@ init()
level.tws_lb_pending = undefined;
level.tws_lb_cache = undefined;
level.tws_lb_attempt = 0;
scripts\zm\tws_t6_common::register_help_command(".leaders", "Show the map record again");
scripts\zm\tws_t6_common::register_connect_handler(::lb_welcome);
level thread lb_chat_listener();
level thread lb_worker();
}
@@ -67,6 +69,7 @@ lb_worker()
lb_welcome()
{
self endon("disconnect");
self endon("tws_lb_requested");
level endon("end_game");
level endon("game_ended");
level endon("intermission");
@@ -87,18 +90,62 @@ lb_welcome()
while (!isDefined(level.tws_lb_cache) && level.tws_lb_attempt == 0)
wait 0.25;
self lb_request(false);
}
lb_chat_listener()
{
level endon("end_game");
level endon("game_ended");
level endon("intermission");
for (;;)
{
level waittill("say", message, player);
if (!isDefined(player) || !isDefined(message))
continue;
if (toLower(message) == ".leaders")
player thread lb_request(true);
}
}
lb_cached_record()
{
if (!isDefined(level.tws_lb_cache))
return;
return undefined;
snapshot = level.tws_lb_cache;
// Do not advertise an indefinitely stale record during an outage.
if (getTime() - snapshot.saved_at > 900000)
return;
return undefined;
return snapshot;
}
lb_request(manual)
{
self endon("disconnect");
level endon("end_game");
level endon("game_ended");
level endon("intermission");
snapshot = lb_cached_record();
if (!isDefined(snapshot))
{
if (manual)
self iPrintLnBold("^3Map record unavailable; try .leaders again shortly");
return;
}
// A manual replay replaces any welcome announcement still waiting to run.
if (manual)
self notify("tws_lb_requested");
self lb_show_hud(snapshot);
}
lb_show_hud(snapshot)
{
// Stop the previous display and its cleanup threads before replacing it.
self notify("tws_lb_hud_done");
if (isDefined(self.tws_lb_elems))
lb_hud_destroy(self.tws_lb_elems);
self endon("disconnect");
self endon("tws_lb_hud_done");
@@ -124,8 +171,9 @@ lb_show_hud(snapshot)
elems[elems.size] = row;
}
// Cleanup runs independently of the welcome thread's endon handlers.
self thread scripts\zm\tws_t6_common::destroy_on_disconnect(elems);
// Cleanup runs independently of the display and ends when it is replaced.
self.tws_lb_elems = elems;
self thread lb_hud_disconnect(elems);
self thread lb_hud_end_game(elems, "end_game");
self thread lb_hud_end_game(elems, "game_ended");
self thread lb_hud_end_game(elems, "intermission");
@@ -159,6 +207,7 @@ lb_show_hud(snapshot)
break;
}
lb_hud_destroy(elems);
self.tws_lb_elems = undefined;
self notify("tws_lb_hud_done");
}
@@ -168,12 +217,20 @@ lb_hud_help_visible()
isDefined(self.tws_help_elems[0]) && self.tws_help_elems[0].alpha > 0;
}
lb_hud_disconnect(elems)
{
self endon("tws_lb_hud_done");
self waittill("disconnect");
lb_hud_destroy(elems);
}
lb_hud_end_game(elems, event)
{
self endon("disconnect");
self endon("tws_lb_hud_done");
level waittill(event);
lb_hud_destroy(elems);
self.tws_lb_elems = undefined;
self notify("tws_lb_hud_done");
}
+16 -1
View File
@@ -44,7 +44,7 @@ function functions(source) {
function engine(game) {
const source = readFileSync(new URL(`${game}/tws_${game}_leaderboard.gsc`, root), 'utf8');
const all = functions(source);
const excluded = new Set(['lb_worker', 'lb_welcome', 'lb_get', 'lb_collect', 'lb_log', 'lb_show_hud', 'lb_hud_help_visible', 'lb_hud_end_game', 'lb_hud_destroy']);
const excluded = new Set(['lb_worker', 'lb_welcome', 'lb_get', 'lb_collect', 'lb_log', 'lb_show_hud', 'lb_hud_help_visible', 'lb_hud_end_game', 'lb_hud_destroy', 'lb_chat_listener', 'lb_request', 'lb_hud_disconnect']);
const table = [];
for (let i = 1; i < 256; i++) table[String.fromCharCode(i)] = i;
const dvars = { mapname: game === 't5' ? 'zombie_theater' : 'zm_transit', ui_gametype: 'zstandard', ui_zm_mapstartlocation: 'town' };
@@ -52,6 +52,7 @@ function engine(game) {
const context = vm.createContext({
level: { tws_lb_game: game, tws_lb_override: '', tws_lb_bytes: table },
GetDvar: (key) => dvars[key] ?? '',
getTime: () => 1000000,
spawnStruct: () => ({}),
isDefined: (value) => value !== undefined && value !== null,
gscType: (value) => value === undefined || value === null ? 'undefined' : typeof value === 'object' ? 'array'
@@ -96,6 +97,20 @@ 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} record replay uses only a fresh complete cache`, () => {
assert.equal(c.lb_cached_record(), undefined);
const snapshot = { saved_at: 1000000, round: 52, lines: ['Alice'] };
c.level.tws_lb_cache = snapshot;
assert.equal(c.lb_cached_record(), snapshot);
snapshot.saved_at = 100000;
assert.equal(c.lb_cached_record(), snapshot);
snapshot.saved_at--;
assert.equal(c.lb_cached_record(), undefined);
c.level.tws_lb_cache = { saved_at: 1000000, round: 0, lines: [] };
assert.equal(c.lb_cached_record().round, 0);
c.level.tws_lb_cache = undefined;
assert.equal(queries.length, 0);
});
check(`${game} unicode and URL encoding`, () => {
assert.equal(c.lb_url_encode(bytes('t6:zm_transit:town /?&=#é')), 't6%3Azm_transit%3Atown%20%2F%3F%26%3D%23%C3%A9');
assert.equal(text(c.lb_display_text(bytes('^1Alice\n^7 René 🧟'), 64)), 'Alice René 🧟');