Add map record HUD announcements for T5 and T6
This commit is contained in:
@@ -0,0 +1,744 @@
|
||||
/* tws_leaderboard.gsc - private map record announcement, once per connection. */
|
||||
|
||||
#include maps\_utility;
|
||||
#include common_scripts\utility;
|
||||
|
||||
init()
|
||||
{
|
||||
if (GetDvar(#"zombiemode") != "1")
|
||||
return;
|
||||
if (GetDvar("tws_leaderboard_enabled") == "0")
|
||||
return;
|
||||
|
||||
level.tws_lb_game = "t5";
|
||||
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\sp\tws_common::register_connect_handler(::lb_welcome);
|
||||
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");
|
||||
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;
|
||||
while (isDefined(self.hasSpawned) && !self.hasSpawned)
|
||||
wait 0.25;
|
||||
wait 5;
|
||||
|
||||
while (!isDefined(level.tws_lb_cache) && level.tws_lb_attempt == 0)
|
||||
wait 0.25;
|
||||
|
||||
if (!isDefined(level.tws_lb_cache))
|
||||
return;
|
||||
snapshot = level.tws_lb_cache;
|
||||
// Do not advertise an indefinitely stale record during an outage.
|
||||
if (getTime() - snapshot.saved_at > 900000)
|
||||
return;
|
||||
|
||||
self lb_show_hud(snapshot);
|
||||
}
|
||||
|
||||
lb_show_hud(snapshot)
|
||||
{
|
||||
self endon("disconnect");
|
||||
self endon("tws_lb_hud_done");
|
||||
|
||||
scripts\sp\tws_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\sp\tws_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\sp\tws_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 welcome thread's endon handlers.
|
||||
self thread scripts\sp\tws_common::destroy_on_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 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_end_game(elems, event)
|
||||
{
|
||||
self endon("disconnect");
|
||||
self endon("tws_lb_hud_done");
|
||||
level waittill(event);
|
||||
lb_hud_destroy(elems);
|
||||
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 != "";
|
||||
return selection;
|
||||
}
|
||||
|
||||
// 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;
|
||||
request = http::request(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)
|
||||
{
|
||||
print("[tws_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;
|
||||
}
|
||||
Reference in New Issue
Block a user