Organize scripts and add TWS plugins with HTTP timeouts

Vendor pinned T5 and T6 plugin sources under the new plugins layout. Add native HTTP deadlines, status and timing diagnostics, telemetry integration, and build/test tooling.
This commit is contained in:
2026-10-09 11:02:20 -04:00
parent b172086301
commit 1121b44857
253 changed files with 28028 additions and 59 deletions
+850
View File
@@ -0,0 +1,850 @@
/* tws_t6_leaderboard.gsc - private map record announcements and .leaders replay. */
#include maps\mp\_utility;
#include common_scripts\utility;
main()
{
}
init()
{
if (GetDvar("tws_leaderboard_enabled") == "0")
return;
level.tws_lb_game = "t6";
level.tws_lb_url = GetDvar("tws_leaderboard_url");
if (level.tws_lb_url == "")
level.tws_lb_url = "https://ec.thewrightserver.net/api/leaderboard";
if (getSubStr(toLower(level.tws_lb_url), 0, 8) != "https://")
{
lb_log("Disabled: tws_leaderboard_url must use HTTPS");
return;
}
level.tws_lb_override = GetDvar("tws_leaderboard_map_key");
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();
}
lb_worker()
{
level endon("end_game");
level endon("game_ended");
level endon("intermission");
// The same byte table supports UTF-8 validation and URL percent encoding.
level.tws_lb_bytes = [];
for (i = 1; i < 256; i++)
level.tws_lb_bytes[formatString("%c", i)] = i;
wait 1;
for (;;)
{
// A timed-out native request cannot be cancelled. Never overlap it.
while (isDefined(level.tws_lb_pending))
wait 1;
snapshot = lb_refresh();
level.tws_lb_attempt++;
if (isDefined(snapshot))
{
snapshot.saved_at = getTime();
level.tws_lb_cache = snapshot;
wait 300;
}
else
{
lb_log("Refresh failed; retaining the last complete record, if available");
wait 30;
}
}
}
lb_welcome()
{
self endon("disconnect");
self endon("tws_lb_requested");
level endon("end_game");
level endon("game_ended");
level endon("intermission");
if (isDefined(self.tws_lb_welcomed))
return;
self.tws_lb_welcomed = true;
// Spectator readiness matters too: do not wait for a playable spawn.
while (!isDefined(self.sessionstate) ||
(self.sessionstate != "playing" && self.sessionstate != "spectator"))
wait 0.25;
// T6 keeps hasSpawned false for newcomers in spectator mode.
while (isDefined(level.passed_introscreen) && !level.passed_introscreen)
wait 0.25;
wait 5;
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 undefined;
snapshot = level.tws_lb_cache;
// Do not advertise an indefinitely stale record during an outage.
if (getTime() - snapshot.saved_at > 900000)
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");
scripts\zm\tws_t6_common::ensure_config();
// Match .help's position, typography and row spacing below the health bar.
y = level.hud_row_bar + level.hud_bar_height + 20;
elems = [];
title = scripts\zm\tws_t6_common::make_text_elem(
self, level.hud_x, y, "left", level.hud_font_mid, level.hud_c_afk);
title.alpha = 0;
if (snapshot.round == 0)
title SetText("No recorded high round yet");
else
title SetText("Record: Round " + snapshot.round);
elems[elems.size] = title;
for (i = 0; i < 3; i++)
{
row = scripts\zm\tws_t6_common::make_text_elem(
self, level.hud_x, y + 22 + i * 18,
"left", level.hud_font_small, level.hud_c_count);
row.alpha = 0;
elems[elems.size] = row;
}
// 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");
offset = 0;
for (;;)
{
// Text changes only once per page, never during the visibility loop.
for (i = 0; i < 3; i++)
{
row = elems[i + 1];
if (offset + i < snapshot.lines.size)
row SetText("^3Held by: ^7" + snapshot.lines[offset + i]);
else
row SetText("");
}
remaining = 12;
while (remaining > 0)
{
visible = !self lb_hud_help_visible();
for (i = 0; i < elems.size; i++)
elems[i].alpha = visible;
wait 0.1;
// Pause the page while .help occupies the same area.
if (visible)
remaining -= 0.1;
}
offset += 3;
if (offset >= snapshot.lines.size)
break;
}
lb_hud_destroy(elems);
self.tws_lb_elems = undefined;
self notify("tws_lb_hud_done");
}
lb_hud_help_visible()
{
return isDefined(self.tws_help_elems) && self.tws_help_elems.size > 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)
{
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");
}
lb_hud_destroy(elems)
{
for (i = 0; i < elems.size; i++)
{
if (isDefined(elems[i]))
elems[i] Destroy();
}
}
lb_refresh()
{
selection = lb_selection();
if (!isDefined(selection))
{
lb_log("Cannot identify this map variant; set tws_leaderboard_map_key to its Electric Cherry key");
return undefined;
}
// Map keys are discovered and checked against the API's available maps.
discovery = lb_get("&limit=1&offset=0");
if (!isDefined(discovery) || !lb_page_valid(discovery, 0, 1))
return undefined;
if (!lb_resolve_map(selection, discovery["maps"]))
return undefined;
snapshot = spawnStruct();
snapshot.name = lb_display_text(selection.name, 64);
snapshot.round = 0;
snapshot.names = [];
snapshot.guids = [];
snapshot.lines = [];
snapshot.total = -1;
snapshot.last_round = 2147483647;
// No map option means no completed match has been recorded for this map.
if (!selection.found)
return snapshot;
offset = 0;
for (;;)
{
page = lb_get("&map=" + lb_url_encode(selection.key) + "&limit=3&offset=" + offset);
if (!isDefined(page) || !lb_page_valid(page, offset, 3))
return undefined;
result = lb_take_page(snapshot, page, selection.key, offset);
if (result < 0)
{
lb_log("Invalid or changing leaderboard; discarding incomplete refresh");
return undefined;
}
if (result == 1)
{
snapshot.lines = lb_holder_lines(snapshot.names);
return snapshot;
}
offset += page["entries"].size;
if (offset > 1000000)
{
lb_log("Leaderboard exceeds the API pagination limit");
return undefined;
}
wait 0.05;
}
}
lb_selection()
{
selection = spawnStruct();
selection.map = toLower(GetDvar("mapname"));
selection.name = selection.map;
selection.key = level.tws_lb_override;
selection.explicit = selection.key != "";
if (selection.explicit)
return selection;
mode = toLower(GetDvar("ui_gametype"));
location = toLower(GetDvar("ui_zm_mapstartlocation"));
if (isDefined(level.scr_zm_ui_gametype))
mode = toLower(level.scr_zm_ui_gametype);
if (isDefined(level.scr_zm_map_start_location))
location = toLower(level.scr_zm_map_start_location);
if ((location == "" || location == "default") && isDefined(level.default_start_location))
location = toLower(level.default_start_location);
return lb_t6_selection(selection, mode, location);
}
// Runtime mode/location -> Electric Cherry's registered variant catalogue.
// Never select the sole available zm_transit option by engine map alone.
lb_t6_selection(selection, mode, location)
{
rows = [];
rows[0] = "zm_transit|zclassic|transit|transit|TranZit";
rows[1] = "zm_transit|zstandard|town|town|Town Survival";
rows[2] = "zm_transit|zstandard|farm|farm|Farm Survival";
rows[3] = "zm_transit|zstandard|transit|busdepot|Bus Depot Survival";
rows[4] = "zm_nuked|zstandard|nuked|nuked|Nuketown";
rows[5] = "zm_highrise|zclassic|rooftop|highrise|Die Rise";
rows[6] = "zm_prison|zclassic|prison|prison|Mob of the Dead";
rows[7] = "zm_buried|zclassic|processing|buried|Buried";
rows[8] = "zm_tomb|zclassic|tomb|tomb|Origins";
rows[9] = "zm_transit|zgrief|town|towngrief|Town Grief";
rows[10] = "zm_transit|zgrief|farm|farmgrief|Farm Grief";
rows[11] = "zm_transit|zgrief|transit|busdepotgrief|Bus Depot Grief";
rows[12] = "zm_prison|zgrief|cellblock|cellblock|Cell Block Grief";
rows[13] = "zm_buried|zgrief|street|buriedgrief|Buried Grief";
rows[14] = "zm_transit_dr|zcleansed|diner|diner|Diner Turned";
rows[15] = "zm_buried|zcleansed|street|buriedturned|Buried Turned";
for (i = 0; i < rows.size; i++)
{
parts = strTok(rows[i], "|");
if (selection.map == parts[0] && mode == parts[1] && location == parts[2])
{
selection.key = "t6:" + selection.map + ":" + parts[3];
selection.name = parts[4];
return selection;
}
}
return undefined;
}
// All HTTP and parsing work belongs to the level worker, never the join thread.
lb_get(query)
{
options = [];
options["method"] = "GET";
headers = [];
headers["Accept"] = "application/json";
options["headers"] = headers;
url = level.tws_lb_url + "?game=" + level.tws_lb_game + "&metric=highestRound" + query;
// v3.1.1 accepts the GET method override despite the alias name.
request = httpPost(url, options);
if (!isDefined(request))
{
lb_log("HTTP request could not be created");
return undefined;
}
pending = spawnStruct();
pending.done = false;
level.tws_lb_pending = pending;
level thread lb_collect(request, pending);
started = getTime();
while (!pending.done && getTime() - started < 15000)
wait 0.1;
if (!pending.done)
{
lb_log("HTTP timed out; waiting for native completion before requesting again");
return undefined;
}
if (!isDefined(pending.success) || !pending.success)
{
lb_log("HTTP transport failed");
return undefined;
}
// Plugins expose neither HTTP status nor response headers. Reject error
// bodies by grammar and success-schema checks, not the transport flag.
if (!lb_json_valid(pending.body))
{
lb_log("Rejected malformed, oversized or non-JSON HTTP response");
return undefined;
}
return jsonParse(pending.body);
}
lb_collect(request, pending)
{
// Keep draining after game over; ending a GSC thread cannot cancel HTTP.
request waittill("done", body, success, error);
pending.body = body;
pending.success = success;
pending.done = true;
level.tws_lb_pending = undefined;
}
lb_log(message)
{
println("[tws_t6_leaderboard] " + message);
}
// Pure data helpers below are exercised by tools/check_leaderboard.mjs.
lb_resolve_map(selection, maps)
{
selection.found = false;
matches = 0;
// T5 requires initialization before the loop, even with matches == 1 below.
chosen = undefined;
for (i = 0; i < maps.size; i++)
{
item = maps[i];
if (typeof(item) != "array" || typeof(item["game"]) != "string" ||
typeof(item["map"]) != "string" || typeof(item["key"]) != "string" ||
typeof(item["name"]) != "string")
return false;
if (item["game"] != level.tws_lb_game || item["map"] != selection.map)
continue;
if (selection.key != "" && item["key"] != selection.key)
continue;
matches++;
chosen = item;
}
if (matches > 1 || (matches == 0 && selection.explicit))
{
lb_log("Map key is absent, ambiguous or belongs to another map/game");
return false;
}
if (matches == 1)
{
selection.key = chosen["key"];
selection.name = chosen["name"];
selection.found = true;
}
return true;
}
lb_page_valid(page, offset, limit)
{
if (typeof(page) != "array" || typeof(page["entries"]) != "array" ||
typeof(page["maps"]) != "array" || typeof(page["summary"]) != "array")
return false;
if (typeof(page["metric"]) != "string" || !lb_uint(page["offset"]) || !lb_uint(page["limit"]))
return false;
if (page["metric"] != "highestRound" || page["offset"] != offset || page["limit"] != limit)
return false;
return lb_uint(page["total"]) && lb_uint(page["summary"]["highestRound"]) &&
page["entries"].size <= limit;
}
// -1 rejects the whole refresh, 0 requests another page, 1 is complete.
lb_take_page(snapshot, page, key, offset)
{
entries = page["entries"];
if (offset == 0)
{
snapshot.round = page["summary"]["highestRound"];
snapshot.total = page["total"];
}
if (snapshot.round != page["summary"]["highestRound"] || snapshot.total != page["total"])
return -1;
if (entries.size == 0)
{
if (offset == 0 && snapshot.total == 0 && snapshot.round == 0)
return 1;
return -1;
}
if (snapshot.round < 2 || offset + entries.size > snapshot.total)
return -1;
for (i = 0; i < entries.size; i++)
{
entry = entries[i];
if (typeof(entry) != "array" || !lb_uint(entry["highestRound"]) || !lb_uint(entry["rank"]))
return -1;
value = entry["highestRound"];
if (value > snapshot.last_round || value > snapshot.round)
return -1;
snapshot.last_round = value;
if (value < snapshot.round)
{
if (snapshot.names.size == 0)
return -1;
return 1;
}
if (entry["rank"] != 1 || typeof(entry["guid"]) != "string" || entry["guid"] == "" ||
typeof(entry["name"]) != "string" || typeof(entry["records"]) != "array")
return -1;
evidence = entry["records"]["highestRound"];
if (typeof(evidence) != "array" || typeof(evidence["game"]) != "string" ||
typeof(evidence["mapKey"]) != "string" || !lb_uint(evidence["value"]))
return -1;
if (evidence["game"] != level.tws_lb_game ||
evidence["mapKey"] != key || evidence["value"] != snapshot.round)
return -1;
// GUIDs remain strings; equal names are distinct players.
guid = entry["guid"];
if (!isDefined(snapshot.guids[guid]))
{
snapshot.guids[guid] = true;
name = lb_display_text(entry["name"], 64);
if (name == "")
name = lb_display_text(guid, 64);
snapshot.names[snapshot.names.size] = name;
}
}
if (offset + entries.size >= snapshot.total)
return 1;
return 0;
}
lb_uint(value)
{
return typeof(value) == "int" && value >= 0;
}
lb_holder_lines(names)
{
lines = [];
line = "";
for (i = 0; i < names.size; i++)
{
if (line != "" && line.size + names[i].size + 2 > 100)
{
lines[lines.size] = line;
line = "";
}
if (line != "")
line += ", ";
line += names[i];
}
if (line != "")
lines[lines.size] = line;
return lines;
}
lb_display_text(value, limit)
{
output = "";
for (i = 0; i < value.size; i++)
{
ch = getSubStr(value, i, i + 1);
code = level.tws_lb_bytes[ch];
if (!isDefined(code) || code < 32 || code == 127)
continue;
if (ch == "^")
{
// Strip game colour escapes; never let a name inject formatting.
if (i + 1 < value.size && lb_contains("0123456789", getSubStr(value, i + 1, i + 2)))
i++;
continue;
}
width = 1;
if (code >= 240)
width = 4;
else if (code >= 224)
width = 3;
else if (code >= 192)
width = 2;
if (output.size + width > limit)
return output + "...";
output += getSubStr(value, i, i + width);
i += width - 1;
}
return output;
}
lb_url_encode(value)
{
hex = "0123456789ABCDEF";
encoded = "";
for (i = 0; i < value.size; i++)
{
ch = getSubStr(value, i, i + 1);
if (lb_contains("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~", ch))
encoded += ch;
else
{
code = level.tws_lb_bytes[ch];
high = int(code / 16);
low = code % 16;
encoded += "%" + getSubStr(hex, high, high + 1) + getSubStr(hex, low, low + 1);
}
}
return encoded;
}
lb_contains(chars, ch)
{
if (ch == "")
return false;
for (j = 0; j < chars.size; j++)
{
if (getSubStr(chars, j, j + 1) == ch)
return true;
}
return false;
}
// jsonParse raises a script error on malformed input. Validate the complete
// grammar, UTF-8 and escapes first, including truncated bodies and bad proxies.
// Yield during scanning so network data cannot monopolize a server frame.
lb_json_valid(body)
{
if (typeof(body) != "string" || body.size == 0 || body.size >= 20480)
return false;
if (!lb_utf8_valid(body))
return false;
state = spawnStruct();
state.body = body;
state.pos = 0;
state.yield_at = 256;
if (!lb_json_value(state, 0))
return false;
lb_json_space(state);
return state.pos == body.size;
}
lb_utf8_valid(body)
{
for (i = 0; i < body.size; i++)
{
if (i % 256 == 0)
wait 0.05;
code = level.tws_lb_bytes[getSubStr(body, i, i + 1)];
if (!isDefined(code))
return false;
if (code < 128)
continue;
count = 0;
minimum = 128;
maximum = 191;
if (code >= 194 && code <= 223)
count = 1;
else if (code >= 224 && code <= 239)
{
count = 2;
if (code == 224)
minimum = 160;
if (code == 237)
maximum = 159;
}
else if (code >= 240 && code <= 244)
{
count = 3;
if (code == 240)
minimum = 144;
if (code == 244)
maximum = 143;
}
if (count == 0 || i + count >= body.size)
return false;
for (j = 1; j <= count; j++)
{
next = level.tws_lb_bytes[getSubStr(body, i + j, i + j + 1)];
if (!isDefined(next) || next < minimum || next > maximum)
return false;
minimum = 128;
maximum = 191;
}
i += count;
}
return true;
}
lb_json_char(state)
{
if (state.pos >= state.yield_at)
{
wait 0.05;
state.yield_at = state.pos + 256;
}
if (state.pos >= state.body.size)
return "";
return getSubStr(state.body, state.pos, state.pos + 1);
}
lb_json_space(state)
{
while (lb_contains(" \t\r\n", lb_json_char(state)))
state.pos++;
}
lb_json_value(state, depth)
{
if (depth > 16)
return false;
lb_json_space(state);
ch = lb_json_char(state);
if (ch == "\"")
return lb_json_string(state);
if (ch == "{" || ch == "[")
{
object = ch == "{";
close = "]";
if (object)
close = "}";
state.pos++;
lb_json_space(state);
if (lb_json_char(state) == close)
{
state.pos++;
return true;
}
for (;;)
{
if (object)
{
if (!lb_json_string(state))
return false;
lb_json_space(state);
if (lb_json_char(state) != ":")
return false;
state.pos++;
}
if (!lb_json_value(state, depth + 1))
return false;
lb_json_space(state);
ch = lb_json_char(state);
state.pos++;
if (ch == close)
return true;
if (ch != ",")
return false;
lb_json_space(state);
}
}
literals = strTok("true,false,null", ",");
for (i = 0; i < literals.size; i++)
{
literal = literals[i];
if (state.pos + literal.size <= state.body.size &&
getSubStr(state.body, state.pos, state.pos + literal.size) == literal)
{
state.pos += literal.size;
return true;
}
}
return lb_json_number(state);
}
lb_json_string(state)
{
if (lb_json_char(state) != "\"")
return false;
state.pos++;
while (state.pos < state.body.size)
{
ch = lb_json_char(state);
state.pos++;
if (ch == "\"")
return true;
if (level.tws_lb_bytes[ch] < 32)
return false;
if (ch != "\\")
continue;
ch = lb_json_char(state);
state.pos++;
if (lb_contains("\"\\/bfnrt", ch))
continue;
if (ch != "u")
return false;
code = lb_json_hex4(state);
// Embedded NUL does not round-trip through engine strings.
if (code <= 0 || (code >= 56320 && code <= 57343))
return false;
if (code >= 55296 && code <= 56319)
{
if (state.pos + 2 > state.body.size ||
getSubStr(state.body, state.pos, state.pos + 2) != "\\u")
return false;
state.pos += 2;
code = lb_json_hex4(state);
if (code < 56320 || code > 57343)
return false;
}
}
return false;
}
lb_json_hex4(state)
{
hex = "0123456789abcdef";
result = 0;
for (i = 0; i < 4; i++)
{
ch = toLower(lb_json_char(state));
if (!lb_contains(hex, ch))
return -1;
for (j = 0; j < 16; j++)
{
if (getSubStr(hex, j, j + 1) == ch)
break;
}
result = result * 16 + j;
state.pos++;
}
return result;
}
lb_json_number(state)
{
start = state.pos;
if (lb_json_char(state) == "-")
state.pos++;
ch = lb_json_char(state);
if (ch == "0")
state.pos++;
else
{
if (!lb_contains("123456789", ch))
return false;
while (lb_contains("0123456789", lb_json_char(state)))
state.pos++;
}
if (lb_json_char(state) == ".")
{
state.pos++;
if (!lb_contains("0123456789", lb_json_char(state)))
return false;
while (lb_contains("0123456789", lb_json_char(state)))
state.pos++;
}
if (lb_contains("eE", lb_json_char(state)))
{
state.pos++;
if (lb_contains("+-", lb_json_char(state)))
state.pos++;
exponent = state.pos;
if (!lb_contains("0123456789", lb_json_char(state)))
return false;
while (lb_contains("0123456789", lb_json_char(state)))
state.pos++;
// API counters/ratios never need huge exponents; prevent native overflow.
if (state.pos - exponent > 2 || int(getSubStr(state.body, exponent, state.pos)) > 38)
return false;
}
return state.pos - start <= 32;
}