Add proximity-based map record reminders

This commit is contained in:
2026-10-17 21:54:13 -04:00
parent 6781a4ed5a
commit c19bef6dc6
11 changed files with 738 additions and 32 deletions
+20 -4
View File
@@ -13,7 +13,7 @@ private server token.
| `tws_t6_lock.gsc` | Lock at round 20; clear password on game over | Common, HUD |
| `tws_t6_telemetry.gsc` | Async queued game reports (game_start/heartbeat/round_change/game_end) | tws-t6-gsc-utils |
| `tws_t6_join.gsc` | `.join` chat command lets a spectator newcomer enter mid-match once | Common |
| `tws_t6_leaderboard.gsc` | Private map high-round record and every tied holder on connection | Common 1.2.0+, tws-t6-gsc-utils |
| `tws_t6_leaderboard.gsc` | Private map record on connection, `.leaders` replay and adaptive round-start reminders | Common 1.5.0+, tws-t6-gsc-utils |
| `tws_t6_bank.gsc` | Persistent shared GUID bank | Common, telemetry, TWS plugin 1.2.0 |
| `tws_t6_moderation.gsc` | Optional controller-managed bans, mutes and live moderation | Common 1.2.0+, TWS plugin 1.3.0+ |
@@ -102,8 +102,8 @@ the installed command list in pages of nine rows in aligned rows. Services remai
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), or four for records (five with a multi-page
counter). Pages reuse elements; switching views or closing the service releases
(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
them. Idle services hold no HUD elements, while the controller retains pending
requests and interruption/resume behavior. Core + AFK use 10 + 2 elements.
@@ -112,7 +112,8 @@ 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.1.0` remains compatible. AFK `2.0.0` requires the matching
full restarts first. Leaderboard `1.2.0` requires common `1.5.0+` for its
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
@@ -121,6 +122,21 @@ plugin `1.4.0+` and EC reserve service; see the [AFK guide](../../docs/afk.md).
It works independently of telemetry and HUD, and privately announces records
once per connection, including newcomers waiting in spectator mode.
Leaderboard `1.2.0` requires common `1.5.0+`. Copy both scripts to
`Plutonium/storage/t6/scripts/zm/` and restart the map. This script update adds
no native-plugin or backend dependency and requires no full server-process
restart when the existing plugin is installed.
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.
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
for cadence, interruption, cleanup and operation without the optional HUD.
Map selection uses stock `level.scr_zm_ui_gametype` and
`level.scr_zm_map_start_location`, falling back to the corresponding
`ui_gametype` / `ui_zm_mapstartlocation` dvars when absent. For example,
+38 -1
View File
@@ -166,6 +166,9 @@ show_help()
self service_ensure();
state = self.tws_service;
service_advance(state, getTime());
// Round-start notices are timely hints, not requests to resume after help.
if (isDefined(state.record) && state.record.kind == "reminder")
state.record = undefined;
state.generation++;
state.help = service_request("help", service_page_count(state.commands.size, level.hud_service_help_rows), state.generation);
service_render(state);
@@ -188,6 +191,29 @@ show_record(snapshot)
service_render(state);
}
// Automatic round-start hints yield to any existing service and never queue.
show_record_reminder(snapshot, message)
{
if (isDefined(level.tws_service_ended) || !isDefined(snapshot) ||
!isDefined(snapshot.round) || snapshot.round <= 0 ||
!isDefined(message) || message == "")
return false;
self service_ensure();
state = self.tws_service;
if (state.closed)
return false;
service_advance(state, getTime());
if (isDefined(state.help) || isDefined(state.record))
return false;
state.generation++;
state.record = service_request("reminder", 1, state.generation);
state.record.snapshot = snapshot;
state.record.message = message;
service_render(state);
return true;
}
service_page_count(count, rows)
{
return int((max(count, 1) + rows - 1) / rows);
@@ -201,6 +227,8 @@ service_request(kind, pages, generation)
request.page = 0;
request.pages = pages;
request.remaining = 12000;
if (kind == "reminder")
request.remaining = 6000;
return request;
}
@@ -347,7 +375,10 @@ 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);
for (i = 0; i < 3; i++)
rows = 3;
if (request.kind == "reminder")
rows = 1;
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);
@@ -463,6 +494,12 @@ service_render(state)
state.record_title.alpha = 1;
if (!isDefined(snapshot) || snapshot.round == 0)
return;
if (request.kind == "reminder")
{
state.rows[0] SetText(request.message);
state.rows[0].alpha = 1;
return;
}
for (i = 0; i < 3; i++)
{
index = page * 3 + i;
+86 -1
View File
@@ -1,4 +1,4 @@
/* tws_t6_leaderboard.gsc - private map record announcements and .leaders replay. */
/* tws_t6_leaderboard.gsc - private map records, round reminders and .leaders replay. */
#include maps\mp\_utility;
#include common_scripts\utility;
@@ -26,10 +26,12 @@ init()
level.tws_lb_pending = undefined;
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_connect_handler(::lb_welcome);
level thread lb_chat_listener();
level thread lb_worker();
level thread lb_round_watcher();
}
lb_worker()
@@ -66,6 +68,88 @@ lb_worker()
}
}
lb_round_watcher()
{
level endon("end_game");
level endon("game_ended");
level endon("intermission");
last_round = 0;
for (;;)
{
// T6 advances its counter before the between-round pause is over.
// Both engines notify the actual start; never synthesize a late start.
level waittill("start_of_round");
if (!isDefined(level.round_number) || level.round_number <= last_round)
continue;
last_round = level.round_number;
lb_round_start(last_round);
}
}
lb_round_start(round)
{
if (isDefined(level.tws_lb_chase_finished))
return;
snapshot = lb_cached_record();
if (!isDefined(snapshot) || snapshot.round < 2)
return;
message = lb_round_message(round, snapshot.round);
if (message == "")
return;
// Celebrate at most once, even if a later refresh raises the fleet record.
// Busy clients skip the milestone too; no delayed mid-round announcements.
if (round > snapshot.round)
level.tws_lb_chase_finished = true;
players = getPlayers();
for (i = 0; i < players.size; i++)
players[i] lb_remind_player(snapshot, message);
}
lb_round_message(round, record)
{
if (round < 1 || record < 2)
return "";
gap = record - round;
if (gap < 0)
return "Recorded high round passed!";
if (gap == 0)
return "Record round reached!";
if (gap > 20)
return "";
// Anchor the cadence to the target, not to round numbers divisible by 5/2.
if (gap > 10 && gap % 5 != 0)
return "";
if (gap > 5 && gap <= 10 && gap % 2 != 0)
return "";
if (gap == 1)
return "1 round away from matching it";
return gap + " rounds away from matching it";
}
lb_player_ready(player)
{
// A welcome attempt (even with no record) or manual request releases this
// gate. Give that full announcement at least 30 seconds before a reminder.
if (!isDefined(player.tws_lb_last_request) ||
getTime() - player.tws_lb_last_request < 30000)
return false;
if (!isDefined(player.sessionstate) ||
(player.sessionstate != "playing" && player.sessionstate != "spectator"))
return false;
if (isDefined(level.passed_introscreen) && !level.passed_introscreen)
return false;
return true;
}
lb_remind_player(snapshot, message)
{
if (!lb_player_ready(self))
return;
self scripts\zm\tws_t6_common::show_record_reminder(snapshot, message);
}
lb_welcome()
{
self endon("disconnect");
@@ -131,6 +215,7 @@ lb_request(manual)
// This also applies when the cache is unavailable.
if (manual)
self notify("tws_lb_requested");
self.tws_lb_last_request = getTime();
snapshot = lb_cached_record();
if (!isDefined(snapshot) && !manual)
return;