Show personal map records on join and replay
This commit is contained in:
+24
-18
@@ -16,7 +16,7 @@ Platform quirks and deploy commands: [NOTES.md](NOTES.md).
|
||||
| [tws_t5_join.gsc](tws_t5_join.gsc) | `.join` lets newcomers spawn before the current round ends |
|
||||
| [tws_t5_lock.gsc](tws_t5_lock.gsc) | Automatic lobby lock plus majority `.lock` / `.unlock` votes |
|
||||
| [tws_t5_telemetry.gsc](tws_t5_telemetry.gsc) | Direct HTTP game start, round, heartbeat, and game end reports |
|
||||
| [tws_t5_leaderboard.gsc](tws_t5_leaderboard.gsc) | Private map record on connection, `.leaders` replay and adaptive round-start reminders |
|
||||
| [tws_t5_leaderboard.gsc](tws_t5_leaderboard.gsc) | Private map and personal records on connection, `.leaders` replay and adaptive round-start reminders |
|
||||
| [tws_t5_bank.gsc](tws_t5_bank.gsc) | Persistent shared GUID bank and private balance/deposit/withdraw commands |
|
||||
| [tws_t5_moderation.gsc](tws_t5_moderation.gsc) | Optional controller-managed bans, mutes and live moderation; requires common 1.2.0+ and native plugin 1.3.0+ |
|
||||
|
||||
@@ -83,14 +83,16 @@ continue updating while hidden; game-over values remain frozen. Help, records
|
||||
and the stock HUD are unaffected. `.hud` requires the HUD module.
|
||||
|
||||
Common provides one unboxed upper-right service area below the Plutonium tag.
|
||||
Records are right aligned, with `Map record | Round N` followed by up to three
|
||||
holder lines; there is no replay footer or single-page counter. Help displays
|
||||
Records are right aligned: `Map Record | Round N`, every tied holder on its own
|
||||
line, then a blank row and a separate `Your Best | Round N` heading. The whole
|
||||
record is shown at once for 12 seconds, with no pages, counter or footer. Help displays
|
||||
the installed command list in pages of nine rows in aligned rows. Services remain private, leave the
|
||||
core in place and work without the HUD module installed. See the
|
||||
[shared layout and validation guide](../../README.md#core-hud-and-service-window-t5-and-t6).
|
||||
Only the active service view holds HUD elements: 19 for nine-command help
|
||||
(20 with a multi-page counter; the standard AFK command set uses multiple pages), four for records (five with a multi-page
|
||||
counter), or two for compact round-start reminders. Pages reuse elements; switching views or closing the service releases
|
||||
(20 with a multi-page counter), one element per holder plus two headings for
|
||||
full records, or two for compact round-start reminders. Help pages reuse elements;
|
||||
switching views or closing the service releases
|
||||
them. Idle services hold no HUD elements, while the controller retains pending
|
||||
requests and interruption/resume behavior. Core + AFK use 10 + 2 elements.
|
||||
|
||||
@@ -99,15 +101,15 @@ HUD `1.4.2` requires common `1.4.0+` and
|
||||
`SetTextUnlimited` counter, updated only when the count changes. Deploy the
|
||||
updated HUD script and restart the map on r5334+; no full server-process
|
||||
restart is required. Older Plutonium revisions need client/server updates and
|
||||
full restarts first. Leaderboard `1.2.0` requires common `1.5.0+` for its
|
||||
round-start reminders. AFK `2.0.0` requires the matching
|
||||
full restarts first. Leaderboard `1.3.1` requires common `1.6.1+` for its
|
||||
personal records and round-start reminders. AFK `2.0.0` requires the matching
|
||||
plugin `1.4.0+` and EC reserve service; see the [AFK guide](../../docs/afk.md).
|
||||
|
||||
## Map record announcements
|
||||
|
||||
Copy `tws_t5_leaderboard.gsc` and the updated `tws_t5_common.gsc` to
|
||||
`storage/t5/scripts/sp/`, then restart the map. Leaderboard `1.2.0` requires
|
||||
common `1.5.0+`; this script update adds no native-plugin or backend dependency
|
||||
`storage/t5/scripts/sp/`, then restart the map. Leaderboard `1.3.1` requires
|
||||
common `1.6.1+`; this script update adds no native-plugin or backend dependency
|
||||
and requires no full server-process restart. Leaderboard requires a
|
||||
[t5-gsc-utils](https://github.com/alicealys/t5-gsc-utils) build exposing
|
||||
`http::request`, `jsonParse`, `typeof` and `formatString`. A plugin upgrade
|
||||
@@ -124,10 +126,14 @@ Round-start reminders use the latest usable cache at gaps of 20, 15, 10, 8 and
|
||||
6 rounds, then every round for the final five, followed by matching/overtake
|
||||
milestones. After the first overtake, reminders stop for that match; `.leaders`
|
||||
still works. A reminder shows the record and one short status line for six
|
||||
seconds. Join and manual announcements keep all holders and 12-second pages.
|
||||
seconds. Join and manual announcements show your personal best on the exact map variant
|
||||
below all holders, with one player per line and a blank row before the personal
|
||||
heading. Everything is visible at once for 12 seconds, with no record pages or
|
||||
counter. New players see `Your Best | None yet`; failed personal lookups show
|
||||
`Your Best | Unavailable`.
|
||||
Reminders skip busy help/record views, unready players, recent welcome/manual
|
||||
requests and empty/unavailable/expired records. Help discards a reminder instead
|
||||
of pausing it. No extra HTTP requests are made. Follow the shared live checks
|
||||
of pausing it. Round reminders make no extra HTTP requests. Follow the shared live checks
|
||||
for cadence, interruption, cleanup and operation without the optional HUD.
|
||||
|
||||
## Player commands
|
||||
@@ -149,7 +155,7 @@ explicit row positions so T5 does not concatenate newline-separated text.
|
||||
| `.afk` | Enter protected AFK using your time reserve |
|
||||
| `.back` | Return from AFK |
|
||||
| `.join` | Join mid-round (newcomers only) |
|
||||
| `.leaders` | Show the map record again (leaderboard module required) |
|
||||
| `.leaders` | Show map and personal records (leaderboard module required) |
|
||||
| `.b` | Check bank balance (bank module required) |
|
||||
| `.d <amount\|all>` | Deposit points (bank module required) |
|
||||
| `.w <amount\|all>` | Withdraw points (bank module required) |
|
||||
@@ -162,9 +168,9 @@ also removes its entries from `.help` after a map restart. Lock and extended AFK
|
||||
commands appear on additional pages. Bank entries retain the full
|
||||
`.d <amount|all>` / `.w <amount|all>` syntax. Removing HUD removes `.hud` from the list.
|
||||
The enabled leaderboard module registers `.leaders`, which replays the latest
|
||||
cached map record privately. Repeating it restarts the record from the first page;
|
||||
help pauses the record's remaining page time and resumes it after the final
|
||||
help page. `.leaders` during help replaces the pending record replay from page one.
|
||||
map and personal records privately. Repeating it restarts the full 12-second
|
||||
announcement; help pauses the record's remaining time and resumes it after the
|
||||
final help page. `.leaders` during help replaces the pending record replay.
|
||||
|
||||
## Dependency graph
|
||||
|
||||
@@ -224,7 +230,7 @@ connect listener anywhere else crashes the T5 SP handshake.
|
||||
- Spawn-handler registry
|
||||
- Connection-handler registry for optional modules that serve spectator newcomers
|
||||
- `.help` / `.commands` listener and ordered help registry populated by installed modules
|
||||
- One service controller per player, with help/record pagination,
|
||||
- One service controller per player, with help pagination and complete record views,
|
||||
interruption/resume, request replacement and disconnect/game-end cleanup;
|
||||
display elements exist only for the active view and are released when idle
|
||||
- Core element registry with per-element alpha state and a per-player visibility mask
|
||||
@@ -272,8 +278,8 @@ connect listener anywhere else crashes the T5 SP handshake.
|
||||
`.help` and the remaining commands still work. Restore the scripts afterward.
|
||||
3. Verify help closes after 12 seconds. Add custom registered commands to test
|
||||
the nine-row page limit, counter and repeat-to-restart behavior. Interrupt a record
|
||||
with help and verify remaining page time resumes; repeat `.leaders` during
|
||||
help and verify only the latest replay starts from page one.
|
||||
with help and verify remaining display time resumes; repeat `.leaders` during
|
||||
help and verify only the latest complete replay starts.
|
||||
4. Reopen after expiry, die/respawn and reconnect; verify no duplicate text or
|
||||
leftover elements. Repeat without the HUD module. Follow the complete
|
||||
[HUD validation guide](../../README.md#hud-validation) before rollout.
|
||||
|
||||
@@ -179,18 +179,16 @@ show_help()
|
||||
}
|
||||
|
||||
// An undefined snapshot is the explicit unavailable view for manual requests.
|
||||
show_record(snapshot)
|
||||
show_record(snapshot, personal)
|
||||
{
|
||||
if (isDefined(level.tws_service_ended))
|
||||
return;
|
||||
self service_ensure();
|
||||
state = self.tws_service;
|
||||
service_advance(state, getTime());
|
||||
count = 0;
|
||||
if (isDefined(snapshot))
|
||||
count = snapshot.lines.size;
|
||||
state.generation++;
|
||||
state.record = service_request("record", service_page_count(count, 3), state.generation);
|
||||
state.record = service_request("record", 1, state.generation);
|
||||
state.record.personal = personal;
|
||||
state.record.snapshot = snapshot;
|
||||
service_render(state);
|
||||
}
|
||||
@@ -348,12 +346,21 @@ service_ensure()
|
||||
self thread service_end_game(state);
|
||||
}
|
||||
|
||||
service_record_rows(request)
|
||||
{
|
||||
if (request.kind == "reminder")
|
||||
return 1;
|
||||
if (request.kind == "record" && isDefined(request.snapshot) && request.snapshot.round > 0)
|
||||
return request.snapshot.lines.size;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Allocate only the active view. Alpha-zero elements still occupy HUD slots.
|
||||
service_view_create(state, request)
|
||||
{
|
||||
service_view_destroy(state);
|
||||
state.view_kind = request.kind;
|
||||
state.view_paged = request.pages > 1;
|
||||
state.view_paged = request.kind == "help" && request.pages > 1;
|
||||
if (state.view_paged)
|
||||
state.page_counter = service_text(self, state, level.hud_service_right,
|
||||
level.hud_service_title_y, "right", 1.0, "", level.hud_c_dim);
|
||||
@@ -379,15 +386,16 @@ service_view_create(state, request)
|
||||
|
||||
state.record_title = service_text(self, state, level.hud_service_right,
|
||||
level.hud_service_record_y, "right", 1.1, "", level.hud_c_count);
|
||||
rows = 3;
|
||||
if (request.kind == "reminder")
|
||||
rows = 1;
|
||||
rows = service_record_rows(request);
|
||||
for (i = 0; i < rows; i++)
|
||||
state.rows[i] = service_text(self, state, level.hud_service_right,
|
||||
level.hud_service_holders_y + i * level.hud_service_holders_pitch,
|
||||
"right", 1.05, "", level.hud_c_label);
|
||||
if (isDefined(state.page_counter))
|
||||
state.page_counter.y = level.hud_service_holders_y + 3 * level.hud_service_holders_pitch;
|
||||
// Give the personal record its own heading after every holder and a blank row.
|
||||
if (isDefined(request.personal))
|
||||
state.personal_title = service_text(self, state, level.hud_service_right,
|
||||
level.hud_service_holders_y + (rows + 1) * level.hud_service_holders_pitch,
|
||||
"right", 1.1, "", level.hud_c_count);
|
||||
}
|
||||
|
||||
service_view_destroy(state)
|
||||
@@ -405,6 +413,8 @@ service_view_destroy(state)
|
||||
state.command_rows = [];
|
||||
state.description_rows = [];
|
||||
state.record_title = undefined;
|
||||
state.personal_title = undefined;
|
||||
state.personal_text = undefined;
|
||||
state.rows = [];
|
||||
state.page_text = undefined;
|
||||
state.record_header_text = undefined;
|
||||
@@ -445,8 +455,10 @@ service_render(state)
|
||||
service_view_destroy(state);
|
||||
return;
|
||||
}
|
||||
paged = request.pages > 1;
|
||||
if (!isDefined(state.view_kind) || state.view_kind != request.kind || state.view_paged != paged)
|
||||
paged = request.kind == "help" && request.pages > 1;
|
||||
if (!isDefined(state.view_kind) || state.view_kind != request.kind || state.view_paged != paged ||
|
||||
state.rows.size != service_record_rows(request) ||
|
||||
isDefined(state.personal_title) != isDefined(request.personal))
|
||||
service_view_create(state, request);
|
||||
for (i = 0; i < state.elems.size; i++)
|
||||
state.elems[i].alpha = 0;
|
||||
@@ -482,13 +494,13 @@ service_render(state)
|
||||
return;
|
||||
}
|
||||
|
||||
header = "Map record unavailable";
|
||||
header = "Map Record unavailable";
|
||||
snapshot = request.snapshot;
|
||||
if (isDefined(snapshot))
|
||||
{
|
||||
header = "No recorded high round yet";
|
||||
if (snapshot.round > 0)
|
||||
header = "Map record | Round " + snapshot.round;
|
||||
header = "Map Record | Round " + snapshot.round;
|
||||
}
|
||||
if (!isDefined(state.record_header_text) || state.record_header_text != header)
|
||||
{
|
||||
@@ -496,6 +508,15 @@ service_render(state)
|
||||
state.record_header_text = header;
|
||||
}
|
||||
state.record_title.alpha = 1;
|
||||
if (isDefined(request.personal))
|
||||
{
|
||||
if (!isDefined(state.personal_text) || state.personal_text != request.personal)
|
||||
{
|
||||
state.personal_title SetText(request.personal);
|
||||
state.personal_text = request.personal;
|
||||
}
|
||||
state.personal_title.alpha = 1;
|
||||
}
|
||||
if (!isDefined(snapshot) || snapshot.round == 0)
|
||||
return;
|
||||
if (request.kind == "reminder")
|
||||
@@ -504,9 +525,9 @@ service_render(state)
|
||||
state.rows[0].alpha = 1;
|
||||
return;
|
||||
}
|
||||
for (i = 0; i < 3; i++)
|
||||
for (i = 0; i < state.rows.size; i++)
|
||||
{
|
||||
index = page * 3 + i;
|
||||
index = i;
|
||||
if (index < snapshot.lines.size)
|
||||
{
|
||||
text = snapshot.lines[index];
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* tws_t5_leaderboard.gsc - private map records, round reminders and .leaders replay. */
|
||||
/* tws_t5_leaderboard.gsc - private map/personal records, round reminders and .leaders replay. */
|
||||
|
||||
#include maps\_utility;
|
||||
#include common_scripts\utility;
|
||||
@@ -25,7 +25,7 @@ init()
|
||||
level.tws_lb_cache = undefined;
|
||||
level.tws_lb_attempt = 0;
|
||||
level.tws_lb_chase_finished = undefined;
|
||||
scripts\sp\tws_t5_common::register_help_command(".leaders", "Show the map record again");
|
||||
scripts\sp\tws_t5_common::register_help_command(".leaders", "Show map and personal records");
|
||||
scripts\sp\tws_t5_common::register_connect_handler(::lb_welcome);
|
||||
level thread lb_chat_listener();
|
||||
level thread lb_worker();
|
||||
@@ -212,14 +212,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\sp\tws_t5_common::show_record(snapshot);
|
||||
self scripts\sp\tws_t5_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 getGuid();
|
||||
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()
|
||||
@@ -239,6 +338,7 @@ lb_refresh()
|
||||
return undefined;
|
||||
|
||||
snapshot = spawnStruct();
|
||||
snapshot.key = selection.key;
|
||||
snapshot.name = lb_display_text(selection.name, 64);
|
||||
snapshot.round = 0;
|
||||
snapshot.names = [];
|
||||
@@ -288,9 +388,18 @@ lb_selection()
|
||||
return selection;
|
||||
}
|
||||
|
||||
// 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 = [];
|
||||
@@ -461,28 +570,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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user