Show personal map records on join and replay
This commit is contained in:
@@ -6,6 +6,50 @@ T5/T6 script and each native plugin. Current versions are recorded in
|
|||||||
|
|
||||||
## 2026-10-18
|
## 2026-10-18
|
||||||
|
|
||||||
|
### Show each player's personal map record on join and replay
|
||||||
|
|
||||||
|
| Component | Version | Affected source |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `tws_t5_leaderboard.gsc` | `1.3.1` | `t5/scripts/tws_t5_leaderboard.gsc` |
|
||||||
|
| `tws_t6_leaderboard.gsc` | `1.3.1` | `t6/scripts/tws_t6_leaderboard.gsc` |
|
||||||
|
| `tws_t5_common.gsc` | `1.6.1` | `t5/scripts/tws_t5_common.gsc` |
|
||||||
|
| `tws_t6_common.gsc` | `1.6.1` | `t6/scripts/tws_t6_common.gsc` |
|
||||||
|
|
||||||
|
- Add a private `Your Best | Round N` heading to join announcements and `.leaders`,
|
||||||
|
using the same resolved map variant as the fleet record. Distinguish confirmed
|
||||||
|
empty history (`None yet`) from failed/malformed lookups (`Unavailable`).
|
||||||
|
- Show `Map Record | Round N`, every tied holder on a separate line, a blank row,
|
||||||
|
then the personal heading. Both headings use the brighter heading colour and
|
||||||
|
size, distinct from player names. Remove record pagination and counters: show
|
||||||
|
the entire record for 12 seconds, retaining help priority and pause/resume.
|
||||||
|
Allocate one element per holder plus the two headings, resize when the holder
|
||||||
|
count changes, and release the complete view on expiry, interruption or cleanup.
|
||||||
|
Help retains its existing command pagination; compact reminders are unchanged.
|
||||||
|
- Query the existing public leaderboard search by GUID, paginate past name and
|
||||||
|
substring matches, and accept only exact string identity with matching game/map
|
||||||
|
evidence. Cache successes per connection for five minutes and failures for
|
||||||
|
30 seconds. Bound pagination time and queue waits; serialize personal and map
|
||||||
|
HTTP requests, retaining the native slot until collection even after a caller
|
||||||
|
times out or disconnects. New manual replays cancel older waiting announcements.
|
||||||
|
- Apply matching behavior to T5 and T6 using their existing GUID access and HTTP
|
||||||
|
APIs. Round reminders still use only the map cache. Common's new personal text
|
||||||
|
argument is optional for existing consumers.
|
||||||
|
- Compatibility: leaderboard `1.3.1` requires matching common `1.6.1+`. No new
|
||||||
|
backend, native-plugin or upstream dependency requirement.
|
||||||
|
- Validation: 177 leaderboard, 76 service HUD, 14 core HUD, 42 bank, 17 join,
|
||||||
|
23 telemetry, 76 lock and 47 AFK checks passed, plus both moderation checks.
|
||||||
|
Record fixtures verify all-holder rendering, heading order/style/spacing, no
|
||||||
|
record counter, resizing, expiry and help interruption.
|
||||||
|
Changed T6 scripts compiled and assembled with upstream gsc-tool `8f1d6bd`
|
||||||
|
libraries. These are engine-shim checks; live Windows/Plutonium rendering,
|
||||||
|
concurrent joins and backend integration remain unverified. T5 has no gsc-tool
|
||||||
|
backend. JSON uniqueness, script manifest coverage, matching versions/changelog,
|
||||||
|
changed documentation's local links and `git diff --check` passed.
|
||||||
|
- Deployment: copy the updated leaderboard and common scripts together for each
|
||||||
|
installed game and restart the map. No full server-process restart or additional
|
||||||
|
deployment action is required. No deployment was performed.
|
||||||
|
|
||||||
|
|
||||||
### Add majority votes to lock and unlock the lobby
|
### Add majority votes to lock and unlock the lobby
|
||||||
|
|
||||||
| Component | Version | Affected source |
|
| Component | Version | Affected source |
|
||||||
|
|||||||
@@ -458,12 +458,13 @@ count appears only when more than one page is needed. Each page lasts
|
|||||||
command restarts page one. Both engines position rows explicitly, avoiding
|
command restarts page one. Both engines position rows explicitly, avoiding
|
||||||
T5's newline-rendering problem.
|
T5's newline-rendering problem.
|
||||||
|
|
||||||
Records use the same service area, right aligned: `Map record | Round N`
|
Records use the same service area, right aligned: `Map Record | Round N`,
|
||||||
followed by up to three bounded holder lines. Single-page records have no
|
followed by every tied holder on a separate line. After a blank row,
|
||||||
counter or footer; ties spanning multiple pages retain a page count and
|
`Your Best | Round N` uses the same heading style as the map record, distinct
|
||||||
12 seconds per page. Help pauses a visible record and resumes its remaining
|
from the holder names. The entire announcement is visible at once for 12 seconds;
|
||||||
page time when help ends. `.leaders` during help replaces the pending record
|
records have no pages, counter or footer. Help pauses a visible record and
|
||||||
with a fresh replay from page one. Round-start reminders use just the record
|
resumes its remaining time when help ends. `.leaders` during help replaces the
|
||||||
|
pending record with a fresh replay. Round-start reminders use just the record
|
||||||
header and one proximity or milestone line for six seconds. They skip busy
|
header and one proximity or milestone line for six seconds. They skip busy
|
||||||
services; opening help discards a reminder instead of resuming it later.
|
services; opening help discards a reminder instead of resuming it later.
|
||||||
One service is visible at a time. Services remain private and work when the
|
One service is visible at a time. Services remain private and work when the
|
||||||
@@ -472,7 +473,8 @@ HUD module is omitted.
|
|||||||
Both games allocate only the active service view and release it when idle or
|
Both games allocate only the active service view and release it when idle or
|
||||||
before switching between help and records. Pages reuse the active view's
|
before switching between help and records. Pages reuse the active view's
|
||||||
elements. The core uses 10 archived elements, plus two independent elements for AFK. A nine-row help page uses 19 nonarchived elements, or 20 with a multi-page
|
elements. The core uses 10 archived elements, plus two independent elements for AFK. A nine-row help page uses 19 nonarchived elements, or 20 with a multi-page
|
||||||
counter; records use four, or five with a multi-page counter. Hidden elements
|
counter; full records use one element per holder plus the map and personal
|
||||||
|
headings. Hidden elements
|
||||||
still occupy slots. Compact reminders use two nonarchived elements.
|
still occupy slots. Compact reminders use two nonarchived elements.
|
||||||
The controller and pending requests are separate from
|
The controller and pending requests are separate from
|
||||||
display lifetime, so an idle service holds no HUD elements.
|
display lifetime, so an idle service holds no HUD elements.
|
||||||
@@ -484,7 +486,8 @@ avoiding dynamic counter strings in the text config-string pool. Numeric HP
|
|||||||
still uses `SetValue`. On r5334+, deploy the updated HUD script and restart
|
still uses `SetValue`. On r5334+, deploy the updated HUD script and restart
|
||||||
the map; no full server-process restart is required. On older Plutonium
|
the map; no full server-process restart is required. On older Plutonium
|
||||||
revisions, update clients and server and fully restart them before deploying.
|
revisions, update clients and server and fully restart them before deploying.
|
||||||
Leaderboard `1.2.0` requires matching common `1.5.0+` for round-start reminders.
|
Leaderboard `1.3.1` requires matching common `1.6.1+` for personal records
|
||||||
|
and round-start reminders.
|
||||||
AFK `2.0.0` requires plugin `1.4.0+`,
|
AFK `2.0.0` requires plugin `1.4.0+`,
|
||||||
common `1.4.0+` and the EC reserve service; see the [AFK guide](docs/afk.md). Existing
|
common `1.4.0+` and the EC reserve service; see the [AFK guide](docs/afk.md). Existing
|
||||||
leaderboard plugin requirements still apply. Common retains its older panel
|
leaderboard plugin requirements still apply. Common retains its older panel
|
||||||
@@ -526,10 +529,9 @@ Before rollout, validate both engines in Plutonium:
|
|||||||
exercise the nine-row page limit, page counter and repeat-to-restart behavior.
|
exercise the nine-row page limit, page counter and repeat-to-restart behavior.
|
||||||
Remove optional modules and restart the map to confirm filtering, including
|
Remove optional modules and restart the map to confirm filtering, including
|
||||||
`.hud` disappearing without HUD, then restore them.
|
`.hud` disappearing without HUD, then restore them.
|
||||||
4. Interrupt records with help and check that the interrupted page resumes for
|
4. Interrupt records with help and check that the full announcement resumes for
|
||||||
its remaining time. Repeat `.leaders` while help is open; only the latest
|
its remaining time. Repeat `.leaders` while help is open; only the latest
|
||||||
replay should start, at page one. Check long holder names, ties spanning
|
replay should start. Check long holder names, all tied holders visible at once, no records, and unavailable records. Repeat without the HUD installed.
|
||||||
pages, no records, and unavailable records. Repeat without the HUD installed.
|
|
||||||
5. Disconnect or end the game while help/records are active, reconnect and
|
5. Disconnect or end the game while help/records are active, reconnect and
|
||||||
request them again. Verify no duplicate elements, stale timeout closures or
|
request them again. Verify no duplicate elements, stale timeout closures or
|
||||||
surviving service windows. Confirm `.join`, bank commands and lobby lock
|
surviving service windows. Confirm `.join`, bank commands and lobby lock
|
||||||
@@ -548,13 +550,14 @@ Before rollout, validate both engines in Plutonium:
|
|||||||
## Map record announcements
|
## Map record announcements
|
||||||
|
|
||||||
Install `tws_t5_leaderboard.gsc` for T5 or `tws_t6_leaderboard.gsc` for T6,
|
Install `tws_t5_leaderboard.gsc` for T5 or `tws_t6_leaderboard.gsc` for T6,
|
||||||
**along with common `1.5.0+`** for that game when using leaderboard `1.2.0`.
|
**along with common `1.6.1+`** for that game when using leaderboard `1.3.1`.
|
||||||
Copy both scripts for each installed game and restart the map. This update
|
Copy both scripts for each installed game and restart the map. This update
|
||||||
adds no native-plugin or backend requirement and needs no full server-process
|
adds no native-plugin or backend requirement and needs no full server-process
|
||||||
restart when existing leaderboard dependencies are already installed.
|
restart when existing leaderboard dependencies are already installed.
|
||||||
The module privately announces the current map's fleet-wide highest recorded
|
The module privately announces the current map's fleet-wide highest recorded
|
||||||
round and every tied holder once per connection, including spectator newcomers.
|
round, every tied holder and the joining player’s personal best on that map once
|
||||||
Type `.leaders` to replay the record privately from the latest usable cache.
|
per connection, including spectator newcomers. Type `.leaders` to replay both
|
||||||
|
records privately. Personal bests use the same game and exact map variant.
|
||||||
Repeating the command restarts the announcement from its first page. It appears
|
Repeating the command restarts the announcement from its first page. It appears
|
||||||
in `.help` when the leaderboard module is enabled. If no usable record is
|
in `.help` when the leaderboard module is enabled. If no usable record is
|
||||||
available, the service identifies the record as unavailable; this differs from
|
available, the service identifies the record as unavailable; this differs from
|
||||||
@@ -580,18 +583,32 @@ exist in the API's map list and match the current game and engine map. Clear it
|
|||||||
when changing variants; restart the map after changing these settings.
|
when changing variants; restart the map after changing these settings.
|
||||||
|
|
||||||
The announcement starts five seconds after player readiness and waits for the
|
The announcement starts five seconds after player readiness and waits for the
|
||||||
initial record fetch if necessary. It appears as right-aligned text in the
|
initial map and personal record fetches if necessary. It appears as right-aligned
|
||||||
common-owned upper-right service area: `Map record | Round N`, followed by up
|
text in the common-owned upper-right service area, all at once:
|
||||||
to three holder lines. There is no background or replay footer, and a page
|
|
||||||
count appears only for multi-page records. Each page lasts 12 seconds. Opening `.help` pauses the record's remaining
|
```text
|
||||||
page time until help closes. A `.leaders` request during help replaces any
|
Map Record | Round 25
|
||||||
pending replay and starts from page one after help closes.
|
Player 1
|
||||||
|
Player 2
|
||||||
|
Player 3
|
||||||
|
Player 4
|
||||||
|
|
||||||
|
Your Best | Round 15
|
||||||
|
```
|
||||||
|
|
||||||
|
Each holder has a separate line. The personal best has its own heading below a
|
||||||
|
blank row and uses the map heading's brighter colour and font size. A confirmed
|
||||||
|
empty personal history shows `Your Best | None yet`; a failed lookup shows
|
||||||
|
`Your Best | Unavailable`. There is no background, replay footer, pagination or
|
||||||
|
page counter. The whole view lasts 12 seconds. Opening `.help` pauses its
|
||||||
|
remaining time until help closes. A `.leaders` request during help replaces any
|
||||||
|
pending replay and starts a fresh 12-second view after help closes.
|
||||||
All holders are included; colour/control codes are removed, and overlong
|
All holders are included; colour/control codes are removed, and overlong
|
||||||
individual names are abbreviated without splitting UTF-8 characters. Bounded
|
individual names are abbreviated without splitting UTF-8 characters. The window
|
||||||
lines and additional pages preserve every tied holder. The window closes when
|
closes when the announcement finishes. Both games release their view elements
|
||||||
the announcement finishes. Both games release their view elements when idle or
|
when idle or changing service kinds, and resize the holder list when a new record
|
||||||
changing service kinds. Disconnect or game end cleans up any remaining service
|
has a different number of holders. Disconnect or game end cleans up any remaining
|
||||||
elements.
|
service elements.
|
||||||
|
|
||||||
At round start, reminders become more frequent as the current round approaches
|
At round start, reminders become more frequent as the current round approaches
|
||||||
the latest usable cached record. The cadence is measured back from that record:
|
the latest usable cached record. The cadence is measured back from that record:
|
||||||
@@ -607,10 +624,10 @@ the latest usable cached record. The cadence is measured back from that record:
|
|||||||
|
|
||||||
For a round 50 record, reminders appear on rounds 30, 35, 40, 42, 44 and
|
For a round 50 record, reminders appear on rounds 30, 35, 40, 42, 44 and
|
||||||
45–49, with milestones at 50 and 51. Each uses the existing record header and
|
45–49, with milestones at 50 and 51. Each uses the existing record header and
|
||||||
one short proximity or milestone line for six seconds, without holder pages.
|
one short proximity or milestone line for six seconds, without the holder list.
|
||||||
Milestones describe the round reached; the new result is recorded only after
|
Milestones describe the round reached; the new result is recorded only after
|
||||||
the match ends. The full join announcement and `.leaders` retain all holders
|
the match ends. The full join announcement and `.leaders` retain all holders
|
||||||
and their 12-second pages. `.leaders` remains available after the overtake.
|
in one 12-second view. `.leaders` remains available after the overtake.
|
||||||
|
|
||||||
Reminders require player readiness and skip players with a pending welcome,
|
Reminders require player readiness and skip players with a pending welcome,
|
||||||
help or record view, or a welcome/manual record request in the last 30 seconds.
|
help or record view, or a welcome/manual record request in the last 30 seconds.
|
||||||
@@ -628,10 +645,21 @@ Partial page results never replace the cache. The API has no pagination snapshot
|
|||||||
token: changed totals/record rounds reject the refresh, but concurrent renames can
|
token: changed totals/record rounds reject the refresh, but concurrent renames can
|
||||||
still move entries between pages. Refreshes deduplicate GUIDs.
|
still move entries between pages. Refreshes deduplicate GUIDs.
|
||||||
|
|
||||||
|
Personal lookups use the existing public endpoint’s `q` search, scoped to the
|
||||||
|
resolved map key. Search results can match names or partial GUIDs, so only an
|
||||||
|
exact string GUID with matching game/map record evidence is accepted. Successful
|
||||||
|
personal results cache for five minutes per connection; failures cache as
|
||||||
|
unavailable for 30 seconds. Searches paginate in groups of three and stop
|
||||||
|
starting further pages after 15 seconds; incomplete searches are unavailable.
|
||||||
|
A valid empty map history needs no personal request. No backend or plugin update
|
||||||
|
is needed. Round reminders continue to use only the map cache.
|
||||||
|
|
||||||
Requests have a 15-second GSC wait limit. The TWS plugins also enforce native
|
Requests have a 15-second GSC wait limit. The TWS plugins also enforce native
|
||||||
5-second connection and 15-second transfer limits. The leaderboard waits for native
|
5-second connection and 15-second transfer limits. The leaderboard waits for native
|
||||||
completion before starting another request;
|
completion before starting another request. Personal lookups share this request
|
||||||
player threads never wait directly on HTTP. Responses at the plugins' 20 KiB
|
slot with the map worker, with a 15-second queue wait limit. Disconnect, game
|
||||||
|
end and a newer manual replay cancel the waiting announcement; the level
|
||||||
|
collector still drains any native request already started. Responses at the plugins' 20 KiB
|
||||||
truncation boundary are rejected. The complete JSON grammar, UTF-8, string escapes
|
truncation boundary are rejected. The complete JSON grammar, UTF-8, string escapes
|
||||||
and response shape are checked before consuming data. These guards also reject
|
and response shape are checked before consuming data. These guards also reject
|
||||||
HTML/error bodies. Leaderboard still uses body validation; the new native HTTP
|
HTML/error bodies. Leaderboard still uses body validation; the new native HTTP
|
||||||
@@ -640,14 +668,16 @@ status diagnostics are consumed by telemetry.
|
|||||||
Run the portable data/structure checks with `node tools/check_leaderboard.mjs`.
|
Run the portable data/structure checks with `node tools/check_leaderboard.mjs`.
|
||||||
They execute the scripts' data helpers with engine-value shims, not a game VM.
|
They execute the scripts' data helpers with engine-value shims, not a game VM.
|
||||||
Live checks: join while playing and spectating; die/respawn (no repeat); reconnect
|
Live checks: join while playing and spectating; die/respawn (no repeat); reconnect
|
||||||
(one new announcement); compare Town/TranZit/Grief; check one holder, ties across
|
(one new announcement); compare Town/TranZit/Grief; check one holder, multiple tied holders, no records, and a slow/unavailable/malformed API. Check a personal best
|
||||||
pages, no records, and a slow/unavailable/malformed API. Disconnect or end the game
|
below the map record, a tied holder, a new player, and two players with different
|
||||||
|
bests. Confirm GUID substring/name matches cannot supply someone else’s record,
|
||||||
|
and unavailable lookups never claim an empty history. Disconnect or end the game
|
||||||
mid-announcement and confirm messages stop. Verify existing HUD, `.help`, `.join`
|
mid-announcement and confirm messages stop. Verify existing HUD, `.help`, `.join`
|
||||||
and telemetry still work. Open `.help` during a record page and check that the
|
and telemetry still work. Open `.help` during a record announcement and check that the
|
||||||
record resumes afterwards without overlapping it. Runtime linking and client
|
record resumes afterwards without overlapping it. Runtime linking and client
|
||||||
readiness require Plutonium.
|
readiness require Plutonium.
|
||||||
Try `.leaders` and `.LEADERS` after the announcement ends and during a later
|
Try `.leaders` and `.LEADERS` after the announcement ends and while it is
|
||||||
page: each should start one private announcement from the first page. Repeat
|
visible: each should restart one complete private announcement. Repeat
|
||||||
while help is open, then disconnect or end the game during a replay to check
|
while help is open, then disconnect or end the game during a replay to check
|
||||||
cleanup. Disable the leaderboard module and verify `.leaders` is absent from help.
|
cleanup. Disable the leaderboard module and verify `.leaders` is absent from help.
|
||||||
|
|
||||||
|
|||||||
+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_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_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_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_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+ |
|
| [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.
|
and the stock HUD are unaffected. `.hud` requires the HUD module.
|
||||||
|
|
||||||
Common provides one unboxed upper-right service area below the Plutonium tag.
|
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
|
Records are right aligned: `Map Record | Round N`, every tied holder on its own
|
||||||
holder lines; there is no replay footer or single-page counter. Help displays
|
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
|
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
|
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).
|
[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
|
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
|
(20 with a multi-page counter), one element per holder plus two headings for
|
||||||
counter), or two for compact round-start reminders. Pages reuse elements; switching views or closing the service releases
|
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
|
them. Idle services hold no HUD elements, while the controller retains pending
|
||||||
requests and interruption/resume behavior. Core + AFK use 10 + 2 elements.
|
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
|
`SetTextUnlimited` counter, updated only when the count changes. Deploy the
|
||||||
updated HUD script and restart the map on r5334+; no full server-process
|
updated HUD script and restart the map on r5334+; no full server-process
|
||||||
restart is required. Older Plutonium revisions need client/server updates and
|
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
|
full restarts first. Leaderboard `1.3.1` requires common `1.6.1+` for its
|
||||||
round-start reminders. AFK `2.0.0` requires the matching
|
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).
|
plugin `1.4.0+` and EC reserve service; see the [AFK guide](../../docs/afk.md).
|
||||||
|
|
||||||
## Map record announcements
|
## Map record announcements
|
||||||
|
|
||||||
Copy `tws_t5_leaderboard.gsc` and the updated `tws_t5_common.gsc` to
|
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
|
`storage/t5/scripts/sp/`, then restart the map. Leaderboard `1.3.1` requires
|
||||||
common `1.5.0+`; this script update adds no native-plugin or backend dependency
|
common `1.6.1+`; this script update adds no native-plugin or backend dependency
|
||||||
and requires no full server-process restart. Leaderboard requires a
|
and requires no full server-process restart. Leaderboard requires a
|
||||||
[t5-gsc-utils](https://github.com/alicealys/t5-gsc-utils) build exposing
|
[t5-gsc-utils](https://github.com/alicealys/t5-gsc-utils) build exposing
|
||||||
`http::request`, `jsonParse`, `typeof` and `formatString`. A plugin upgrade
|
`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
|
6 rounds, then every round for the final five, followed by matching/overtake
|
||||||
milestones. After the first overtake, reminders stop for that match; `.leaders`
|
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
|
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
|
Reminders skip busy help/record views, unready players, recent welcome/manual
|
||||||
requests and empty/unavailable/expired records. Help discards a reminder instead
|
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.
|
for cadence, interruption, cleanup and operation without the optional HUD.
|
||||||
|
|
||||||
## Player commands
|
## 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 |
|
| `.afk` | Enter protected AFK using your time reserve |
|
||||||
| `.back` | Return from AFK |
|
| `.back` | Return from AFK |
|
||||||
| `.join` | Join mid-round (newcomers only) |
|
| `.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) |
|
| `.b` | Check bank balance (bank module required) |
|
||||||
| `.d <amount\|all>` | Deposit points (bank module required) |
|
| `.d <amount\|all>` | Deposit points (bank module required) |
|
||||||
| `.w <amount\|all>` | Withdraw 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
|
commands appear on additional pages. Bank entries retain the full
|
||||||
`.d <amount|all>` / `.w <amount|all>` syntax. Removing HUD removes `.hud` from the list.
|
`.d <amount|all>` / `.w <amount|all>` syntax. Removing HUD removes `.hud` from the list.
|
||||||
The enabled leaderboard module registers `.leaders`, which replays the latest
|
The enabled leaderboard module registers `.leaders`, which replays the latest
|
||||||
cached map record privately. Repeating it restarts the record from the first page;
|
map and personal records privately. Repeating it restarts the full 12-second
|
||||||
help pauses the record's remaining page time and resumes it after the final
|
announcement; help pauses the record's remaining time and resumes it after the
|
||||||
help page. `.leaders` during help replaces the pending record replay from page one.
|
final help page. `.leaders` during help replaces the pending record replay.
|
||||||
|
|
||||||
## Dependency graph
|
## Dependency graph
|
||||||
|
|
||||||
@@ -224,7 +230,7 @@ connect listener anywhere else crashes the T5 SP handshake.
|
|||||||
- Spawn-handler registry
|
- Spawn-handler registry
|
||||||
- Connection-handler registry for optional modules that serve spectator newcomers
|
- Connection-handler registry for optional modules that serve spectator newcomers
|
||||||
- `.help` / `.commands` listener and ordered help registry populated by installed modules
|
- `.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;
|
interruption/resume, request replacement and disconnect/game-end cleanup;
|
||||||
display elements exist only for the active view and are released when idle
|
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
|
- 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.
|
`.help` and the remaining commands still work. Restore the scripts afterward.
|
||||||
3. Verify help closes after 12 seconds. Add custom registered commands to test
|
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
|
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
|
with help and verify remaining display time resumes; repeat `.leaders` during
|
||||||
help and verify only the latest replay starts from page one.
|
help and verify only the latest complete replay starts.
|
||||||
4. Reopen after expiry, die/respawn and reconnect; verify no duplicate text or
|
4. Reopen after expiry, die/respawn and reconnect; verify no duplicate text or
|
||||||
leftover elements. Repeat without the HUD module. Follow the complete
|
leftover elements. Repeat without the HUD module. Follow the complete
|
||||||
[HUD validation guide](../../README.md#hud-validation) before rollout.
|
[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.
|
// An undefined snapshot is the explicit unavailable view for manual requests.
|
||||||
show_record(snapshot)
|
show_record(snapshot, personal)
|
||||||
{
|
{
|
||||||
if (isDefined(level.tws_service_ended))
|
if (isDefined(level.tws_service_ended))
|
||||||
return;
|
return;
|
||||||
self service_ensure();
|
self service_ensure();
|
||||||
state = self.tws_service;
|
state = self.tws_service;
|
||||||
service_advance(state, getTime());
|
service_advance(state, getTime());
|
||||||
count = 0;
|
|
||||||
if (isDefined(snapshot))
|
|
||||||
count = snapshot.lines.size;
|
|
||||||
state.generation++;
|
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;
|
state.record.snapshot = snapshot;
|
||||||
service_render(state);
|
service_render(state);
|
||||||
}
|
}
|
||||||
@@ -348,12 +346,21 @@ service_ensure()
|
|||||||
self thread service_end_game(state);
|
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.
|
// Allocate only the active view. Alpha-zero elements still occupy HUD slots.
|
||||||
service_view_create(state, request)
|
service_view_create(state, request)
|
||||||
{
|
{
|
||||||
service_view_destroy(state);
|
service_view_destroy(state);
|
||||||
state.view_kind = request.kind;
|
state.view_kind = request.kind;
|
||||||
state.view_paged = request.pages > 1;
|
state.view_paged = request.kind == "help" && request.pages > 1;
|
||||||
if (state.view_paged)
|
if (state.view_paged)
|
||||||
state.page_counter = service_text(self, state, level.hud_service_right,
|
state.page_counter = service_text(self, state, level.hud_service_right,
|
||||||
level.hud_service_title_y, "right", 1.0, "", level.hud_c_dim);
|
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,
|
state.record_title = service_text(self, state, level.hud_service_right,
|
||||||
level.hud_service_record_y, "right", 1.1, "", level.hud_c_count);
|
level.hud_service_record_y, "right", 1.1, "", level.hud_c_count);
|
||||||
rows = 3;
|
rows = service_record_rows(request);
|
||||||
if (request.kind == "reminder")
|
|
||||||
rows = 1;
|
|
||||||
for (i = 0; i < rows; i++)
|
for (i = 0; i < rows; i++)
|
||||||
state.rows[i] = service_text(self, state, level.hud_service_right,
|
state.rows[i] = service_text(self, state, level.hud_service_right,
|
||||||
level.hud_service_holders_y + i * level.hud_service_holders_pitch,
|
level.hud_service_holders_y + i * level.hud_service_holders_pitch,
|
||||||
"right", 1.05, "", level.hud_c_label);
|
"right", 1.05, "", level.hud_c_label);
|
||||||
if (isDefined(state.page_counter))
|
// Give the personal record its own heading after every holder and a blank row.
|
||||||
state.page_counter.y = level.hud_service_holders_y + 3 * level.hud_service_holders_pitch;
|
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)
|
service_view_destroy(state)
|
||||||
@@ -405,6 +413,8 @@ service_view_destroy(state)
|
|||||||
state.command_rows = [];
|
state.command_rows = [];
|
||||||
state.description_rows = [];
|
state.description_rows = [];
|
||||||
state.record_title = undefined;
|
state.record_title = undefined;
|
||||||
|
state.personal_title = undefined;
|
||||||
|
state.personal_text = undefined;
|
||||||
state.rows = [];
|
state.rows = [];
|
||||||
state.page_text = undefined;
|
state.page_text = undefined;
|
||||||
state.record_header_text = undefined;
|
state.record_header_text = undefined;
|
||||||
@@ -445,8 +455,10 @@ service_render(state)
|
|||||||
service_view_destroy(state);
|
service_view_destroy(state);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
paged = request.pages > 1;
|
paged = request.kind == "help" && request.pages > 1;
|
||||||
if (!isDefined(state.view_kind) || state.view_kind != request.kind || state.view_paged != paged)
|
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);
|
service_view_create(state, request);
|
||||||
for (i = 0; i < state.elems.size; i++)
|
for (i = 0; i < state.elems.size; i++)
|
||||||
state.elems[i].alpha = 0;
|
state.elems[i].alpha = 0;
|
||||||
@@ -482,13 +494,13 @@ service_render(state)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
header = "Map record unavailable";
|
header = "Map Record unavailable";
|
||||||
snapshot = request.snapshot;
|
snapshot = request.snapshot;
|
||||||
if (isDefined(snapshot))
|
if (isDefined(snapshot))
|
||||||
{
|
{
|
||||||
header = "No recorded high round yet";
|
header = "No recorded high round yet";
|
||||||
if (snapshot.round > 0)
|
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)
|
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_header_text = header;
|
||||||
}
|
}
|
||||||
state.record_title.alpha = 1;
|
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)
|
if (!isDefined(snapshot) || snapshot.round == 0)
|
||||||
return;
|
return;
|
||||||
if (request.kind == "reminder")
|
if (request.kind == "reminder")
|
||||||
@@ -504,9 +525,9 @@ service_render(state)
|
|||||||
state.rows[0].alpha = 1;
|
state.rows[0].alpha = 1;
|
||||||
return;
|
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)
|
if (index < snapshot.lines.size)
|
||||||
{
|
{
|
||||||
text = snapshot.lines[index];
|
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 maps\_utility;
|
||||||
#include common_scripts\utility;
|
#include common_scripts\utility;
|
||||||
@@ -25,7 +25,7 @@ init()
|
|||||||
level.tws_lb_cache = undefined;
|
level.tws_lb_cache = undefined;
|
||||||
level.tws_lb_attempt = 0;
|
level.tws_lb_attempt = 0;
|
||||||
level.tws_lb_chase_finished = undefined;
|
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);
|
scripts\sp\tws_t5_common::register_connect_handler(::lb_welcome);
|
||||||
level thread lb_chat_listener();
|
level thread lb_chat_listener();
|
||||||
level thread lb_worker();
|
level thread lb_worker();
|
||||||
@@ -212,14 +212,113 @@ lb_request(manual)
|
|||||||
// This also applies when the cache is unavailable.
|
// This also applies when the cache is unavailable.
|
||||||
if (manual)
|
if (manual)
|
||||||
self notify("tws_lb_requested");
|
self notify("tws_lb_requested");
|
||||||
|
self endon("tws_lb_requested");
|
||||||
self.tws_lb_last_request = getTime();
|
self.tws_lb_last_request = getTime();
|
||||||
snapshot = lb_cached_record();
|
snapshot = lb_cached_record();
|
||||||
if (!isDefined(snapshot) && !manual)
|
if (!isDefined(snapshot) && !manual)
|
||||||
return;
|
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.
|
// 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()
|
lb_refresh()
|
||||||
@@ -239,6 +338,7 @@ lb_refresh()
|
|||||||
return undefined;
|
return undefined;
|
||||||
|
|
||||||
snapshot = spawnStruct();
|
snapshot = spawnStruct();
|
||||||
|
snapshot.key = selection.key;
|
||||||
snapshot.name = lb_display_text(selection.name, 64);
|
snapshot.name = lb_display_text(selection.name, 64);
|
||||||
snapshot.round = 0;
|
snapshot.round = 0;
|
||||||
snapshot.names = [];
|
snapshot.names = [];
|
||||||
@@ -288,9 +388,18 @@ lb_selection()
|
|||||||
return 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)
|
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 = [];
|
||||||
options["method"] = "GET";
|
options["method"] = "GET";
|
||||||
headers = [];
|
headers = [];
|
||||||
@@ -461,28 +570,9 @@ lb_uint(value)
|
|||||||
lb_holder_lines(names)
|
lb_holder_lines(names)
|
||||||
{
|
{
|
||||||
lines = [];
|
lines = [];
|
||||||
line = "";
|
// Each tied player gets a separate row in the full record announcement.
|
||||||
columns = 0;
|
|
||||||
for (i = 0; i < names.size; i++)
|
for (i = 0; i < names.size; i++)
|
||||||
{
|
lines[lines.size] = lb_holder_name(names[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;
|
return lines;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+21
-15
@@ -13,7 +13,7 @@ private server token.
|
|||||||
| `tws_t6_lock.gsc` | Auto-lock at round 20 plus majority `.lock` / `.unlock` votes; clear on game over | Common, HUD |
|
| `tws_t6_lock.gsc` | Auto-lock at round 20 plus majority `.lock` / `.unlock` votes; clear 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_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_join.gsc` | `.join` chat command lets a spectator newcomer enter mid-match once | Common |
|
||||||
| `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_leaderboard.gsc` | Private map and personal records on connection, `.leaders` replay and adaptive round-start reminders | Common 1.6.1+, tws-t6-gsc-utils |
|
||||||
| `tws_t6_bank.gsc` | Persistent shared GUID bank | Common, telemetry, TWS plugin 1.2.0 |
|
| `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+ |
|
| `tws_t6_moderation.gsc` | Optional controller-managed bans, mutes and live moderation | Common 1.2.0+, TWS plugin 1.3.0+ |
|
||||||
|
|
||||||
@@ -96,14 +96,16 @@ continue updating while hidden; game-over values remain frozen. Help, records
|
|||||||
and the stock HUD are unaffected. `.hud` requires the HUD module.
|
and the stock HUD are unaffected. `.hud` requires the HUD module.
|
||||||
|
|
||||||
Common provides one unboxed upper-right service area below the Plutonium tag.
|
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
|
Records are right aligned: `Map Record | Round N`, every tied holder on its own
|
||||||
holder lines; there is no replay footer or single-page counter. Help displays
|
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
|
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
|
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).
|
[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
|
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
|
(20 with a multi-page counter), one element per holder plus two headings for
|
||||||
counter), or two for compact round-start reminders. Pages reuse elements; switching views or closing the service releases
|
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
|
them. Idle services hold no HUD elements, while the controller retains pending
|
||||||
requests and interruption/resume behavior. Core + AFK use 10 + 2 elements.
|
requests and interruption/resume behavior. Core + AFK use 10 + 2 elements.
|
||||||
|
|
||||||
@@ -112,8 +114,8 @@ HUD `1.4.2` requires common `1.4.0+` and
|
|||||||
`SetTextUnlimited` counter, updated only when the count changes. Deploy the
|
`SetTextUnlimited` counter, updated only when the count changes. Deploy the
|
||||||
updated HUD script and restart the map on r5334+; no full server-process
|
updated HUD script and restart the map on r5334+; no full server-process
|
||||||
restart is required. Older Plutonium revisions need client/server updates and
|
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
|
full restarts first. Leaderboard `1.3.1` requires common `1.6.1+` for its
|
||||||
round-start reminders. AFK `2.0.0` requires the matching
|
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).
|
plugin `1.4.0+` and EC reserve service; see the [AFK guide](../../docs/afk.md).
|
||||||
|
|
||||||
## Map record announcements
|
## Map record announcements
|
||||||
@@ -122,7 +124,7 @@ 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
|
It works independently of telemetry and HUD, and privately announces records
|
||||||
once per connection, including newcomers waiting in spectator mode.
|
once per connection, including newcomers waiting in spectator mode.
|
||||||
|
|
||||||
Leaderboard `1.2.0` requires common `1.5.0+`. Copy both scripts to
|
Leaderboard `1.3.1` requires common `1.6.1+`. Copy both scripts to
|
||||||
`Plutonium/storage/t6/scripts/zm/` and restart the map. This script update adds
|
`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
|
no native-plugin or backend dependency and requires no full server-process
|
||||||
restart when the existing plugin is installed.
|
restart when the existing plugin is installed.
|
||||||
@@ -131,10 +133,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
|
6 rounds, then every round for the final five, followed by matching/overtake
|
||||||
milestones. After the first overtake, reminders stop for that match; `.leaders`
|
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
|
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
|
Reminders skip busy help/record views, unready players, recent welcome/manual
|
||||||
requests and empty/unavailable/expired records. Help discards a reminder instead
|
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.
|
for cadence, interruption, cleanup and operation without the optional HUD.
|
||||||
|
|
||||||
Map selection uses stock `level.scr_zm_ui_gametype` and
|
Map selection uses stock `level.scr_zm_ui_gametype` and
|
||||||
@@ -168,7 +174,7 @@ explicit row positions so T5 does not concatenate newline-separated text.
|
|||||||
| `.afk` | Enter protected AFK using your time reserve |
|
| `.afk` | Enter protected AFK using your time reserve |
|
||||||
| `.back` | Return from AFK |
|
| `.back` | Return from AFK |
|
||||||
| `.join` | Join mid-round (newcomers only) |
|
| `.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) |
|
| `.b` | Check bank balance (bank module required) |
|
||||||
| `.d <amount\|all>` | Deposit points (bank module required) |
|
| `.d <amount\|all>` | Deposit points (bank module required) |
|
||||||
| `.w <amount\|all>` | Withdraw points (bank module required) |
|
| `.w <amount\|all>` | Withdraw points (bank module required) |
|
||||||
@@ -184,9 +190,9 @@ 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`
|
the full `.d <amount|all>` / `.w <amount|all>` syntax. Removing HUD removes `.hud`
|
||||||
from the list.
|
from the list.
|
||||||
The enabled leaderboard module registers `.leaders`, which replays the latest
|
The enabled leaderboard module registers `.leaders`, which replays the latest
|
||||||
cached map record privately. Repeating it restarts the record from the first page;
|
map and personal records privately. Repeating it restarts the full 12-second
|
||||||
help pauses the record's remaining page time and resumes it after the final
|
announcement; help pauses the record's remaining time and resumes it after the
|
||||||
help page. `.leaders` during help replaces the pending record replay from page one.
|
final help page. `.leaders` during help replaces the pending record replay.
|
||||||
|
|
||||||
## Defaults, matching T5
|
## Defaults, matching T5
|
||||||
|
|
||||||
@@ -288,7 +294,7 @@ another mod installed one first.
|
|||||||
## Validation
|
## Validation
|
||||||
|
|
||||||
Run the shared [portable and live HUD checks](../../README.md#hud-validation)
|
Run the shared [portable and live HUD checks](../../README.md#hud-validation)
|
||||||
for core placement, service pagination, record interruptions and command regressions.
|
for core placement, help pagination, record interruptions and command regressions.
|
||||||
|
|
||||||
To compile all nine scripts for T6 PC with gsc-tool:
|
To compile all nine scripts for T6 PC with gsc-tool:
|
||||||
|
|
||||||
|
|||||||
@@ -175,18 +175,16 @@ show_help()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// An undefined snapshot is the explicit unavailable view for manual requests.
|
// An undefined snapshot is the explicit unavailable view for manual requests.
|
||||||
show_record(snapshot)
|
show_record(snapshot, personal)
|
||||||
{
|
{
|
||||||
if (isDefined(level.tws_service_ended))
|
if (isDefined(level.tws_service_ended))
|
||||||
return;
|
return;
|
||||||
self service_ensure();
|
self service_ensure();
|
||||||
state = self.tws_service;
|
state = self.tws_service;
|
||||||
service_advance(state, getTime());
|
service_advance(state, getTime());
|
||||||
count = 0;
|
|
||||||
if (isDefined(snapshot))
|
|
||||||
count = snapshot.lines.size;
|
|
||||||
state.generation++;
|
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;
|
state.record.snapshot = snapshot;
|
||||||
service_render(state);
|
service_render(state);
|
||||||
}
|
}
|
||||||
@@ -344,12 +342,21 @@ service_ensure()
|
|||||||
self thread service_end_game(state);
|
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.
|
// Allocate only the active view. Alpha-zero elements still occupy HUD slots.
|
||||||
service_view_create(state, request)
|
service_view_create(state, request)
|
||||||
{
|
{
|
||||||
service_view_destroy(state);
|
service_view_destroy(state);
|
||||||
state.view_kind = request.kind;
|
state.view_kind = request.kind;
|
||||||
state.view_paged = request.pages > 1;
|
state.view_paged = request.kind == "help" && request.pages > 1;
|
||||||
if (state.view_paged)
|
if (state.view_paged)
|
||||||
state.page_counter = service_text(self, state, level.hud_service_right,
|
state.page_counter = service_text(self, state, level.hud_service_right,
|
||||||
level.hud_service_title_y, "right", 1.0, "", level.hud_c_dim);
|
level.hud_service_title_y, "right", 1.0, "", level.hud_c_dim);
|
||||||
@@ -375,15 +382,16 @@ service_view_create(state, request)
|
|||||||
|
|
||||||
state.record_title = service_text(self, state, level.hud_service_right,
|
state.record_title = service_text(self, state, level.hud_service_right,
|
||||||
level.hud_service_record_y, "right", 1.1, "", level.hud_c_count);
|
level.hud_service_record_y, "right", 1.1, "", level.hud_c_count);
|
||||||
rows = 3;
|
rows = service_record_rows(request);
|
||||||
if (request.kind == "reminder")
|
|
||||||
rows = 1;
|
|
||||||
for (i = 0; i < rows; i++)
|
for (i = 0; i < rows; i++)
|
||||||
state.rows[i] = service_text(self, state, level.hud_service_right,
|
state.rows[i] = service_text(self, state, level.hud_service_right,
|
||||||
level.hud_service_holders_y + i * level.hud_service_holders_pitch,
|
level.hud_service_holders_y + i * level.hud_service_holders_pitch,
|
||||||
"right", 1.05, "", level.hud_c_label);
|
"right", 1.05, "", level.hud_c_label);
|
||||||
if (isDefined(state.page_counter))
|
// Give the personal record its own heading after every holder and a blank row.
|
||||||
state.page_counter.y = level.hud_service_holders_y + 3 * level.hud_service_holders_pitch;
|
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)
|
service_view_destroy(state)
|
||||||
@@ -401,6 +409,8 @@ service_view_destroy(state)
|
|||||||
state.command_rows = [];
|
state.command_rows = [];
|
||||||
state.description_rows = [];
|
state.description_rows = [];
|
||||||
state.record_title = undefined;
|
state.record_title = undefined;
|
||||||
|
state.personal_title = undefined;
|
||||||
|
state.personal_text = undefined;
|
||||||
state.rows = [];
|
state.rows = [];
|
||||||
state.page_text = undefined;
|
state.page_text = undefined;
|
||||||
state.record_header_text = undefined;
|
state.record_header_text = undefined;
|
||||||
@@ -441,8 +451,10 @@ service_render(state)
|
|||||||
service_view_destroy(state);
|
service_view_destroy(state);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
paged = request.pages > 1;
|
paged = request.kind == "help" && request.pages > 1;
|
||||||
if (!isDefined(state.view_kind) || state.view_kind != request.kind || state.view_paged != paged)
|
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);
|
service_view_create(state, request);
|
||||||
for (i = 0; i < state.elems.size; i++)
|
for (i = 0; i < state.elems.size; i++)
|
||||||
state.elems[i].alpha = 0;
|
state.elems[i].alpha = 0;
|
||||||
@@ -478,13 +490,13 @@ service_render(state)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
header = "Map record unavailable";
|
header = "Map Record unavailable";
|
||||||
snapshot = request.snapshot;
|
snapshot = request.snapshot;
|
||||||
if (isDefined(snapshot))
|
if (isDefined(snapshot))
|
||||||
{
|
{
|
||||||
header = "No recorded high round yet";
|
header = "No recorded high round yet";
|
||||||
if (snapshot.round > 0)
|
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)
|
if (!isDefined(state.record_header_text) || state.record_header_text != header)
|
||||||
{
|
{
|
||||||
@@ -492,6 +504,15 @@ service_render(state)
|
|||||||
state.record_header_text = header;
|
state.record_header_text = header;
|
||||||
}
|
}
|
||||||
state.record_title.alpha = 1;
|
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)
|
if (!isDefined(snapshot) || snapshot.round == 0)
|
||||||
return;
|
return;
|
||||||
if (request.kind == "reminder")
|
if (request.kind == "reminder")
|
||||||
@@ -500,9 +521,9 @@ service_render(state)
|
|||||||
state.rows[0].alpha = 1;
|
state.rows[0].alpha = 1;
|
||||||
return;
|
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)
|
if (index < snapshot.lines.size)
|
||||||
{
|
{
|
||||||
text = snapshot.lines[index];
|
text = snapshot.lines[index];
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/* tws_t6_leaderboard.gsc - private map records, round reminders and .leaders replay. */
|
/* tws_t6_leaderboard.gsc - private map/personal records, round reminders and .leaders replay. */
|
||||||
|
|
||||||
#include maps\mp\_utility;
|
#include maps\mp\_utility;
|
||||||
#include common_scripts\utility;
|
#include common_scripts\utility;
|
||||||
@@ -27,7 +27,7 @@ init()
|
|||||||
level.tws_lb_cache = undefined;
|
level.tws_lb_cache = undefined;
|
||||||
level.tws_lb_attempt = 0;
|
level.tws_lb_attempt = 0;
|
||||||
level.tws_lb_chase_finished = undefined;
|
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_help_command(".leaders", "Show map and personal records");
|
||||||
scripts\zm\tws_t6_common::register_connect_handler(::lb_welcome);
|
scripts\zm\tws_t6_common::register_connect_handler(::lb_welcome);
|
||||||
level thread lb_chat_listener();
|
level thread lb_chat_listener();
|
||||||
level thread lb_worker();
|
level thread lb_worker();
|
||||||
@@ -215,14 +215,113 @@ lb_request(manual)
|
|||||||
// This also applies when the cache is unavailable.
|
// This also applies when the cache is unavailable.
|
||||||
if (manual)
|
if (manual)
|
||||||
self notify("tws_lb_requested");
|
self notify("tws_lb_requested");
|
||||||
|
self endon("tws_lb_requested");
|
||||||
self.tws_lb_last_request = getTime();
|
self.tws_lb_last_request = getTime();
|
||||||
snapshot = lb_cached_record();
|
snapshot = lb_cached_record();
|
||||||
if (!isDefined(snapshot) && !manual)
|
if (!isDefined(snapshot) && !manual)
|
||||||
return;
|
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.
|
// Undefined snapshots produce a private unavailable page for manual replay.
|
||||||
self scripts\zm\tws_t6_common::show_record(snapshot);
|
self scripts\zm\tws_t6_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.guid;
|
||||||
|
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()
|
lb_refresh()
|
||||||
@@ -242,6 +341,7 @@ lb_refresh()
|
|||||||
return undefined;
|
return undefined;
|
||||||
|
|
||||||
snapshot = spawnStruct();
|
snapshot = spawnStruct();
|
||||||
|
snapshot.key = selection.key;
|
||||||
snapshot.name = lb_display_text(selection.name, 64);
|
snapshot.name = lb_display_text(selection.name, 64);
|
||||||
snapshot.round = 0;
|
snapshot.round = 0;
|
||||||
snapshot.names = [];
|
snapshot.names = [];
|
||||||
@@ -336,9 +436,18 @@ lb_t6_selection(selection, mode, location)
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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)
|
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 = [];
|
||||||
options["method"] = "GET";
|
options["method"] = "GET";
|
||||||
headers = [];
|
headers = [];
|
||||||
@@ -510,28 +619,9 @@ lb_uint(value)
|
|||||||
lb_holder_lines(names)
|
lb_holder_lines(names)
|
||||||
{
|
{
|
||||||
lines = [];
|
lines = [];
|
||||||
line = "";
|
// Each tied player gets a separate row in the full record announcement.
|
||||||
columns = 0;
|
|
||||||
for (i = 0; i < names.size; i++)
|
for (i = 0; i < names.size; i++)
|
||||||
{
|
lines[lines.size] = lb_holder_name(names[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;
|
return lines;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+65
-35
@@ -132,7 +132,7 @@ function engine(game, count = 8, withHud = false) {
|
|||||||
c.hud_connect(); c.afk_hud_create();
|
c.hud_connect(); c.afk_hud_create();
|
||||||
},
|
},
|
||||||
addPlayer() { const player = { sessionstate: 'spectator' }; players.add(player); return player; },
|
addPlayer() { const player = { sessionstate: 'spectator' }; players.add(player); return player; },
|
||||||
help: () => c.show_help(), record: snapshot => c.show_record(snapshot),
|
help: () => c.show_help(), record: (snapshot, personal) => c.show_record(snapshot, personal),
|
||||||
reminder: (snapshot, message) => c.show_record_reminder(snapshot, message),
|
reminder: (snapshot, message) => c.show_record_reminder(snapshot, message),
|
||||||
event: name => emit(self, name), end: name => emit(level, name),
|
event: name => emit(self, name), end: name => emit(level, name),
|
||||||
get state() { return self.tws_service; },
|
get state() { return self.tws_service; },
|
||||||
@@ -295,18 +295,18 @@ for (const game of ['t5', 't6']) {
|
|||||||
assert.equal(e.elements.length, count);
|
assert.equal(e.elements.length, count);
|
||||||
assert.equal(e.live.length, 0);
|
assert.equal(e.live.length, 0);
|
||||||
});
|
});
|
||||||
check(`${game} help pauses record time and resumes the same page`, () => {
|
check(`${game} help pauses the complete record and resumes its remaining time`, () => {
|
||||||
const e = engine(game);
|
const e = engine(game);
|
||||||
e.record(snapshot(['One', 'Two', 'Three', 'Four']));
|
e.record(snapshot(['One', 'Two', 'Three', 'Four']));
|
||||||
e.advance(15000);
|
e.advance(3000);
|
||||||
assert.equal(e.state.record.page, 1);
|
assert.equal(e.state.record.page, 0);
|
||||||
const remaining = e.state.record.remaining;
|
const remaining = e.state.record.remaining;
|
||||||
e.help();
|
e.help();
|
||||||
e.advance(12000);
|
e.advance(12000);
|
||||||
assert.equal(e.state.help, undefined);
|
assert.equal(e.state.help, undefined);
|
||||||
assert.equal(e.state.record.page, 1);
|
assert.equal(e.state.record.page, 0);
|
||||||
assert.equal(e.state.record.remaining, remaining);
|
assert.equal(e.state.record.remaining, remaining);
|
||||||
assert.equal(e.state.rows[0].text, 'Four');
|
assert.deepEqual(Array.from(e.state.rows, row => row.text), ['One', 'Two', 'Three', 'Four']);
|
||||||
assert.equal(e.live.length, 5);
|
assert.equal(e.live.length, 5);
|
||||||
const resumedTexts = e.textCalls;
|
const resumedTexts = e.textCalls;
|
||||||
e.c.service_render(e.state);
|
e.c.service_render(e.state);
|
||||||
@@ -344,7 +344,7 @@ for (const game of ['t5', 't6']) {
|
|||||||
assert.equal(request.kind, 'reminder');
|
assert.equal(request.kind, 'reminder');
|
||||||
assert.equal(request.pages, 1);
|
assert.equal(request.pages, 1);
|
||||||
assert.equal(request.remaining, 6000);
|
assert.equal(request.remaining, 6000);
|
||||||
assert.equal(e.state.record_title.text, 'Map record | Round 82');
|
assert.equal(e.state.record_title.text, 'Map Record | Round 82');
|
||||||
assert.equal(e.state.rows.length, 1);
|
assert.equal(e.state.rows.length, 1);
|
||||||
assert.equal(e.state.rows[0].text, '5 rounds to tie');
|
assert.equal(e.state.rows[0].text, '5 rounds to tie');
|
||||||
assert.equal(e.live.length, 2);
|
assert.equal(e.live.length, 2);
|
||||||
@@ -416,16 +416,14 @@ for (const game of ['t5', 't6']) {
|
|||||||
e.record(snapshot(['One', 'Two', 'Three', 'Four']));
|
e.record(snapshot(['One', 'Two', 'Three', 'Four']));
|
||||||
assert.equal(e.state.record.kind, 'record');
|
assert.equal(e.state.record.kind, 'record');
|
||||||
assert.equal(e.state.record.remaining, 12000);
|
assert.equal(e.state.record.remaining, 12000);
|
||||||
assert.equal(e.state.record.pages, 2);
|
assert.equal(e.state.record.pages, 1);
|
||||||
assert.equal(e.live.length, 5);
|
assert.equal(e.live.length, 5);
|
||||||
assert(replaced.every(elem => elem.destroyCalls === 1));
|
assert(replaced.every(elem => elem.destroyCalls === 1));
|
||||||
e.advance(100);
|
e.advance(100);
|
||||||
assert.equal(e.state.record.page, 0, 'old reminder expiry cannot dismiss manual replay');
|
assert.equal(e.state.record.page, 0, 'old reminder expiry cannot dismiss manual replay');
|
||||||
assert.equal(e.state.record.remaining, 11900);
|
assert.equal(e.state.record.remaining, 11900);
|
||||||
e.advance(11900);
|
e.advance(11900);
|
||||||
assert.equal(e.state.record.page, 1);
|
assert.equal(e.state.record, undefined);
|
||||||
assert.equal(e.state.rows[0].text, 'Four');
|
|
||||||
e.advance(12000);
|
|
||||||
assert.equal(e.live.length, 0);
|
assert.equal(e.live.length, 0);
|
||||||
});
|
});
|
||||||
check(`${game} invalid reminders do not initialize a service or allocate elements`, () => {
|
check(`${game} invalid reminders do not initialize a service or allocate elements`, () => {
|
||||||
@@ -451,35 +449,67 @@ for (const game of ['t5', 't6']) {
|
|||||||
assert.equal(e.elements.length, 2);
|
assert.equal(e.elements.length, 2);
|
||||||
assert(e.elements.every(elem => elem.destroyCalls === 1));
|
assert(e.elements.every(elem => elem.destroyCalls === 1));
|
||||||
});
|
});
|
||||||
check(`${game} unavailable, empty and paged records rebuild only when their allocation changes`, () => {
|
check(`${game} all holders precede a separate personal heading without record pages`, () => {
|
||||||
|
const e = engine(game);
|
||||||
|
e.record(snapshot(['Player 1', 'Player 2', 'Player 3', 'Player 4']), 'Your Best | Round 15');
|
||||||
|
assert.equal(e.state.record.pages, 1);
|
||||||
|
assert.equal(e.live.length, 6);
|
||||||
|
assertCounter(e, 1, 1);
|
||||||
|
assert.deepEqual(e.visible.map(x => x.text), [
|
||||||
|
'Map Record | Round 82', 'Player 1', 'Player 2', 'Player 3', 'Player 4', 'Your Best | Round 15',
|
||||||
|
]);
|
||||||
|
const personal = e.state.personal_title, header = e.state.record_title;
|
||||||
|
assert.equal(personal.y, 150, 'leave a blank row after all four holders');
|
||||||
|
assert.deepEqual(personal.color, header.color);
|
||||||
|
assert.equal(personal.fontScale, header.fontScale);
|
||||||
|
assert.notDeepEqual(personal.color, e.state.rows[0].color, 'personal best uses the heading style');
|
||||||
|
e.advance(11000);
|
||||||
|
assert.equal(e.state.personal_title, personal);
|
||||||
|
assert.equal(e.visible.length, 6, 'all holders stay visible until dismissal');
|
||||||
|
e.advance(1000);
|
||||||
|
assert.equal(e.live.length, 0);
|
||||||
|
e.record({ round: 0, lines: [] }, 'Your Best | None yet');
|
||||||
|
assert.equal(e.state.personal_title.text, 'Your Best | None yet');
|
||||||
|
assert.equal(e.state.personal_title.alpha, 1);
|
||||||
|
e.record(undefined, 'Your Best | Unavailable');
|
||||||
|
assert.equal(e.state.personal_title.text, 'Your Best | Unavailable');
|
||||||
|
assert.equal(e.state.personal_title.alpha, 1);
|
||||||
|
e.record(snapshot(['A', 'B', 'C']), 'Your Best | Round 10');
|
||||||
|
e.advance(2000); e.help(); e.advance(12000);
|
||||||
|
assert.equal(e.state.record.remaining, 10000);
|
||||||
|
assert.equal(e.state.personal_title.text, 'Your Best | Round 10');
|
||||||
|
e.record(snapshot(['A', 'B', 'C']));
|
||||||
|
assert.equal(e.state.personal_title, undefined, 'legacy callers clear the personal heading');
|
||||||
|
e.event('disconnect');
|
||||||
|
assert.equal(e.live.length, 0);
|
||||||
|
});
|
||||||
|
check(`${game} record allocation follows the full holder count and reuses unchanged layouts`, () => {
|
||||||
const e = engine(game);
|
const e = engine(game);
|
||||||
e.record(undefined);
|
e.record(undefined);
|
||||||
assert.equal(e.state.record_title.alpha, 1);
|
assert.equal(e.state.record_title.alpha, 1);
|
||||||
assert.equal(e.state.record_title.text, 'Map record unavailable');
|
assert.equal(e.state.record_title.text, 'Map Record unavailable');
|
||||||
assert.equal(e.live.length, 4);
|
assert.equal(e.live.length, 1);
|
||||||
assertCounter(e, 1, 1);
|
assertCounter(e, 1, 1);
|
||||||
const first = [...e.live];
|
const first = [...e.live];
|
||||||
e.record({ round: 0, lines: [] });
|
e.record({ round: 0, lines: [] });
|
||||||
assert.equal(e.state.record_title.text, 'No recorded high round yet');
|
assert.equal(e.state.record_title.text, 'No recorded high round yet');
|
||||||
assert.deepEqual(e.live, first);
|
assert.deepEqual(e.live, first);
|
||||||
|
e.record(snapshot(['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']));
|
||||||
|
assert(first.every(elem => elem.destroyCalls === 1), 'holder count changes release the old view first');
|
||||||
|
assert.equal(e.live.length, 9);
|
||||||
assertCounter(e, 1, 1);
|
assertCounter(e, 1, 1);
|
||||||
e.record(snapshot(['A', 'B', 'C', 'D']));
|
const full = [...e.live];
|
||||||
assert(first.every(elem => elem.destroyCalls === 1), 'pagedness changes release the old view first');
|
e.advance(1000);
|
||||||
assert.equal(e.state.record_title.text, 'Map record | Round 82');
|
assert(e.state.rows.every(row => row.alpha === 1));
|
||||||
assert.equal(e.live.length, 5);
|
e.record(snapshot(['I', 'J', 'K', 'L', 'M', 'N', 'O', 'P']));
|
||||||
const paged = [...e.live];
|
assert.deepEqual(e.live, full);
|
||||||
assertCounter(e, 1, 2);
|
assert.deepEqual(Array.from(e.state.rows, row => row.text), ['I', 'J', 'K', 'L', 'M', 'N', 'O', 'P']);
|
||||||
e.advance(12000);
|
e.record(snapshot(['Single holder']));
|
||||||
assert.deepEqual(Array.from(e.state.rows, row => row.alpha), [1, 0, 0]);
|
assert(full.every(elem => elem.destroyCalls === 1));
|
||||||
assert.equal(e.state.rows[0].text, 'D');
|
|
||||||
assertCounter(e, 2, 2);
|
|
||||||
assert.deepEqual(e.live, paged, 'page changes reuse the same allocation');
|
|
||||||
e.record(snapshot(['Single page']));
|
|
||||||
assert(paged.every(elem => elem.destroyCalls === 1));
|
|
||||||
assertCounter(e, 1, 1);
|
assertCounter(e, 1, 1);
|
||||||
assert.equal(e.state.rows[0].text, 'Single page');
|
assert.equal(e.state.rows[0].text, 'Single holder');
|
||||||
assert.equal(e.live.length, 4);
|
assert.equal(e.live.length, 2);
|
||||||
assert.equal(e.peakLive, 5, 'new allocations never overlap the released view');
|
assert.equal(e.peakLive, 9, 'new allocations never overlap the released view');
|
||||||
assertTextOnly(e);
|
assertTextOnly(e);
|
||||||
});
|
});
|
||||||
for (const event of ['end_game', 'game_ended', 'intermission', 'disconnect']) check(`${game} ${event} cleans active and pending pages once`, () => {
|
for (const event of ['end_game', 'game_ended', 'intermission', 'disconnect']) check(`${game} ${event} cleans active and pending pages once`, () => {
|
||||||
@@ -512,11 +542,11 @@ for (const game of ['t5', 't6']) {
|
|||||||
e.advance(11000);
|
e.advance(11000);
|
||||||
assert.equal(e.textCalls, calls);
|
assert.equal(e.textCalls, calls);
|
||||||
assert.equal(e.elements.length, count);
|
assert.equal(e.elements.length, count);
|
||||||
assert.equal(e.state.record_title.text, 'Map record | Round 82');
|
assert.equal(e.state.record_title.text, 'Map Record | Round 82');
|
||||||
assert.equal(e.state.commands[3].command, '.join');
|
assert.equal(e.state.commands[3].command, '.join');
|
||||||
assert.match(e.c.help_description(e.state.commands[3]), /newcomer/);
|
assert.match(e.c.help_description(e.state.commands[3]), /newcomer/);
|
||||||
assert.equal(e.c.help_command_label(e.state.commands[6].command), '.d <amount|all>');
|
assert.equal(e.c.help_command_label(e.state.commands[6].command), '.d <amount|all>');
|
||||||
assert.equal(e.elements.length, 4, 'record allocation includes all hidden holder rows');
|
assert.equal(e.elements.length, 2, 'record allocation includes only the header and actual holders');
|
||||||
assertTextOnly(e);
|
assertTextOnly(e);
|
||||||
});
|
});
|
||||||
check(`${game} service coordinates clear the watermark and stock lower-right HUD`, () => {
|
check(`${game} service coordinates clear the watermark and stock lower-right HUD`, () => {
|
||||||
@@ -606,7 +636,7 @@ for (const game of ['t5', 't6']) {
|
|||||||
assert.equal(e.textCalls, calls);
|
assert.equal(e.textCalls, calls);
|
||||||
e.record(snapshot()); e.advance(1000);
|
e.record(snapshot()); e.advance(1000);
|
||||||
assert(rows.every(row => row.destroyCalls === 1));
|
assert(rows.every(row => row.destroyCalls === 1));
|
||||||
assert.equal(e.live.length, 4);
|
assert.equal(e.live.length, 2);
|
||||||
e.help();
|
e.help();
|
||||||
assert.equal(e.live.length, 19);
|
assert.equal(e.live.length, 19);
|
||||||
assert(e.state.command_rows.every(row => !rows.includes(row)));
|
assert(e.state.command_rows.every(row => !rows.includes(row)));
|
||||||
@@ -677,7 +707,7 @@ check('T5 service transitions preserve actual core element identities, styles, d
|
|||||||
assert.equal(e.live.length, 12);
|
assert.equal(e.live.length, 12);
|
||||||
assert.deepEqual(state(), initial);
|
assert.deepEqual(state(), initial);
|
||||||
for (let i = 0; i < 4; i++) {
|
for (let i = 0; i < 4; i++) {
|
||||||
e.record(snapshot(['One', 'Two', 'Three', 'Four'])); e.advance(15000); e.help();
|
e.record(snapshot(['One', 'Two', 'Three', 'Four'])); e.advance(3000); e.help();
|
||||||
assert.equal(e.live.length, 31, '10 core plus two AFK plus 19 help elements; this is not an engine pool-limit assertion');
|
assert.equal(e.live.length, 31, '10 core plus two AFK plus 19 help elements; this is not an engine pool-limit assertion');
|
||||||
assert.equal(e.live.filter(elem => elem.archived === false).length, 19);
|
assert.equal(e.live.filter(elem => elem.archived === false).length, 19);
|
||||||
e.c.toggle_core_hud();
|
e.c.toggle_core_hud();
|
||||||
@@ -688,7 +718,7 @@ check('T5 service transitions preserve actual core element identities, styles, d
|
|||||||
e.advance(12000);
|
e.advance(12000);
|
||||||
assert.equal(e.state.rows[0].text, 'Latest');
|
assert.equal(e.state.rows[0].text, 'Latest');
|
||||||
assert.equal(e.state.record.remaining, 12000);
|
assert.equal(e.state.record.remaining, 12000);
|
||||||
assert.equal(e.live.length, 16);
|
assert.equal(e.live.length, 14);
|
||||||
assert(core.every(elem => elem.alpha === 0 && elem.destroyCalls === 0));
|
assert(core.every(elem => elem.alpha === 0 && elem.destroyCalls === 0));
|
||||||
e.c.toggle_core_hud();
|
e.c.toggle_core_hud();
|
||||||
assert.deepEqual(state(), initial);
|
assert.deepEqual(state(), initial);
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ function functions(source) {
|
|||||||
let body = protectedSource.slice(headers.lastIndex, end - 1);
|
let body = protectedSource.slice(headers.lastIndex, end - 1);
|
||||||
// Each GSC function has its own implicit locals, including recursion.
|
// Each GSC function has its own implicit locals, including recursion.
|
||||||
const locals = new Set([...body.matchAll(/(?<![\w.])(\w+)\s*(?:[+*/%-]?=(?!=)|\+\+|--)/g)].map((x) => x[1]));
|
const locals = new Set([...body.matchAll(/(?<![\w.])(\w+)\s*(?:[+*/%-]?=(?!=)|\+\+|--)/g)].map((x) => x[1]));
|
||||||
const coroutine = match[1] === 'lb_round_watcher';
|
const coroutine = ['lb_round_watcher', 'lb_get'].includes(match[1]);
|
||||||
body = body.replace(/\btypeof\(/g, 'gscType(').replace(/\.size\b/g, '.length')
|
body = body.replace(/player getGuid\(/g, 'player.getGuid(').replace(/\btypeof\(/g, 'gscType(').replace(/\.size\b/g, '.length')
|
||||||
.replace(/\bwait\s+([\d.]+);/g, coroutine ? 'yield { milliseconds: $1 * 1000 };' : ';');
|
.replace(/\bwait\s+([\d.]+);/g, coroutine ? 'yield { milliseconds: $1 * 1000 };' : ';');
|
||||||
if (coroutine)
|
if (coroutine)
|
||||||
body = body.replace(/level waittill\(([^;]+)\);/g, 'yield { entity: level, event: $1 };');
|
body = body.replace(/level waittill\(([^;]+)\);/g, 'yield { entity: level, event: $1 };');
|
||||||
@@ -127,7 +127,7 @@ function roundRuntime(game) {
|
|||||||
return { ...e, players, attempts, shown, records, notifications, endEvents,
|
return { ...e, players, attempts, shown, records, notifications, endEvents,
|
||||||
get now() { return e.now; },
|
get now() { return e.now; },
|
||||||
readyPlayer(fields = {}) {
|
readyPlayer(fields = {}) {
|
||||||
const player = { sessionstate: 'playing', tws_lb_last_request: e.now - 30000, ...fields };
|
const player = { getGuid: () => undefined, sessionstate: 'playing', tws_lb_last_request: e.now - 30000, ...fields };
|
||||||
players.push(player);
|
players.push(player);
|
||||||
return player;
|
return player;
|
||||||
},
|
},
|
||||||
@@ -188,7 +188,7 @@ for (const game of ['t5', 't6']) {
|
|||||||
});
|
});
|
||||||
check(`${game} common owns the record service window`, () => {
|
check(`${game} common owns the record service window`, () => {
|
||||||
const commonPath = game === 't5' ? 'scripts\\sp\\tws_t5_common' : 'scripts\\zm\\tws_t6_common';
|
const commonPath = game === 't5' ? 'scripts\\sp\\tws_t5_common' : 'scripts\\zm\\tws_t6_common';
|
||||||
assert(source.includes(`self ${commonPath}::show_record(snapshot);`));
|
assert(source.includes(`self ${commonPath}::show_record(snapshot, personal);`));
|
||||||
assert(!source.includes('lb_show_hud'));
|
assert(!source.includes('lb_show_hud'));
|
||||||
assert(!source.includes('lb_hud_'));
|
assert(!source.includes('lb_hud_'));
|
||||||
assert(!source.includes('tws_help_elems'));
|
assert(!source.includes('tws_help_elems'));
|
||||||
@@ -198,7 +198,7 @@ for (const game of ['t5', 't6']) {
|
|||||||
});
|
});
|
||||||
check(`${game} manual missing/stale requests cancel welcome and reach the service`, () => {
|
check(`${game} manual missing/stale requests cancel welcome and reach the service`, () => {
|
||||||
const calls = [];
|
const calls = [];
|
||||||
c.self = { notify: (event) => calls.push(['notify', event]) };
|
c.self = { getGuid: () => undefined, notify: (event) => calls.push(['notify', event]) };
|
||||||
c.recordService = (snapshot) => calls.push(['record', snapshot]);
|
c.recordService = (snapshot) => calls.push(['record', snapshot]);
|
||||||
const request = all.get('lb_request')
|
const request = all.get('lb_request')
|
||||||
.replace(/\b(?:self|level) endon\([^;]*\);/g, '')
|
.replace(/\b(?:self|level) endon\([^;]*\);/g, '')
|
||||||
@@ -406,6 +406,88 @@ for (const game of ['t5', 't6']) {
|
|||||||
assert.equal(c.lb_page_valid(value, 0, 1), false);
|
assert.equal(c.lb_page_valid(value, 0, 1), false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
check(`${game} HTTP callers serialize and native timeouts retain the slot until collection`, () => {
|
||||||
|
const e = engine(game), c = e.context;
|
||||||
|
const started = [], pending = [];
|
||||||
|
c.httpRequest = (url) => { started.push(url); return {}; };
|
||||||
|
c.collect = (request, job) => pending.push(job);
|
||||||
|
c.jsonParse = JSON.parse;
|
||||||
|
vm.runInContext(e.all.get('lb_get')
|
||||||
|
.replace(/http::request|httpPost/g, 'httpRequest')
|
||||||
|
.replace(/level thread lb_collect\(/g, 'collect('), c);
|
||||||
|
const first = c.lb_get('&first');
|
||||||
|
assert.equal(first.next().done, false);
|
||||||
|
const second = c.lb_get('&second');
|
||||||
|
assert.equal(second.next().done, false);
|
||||||
|
assert.equal(started.length, 1);
|
||||||
|
e.setTime(e.now + 15000);
|
||||||
|
assert.equal(first.next().done, true);
|
||||||
|
assert.equal(second.next().done, true);
|
||||||
|
assert.equal(started.length, 1, 'timed-out queue wait must not start another native request');
|
||||||
|
assert.equal(c.level.tws_lb_pending, pending[0]);
|
||||||
|
const third = c.lb_get('&third');
|
||||||
|
assert.equal(third.next().done, false);
|
||||||
|
Object.assign(pending[0], { done: true, success: true, body: '{}' });
|
||||||
|
c.level.tws_lb_pending = undefined;
|
||||||
|
assert.equal(third.next().done, false);
|
||||||
|
assert.equal(started.length, 2);
|
||||||
|
Object.assign(pending[1], { done: true, success: true, body: '{"ok":true}' });
|
||||||
|
c.level.tws_lb_pending = undefined;
|
||||||
|
const result = third.next();
|
||||||
|
assert.equal(result.done, true);
|
||||||
|
assert.equal(result.value.ok, true);
|
||||||
|
});
|
||||||
|
check(`${game} personal best finds exact GUID below the top rank across search pages`, () => {
|
||||||
|
const guid = '00012345678901234567890';
|
||||||
|
responses.push(page(option, [entry(option, 'prefix' + guid), entry(option, 'other', guid), entry(option, 'third')], 4),
|
||||||
|
page(option, [entry(option, guid, 'Me', 27)], 4, 3));
|
||||||
|
assert.equal(c.lb_personal_fetch(option.key, guid), 27);
|
||||||
|
assert(queries.at(-1).includes(`q=${guid}&limit=3&offset=3`));
|
||||||
|
assert(queries.at(-1).includes(`map=${encodeURIComponent(option.key)}`));
|
||||||
|
});
|
||||||
|
check(`${game} personal absence requires a complete search, failures stay unavailable`, () => {
|
||||||
|
responses.push(page(option, [entry(option, 'not-me', 'me')], 1));
|
||||||
|
assert.equal(c.lb_personal_fetch(option.key, 'me'), 0);
|
||||||
|
responses.push(page(option, [], 0));
|
||||||
|
assert.equal(c.lb_personal_fetch(option.key, 'me'), 0);
|
||||||
|
for (const response of [undefined, { ok: false }, page(option, [], 1),
|
||||||
|
page(option, [entry(option, 'me')], 0),
|
||||||
|
page(option, [{ ...entry(option, 'me'), guid: 123 }], 1),
|
||||||
|
page(option, [entry({ ...option, key: 'wrong' }, 'me')], 1),
|
||||||
|
page(option, [entry({ ...option, game: 'wrong' }, 'me')], 1),
|
||||||
|
page(option, [{ ...entry(option, 'me'), highestRound: 27 }], 1)]) {
|
||||||
|
responses.push(response);
|
||||||
|
assert.equal(c.lb_personal_fetch(option.key, 'me'), undefined);
|
||||||
|
}
|
||||||
|
responses.push(page(option, [entry(option, 'a'), entry(option, 'b'), entry(option, 'c')], 4),
|
||||||
|
page(option, [entry(option, 'me')], 5, 3));
|
||||||
|
assert.equal(c.lb_personal_fetch(option.key, 'me'), undefined);
|
||||||
|
});
|
||||||
|
check(`${game} personal identity, per-player caching, expiry and unavailable retries`, () => {
|
||||||
|
const player = { guid: '00012345678901234567890', getGuid() { return this.guid; } };
|
||||||
|
const other = { guid: 'other', getGuid() { return this.guid; } };
|
||||||
|
const snapshot = { key: option.key, round: 52 };
|
||||||
|
assert.equal(c.lb_player_guid(player), player.guid);
|
||||||
|
player.guid = 123;
|
||||||
|
assert.equal(c.lb_player_guid(player), '123');
|
||||||
|
player.guid = '00012345678901234567890';
|
||||||
|
responses.push(page(option, [entry(option, player.guid, 'Me', 27)], 1));
|
||||||
|
assert.equal(c.lb_personal_record(player, snapshot), 'Your Best | Round 27');
|
||||||
|
assert.equal(c.lb_personal_record(player, snapshot), 'Your Best | Round 27');
|
||||||
|
responses.push(page(option, [], 0));
|
||||||
|
assert.equal(c.lb_personal_record(other, snapshot), 'Your Best | None yet');
|
||||||
|
player.tws_lb_personal.saved_at -= 300000;
|
||||||
|
responses.push(undefined);
|
||||||
|
assert.equal(c.lb_personal_record(player, snapshot), 'Your Best | Unavailable');
|
||||||
|
assert.equal(c.lb_personal_record(player, snapshot), 'Your Best | Unavailable');
|
||||||
|
player.tws_lb_personal.saved_at -= 30000;
|
||||||
|
responses.push(page(option, [entry(option, player.guid, 'Me', 30)], 1));
|
||||||
|
assert.equal(c.lb_personal_record(player, snapshot), 'Your Best | Round 30');
|
||||||
|
assert.equal(c.lb_personal_record(player, undefined), 'Your Best | Unavailable');
|
||||||
|
assert.equal(c.lb_personal_record(player, { round: 0 }), 'Your Best | None yet');
|
||||||
|
player.guid = undefined;
|
||||||
|
assert.equal(c.lb_personal_record(player, snapshot), 'Your Best | Unavailable');
|
||||||
|
});
|
||||||
check(`${game} one holder`, () => {
|
check(`${game} one holder`, () => {
|
||||||
responses.push(discovery(option), page(option, [entry(option, '000123', 'Alice')], 1));
|
responses.push(discovery(option), page(option, [entry(option, '000123', 'Alice')], 1));
|
||||||
const result = c.lb_refresh();
|
const result = c.lb_refresh();
|
||||||
@@ -455,13 +537,13 @@ for (const game of ['t5', 't6']) {
|
|||||||
assert.equal(c.lb_refresh(), undefined);
|
assert.equal(c.lb_refresh(), undefined);
|
||||||
c.level.tws_lb_override = '';
|
c.level.tws_lb_override = '';
|
||||||
});
|
});
|
||||||
check(`${game} all holders fit bounded service rows`, () => {
|
check(`${game} each holder gets a separate bounded service row`, () => {
|
||||||
const names = Array.from({ length: 100 }, (_, i) => `Holder ${i}`);
|
const names = Array.from({ length: 100 }, (_, i) => `Holder ${i}`);
|
||||||
const lines = Array.from(c.lb_holder_lines(names));
|
const lines = Array.from(c.lb_holder_lines(names));
|
||||||
assert(lines.every((line) => c.lb_holder_width(line) <= 16));
|
assert(lines.every((line) => c.lb_holder_width(line) <= 16));
|
||||||
assert.deepEqual(lines.join(', ').split(', '), names);
|
assert.deepEqual(lines.join(', ').split(', '), names);
|
||||||
assert.deepEqual(Array.from(c.lb_holder_lines([])), []);
|
assert.deepEqual(Array.from(c.lb_holder_lines([])), []);
|
||||||
assert.deepEqual(Array.from(c.lb_holder_lines(['Alex', 'Sam', 'Jamie'])), ['Alex, Sam, Jamie']);
|
assert.deepEqual(Array.from(c.lb_holder_lines(['Alex', 'Sam', 'Jamie'])), ['Alex', 'Sam', 'Jamie']);
|
||||||
});
|
});
|
||||||
check(`${game} holder width counts glyphs and clips without breaking UTF-8`, () => {
|
check(`${game} holder width counts glyphs and clips without breaking UTF-8`, () => {
|
||||||
const samples = ['M'.repeat(64), 'é'.repeat(32), '🧟'.repeat(16), '玩家名字'.repeat(5),
|
const samples = ['M'.repeat(64), 'é'.repeat(32), '🧟'.repeat(16), '玩家名字'.repeat(5),
|
||||||
@@ -514,7 +596,7 @@ check('paired data helpers stay identical', () => {
|
|||||||
const t5 = functions(readFileSync(new URL('t5/scripts/tws_t5_leaderboard.gsc', root), 'utf8'));
|
const t5 = functions(readFileSync(new URL('t5/scripts/tws_t5_leaderboard.gsc', root), 'utf8'));
|
||||||
const t6 = functions(readFileSync(new URL('t6/scripts/tws_t6_leaderboard.gsc', root), 'utf8'));
|
const t6 = functions(readFileSync(new URL('t6/scripts/tws_t6_leaderboard.gsc', root), 'utf8'));
|
||||||
for (const [name, fn] of t5) {
|
for (const [name, fn] of t5) {
|
||||||
if (!['lb_selection', 'lb_welcome', 'lb_get', 'lb_log', 'lb_player_ready'].includes(name)) {
|
if (!['lb_selection', 'lb_welcome', 'lb_get', 'lb_log', 'lb_player_ready', 'lb_player_guid'].includes(name)) {
|
||||||
const normalized = t6.get(name).replaceAll('scripts\\zm\\tws_t6_common::', 'scripts\\sp\\tws_t5_common::');
|
const normalized = t6.get(name).replaceAll('scripts\\zm\\tws_t6_common::', 'scripts\\sp\\tws_t5_common::');
|
||||||
assert.equal(normalized, fn, name);
|
assert.equal(normalized, fn, name);
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -1,18 +1,18 @@
|
|||||||
{
|
{
|
||||||
"tws_t5_afk.gsc": "2.1.0",
|
"tws_t5_afk.gsc": "2.1.0",
|
||||||
"tws_t5_bank.gsc": "2.0.3",
|
"tws_t5_bank.gsc": "2.0.3",
|
||||||
"tws_t5_common.gsc": "1.5.0",
|
"tws_t5_common.gsc": "1.6.1",
|
||||||
"tws_t5_hud.gsc": "1.4.2",
|
"tws_t5_hud.gsc": "1.4.2",
|
||||||
"tws_t5_join.gsc": "1.0.0",
|
"tws_t5_join.gsc": "1.0.0",
|
||||||
"tws_t5_leaderboard.gsc": "1.2.0",
|
"tws_t5_leaderboard.gsc": "1.3.1",
|
||||||
"tws_t5_lock.gsc": "1.1.0",
|
"tws_t5_lock.gsc": "1.1.0",
|
||||||
"tws_t5_telemetry.gsc": "1.0.0",
|
"tws_t5_telemetry.gsc": "1.0.0",
|
||||||
"tws_t6_afk.gsc": "2.1.0",
|
"tws_t6_afk.gsc": "2.1.0",
|
||||||
"tws_t6_bank.gsc": "2.0.3",
|
"tws_t6_bank.gsc": "2.0.3",
|
||||||
"tws_t6_common.gsc": "1.5.0",
|
"tws_t6_common.gsc": "1.6.1",
|
||||||
"tws_t6_hud.gsc": "1.4.2",
|
"tws_t6_hud.gsc": "1.4.2",
|
||||||
"tws_t6_join.gsc": "1.0.1",
|
"tws_t6_join.gsc": "1.0.1",
|
||||||
"tws_t6_leaderboard.gsc": "1.2.0",
|
"tws_t6_leaderboard.gsc": "1.3.1",
|
||||||
"tws_t6_lock.gsc": "1.1.0",
|
"tws_t6_lock.gsc": "1.1.0",
|
||||||
"tws_t6_telemetry.gsc": "1.0.0",
|
"tws_t6_telemetry.gsc": "1.0.0",
|
||||||
"t5-plugin": "1.4.0",
|
"t5-plugin": "1.4.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user