Add .leaders command to replay map records in T5 and T6
This commit is contained in:
@@ -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.
|
**along with the updated common module** for that game.
|
||||||
The new module privately announces the current map's fleet-wide highest recorded
|
The new module privately announces the current map's fleet-wide highest recorded
|
||||||
round and every tied holder once per connection, including spectator newcomers.
|
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;
|
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
|
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.
|
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
|
and telemetry still work. Open `.help` during a record page and check that the
|
||||||
record resumes afterwards without overlapping it. Runtime linking and client
|
record resumes afterwards without overlapping it. Runtime linking and client
|
||||||
readiness require Plutonium.
|
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)
|
## Telemetry authentication (T5 and T6)
|
||||||
|
|
||||||
|
|||||||
@@ -60,9 +60,13 @@ counter and health display. Typing either command again restarts that display ti
|
|||||||
| `.afk` | Pause with protection |
|
| `.afk` | Pause with protection |
|
||||||
| `.back` | Return from AFK |
|
| `.back` | Return from AFK |
|
||||||
| `.join` | Join mid-round (newcomers only) |
|
| `.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
|
Each module registers its own help entries; removing AFK or join also removes
|
||||||
those entries from `.help` after a map restart.
|
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
|
## Dependency graph
|
||||||
|
|
||||||
|
|||||||
@@ -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 maps\_utility;
|
||||||
#include common_scripts\utility;
|
#include common_scripts\utility;
|
||||||
@@ -24,7 +24,9 @@ init()
|
|||||||
level.tws_lb_pending = undefined;
|
level.tws_lb_pending = undefined;
|
||||||
level.tws_lb_cache = undefined;
|
level.tws_lb_cache = undefined;
|
||||||
level.tws_lb_attempt = 0;
|
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);
|
scripts\sp\tws_t5_common::register_connect_handler(::lb_welcome);
|
||||||
|
level thread lb_chat_listener();
|
||||||
level thread lb_worker();
|
level thread lb_worker();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,6 +67,7 @@ lb_worker()
|
|||||||
lb_welcome()
|
lb_welcome()
|
||||||
{
|
{
|
||||||
self endon("disconnect");
|
self endon("disconnect");
|
||||||
|
self endon("tws_lb_requested");
|
||||||
level endon("end_game");
|
level endon("end_game");
|
||||||
level endon("game_ended");
|
level endon("game_ended");
|
||||||
level endon("intermission");
|
level endon("intermission");
|
||||||
@@ -84,18 +87,62 @@ lb_welcome()
|
|||||||
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;
|
||||||
|
|
||||||
|
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))
|
if (!isDefined(level.tws_lb_cache))
|
||||||
return;
|
return undefined;
|
||||||
snapshot = level.tws_lb_cache;
|
snapshot = level.tws_lb_cache;
|
||||||
// Do not advertise an indefinitely stale record during an outage.
|
// Do not advertise an indefinitely stale record during an outage.
|
||||||
if (getTime() - snapshot.saved_at > 900000)
|
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);
|
self lb_show_hud(snapshot);
|
||||||
}
|
}
|
||||||
|
|
||||||
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("disconnect");
|
||||||
self endon("tws_lb_hud_done");
|
self endon("tws_lb_hud_done");
|
||||||
|
|
||||||
@@ -121,8 +168,9 @@ lb_show_hud(snapshot)
|
|||||||
elems[elems.size] = row;
|
elems[elems.size] = row;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup runs independently of the welcome thread's endon handlers.
|
// Cleanup runs independently of the display and ends when it is replaced.
|
||||||
self thread scripts\sp\tws_t5_common::destroy_on_disconnect(elems);
|
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, "end_game");
|
||||||
self thread lb_hud_end_game(elems, "game_ended");
|
self thread lb_hud_end_game(elems, "game_ended");
|
||||||
self thread lb_hud_end_game(elems, "intermission");
|
self thread lb_hud_end_game(elems, "intermission");
|
||||||
@@ -156,6 +204,7 @@ lb_show_hud(snapshot)
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
lb_hud_destroy(elems);
|
lb_hud_destroy(elems);
|
||||||
|
self.tws_lb_elems = undefined;
|
||||||
self notify("tws_lb_hud_done");
|
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;
|
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)
|
lb_hud_end_game(elems, event)
|
||||||
{
|
{
|
||||||
self endon("disconnect");
|
self endon("disconnect");
|
||||||
self endon("tws_lb_hud_done");
|
self endon("tws_lb_hud_done");
|
||||||
level waittill(event);
|
level waittill(event);
|
||||||
lb_hud_destroy(elems);
|
lb_hud_destroy(elems);
|
||||||
|
self.tws_lb_elems = undefined;
|
||||||
self notify("tws_lb_hud_done");
|
self notify("tws_lb_hud_done");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -72,11 +72,15 @@ counter and health display. Typing either command again restarts that display ti
|
|||||||
| `.afk` | Pause with protection |
|
| `.afk` | Pause with protection |
|
||||||
| `.back` | Return from AFK |
|
| `.back` | Return from AFK |
|
||||||
| `.join` | Join mid-round (newcomers only) |
|
| `.join` | Join mid-round (newcomers only) |
|
||||||
|
| `.leaders` | Show the map record again (leaderboard module required) |
|
||||||
|
|
||||||
AFK and Join register their commands during `init()` through
|
AFK and Join register their commands during `init()` through
|
||||||
`tws_t6_common::register_help_command(command, description)`. If a module is
|
`tws_t6_common::register_help_command(command, description)`. If a module is
|
||||||
omitted, its commands are omitted from `.help`. Register new commands alongside
|
omitted, its commands are omitted from `.help`. Register new commands alongside
|
||||||
their module's initialization so the list stays current.
|
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
|
## Defaults, matching T5
|
||||||
|
|
||||||
|
|||||||
@@ -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 maps\mp\_utility;
|
||||||
#include common_scripts\utility;
|
#include common_scripts\utility;
|
||||||
@@ -26,7 +26,9 @@ init()
|
|||||||
level.tws_lb_pending = undefined;
|
level.tws_lb_pending = undefined;
|
||||||
level.tws_lb_cache = undefined;
|
level.tws_lb_cache = undefined;
|
||||||
level.tws_lb_attempt = 0;
|
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);
|
scripts\zm\tws_t6_common::register_connect_handler(::lb_welcome);
|
||||||
|
level thread lb_chat_listener();
|
||||||
level thread lb_worker();
|
level thread lb_worker();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,6 +69,7 @@ lb_worker()
|
|||||||
lb_welcome()
|
lb_welcome()
|
||||||
{
|
{
|
||||||
self endon("disconnect");
|
self endon("disconnect");
|
||||||
|
self endon("tws_lb_requested");
|
||||||
level endon("end_game");
|
level endon("end_game");
|
||||||
level endon("game_ended");
|
level endon("game_ended");
|
||||||
level endon("intermission");
|
level endon("intermission");
|
||||||
@@ -87,18 +90,62 @@ lb_welcome()
|
|||||||
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;
|
||||||
|
|
||||||
|
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))
|
if (!isDefined(level.tws_lb_cache))
|
||||||
return;
|
return undefined;
|
||||||
snapshot = level.tws_lb_cache;
|
snapshot = level.tws_lb_cache;
|
||||||
// Do not advertise an indefinitely stale record during an outage.
|
// Do not advertise an indefinitely stale record during an outage.
|
||||||
if (getTime() - snapshot.saved_at > 900000)
|
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);
|
self lb_show_hud(snapshot);
|
||||||
}
|
}
|
||||||
|
|
||||||
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("disconnect");
|
||||||
self endon("tws_lb_hud_done");
|
self endon("tws_lb_hud_done");
|
||||||
|
|
||||||
@@ -124,8 +171,9 @@ lb_show_hud(snapshot)
|
|||||||
elems[elems.size] = row;
|
elems[elems.size] = row;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup runs independently of the welcome thread's endon handlers.
|
// Cleanup runs independently of the display and ends when it is replaced.
|
||||||
self thread scripts\zm\tws_t6_common::destroy_on_disconnect(elems);
|
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, "end_game");
|
||||||
self thread lb_hud_end_game(elems, "game_ended");
|
self thread lb_hud_end_game(elems, "game_ended");
|
||||||
self thread lb_hud_end_game(elems, "intermission");
|
self thread lb_hud_end_game(elems, "intermission");
|
||||||
@@ -159,6 +207,7 @@ lb_show_hud(snapshot)
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
lb_hud_destroy(elems);
|
lb_hud_destroy(elems);
|
||||||
|
self.tws_lb_elems = undefined;
|
||||||
self notify("tws_lb_hud_done");
|
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;
|
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)
|
lb_hud_end_game(elems, event)
|
||||||
{
|
{
|
||||||
self endon("disconnect");
|
self endon("disconnect");
|
||||||
self endon("tws_lb_hud_done");
|
self endon("tws_lb_hud_done");
|
||||||
level waittill(event);
|
level waittill(event);
|
||||||
lb_hud_destroy(elems);
|
lb_hud_destroy(elems);
|
||||||
|
self.tws_lb_elems = undefined;
|
||||||
self notify("tws_lb_hud_done");
|
self notify("tws_lb_hud_done");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ function functions(source) {
|
|||||||
function engine(game) {
|
function engine(game) {
|
||||||
const source = readFileSync(new URL(`${game}/tws_${game}_leaderboard.gsc`, root), 'utf8');
|
const source = readFileSync(new URL(`${game}/tws_${game}_leaderboard.gsc`, root), 'utf8');
|
||||||
const all = functions(source);
|
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 = [];
|
const table = [];
|
||||||
for (let i = 1; i < 256; i++) table[String.fromCharCode(i)] = i;
|
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' };
|
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({
|
const context = vm.createContext({
|
||||||
level: { tws_lb_game: game, tws_lb_override: '', tws_lb_bytes: table },
|
level: { tws_lb_game: game, tws_lb_override: '', tws_lb_bytes: table },
|
||||||
GetDvar: (key) => dvars[key] ?? '',
|
GetDvar: (key) => dvars[key] ?? '',
|
||||||
|
getTime: () => 1000000,
|
||||||
spawnStruct: () => ({}),
|
spawnStruct: () => ({}),
|
||||||
isDefined: (value) => value !== undefined && value !== null,
|
isDefined: (value) => value !== undefined && value !== null,
|
||||||
gscType: (value) => value === undefined || value === null ? 'undefined' : typeof value === 'object' ? 'array'
|
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'));
|
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} 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`, () => {
|
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(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é 🧟');
|
assert.equal(text(c.lb_display_text(bytes('^1Alice\n^7 René 🧟'), 64)), 'Alice René 🧟');
|
||||||
|
|||||||
Reference in New Issue
Block a user