762 lines
21 KiB
Plaintext
762 lines
21 KiB
Plaintext
/* tws_t5_leaderboard.gsc - private map record announcements and .leaders replay. */
|
|
|
|
#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_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();
|
|
}
|
|
|
|
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;
|
|
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;
|
|
|
|
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");
|
|
|
|
// A manual replay replaces any welcome announcement still waiting to run.
|
|
// This also applies when the cache is unavailable.
|
|
if (manual)
|
|
self notify("tws_lb_requested");
|
|
snapshot = lb_cached_record();
|
|
if (!isDefined(snapshot) && !manual)
|
|
return;
|
|
|
|
// Common owns pagination, help priority and the single service window.
|
|
// Undefined snapshots produce a private unavailable page for manual replay.
|
|
self scripts\sp\tws_t5_common::show_record(snapshot);
|
|
}
|
|
|
|
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_t5_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 = "";
|
|
columns = 0;
|
|
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;
|
|
return lines;
|
|
}
|
|
|
|
lb_holder_name(value)
|
|
{
|
|
// The service has 200 virtual units at font scale 1.1. Use a conservative
|
|
// 16-column bound, counting non-ASCII glyphs twice, pending in-game metrics.
|
|
// A long name gets one bounded entry; no tied holder is dropped.
|
|
value = lb_display_text(value, 64);
|
|
if (value == "")
|
|
value = "Unknown player";
|
|
if (lb_holder_width(value) <= 16)
|
|
return value;
|
|
|
|
output = "";
|
|
columns = 0;
|
|
for (i = 0; i < value.size; i++)
|
|
{
|
|
code = level.tws_lb_bytes[getSubStr(value, i, i + 1)];
|
|
width = 1;
|
|
if (code >= 128)
|
|
width = 2;
|
|
if (columns + width > 13)
|
|
break;
|
|
count = lb_glyph_bytes(code);
|
|
output += getSubStr(value, i, i + count);
|
|
columns += width;
|
|
i += count - 1;
|
|
}
|
|
return output + "...";
|
|
}
|
|
|
|
lb_holder_width(value)
|
|
{
|
|
columns = 0;
|
|
for (i = 0; i < value.size; i++)
|
|
{
|
|
code = level.tws_lb_bytes[getSubStr(value, i, i + 1)];
|
|
if (code >= 128)
|
|
columns += 2;
|
|
else
|
|
columns++;
|
|
i += lb_glyph_bytes(code) - 1;
|
|
}
|
|
return columns;
|
|
}
|
|
|
|
lb_glyph_bytes(code)
|
|
{
|
|
if (code >= 240)
|
|
return 4;
|
|
if (code >= 224)
|
|
return 3;
|
|
if (code >= 192)
|
|
return 2;
|
|
return 1;
|
|
}
|
|
|
|
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;
|
|
}
|