Show personal map records on join and replay

This commit is contained in:
2026-10-19 14:11:54 -04:00
parent 9ef5932d36
commit e93182cd02
11 changed files with 620 additions and 200 deletions
+116 -26
View File
@@ -1,4 +1,4 @@
/* tws_t6_leaderboard.gsc - private map records, round reminders and .leaders replay. */
/* tws_t6_leaderboard.gsc - private map/personal records, round reminders and .leaders replay. */
#include maps\mp\_utility;
#include common_scripts\utility;
@@ -27,7 +27,7 @@ init()
level.tws_lb_cache = undefined;
level.tws_lb_attempt = 0;
level.tws_lb_chase_finished = undefined;
scripts\zm\tws_t6_common::register_help_command(".leaders", "Show the map record again");
scripts\zm\tws_t6_common::register_help_command(".leaders", "Show map and personal records");
scripts\zm\tws_t6_common::register_connect_handler(::lb_welcome);
level thread lb_chat_listener();
level thread lb_worker();
@@ -215,14 +215,113 @@ lb_request(manual)
// This also applies when the cache is unavailable.
if (manual)
self notify("tws_lb_requested");
self endon("tws_lb_requested");
self.tws_lb_last_request = getTime();
snapshot = lb_cached_record();
if (!isDefined(snapshot) && !manual)
return;
// Common owns pagination, help priority and the single service window.
personal = lb_personal_record(self, snapshot);
self.tws_lb_last_request = getTime();
// Common owns display lifetime, help priority and the single service window.
// Undefined snapshots produce a private unavailable page for manual replay.
self scripts\zm\tws_t6_common::show_record(snapshot);
self scripts\zm\tws_t6_common::show_record(snapshot, personal);
}
// Cache per connection; a failed lookup is unavailable, never an empty history.
lb_personal_record(player, snapshot)
{
if (!isDefined(snapshot))
return "Your Best | Unavailable";
if (snapshot.round == 0)
return "Your Best | None yet";
guid = lb_player_guid(player);
if (!isDefined(guid))
return "Your Best | Unavailable";
cached = player.tws_lb_personal;
if (isDefined(cached) && cached.key == snapshot.key && cached.guid == guid &&
getTime() - cached.saved_at < cached.ttl)
return cached.text;
round = lb_personal_fetch(snapshot.key, guid);
cached = spawnStruct();
cached.key = snapshot.key;
cached.guid = guid;
cached.saved_at = getTime();
cached.ttl = 30000;
cached.text = "Your Best | Unavailable";
if (isDefined(round))
{
cached.ttl = 300000;
cached.text = "Your Best | None yet";
if (round > 0)
cached.text = "Your Best | Round " + round;
}
player.tws_lb_personal = cached;
return cached.text;
}
lb_player_guid(player)
{
guid = player.guid;
if (!isDefined(guid) || (typeof(guid) != "string" && typeof(guid) != "int"))
return undefined;
guid = "" + guid;
if (guid.size == 0 || guid.size > 64)
return undefined;
return guid;
}
// Search also matches names and GUID substrings. Only exact string identity
// with matching game/map evidence can supply this player's personal best.
lb_personal_fetch(key, guid)
{
offset = 0;
total = -1;
highest = -1;
started = getTime();
for (;;)
{
// Bound broad substring searches; incomplete searches are unavailable.
if (getTime() - started >= 15000)
return undefined;
page = lb_get("&map=" + lb_url_encode(key) + "&q=" + lb_url_encode(guid) + "&limit=3&offset=" + offset);
if (!isDefined(page) || !lb_page_valid(page, offset, 3))
return undefined;
if (offset == 0)
{
total = page["total"];
highest = page["summary"]["highestRound"];
}
if (page["total"] != total || page["summary"]["highestRound"] != highest ||
offset + page["entries"].size > total)
return undefined;
for (i = 0; i < page["entries"].size; i++)
{
entry = page["entries"][i];
if (typeof(entry) != "array" || typeof(entry["guid"]) != "string")
return undefined;
if (entry["guid"] != guid)
continue;
if (!lb_uint(entry["highestRound"]) || entry["highestRound"] < 2 ||
entry["highestRound"] > highest || typeof(entry["records"]) != "array")
return undefined;
evidence = entry["records"]["highestRound"];
if (typeof(evidence) != "array" || typeof(evidence["game"]) != "string" ||
typeof(evidence["mapKey"]) != "string" || !lb_uint(evidence["value"]))
return undefined;
if (evidence["game"] != level.tws_lb_game || evidence["mapKey"] != key ||
evidence["value"] != entry["highestRound"])
return undefined;
return entry["highestRound"];
}
offset += page["entries"].size;
if (offset == total)
return 0;
if (page["entries"].size == 0 || offset > 1000000)
return undefined;
wait 0.05;
}
}
lb_refresh()
@@ -242,6 +341,7 @@ lb_refresh()
return undefined;
snapshot = spawnStruct();
snapshot.key = selection.key;
snapshot.name = lb_display_text(selection.name, 64);
snapshot.round = 0;
snapshot.names = [];
@@ -336,9 +436,18 @@ lb_t6_selection(selection, mode, location)
return undefined;
}
// All HTTP and parsing work belongs to the level worker, never the join thread.
// Map refreshes and personal lookups share one native request slot.
lb_get(query)
{
// A disconnect/replay can abandon a caller, but the collector still drains
// its native request. Bound queue wait and never overlap that request.
queued_at = getTime();
while (isDefined(level.tws_lb_pending))
{
if (getTime() - queued_at >= 15000)
return undefined;
wait 0.1;
}
options = [];
options["method"] = "GET";
headers = [];
@@ -510,28 +619,9 @@ lb_uint(value)
lb_holder_lines(names)
{
lines = [];
line = "";
columns = 0;
// Each tied player gets a separate row in the full record announcement.
for (i = 0; i < names.size; i++)
{
name = lb_holder_name(names[i]);
width = lb_holder_width(name);
if (line != "" && columns + width + 2 > 16)
{
lines[lines.size] = line;
line = "";
columns = 0;
}
if (line != "")
{
line += ", ";
columns += 2;
}
line += name;
columns += width;
}
if (line != "")
lines[lines.size] = line;
lines[lines.size] = lb_holder_name(names[i]);
return lines;
}