Allow solo and last-active-player AFK
This commit is contained in:
@@ -6,6 +6,35 @@ T5/T6 script and each native plugin. Current versions are recorded in
|
|||||||
|
|
||||||
## 2026-10-17
|
## 2026-10-17
|
||||||
|
|
||||||
|
### Allow solo and last-active-player AFK
|
||||||
|
|
||||||
|
| Component | Version | Affected source |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `tws_t5_afk.gsc` | `2.1.0` | `t5/scripts/tws_t5_afk.gsc` |
|
||||||
|
| `tws_t6_afk.gsc` | `2.1.0` | `t6/scripts/tws_t6_afk.gsc` |
|
||||||
|
|
||||||
|
- Remove the teammate requirement before reservation and after start
|
||||||
|
authorization on both engines. Solo players, the last active player and
|
||||||
|
players whose teammates are already AFK or awaiting authorization can enter.
|
||||||
|
- Remove the AFK team watcher that ended matches when no non-AFK survivor
|
||||||
|
remained. All players may be AFK together, and teammate death/disconnect
|
||||||
|
does not cancel protection. Keep reserve accounting, the local deadline,
|
||||||
|
cooldown, player-state checks and normal stock match-end settlement.
|
||||||
|
- Compatibility: no new dependency requirement. Existing common `1.4.0+`,
|
||||||
|
telemetry `1.0.0+`, matching plugin `1.4.0+`, Plutonium r5334+, EC #151 and
|
||||||
|
host agent `1.12.0+` requirements remain unchanged. T5 and T6 receive the same
|
||||||
|
policy change using their existing engine-specific state checks.
|
||||||
|
- Validation: 47 AFK script behavior checks passed, including solo entry,
|
||||||
|
unavailable/pending teammates, teammate loss during authorization, all-AFK
|
||||||
|
play, solo return/expiration and stock match-end cleanup. The changed T6
|
||||||
|
script compiled and assembled with upstream gsc-tool `8f1d6bd` libraries.
|
||||||
|
Portable checks use engine shims; live T5/T6 validation remains required.
|
||||||
|
No Windows plugin build, live server test or deployment was performed.
|
||||||
|
- Deployment: copy the updated AFK script for each installed game and restart
|
||||||
|
the map. No full server-process restart or additional deployment action is
|
||||||
|
required for this script-only update. See the [AFK guide](docs/afk.md) for
|
||||||
|
the updated live validation cases.
|
||||||
|
|
||||||
### Increase map-record reminders as players approach the target
|
### Increase map-record reminders as players approach the target
|
||||||
|
|
||||||
| Component | Version | Affected source |
|
| Component | Version | Affected source |
|
||||||
|
|||||||
@@ -661,7 +661,10 @@ never reused. Native plugin updates require a full game-server process restart.
|
|||||||
## Persistent AFK reserves (T5 and T6)
|
## Persistent AFK reserves (T5 and T6)
|
||||||
|
|
||||||
See [AFK reserves](docs/afk.md) for commands, gameplay rewards, countdown behavior,
|
See [AFK reserves](docs/afk.md) for commands, gameplay rewards, countdown behavior,
|
||||||
recovery, coordinated EC deployment and the required live validation. AFK `2.0.0`
|
recovery, coordinated EC deployment and the required live validation. AFK `2.1.0`
|
||||||
requires matching TWS plugin `1.4.0+`, common `1.4.0+`, telemetry `1.0.0+`,
|
requires matching TWS plugin `1.4.0+`, common `1.4.0+`, telemetry `1.0.0+`,
|
||||||
Plutonium r5334+, EC #151 and host agent `1.12.0+`. Full server-process restart
|
Plutonium r5334+, EC #151 and host agent `1.12.0+`. Full server-process restart
|
||||||
is required when installing the updated plugins.
|
is required when installing the updated plugins.
|
||||||
|
Solo and last-active-player AFK are allowed, including all players being AFK
|
||||||
|
together. Upgrading only the AFK scripts from `2.0.0` requires a map restart,
|
||||||
|
with no new dependencies or full server-process restart.
|
||||||
|
|||||||
+19
-12
@@ -30,18 +30,19 @@ immediately, including in the damage callback; the script restores its own contr
|
|||||||
weapons and zombie-targeting state. Expiration has no spectator transition, purchase
|
weapons and zombie-targeting state. Expiration has no spectator transition, purchase
|
||||||
or extra grace. Repeating `.afk` does not restart the timer or reserve twice.
|
or extra grace. Repeating `.afk` does not restart the timer or reserve twice.
|
||||||
|
|
||||||
Entry rejects downed/dead/spectating players, stock protection/control restrictions,
|
AFK `2.1.0` allows solo players and the last active player to enter; all players
|
||||||
and the last active player. A default 15-second reentry cooldown prevents repeated
|
may be AFK at the same time. Entry still rejects downed/dead/spectating players
|
||||||
|
and stock protection/control restrictions. A default 15-second reentry cooldown prevents repeated
|
||||||
combat toggles. Manual `.back` retains a separate two-second grace. The AFK damage
|
combat toggles. Manual `.back` retains a separate two-second grace. The AFK damage
|
||||||
callback passes through when it does not own protection, preserving stock game
|
callback passes through when it does not own protection, preserving stock game
|
||||||
invulnerability. Other mods that restrict controls should set
|
invulnerability. Other mods that restrict controls should set
|
||||||
`player.tws_afk_external_controls` while their restriction applies.
|
`player.tws_afk_external_controls` while their restriction applies.
|
||||||
|
|
||||||
AFK leaves player session state unchanged, so stock player-count zombie scaling
|
AFK leaves player session state unchanged, so stock player-count zombie scaling
|
||||||
continues to include AFK players. If the last non-AFK active player dies or leaves,
|
continues to include AFK players. Losing the last non-AFK teammate does not end
|
||||||
the scripts release AFK sessions and send the stock end-game signal. Stock afterlife,
|
protected breaks or force a match end. Authorized time continues to count down;
|
||||||
`no_end_game_check` and Grief reset recovery defer this behavior. A lobby with no
|
normal expiration and stock match-end cleanup still apply. Verify these rules
|
||||||
AFK player is unaffected. Verify these rules on each deployed map/mode.
|
on each deployed map/mode.
|
||||||
|
|
||||||
## Rewards
|
## Rewards
|
||||||
|
|
||||||
@@ -90,13 +91,17 @@ Recheck callback contracts if the stock runtime changes.
|
|||||||
2. Update all EC host agents to **1.12.0+** before enabling these scripts. This
|
2. Update all EC host agents to **1.12.0+** before enabling these scripts. This
|
||||||
adds AFK settlement before deleting/rebuilding stopped runtime storage.
|
adds AFK settlement before deleting/rebuilding stopped runtime storage.
|
||||||
3. Build and install TWS `t5-plugin` and `t6-plugin` **1.4.0+** and matching AFK
|
3. Build and install TWS `t5-plugin` and `t6-plugin` **1.4.0+** and matching AFK
|
||||||
**2.0.0** scripts. Require common **1.4.0+**, telemetry **1.0.0+**, and
|
**2.1.0** scripts. Require common **1.4.0+**, telemetry **1.0.0+**, and
|
||||||
Plutonium **r5334+** for `SetTextUnlimited`. Fully restart game-server processes.
|
Plutonium **r5334+** for `SetTextUnlimited`. Fully restart game-server processes.
|
||||||
4. Retain EC's private `tws_bank_token` and exact `tws_server_id`; telemetry must
|
4. Retain EC's private `tws_bank_token` and exact `tws_server_id`; telemetry must
|
||||||
establish the current match before AFK commands can be authorized.
|
establish the current match before AFK commands can be authorized.
|
||||||
5. Keep `tws-afk/t5/` and `tws-afk/t6/`, relative to the process working directory,
|
5. Keep `tws-afk/t5/` and `tws-afk/t6/`, relative to the process working directory,
|
||||||
on persistent writable storage with one process per server/endpoint scope.
|
on persistent writable storage with one process per server/endpoint scope.
|
||||||
|
|
||||||
|
Upgrading AFK scripts from `2.0.0` to `2.1.0` adds no dependency requirement:
|
||||||
|
copy the matching script for each game and restart the map. No full server-process
|
||||||
|
restart is required for this script-only update.
|
||||||
|
|
||||||
| Game dvar | Default | Meaning |
|
| Game dvar | Default | Meaning |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `tws_afk_enabled` | enabled | `0` disables new AFK service startup |
|
| `tws_afk_enabled` | enabled | `0` disables new AFK service startup |
|
||||||
@@ -127,7 +132,7 @@ process crash can lose progress that had not yet reached the local journal.
|
|||||||
Already accepted rewards and journaled snapshots persist.
|
Already accepted rewards and journaled snapshots persist.
|
||||||
|
|
||||||
Protection starts only after reservation and start acknowledgments, a final
|
Protection starts only after reservation and start acknowledgments, a final
|
||||||
player/lobby recheck, and a durable native clock start. Once authorized, an EC
|
player-state recheck, and a durable native clock start. Once authorized, an EC
|
||||||
outage cannot shorten protection; a local monotonic deadline still ends it at zero.
|
outage cannot shorten protection; a local monotonic deadline still ends it at zero.
|
||||||
Manual return, disconnect, death and match end stop accounting immediately. A
|
Manual return, disconnect, death and match end stop accounting immediately. A
|
||||||
failed stop-journal write freezes the original stop time for retry rather than
|
failed stop-journal write freezes the original stop time for retry rather than
|
||||||
@@ -182,14 +187,16 @@ Before production activation, validate **both engines**:
|
|||||||
Drop reserve/start/stop replies, reorder delivery, fill/fail journal storage, and
|
Drop reserve/start/stop replies, reorder delivery, fill/fail journal storage, and
|
||||||
restart. Verify never-started refunds, durable exact stops and unconfirmed crash
|
restart. Verify never-started refunds, durable exact stops and unconfirmed crash
|
||||||
holds. Correct only a proven stopped session and confirm the ledger audit.
|
holds. Correct only a proven stopped session and confirm the ledger audit.
|
||||||
- Reject the last active player's entry. Kill/disconnect the last active teammate
|
- Enter as the only connected player and as the last active player with downed,
|
||||||
while another is AFK; validate normal end reporting and all AFK settlement.
|
dead, spectating or AFK teammates. Put every player in AFK. Kill/disconnect the
|
||||||
Exercise stock recovery exceptions and confirm expected zombie scaling.
|
last active teammate, including while reserve/start replies are pending; verify
|
||||||
|
protection continues without a forced match end. Check solo return/expiration,
|
||||||
|
normal stock match-end settlement, afterlife/Grief recovery and zombie scaling.
|
||||||
- Rebuild/remove with journals from both games, retire the instance credential,
|
- Rebuild/remove with journals from both games, retire the instance credential,
|
||||||
interrupt the agent after a lost reply, and retry. Runtime deletion must wait for
|
interrupt the agent after a lost reply, and retry. Runtime deletion must wait for
|
||||||
acknowledged recovery and cannot authorize a new purchase.
|
acknowledged recovery and cannot authorize a new purchase.
|
||||||
|
|
||||||
Validation performed for this change: 39 AFK behavior checks, 14 core HUD and
|
Validation performed for the initial `2.0.0` change: 39 AFK behavior checks, 14 core HUD and
|
||||||
56 service HUD checks passed. All nine T6 scripts compiled and assembled with
|
56 service HUD checks passed. All nine T6 scripts compiled and assembled with
|
||||||
the upstream [gsc-tool compiler 8f1d6bd](https://github.com/xensik/gsc-tool/tree/8f1d6bd4d51efdc619629b9254fb439427a6d306)
|
the upstream [gsc-tool compiler 8f1d6bd](https://github.com/xensik/gsc-tool/tree/8f1d6bd4d51efdc619629b9254fb439427a6d306)
|
||||||
using its T6 PC compiler/assembler library in a temporary local driver. No Windows
|
using its T6 PC compiler/assembler library in a temporary local driver. No Windows
|
||||||
|
|||||||
@@ -340,7 +340,11 @@ Chat uses `level waittill("say")`; the independent central countdown stays visib
|
|||||||
with `.hud` hidden. Activation rejects laststand before changing state. A stock
|
with `.hud` hidden. Activation rejects laststand before changing state. A stock
|
||||||
damage callback owns only AFK protection; expiration immediately relinquishes it
|
damage callback owns only AFK protection; expiration immediately relinquishes it
|
||||||
and AFK-owned controls/targeting, while manual `.back` keeps its separate two-second
|
and AFK-owned controls/targeting, while manual `.back` keeps its separate two-second
|
||||||
grace. Spawn handler `::afk_spawn` binds contribution and lifecycle watchers.
|
grace. AFK `2.1.0` permits solo and last-active-player entry, including all players
|
||||||
|
being AFK together; teammate loss does not force a match end. Upgrade from `2.0.0`
|
||||||
|
by copying the script and restarting the map; no new dependencies or full
|
||||||
|
server-process restart are required. Spawn handler `::afk_spawn` binds contribution
|
||||||
|
and lifecycle watchers.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ init()
|
|||||||
level thread afk_startup();
|
level thread afk_startup();
|
||||||
level thread afk_end_watcher();
|
level thread afk_end_watcher();
|
||||||
level thread afk_round_watcher();
|
level thread afk_round_watcher();
|
||||||
level thread afk_team_watcher();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
afk_startup()
|
afk_startup()
|
||||||
@@ -408,11 +407,6 @@ afk_activate(player, state)
|
|||||||
afk_say(state, "Wait briefly after returning before entering AFK again.");
|
afk_say(state, "Wait briefly after returning before entering AFK again.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (afk_other_active(player) == 0)
|
|
||||||
{
|
|
||||||
afk_say(state, "The last active player cannot enter AFK. Another player must remain in the fight.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
data = afk_base(state, "reserve");
|
data = afk_base(state, "reserve");
|
||||||
data["session_id"] = telemetryNewMatch();
|
data["session_id"] = telemetryNewMatch();
|
||||||
data["seconds"] = 604800;
|
data["seconds"] = 604800;
|
||||||
@@ -459,7 +453,7 @@ afk_activate(player, state)
|
|||||||
reply = afk_http("start", start_body, start["request_id"]);
|
reply = afk_http("start", start_body, start["request_id"]);
|
||||||
if (!isDefined(reply) || reply["state"] != "started" || reply["session_id"] != session ||
|
if (!isDefined(reply) || reply["state"] != "started" || reply["session_id"] != session ||
|
||||||
reply["authorized_seconds"] != authorized || !state.connected ||
|
reply["authorized_seconds"] != authorized || !state.connected ||
|
||||||
state.cancelled || !afk_can_enter(player) || afk_other_active(player) == 0)
|
state.cancelled || !afk_can_enter(player))
|
||||||
{
|
{
|
||||||
afk_stop_state(state, "never_started");
|
afk_stop_state(state, "never_started");
|
||||||
afk_say(state, "AFK entry cancelled. No protected time used.");
|
afk_say(state, "AFK entry cancelled. No protected time used.");
|
||||||
@@ -710,59 +704,6 @@ afk_end_watcher()
|
|||||||
level.afk_ended = true;
|
level.afk_ended = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
afk_other_active(excluded)
|
|
||||||
{
|
|
||||||
count = 0;
|
|
||||||
players = getPlayers();
|
|
||||||
for (i = 0; i < players.size; i++)
|
|
||||||
{
|
|
||||||
player = players[i];
|
|
||||||
if (isDefined(excluded) && player == excluded)
|
|
||||||
continue;
|
|
||||||
if (!afk_playing(player) || is_true(player.hud_afk_on))
|
|
||||||
continue;
|
|
||||||
if (isDefined(player.tws_afk_state) && player.tws_afk_state.pending)
|
|
||||||
continue;
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
afk_team_watcher()
|
|
||||||
{
|
|
||||||
level endon("end_game");
|
|
||||||
level endon("game_ended");
|
|
||||||
level endon("intermission");
|
|
||||||
for (;;)
|
|
||||||
{
|
|
||||||
players = getPlayers();
|
|
||||||
protected = 0;
|
|
||||||
survivors = 0;
|
|
||||||
stock_recovery = is_true(level.no_end_game_check) || is_true(level.isresetting_grief);
|
|
||||||
for (i = 0; i < players.size; i++)
|
|
||||||
{
|
|
||||||
player = players[i];
|
|
||||||
if (is_true(player.afterlife))
|
|
||||||
stock_recovery = true;
|
|
||||||
if (is_true(player.hud_afk_on))
|
|
||||||
protected++;
|
|
||||||
else if (afk_playing(player))
|
|
||||||
survivors++;
|
|
||||||
}
|
|
||||||
if (protected > 0 && survivors == 0 && !stock_recovery)
|
|
||||||
{
|
|
||||||
for (i = 0; i < players.size; i++)
|
|
||||||
if (is_true(players[i].hud_afk_on))
|
|
||||||
players[i] afk_finish("match_end", false);
|
|
||||||
// AFK does not change sessionstate/player count or zombie scaling.
|
|
||||||
// The verified stock end signal prevents AFK bodies prolonging play.
|
|
||||||
level notify("end_game");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
wait 0.05;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Both stock engines notify the credited attacker, with the actual dead zombie.
|
// Both stock engines notify the credited attacker, with the actual dead zombie.
|
||||||
// Never infer contributions from spendable score, score_total, or bank actions.
|
// Never infer contributions from spendable score, score_total, or bank actions.
|
||||||
afk_kill_watcher()
|
afk_kill_watcher()
|
||||||
|
|||||||
@@ -300,7 +300,10 @@ configuration, persistence, action results and deployment checks.
|
|||||||
|
|
||||||
## Persistent AFK reserve
|
## Persistent AFK reserve
|
||||||
|
|
||||||
AFK `2.0.0` uses EC authorization and a local central countdown. See the
|
AFK `2.1.0` uses EC authorization and a local central countdown. See the
|
||||||
[shared AFK guide](../../docs/afk.md) for exact commands, dependencies, recovery
|
[shared AFK guide](../../docs/afk.md) for exact commands, dependencies, recovery
|
||||||
and live checks. Install plugin `1.4.0+` and fully restart the server process.
|
and live checks. Install plugin `1.4.0+` and fully restart the server process.
|
||||||
The countdown remains visible with `.hud` hidden; expiration has no grace period.
|
The countdown remains visible with `.hud` hidden; expiration has no grace period.
|
||||||
|
Solo and last-active-player entry are allowed, including all players being AFK
|
||||||
|
together; teammate loss does not force a match end. Upgrading from `2.0.0` adds
|
||||||
|
no dependencies and requires only copying the script and restarting the map.
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ init()
|
|||||||
level thread afk_startup();
|
level thread afk_startup();
|
||||||
level thread afk_end_watcher();
|
level thread afk_end_watcher();
|
||||||
level thread afk_round_watcher();
|
level thread afk_round_watcher();
|
||||||
level thread afk_team_watcher();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
afk_startup()
|
afk_startup()
|
||||||
@@ -405,11 +404,6 @@ afk_activate(player, state)
|
|||||||
afk_say(state, "Wait briefly after returning before entering AFK again.");
|
afk_say(state, "Wait briefly after returning before entering AFK again.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (afk_other_active(player) == 0)
|
|
||||||
{
|
|
||||||
afk_say(state, "The last active player cannot enter AFK. Another player must remain in the fight.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
data = afk_base(state, "reserve");
|
data = afk_base(state, "reserve");
|
||||||
data["session_id"] = telemetryNewMatch();
|
data["session_id"] = telemetryNewMatch();
|
||||||
data["seconds"] = 604800;
|
data["seconds"] = 604800;
|
||||||
@@ -456,7 +450,7 @@ afk_activate(player, state)
|
|||||||
reply = afk_http("start", start_body, start["request_id"]);
|
reply = afk_http("start", start_body, start["request_id"]);
|
||||||
if (!isDefined(reply) || reply["state"] != "started" || reply["session_id"] != session ||
|
if (!isDefined(reply) || reply["state"] != "started" || reply["session_id"] != session ||
|
||||||
reply["authorized_seconds"] != authorized || !state.connected ||
|
reply["authorized_seconds"] != authorized || !state.connected ||
|
||||||
state.cancelled || !afk_can_enter(player) || afk_other_active(player) == 0)
|
state.cancelled || !afk_can_enter(player))
|
||||||
{
|
{
|
||||||
afk_stop_state(state, "never_started");
|
afk_stop_state(state, "never_started");
|
||||||
afk_say(state, "AFK entry cancelled. No protected time used.");
|
afk_say(state, "AFK entry cancelled. No protected time used.");
|
||||||
@@ -707,59 +701,6 @@ afk_end_watcher()
|
|||||||
level.afk_ended = true;
|
level.afk_ended = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
afk_other_active(excluded)
|
|
||||||
{
|
|
||||||
count = 0;
|
|
||||||
players = getPlayers();
|
|
||||||
for (i = 0; i < players.size; i++)
|
|
||||||
{
|
|
||||||
player = players[i];
|
|
||||||
if (isDefined(excluded) && player == excluded)
|
|
||||||
continue;
|
|
||||||
if (!afk_playing(player) || is_true(player.hud_afk_on))
|
|
||||||
continue;
|
|
||||||
if (isDefined(player.tws_afk_state) && player.tws_afk_state.pending)
|
|
||||||
continue;
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
afk_team_watcher()
|
|
||||||
{
|
|
||||||
level endon("end_game");
|
|
||||||
level endon("game_ended");
|
|
||||||
level endon("intermission");
|
|
||||||
for (;;)
|
|
||||||
{
|
|
||||||
players = getPlayers();
|
|
||||||
protected = 0;
|
|
||||||
survivors = 0;
|
|
||||||
stock_recovery = is_true(level.no_end_game_check) || is_true(level.isresetting_grief);
|
|
||||||
for (i = 0; i < players.size; i++)
|
|
||||||
{
|
|
||||||
player = players[i];
|
|
||||||
if (is_true(player.afterlife))
|
|
||||||
stock_recovery = true;
|
|
||||||
if (is_true(player.hud_afk_on))
|
|
||||||
protected++;
|
|
||||||
else if (afk_playing(player))
|
|
||||||
survivors++;
|
|
||||||
}
|
|
||||||
if (protected > 0 && survivors == 0 && !stock_recovery)
|
|
||||||
{
|
|
||||||
for (i = 0; i < players.size; i++)
|
|
||||||
if (is_true(players[i].hud_afk_on))
|
|
||||||
players[i] afk_finish("match_end", false);
|
|
||||||
// AFK does not change sessionstate/player count or zombie scaling.
|
|
||||||
// The verified stock end signal prevents AFK bodies prolonging play.
|
|
||||||
level notify("end_game");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
wait 0.05;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Both stock engines notify the credited attacker, with the actual dead zombie.
|
// Both stock engines notify the credited attacker, with the actual dead zombie.
|
||||||
// Never infer contributions from spendable score, score_total, or bank actions.
|
// Never infer contributions from spendable score, score_total, or bank actions.
|
||||||
afk_kill_watcher()
|
afk_kill_watcher()
|
||||||
|
|||||||
+50
-5
@@ -167,11 +167,56 @@ for(const game of ['t5','t6']) {
|
|||||||
assert.equal(e.p.tws_afk_controls_locked,true);e.p.downed=false;e.p.ignoreme=false;e.tick();assert.equal(e.p.tws_afk_controls_locked,false);
|
assert.equal(e.p.tws_afk_controls_locked,true);e.p.downed=false;e.p.ignoreme=false;e.tick();assert.equal(e.p.tws_afk_controls_locked,false);
|
||||||
assert(!e.source.includes('disableInvulnerability('));assert(!e.source.includes('enableInvulnerability('));
|
assert(!e.source.includes('disableInvulnerability('));assert(!e.source.includes('enableInvulnerability('));
|
||||||
});
|
});
|
||||||
test('last active cannot enter; last active death ends only AFK-only matches, respecting afterlife',()=>{
|
test('solo and last active players can enter with unavailable or pending teammates',()=>{
|
||||||
const alone=engine(game);alone.other.downed=true;alone.enter();assert(!alone.p.hud_afk_on);assert.equal(alone.sent.length,0);
|
for(const flags of [null,{downed:true},{alive:false},{sessionstate:'spectator'},{afterlife:true},{hud_afk_on:true},{pending:true}]){
|
||||||
const e=engine(game);e.enter();e.other.alive=false;e.c.afk_team_watcher().next();assert(!e.p.hud_afk_on);assert.equal(e.stops.at(-1).reason,'match_end');assert(e.log.some(x=>x[0]==='notify'&&x[1]==='end_game'));
|
const e=engine(game);
|
||||||
const recovering=engine(game);recovering.enter();recovering.other.afterlife=true;recovering.c.afk_team_watcher().next();assert(recovering.p.hud_afk_on);
|
if(flags===null)e.players.splice(1,1);
|
||||||
const noAfk=engine(game);noAfk.p.alive=false;noAfk.other.alive=false;noAfk.c.afk_team_watcher().next();assert(!noAfk.log.some(x=>x[0]==='notify'));
|
else if(flags.pending)e.other.tws_afk_state.pending=true;
|
||||||
|
else Object.assign(e.other,flags);
|
||||||
|
e.enter();e.advance(1000);e.tick();
|
||||||
|
assert(e.p.hud_afk_on);assert.equal(e.p.afk_remaining(),1799000);
|
||||||
|
assert.equal(e.stops.length,0);assert.equal(e.p.sessionstate,'playing');
|
||||||
|
assert(!e.log.some(x=>x[0]==='notify'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
test('teammate death or disconnect during authorization does not cancel entry',()=>{
|
||||||
|
for(const at of ['reserve','start'])for(const change of ['death','disconnect']){
|
||||||
|
const e=engine(game,{during:request=>{
|
||||||
|
if(request.action!==at)return;
|
||||||
|
if(change==='death')e.other.alive=false;
|
||||||
|
else e.players.splice(1,1);
|
||||||
|
}});
|
||||||
|
e.enter();e.tick();assert(e.p.hud_afk_on);assert.equal(e.stops.length,0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
test('all players can be AFK and teammate loss does not force a match end',()=>{
|
||||||
|
const e=engine(game);e.enter();e.c.afk_enter(e.other,e.other.tws_afk_state);
|
||||||
|
e.advance(1000);e.tick();e.other.afk_tick().next();
|
||||||
|
assert(e.p.hud_afk_on);assert(e.other.hud_afk_on);assert.equal(e.stops.length,0);
|
||||||
|
e.other.alive=false;e.other.afk_tick().next();e.tick();
|
||||||
|
assert(e.p.hud_afk_on);assert.equal(e.stops.at(-1).reason,'death');
|
||||||
|
e.players.splice(1,1);e.advance(1000);e.tick();assert(e.p.hud_afk_on);
|
||||||
|
assert(!e.log.some(x=>x[0]==='notify'));
|
||||||
|
assert(!e.source.includes('afk_team_watcher'));
|
||||||
|
assert(!/level\s+notify\("end_game"\)/.test(e.source));
|
||||||
|
});
|
||||||
|
test('solo protection expires normally and manual return preserves bounded grace',()=>{
|
||||||
|
for(const manual of [false,true]){
|
||||||
|
const e=engine(game,{authorized:3});e.players.splice(1,1);e.enter();
|
||||||
|
e.advance(1000);assert.equal(e.p.afk_damage(null,null,10),0);
|
||||||
|
if(manual){e.p.afk_exit();assert.equal(e.stops.at(-1).reason,'manual');}
|
||||||
|
e.advance(2000);e.tick();
|
||||||
|
assert(!e.p.hud_afk_on);assert(!e.p.tws_afk_protected);assert(!e.p.ignoreme);
|
||||||
|
assert(!e.p.tws_afk_controls_locked);assert.equal(e.p.afk_damage(null,null,10),-1);
|
||||||
|
if(!manual)assert.equal(e.stops.at(-1).reason,'expired');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
test('stock match end still releases and settles every AFK player',()=>{
|
||||||
|
const e=engine(game);e.enter();e.c.afk_enter(e.other,e.other.tws_afk_state);
|
||||||
|
const watcher=e.c.afk_end_watcher();watcher.next();watcher.next();
|
||||||
|
assert(e.level.afk_ended);assert.equal(e.stops.length,2);
|
||||||
|
for(const p of e.players){assert(!p.hud_afk_on);assert(!p.tws_afk_protected);assert(p.tws_afk_state.cancelled);}
|
||||||
|
assert(e.stops.every(s=>s.reason==='match_end'));
|
||||||
});
|
});
|
||||||
test('repeat legitimate teammate revives all count, self/AFK/spectator revives do not',()=>{
|
test('repeat legitimate teammate revives all count, self/AFK/spectator revives do not',()=>{
|
||||||
const e=engine(game);const watcher=e.other.afk_revive_watcher();watcher.next();
|
const e=engine(game);const watcher=e.other.afk_revive_watcher();watcher.next();
|
||||||
|
|||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"tws_t5_afk.gsc": "2.0.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.5.0",
|
||||||
"tws_t5_hud.gsc": "1.4.2",
|
"tws_t5_hud.gsc": "1.4.2",
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
"tws_t5_leaderboard.gsc": "1.2.0",
|
"tws_t5_leaderboard.gsc": "1.2.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": "2.0.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.5.0",
|
||||||
"tws_t6_hud.gsc": "1.4.2",
|
"tws_t6_hud.gsc": "1.4.2",
|
||||||
|
|||||||
Reference in New Issue
Block a user