Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eda2c7e76d | ||
|
|
664e8da81b |
@@ -6,6 +6,48 @@ T5/T6 script and each native plugin. Current versions are recorded in
|
|||||||
|
|
||||||
## 2026-10-12
|
## 2026-10-12
|
||||||
|
|
||||||
|
### T6 newcomer join eligibility — `tws_t6_join.gsc` `1.0.1`
|
||||||
|
|
||||||
|
- Fix `t6/scripts/tws_t6_join.gsc` treating the initial T6 setup spawn as
|
||||||
|
gameplay, which rejected a new spectator's `.join` on Farm Survival round 3.
|
||||||
|
Wait for the stock delayed spectator transition, then record actual play.
|
||||||
|
Continue observing later spawns so waiting for the next round cannot grant
|
||||||
|
an extra life after death. Reject requests while a spawn is settling; a real
|
||||||
|
death during that window also permanently closes eligibility.
|
||||||
|
- T5 is unchanged: its separate SP spawn flow and `self.initialized` guard
|
||||||
|
do not use T6's delayed game-mode spectator transition.
|
||||||
|
- Compatibility: existing T6 common `1.0.0` or later; no new dependency
|
||||||
|
requirement. Deploy the updated join script to `storage/t6/scripts/zm/`
|
||||||
|
and restart the map. No full server-process restart is required.
|
||||||
|
- Portable regression fixtures: `node tools/check_join.mjs`. Compilation and
|
||||||
|
live Plutonium validation remain required; no deployment was performed.
|
||||||
|
|
||||||
|
### Persistent top-left Discord URL
|
||||||
|
|
||||||
|
| Script | Version | Affected source |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `tws_t5_hud.gsc` | `1.1.0` | `t5/scripts/tws_t5_hud.gsc` |
|
||||||
|
| `tws_t5_common.gsc` | `1.1.0` | `t5/scripts/tws_t5_common.gsc` |
|
||||||
|
| `tws_t6_hud.gsc` | `1.1.0` | `t6/scripts/tws_t6_hud.gsc` |
|
||||||
|
| `tws_t6_common.gsc` | `1.1.0` | `t6/scripts/tws_t6_common.gsc` |
|
||||||
|
|
||||||
|
- Add `https://discord.thewrightserver.net` as a small, light-gray top-left
|
||||||
|
HUD label, created once per connection, including spectator newcomers.
|
||||||
|
It has no expiry or death/respawn reset and is cleaned up on disconnect.
|
||||||
|
Move the gameplay stack down 20 HUD units to leave room above the timers;
|
||||||
|
help and record positioning follow the shared layout automatically.
|
||||||
|
- Both engines receive the feature using their existing HUD anchors: T5
|
||||||
|
left/top safe-area alignment and T6 fullscreen alignment.
|
||||||
|
- Compatibility: deploy each game's HUD and common `1.1.0` together for the
|
||||||
|
reserved top line. No new native plugin or external dependency requirement.
|
||||||
|
Other scripts and both plugins retain their existing versions.
|
||||||
|
- Deployment: copy the updated HUD and common scripts to `storage/t5/scripts/sp/`
|
||||||
|
or `storage/t6/scripts/zm/`, respectively, and restart the map. No full
|
||||||
|
server-process restart is required. No deployment has been performed here.
|
||||||
|
- Live validation remains required on both Plutonium games: check placement
|
||||||
|
and readability, join as a spectator, die/respawn, reconnect, and open help
|
||||||
|
and records to verify spacing and that only one URL label appears.
|
||||||
|
|
||||||
### Initial versioning baseline
|
### Initial versioning baseline
|
||||||
|
|
||||||
Established independent version `1.0.0` entries for each of the 14 scripts and
|
Established independent version `1.0.0` entries for each of the 14 scripts and
|
||||||
|
|||||||
+10
-1
@@ -11,7 +11,7 @@ Platform quirks and deploy commands: [NOTES.md](NOTES.md).
|
|||||||
| File | Role |
|
| File | Role |
|
||||||
|---|---|
|
|---|---|
|
||||||
| [tws_t5_common.gsc](tws_t5_common.gsc) | Shared config, HUD helpers, `.help`, connect/spawn registries, single `connected` listener + dispatcher |
|
| [tws_t5_common.gsc](tws_t5_common.gsc) | Shared config, HUD helpers, `.help`, connect/spawn registries, single `connected` listener + dispatcher |
|
||||||
| [tws_t5_hud.gsc](tws_t5_hud.gsc) | Bottom-left HUD stack: timers, zombie count, health bar |
|
| [tws_t5_hud.gsc](tws_t5_hud.gsc) | Top-left Discord URL, timers, zombie count, health bar |
|
||||||
| [tws_t5_afk.gsc](tws_t5_afk.gsc) | `.afk` / `.back` chat toggle + `[AFK]` indicator |
|
| [tws_t5_afk.gsc](tws_t5_afk.gsc) | `.afk` / `.back` chat toggle + `[AFK]` indicator |
|
||||||
| [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) | Lobby `g_password` lock at a milestone round |
|
| [tws_t5_lock.gsc](tws_t5_lock.gsc) | Lobby `g_password` lock at a milestone round |
|
||||||
@@ -31,6 +31,15 @@ that directory:
|
|||||||
Do not leave both common scripts installed: duplicate connection listeners
|
Do not leave both common scripts installed: duplicate connection listeners
|
||||||
break T5's connect handshake. Existing dvar names are unchanged.
|
break T5's connect handshake. Existing dvar names are unchanged.
|
||||||
|
|
||||||
|
## Persistent Discord text
|
||||||
|
|
||||||
|
The HUD shows `https://discord.thewrightserver.net` in small light-gray text
|
||||||
|
at the top-left, including for newcomers waiting in spectator mode. The label
|
||||||
|
is created once per connection and persists across death and respawn. The
|
||||||
|
gameplay HUD starts one line lower, keeping timers, help and records clear.
|
||||||
|
Install HUD and common `1.1.0` together, then restart the map; no plugin update
|
||||||
|
or full server-process restart is required.
|
||||||
|
|
||||||
## 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
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ common_config()
|
|||||||
if (isDefined(level.hud_x))
|
if (isDefined(level.hud_x))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// anchor: move the whole cluster with these two
|
// Anchor the gameplay stack below the persistent Discord line.
|
||||||
level.hud_x = 10;
|
level.hud_x = 10;
|
||||||
level.hud_y = 10;
|
level.hud_y = 30;
|
||||||
|
|
||||||
level.hud_row_match = level.hud_y;
|
level.hud_row_match = level.hud_y;
|
||||||
level.hud_row_round = level.hud_y + 15;
|
level.hud_row_round = level.hud_y + 15;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/* tws_t5_hud.gsc — bottom-left HUD stack. Timer / zombie count / health bar. */
|
/* tws_t5_hud.gsc — top-left Discord URL, timers, zombie count and health bar. */
|
||||||
|
|
||||||
#include maps\_utility;
|
#include maps\_utility;
|
||||||
#include common_scripts\utility;
|
#include common_scripts\utility;
|
||||||
@@ -14,6 +14,21 @@ init()
|
|||||||
level thread game_over_watcher();
|
level thread game_over_watcher();
|
||||||
|
|
||||||
scripts\sp\tws_t5_common::register_spawn_handler(::hud_start);
|
scripts\sp\tws_t5_common::register_spawn_handler(::hud_start);
|
||||||
|
scripts\sp\tws_t5_common::register_connect_handler(::hud_discord);
|
||||||
|
}
|
||||||
|
|
||||||
|
// One static label per connection, including newcomers waiting to spawn.
|
||||||
|
hud_discord()
|
||||||
|
{
|
||||||
|
self endon("disconnect");
|
||||||
|
|
||||||
|
label = scripts\sp\tws_t5_common::make_text_elem(
|
||||||
|
self, 10, 10, "left", level.hud_font_small, level.hud_c_label);
|
||||||
|
label SetText("https://discord.thewrightserver.net");
|
||||||
|
|
||||||
|
elems = [];
|
||||||
|
elems[elems.size] = label;
|
||||||
|
self thread scripts\sp\tws_t5_common::destroy_on_disconnect(elems);
|
||||||
}
|
}
|
||||||
|
|
||||||
hud_start()
|
hud_start()
|
||||||
|
|||||||
@@ -118,6 +118,25 @@ at link time. The other four TWS modules can run without telemetry.
|
|||||||
|
|
||||||
## Checks
|
## Checks
|
||||||
|
|
||||||
|
### Newcomer eligibility
|
||||||
|
|
||||||
|
The stock [`_zm_gametype::onspawnplayer`](https://github.com/plutoniummod/t6-scripts/blob/2d8440321602f22f63f5335817110d60ddf223d5/ZM/Core/maps/mp/gametypes_zm/_zm_gametype.gsc)
|
||||||
|
can schedule `spawnspectator` after 0.05 seconds while
|
||||||
|
[`_globallogic_spawn::spawnplayer`](https://github.com/plutoniummod/t6-scripts/blob/2d8440321602f22f63f5335817110d60ddf223d5/ZM/Core/maps/mp/gametypes_zm/_globallogic_spawn.gsc)
|
||||||
|
emits `spawned_player` at frame end. That event alone does not prove gameplay.
|
||||||
|
Join `1.0.1` waits 0.1 seconds before checking session state and keeps listening
|
||||||
|
after setup spectator spawns. `killed_player` also consumes eligibility so a
|
||||||
|
death during that interval cannot restore newcomer access. The common dispatcher
|
||||||
|
still starts each registered module once per connection.
|
||||||
|
|
||||||
|
Run `node tools/check_join.mjs` for portable lifecycle and request fixtures.
|
||||||
|
These execute the join script with engine shims, not a GSC VM. Live validation:
|
||||||
|
join Farm Survival during round 3 and remain spectating, use `.join` once, then
|
||||||
|
try repeated requests, death, and subsequent rounds. On a fresh connection,
|
||||||
|
wait for a normal round-start spawn, die, and verify `.join` is rejected.
|
||||||
|
Also check a round-1 participant, reconnecting, disconnect during setup, and
|
||||||
|
game over. Repeat on another T6 map; compile with the checker below first.
|
||||||
|
|
||||||
Local: `tools/check.ps1` compiles all seven files for T6 PC and checks shared
|
Local: `tools/check.ps1` compiles all seven files for T6 PC and checks shared
|
||||||
references and entry points. It requires PowerShell and gsc-tool. The `.help`
|
references and entry points. It requires PowerShell and gsc-tool. The `.help`
|
||||||
change passed the static entry-point/reference checks, but compilation and live
|
change passed the static entry-point/reference checks, but compilation and live
|
||||||
|
|||||||
+10
-1
@@ -7,7 +7,7 @@ separate token config or database setup is required.
|
|||||||
| File | Role | Dependencies |
|
| File | Role | Dependencies |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `tws_t6_common.gsc` | Shared HUD values, helpers, `.help`, connection and first-spawn dispatchers | Stock T6 |
|
| `tws_t6_common.gsc` | Shared HUD values, helpers, `.help`, connection and first-spawn dispatchers | Stock T6 |
|
||||||
| `tws_t6_hud.gsc` | Match/round timers, name, zombie count and health bar | Common |
|
| `tws_t6_hud.gsc` | Top-left Discord URL, match/round timers, name, zombie count and health bar | Common |
|
||||||
| `tws_t6_afk.gsc` | `.afk` / `.back`, protection, movement and weapon blocking | Common |
|
| `tws_t6_afk.gsc` | `.afk` / `.back`, protection, movement and weapon blocking | Common |
|
||||||
| `tws_t6_lock.gsc` | Lock at round 20; clear password on game over | Common, HUD |
|
| `tws_t6_lock.gsc` | Lock at round 20; clear password on game over | Common, HUD |
|
||||||
| `tws_t6_telemetry.gsc` | Async queued game reports (game_start/heartbeat/round_change/game_end) | tws-t6-gsc-utils |
|
| `tws_t6_telemetry.gsc` | Async queued game reports (game_start/heartbeat/round_change/game_end) | tws-t6-gsc-utils |
|
||||||
@@ -40,6 +40,15 @@ Keep the filenames and `scripts/zm` location so shared references resolve.
|
|||||||
To omit telemetry, remove its script; disabling it at runtime does not remove
|
To omit telemetry, remove its script; disabling it at runtime does not remove
|
||||||
its plugin dependency during linking. The same rule applies to leaderboard.
|
its plugin dependency during linking. The same rule applies to leaderboard.
|
||||||
|
|
||||||
|
## Persistent Discord text
|
||||||
|
|
||||||
|
The HUD shows `https://discord.thewrightserver.net` in small light-gray text
|
||||||
|
at the top-left, including for newcomers waiting in spectator mode. The label
|
||||||
|
is created once per connection and persists across death and respawn. The
|
||||||
|
gameplay HUD starts one line lower, keeping timers, help and records clear.
|
||||||
|
Install HUD and common `1.1.0` together, then restart the map; no plugin update
|
||||||
|
or full server-process restart is required.
|
||||||
|
|
||||||
## Map record announcements
|
## Map record announcements
|
||||||
|
|
||||||
`tws_t6_leaderboard.gsc` reads the public Electric Cherry API without credentials.
|
`tws_t6_leaderboard.gsc` reads the public Electric Cherry API without credentials.
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ common_config()
|
|||||||
if (isDefined(level.hud_x))
|
if (isDefined(level.hud_x))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Small inset from the physical screen corner (fullscreen HUD coordinates).
|
// Fullscreen coordinates; reserve the top line for the Discord URL.
|
||||||
level.hud_x = 10;
|
level.hud_x = 10;
|
||||||
level.hud_y = 10;
|
level.hud_y = 30;
|
||||||
|
|
||||||
level.hud_row_match = level.hud_y;
|
level.hud_row_match = level.hud_y;
|
||||||
level.hud_row_round = level.hud_y + 15;
|
level.hud_row_round = level.hud_y + 15;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/* tws_t6_hud.gsc - configurable HUD stack: timers, zombie count, health bar. */
|
/* tws_t6_hud.gsc - top-left Discord URL, timers, zombie count and health bar. */
|
||||||
|
|
||||||
#include maps\mp\_utility;
|
#include maps\mp\_utility;
|
||||||
#include common_scripts\utility;
|
#include common_scripts\utility;
|
||||||
@@ -16,6 +16,21 @@ init()
|
|||||||
level thread game_over_watcher();
|
level thread game_over_watcher();
|
||||||
|
|
||||||
scripts\zm\tws_t6_common::register_spawn_handler(::hud_start);
|
scripts\zm\tws_t6_common::register_spawn_handler(::hud_start);
|
||||||
|
scripts\zm\tws_t6_common::register_connect_handler(::hud_discord);
|
||||||
|
}
|
||||||
|
|
||||||
|
// One static label per connection, including newcomers waiting to spawn.
|
||||||
|
hud_discord()
|
||||||
|
{
|
||||||
|
self endon("disconnect");
|
||||||
|
|
||||||
|
label = scripts\zm\tws_t6_common::make_text_elem(
|
||||||
|
self, 10, 10, "left", level.hud_font_small, level.hud_c_label);
|
||||||
|
label SetText("https://discord.thewrightserver.net");
|
||||||
|
|
||||||
|
elems = [];
|
||||||
|
elems[elems.size] = label;
|
||||||
|
self thread scripts\zm\tws_t6_common::destroy_on_disconnect(elems);
|
||||||
}
|
}
|
||||||
|
|
||||||
hud_start()
|
hud_start()
|
||||||
|
|||||||
@@ -20,9 +20,44 @@ init()
|
|||||||
level thread join_chat_listener();
|
level thread join_chat_listener();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Never reset this on death or round change. Spectator spawns do not count.
|
// T6 first emits spawned_player during setup, then moves late connectors
|
||||||
|
// into spectator mode after 0.05 seconds. Observe the settled state instead.
|
||||||
|
// The common spawn dispatcher runs only once, so keep listening ourselves.
|
||||||
join_spawn()
|
join_spawn()
|
||||||
{
|
{
|
||||||
|
self endon("disconnect");
|
||||||
|
level endon("end_game");
|
||||||
|
level endon("game_ended");
|
||||||
|
level endon("intermission");
|
||||||
|
|
||||||
|
self thread join_track_death();
|
||||||
|
|
||||||
|
for (;;)
|
||||||
|
{
|
||||||
|
self.tws_join_spawn_pending = true;
|
||||||
|
wait 0.1;
|
||||||
|
|
||||||
|
if (isDefined(self.sessionstate) && self.sessionstate == "playing")
|
||||||
|
self.tws_join_has_played = true;
|
||||||
|
|
||||||
|
self.tws_join_spawn_pending = false;
|
||||||
|
if (is_true(self.tws_join_has_played))
|
||||||
|
return;
|
||||||
|
|
||||||
|
self waittill("spawned_player");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A real death during the settling window still consumes newcomer eligibility.
|
||||||
|
// Never clear this flag on death, a round change, or a subsequent spawn.
|
||||||
|
join_track_death()
|
||||||
|
{
|
||||||
|
self endon("disconnect");
|
||||||
|
level endon("end_game");
|
||||||
|
level endon("game_ended");
|
||||||
|
level endon("intermission");
|
||||||
|
|
||||||
|
self waittill("killed_player");
|
||||||
self.tws_join_has_played = true;
|
self.tws_join_has_played = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,8 +86,7 @@ join_request()
|
|||||||
level endon("game_ended");
|
level endon("game_ended");
|
||||||
level endon("intermission");
|
level endon("intermission");
|
||||||
|
|
||||||
// The spawn handler flag is set on first playable spawn only; spectator
|
// Setup spawns do not consume eligibility; actual play and .join do.
|
||||||
// spawns do not trigger it. Belt-and-suspenders against re-triggering.
|
|
||||||
if (is_true(self.tws_join_has_played) || is_true(self.tws_join_used))
|
if (is_true(self.tws_join_has_played) || is_true(self.tws_join_used))
|
||||||
{
|
{
|
||||||
self iPrintLnBold("^3.join is only for newcomers who have not played yet");
|
self iPrintLnBold("^3.join is only for newcomers who have not played yet");
|
||||||
@@ -62,7 +96,8 @@ join_request()
|
|||||||
// Fail closed while the stock connection setup is still in progress.
|
// Fail closed while the stock connection setup is still in progress.
|
||||||
// T6 stock does not populate self.initialized the way T5 SP zombiemode
|
// T6 stock does not populate self.initialized the way T5 SP zombiemode
|
||||||
// does; use sessionstate as the readiness signal.
|
// does; use sessionstate as the readiness signal.
|
||||||
if (!isDefined(self.sessionstate) || self.sessionstate != "spectator")
|
if (is_true(self.tws_join_spawn_pending) ||
|
||||||
|
!isDefined(self.sessionstate) || self.sessionstate != "spectator")
|
||||||
{
|
{
|
||||||
self iPrintLnBold("^3.join is only available while waiting to enter the game");
|
self iPrintLnBold("^3.join is only available while waiting to enter the game");
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -0,0 +1,227 @@
|
|||||||
|
// Execute the actual T6 join functions with a deterministic event/timer shim.
|
||||||
|
// This checks script decisions, not GSC compilation or live engine behavior.
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import vm from 'node:vm';
|
||||||
|
|
||||||
|
const source = readFileSync(new URL('../t6/scripts/tws_t6_join.gsc', import.meta.url), 'utf8');
|
||||||
|
const functions = [];
|
||||||
|
for (const name of ['join_spawn', 'join_track_death', 'join_request']) {
|
||||||
|
const match = new RegExp(`^${name}\\(\\)\\s*\\n\\{`, 'm').exec(source);
|
||||||
|
// The original implementation has no death observer; allow regression runs.
|
||||||
|
if (!match && name === 'join_track_death') continue;
|
||||||
|
assert(match, `missing ${name}`);
|
||||||
|
let end = match.index + match[0].length, depth = 1;
|
||||||
|
const start = end;
|
||||||
|
while (depth) {
|
||||||
|
assert(end < source.length, `unclosed ${name}`);
|
||||||
|
const ch = source[end++];
|
||||||
|
if (ch === '{') depth++;
|
||||||
|
if (ch === '}') depth--;
|
||||||
|
}
|
||||||
|
let body = source.slice(start, end - 1).replace(/\/\/[^\n]*/g, '');
|
||||||
|
body = body
|
||||||
|
.replace(/(self|level) endon\(([^;]+)\);/g, 'endon($1, $2);')
|
||||||
|
.replace(/self thread (\w+)\(\);/g, 'startThread($1);')
|
||||||
|
.replace(/self waittill\(([^;]+)\);/g, 'yield { entity: self, event: $1 };')
|
||||||
|
.replace(/\bwait ([\d.]+);/g, 'yield { seconds: $1 };')
|
||||||
|
.replace(/self iPrintLnBold\(/g, 'messages.push(')
|
||||||
|
.replace(/self \[\[level.spawnplayer\]\]\(\);/g, 'spawnPlayer();')
|
||||||
|
.replace(/maps\\mp\\zombies\\_zm::spectator_respawn/g, 'stockRespawn')
|
||||||
|
.replace(/!join_has_active_player\(\)/g, '!activePlayer')
|
||||||
|
.replace(/\.size\b/g, '.length');
|
||||||
|
functions.push(`function* ${name}() { let points; ${body} }`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function engine() {
|
||||||
|
let now = 0, current;
|
||||||
|
const tasks = [], messages = [];
|
||||||
|
const self = { sessionstate: 'playing', spectator_respawn: {} };
|
||||||
|
const level = { round_number: 3, spawnplayer: true };
|
||||||
|
let spawns = 0;
|
||||||
|
function resume(task) {
|
||||||
|
const previous = current;
|
||||||
|
current = task;
|
||||||
|
const result = task.generator.next();
|
||||||
|
task.done = result.done;
|
||||||
|
task.wait = result.value;
|
||||||
|
if (task.wait?.seconds !== undefined) task.at = now + task.wait.seconds;
|
||||||
|
else task.at = Infinity;
|
||||||
|
current = previous;
|
||||||
|
}
|
||||||
|
function startThread(fn) {
|
||||||
|
const task = { generator: fn(), ends: [] };
|
||||||
|
tasks.push(task);
|
||||||
|
resume(task);
|
||||||
|
}
|
||||||
|
function event(entity, event) {
|
||||||
|
const listeners = tasks.filter((task) => !task.done);
|
||||||
|
for (const task of listeners) {
|
||||||
|
if (task.ends.some(([e, name]) => e === entity && name === event)) task.done = true;
|
||||||
|
}
|
||||||
|
for (const task of listeners) {
|
||||||
|
if (!task.done && task.wait?.entity === entity && task.wait.event === event) resume(task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function advance(seconds) {
|
||||||
|
const until = now + seconds;
|
||||||
|
for (;;) {
|
||||||
|
const next = tasks.filter((task) => !task.done && task.at <= until)
|
||||||
|
.sort((a, b) => a.at - b.at)[0];
|
||||||
|
if (!next) break;
|
||||||
|
now = next.at;
|
||||||
|
resume(next);
|
||||||
|
}
|
||||||
|
now = until;
|
||||||
|
}
|
||||||
|
const c = vm.createContext({
|
||||||
|
self, level, messages, activePlayer: true, stockRespawn: () => {},
|
||||||
|
isDefined: (value) => value !== undefined && value !== null,
|
||||||
|
is_true: (value) => value === true || value === 1,
|
||||||
|
endon: (entity, event) => current.ends.push([entity, event]),
|
||||||
|
startThread,
|
||||||
|
getstructarray: () => [{}], randomInt: () => 0,
|
||||||
|
spawnPlayer: () => {
|
||||||
|
spawns++;
|
||||||
|
self.sessionstate = 'playing';
|
||||||
|
event(self, 'spawned_player');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
vm.runInContext(functions.join('\n'), c);
|
||||||
|
return { self, level, c, messages, tasks, advance,
|
||||||
|
start: () => startThread(c.join_spawn),
|
||||||
|
request: () => startThread(c.join_request),
|
||||||
|
event: (name) => event(self, name),
|
||||||
|
end: (name) => event(level, name),
|
||||||
|
get spawns() { return spawns; },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let checks = 0;
|
||||||
|
function check(name, run) {
|
||||||
|
try { run(); checks++; }
|
||||||
|
catch (error) { throw new Error(name, { cause: error }); }
|
||||||
|
}
|
||||||
|
function newcomer() {
|
||||||
|
const e = engine();
|
||||||
|
e.start(); // Common's once-per-connection callback, at spawned_player.
|
||||||
|
e.advance(0.05);
|
||||||
|
e.self.sessionstate = 'spectator'; // Stock delayed setup transition.
|
||||||
|
e.event('spawned_spectator');
|
||||||
|
e.advance(0.1);
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
check('Farm round 3 setup spawn leaves a newcomer eligible for exactly one join', () => {
|
||||||
|
const e = newcomer();
|
||||||
|
assert(!e.self.tws_join_has_played);
|
||||||
|
e.request();
|
||||||
|
assert.equal(e.spawns, 1);
|
||||||
|
assert.equal(e.self.tws_join_used, true);
|
||||||
|
e.request();
|
||||||
|
e.self.sessionstate = 'spectator';
|
||||||
|
e.event('killed_player');
|
||||||
|
e.level.round_number++;
|
||||||
|
e.request();
|
||||||
|
assert.equal(e.spawns, 1);
|
||||||
|
assert.match(e.messages.at(-1), /only for newcomers/);
|
||||||
|
});
|
||||||
|
check('setup transition blocks requests until settled', () => {
|
||||||
|
const e = engine();
|
||||||
|
e.start();
|
||||||
|
e.advance(0.05);
|
||||||
|
e.self.sessionstate = 'spectator';
|
||||||
|
e.request();
|
||||||
|
assert.equal(e.spawns, 0);
|
||||||
|
assert.match(e.messages.at(-1), /waiting to enter/);
|
||||||
|
e.advance(0.1);
|
||||||
|
e.request();
|
||||||
|
assert.equal(e.spawns, 1);
|
||||||
|
});
|
||||||
|
check('a normal round-start spawn closes eligibility even after later death', () => {
|
||||||
|
const e = newcomer();
|
||||||
|
e.level.round_number++;
|
||||||
|
e.self.sessionstate = 'playing';
|
||||||
|
e.event('spawned_player');
|
||||||
|
e.advance(0.11);
|
||||||
|
assert.equal(e.self.tws_join_has_played, true);
|
||||||
|
e.self.sessionstate = 'spectator';
|
||||||
|
e.request();
|
||||||
|
assert.equal(e.spawns, 0);
|
||||||
|
assert.match(e.messages.at(-1), /only for newcomers/);
|
||||||
|
});
|
||||||
|
check('round-1 participants cannot join again after death', () => {
|
||||||
|
const e = engine();
|
||||||
|
e.level.round_number = 1;
|
||||||
|
e.start();
|
||||||
|
e.advance(0.11);
|
||||||
|
e.self.sessionstate = 'spectator';
|
||||||
|
e.request();
|
||||||
|
assert.equal(e.spawns, 0);
|
||||||
|
assert.equal(e.self.tws_join_has_played, true);
|
||||||
|
});
|
||||||
|
for (const initial of [true, false]) {
|
||||||
|
check(`death during ${initial ? 'initial' : 'later'} spawn settling consumes eligibility`, () => {
|
||||||
|
const e = initial ? engine() : newcomer();
|
||||||
|
if (initial) e.start();
|
||||||
|
else {
|
||||||
|
e.self.sessionstate = 'playing';
|
||||||
|
e.event('spawned_player');
|
||||||
|
}
|
||||||
|
e.advance(0.02);
|
||||||
|
e.event('killed_player');
|
||||||
|
e.self.sessionstate = 'spectator';
|
||||||
|
e.advance(0.11);
|
||||||
|
e.request();
|
||||||
|
assert.equal(e.self.tws_join_has_played, true);
|
||||||
|
assert.equal(e.spawns, 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
check('repeated spectator setup events keep listening for an actual spawn', () => {
|
||||||
|
const e = newcomer();
|
||||||
|
e.event('spawned_player');
|
||||||
|
e.advance(0.11);
|
||||||
|
assert(!e.self.tws_join_has_played);
|
||||||
|
e.self.sessionstate = 'playing';
|
||||||
|
e.event('spawned_player');
|
||||||
|
e.advance(0.11);
|
||||||
|
assert.equal(e.self.tws_join_has_played, true);
|
||||||
|
});
|
||||||
|
for (const name of ['disconnect', 'end_game', 'game_ended', 'intermission']) {
|
||||||
|
check(`${name} stops observers during setup and while waiting`, () => {
|
||||||
|
for (const setup of [true, false]) {
|
||||||
|
const e = setup ? engine() : newcomer();
|
||||||
|
if (setup) e.start();
|
||||||
|
if (name === 'disconnect') e.event(name);
|
||||||
|
else e.end(name);
|
||||||
|
e.advance(1);
|
||||||
|
e.event('killed_player');
|
||||||
|
assert(!e.self.tws_join_has_played);
|
||||||
|
assert(e.tasks.every((task) => task.done));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (const [name, set] of [
|
||||||
|
['intermission', (e) => { e.level.intermission = true; }],
|
||||||
|
['game ended', (e) => { e.level.gameended = true; }],
|
||||||
|
['no active player', (e) => { e.c.activePlayer = false; }],
|
||||||
|
['missing round', (e) => { delete e.level.round_number; }],
|
||||||
|
['missing spawn callback', (e) => { delete e.level.spawnplayer; }],
|
||||||
|
]) {
|
||||||
|
check(`${name} still rejects joining`, () => {
|
||||||
|
const e = newcomer();
|
||||||
|
set(e);
|
||||||
|
e.request();
|
||||||
|
assert.equal(e.spawns, 0);
|
||||||
|
assert.match(e.messages.at(-1), /not ready/);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
check('reconnecting starts independent eligibility', () => {
|
||||||
|
const first = newcomer();
|
||||||
|
first.request();
|
||||||
|
first.event('disconnect');
|
||||||
|
const second = newcomer();
|
||||||
|
second.request();
|
||||||
|
assert.equal(second.spawns, 1);
|
||||||
|
});
|
||||||
|
console.log(`PASS ${checks} T6 join lifecycle/request checks (engine shims; live validation required)`);
|
||||||
+5
-5
@@ -1,15 +1,15 @@
|
|||||||
{
|
{
|
||||||
"tws_t5_afk.gsc": "1.0.0",
|
"tws_t5_afk.gsc": "1.0.0",
|
||||||
"tws_t5_common.gsc": "1.0.0",
|
"tws_t5_common.gsc": "1.1.0",
|
||||||
"tws_t5_hud.gsc": "1.0.0",
|
"tws_t5_hud.gsc": "1.1.0",
|
||||||
"tws_t5_join.gsc": "1.0.0",
|
"tws_t5_join.gsc": "1.0.0",
|
||||||
"tws_t5_leaderboard.gsc": "1.0.0",
|
"tws_t5_leaderboard.gsc": "1.0.0",
|
||||||
"tws_t5_lock.gsc": "1.0.0",
|
"tws_t5_lock.gsc": "1.0.0",
|
||||||
"tws_t5_telemetry.gsc": "1.0.0",
|
"tws_t5_telemetry.gsc": "1.0.0",
|
||||||
"tws_t6_afk.gsc": "1.0.0",
|
"tws_t6_afk.gsc": "1.0.0",
|
||||||
"tws_t6_common.gsc": "1.0.0",
|
"tws_t6_common.gsc": "1.1.0",
|
||||||
"tws_t6_hud.gsc": "1.0.0",
|
"tws_t6_hud.gsc": "1.1.0",
|
||||||
"tws_t6_join.gsc": "1.0.0",
|
"tws_t6_join.gsc": "1.0.1",
|
||||||
"tws_t6_leaderboard.gsc": "1.0.0",
|
"tws_t6_leaderboard.gsc": "1.0.0",
|
||||||
"tws_t6_lock.gsc": "1.0.0",
|
"tws_t6_lock.gsc": "1.0.0",
|
||||||
"tws_t6_telemetry.gsc": "1.0.0",
|
"tws_t6_telemetry.gsc": "1.0.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user