Import T5-AIO and T6 into combined repo

This commit is contained in:
2026-09-23 23:33:09 -04:00
commit f77cc7bdee
22 changed files with 3256 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
.research/
compiled/
__pycache__/
+98
View File
@@ -0,0 +1,98 @@
# T6 port notes
Baseline: the current files in `C:\Users\josh1\Documents\Code\TWS-T5-AIO`.
The T5 originals were not modified. DB, bank, their schema and the added example
config have been removed from this project.
## Differences retained for T6 and requested fixes
| Area | Difference from T5 | Reason |
| --- | --- | --- |
| Paths | `maps/mp/_utility`, `scripts/zm/tws_t6_common` | T6 namespaces and requested filenames |
| Loading | No T5 `zombiemode` dvar gate; each module has `main()` and `init()` | Zombies-only folder loading; avoid r5346 missing-main warnings |
| HUD positioning | Fullscreen screen anchors, same 10-unit margin | Requested fix for inset HUD |
| HUD rendering | Precached white shader, local max-health fallback, floating-point ratio, `getPlayers()` | T6 rendering/state handling without changing player max health |
| Enemy count | Stock `_zm_utility::get_current_zombie_count()` plus queued zombies | Uses the map's zombie team and enemy filters |
| Timers | Seed elapsed time for late joiners; stop round watcher/reset on game over | T6 timer behavior; keep frozen timers frozen |
| AFK | Maintain control freeze, disable guns/grenades, guard player state and stale grace threads | Requested movement/shooting fix; retain `.afk` / `.back` and two-second grace |
| Telemetry | Global `httpPost(url, options)`; no unnecessary common dependency | Avoid raw-loader `http.gsc` and obsolete common-path lookup errors |
Common retains the T5 HUD constants, helpers and first-spawn dispatcher.
HUD again owns the `hud_game_over` notification fan-out. Lock again waits on HUD
notifications, applies T5's random password, and clears it on startup/game over.
The added password-preservation dvars and independent lock watcher are gone.
AFK again listens to the native `say` notification, so it no longer needs plugin
chat registration. The additional T6 controls are only held while AFK; `.back`
releases them before the protection grace period. Restoration is deferred during
death/laststand/intermission until active play. Other mods controlling the same
invulnerability or weapon flags can still conflict and need live testing.
The four optional T5 dvars remain; no new configuration switches are introduced.
`common_config()` and `ensure_config()` are original T5 in-memory HUD helpers,
not an external configuration system.
## Verified primary sources
- [Plutonium loading guide](https://plutonium.pw/docs/modding/loading-mods/): T6
source scripts load from `storage/t6/scripts/zm`.
- [Entry-point lifecycle](https://plutonium.pw/docs/modding/gsc/new-scripting-features/):
`main()` runs early; `init()` runs after gametype startup. Empty `main()` must
not call `init()` and duplicate initialization.
- [Plutonium changelog, r2234](https://plutonium.pw/docs/changelog/): documents T6
`level waittill("say", message, player)`.
- [Stock T6 Zombies](https://github.com/plutoniummod/t6-scripts/blob/2d8440321602f22f63f5335817110d60ddf223d5/ZM/Core/maps/mp/zombies/_zm.gsc):
round state, `end_game`, invulnerability, and stock control resets.
- [Stock zombie utility](https://github.com/plutoniummod/t6-scripts/blob/2d8440321602f22f63f5335817110d60ddf223d5/ZM/Core/maps/mp/zombies/_zm_utility.gsc):
map-aware enemy counting.
- [Released plugin HTTP implementation](https://github.com/alicealys/t6-gsc-utils/blob/v3.1.1/src/component/http.cpp):
global `httpPost(url, options)` takes an options array with `body` and `headers`.
Its positional README example does not match this implementation.
Completion is `done(response, success, error)`; success reports transport only.
- [Plugin JSON implementation](https://github.com/alicealys/t6-gsc-utils/blob/v3.1.1/src/component/json.cpp):
`jsonSerialize` handles the event payload.
Do not create a dummy `http.gsc`. An unresolved `httpPost`/`jsonSerialize` means
the installed server/plugin must be checked. Installing a DLL requires a full
server-process restart, not just a map restart.
## Telemetry reboot investigation
The T5 and T6 implementations are not runtime-verified equivalents. In particular,
[T6 plugin v3.0.4](https://github.com/alicealys/t6-gsc-utils/blob/v3.0.4/src/component/http.cpp)
expects `httpPost(url, body, headers)` and notifies only the response body;
v3.1.1 expects `httpPost(url, options)` and also provides success/error values.
The current script targets v3.1.1. Switching the function name does not establish
compatibility with an unknown installed DLL.
Console markers identify `main`, `init`, round serialization, HTTP invocation,
handle return and completion. They do not log the URL or response body. If startup
fails before `main`, inspect the first linker/plugin error, not the last supervisor
restart message. If no occupied positive round was observed, this script has not
called JSON serialization or HTTP yet.
Earlier live validation: the operator confirmed successful server startup and
Fleet delivery with the original direct reporter. The earlier reboot cause was
not established; logging alone should not be treated as a proven fix.
To recover the server while investigating, move `tws_t6_telemetry.gsc` outside the
autoload directory. The enable dvar cannot bypass unresolved native functions
at link time. The other four TWS modules can run without telemetry.
## Checks
Local: all five files pass gsc-tool 1.4.10 T6 PC compilation; shared references and
entry points are checked by `tools/check.ps1`. No live server was run for this
baseline simplification. Compiler success does not certify runtime linking.
Live checks after deploying:
1. Join, late-join, die/respawn, and reconnect: one HUD per player, correct corner
position, health/Juggernog changes, zombies remaining, timers frozen at game over.
2. `.afk` blocks movement, shooting and grenades; `.back` restores them. Try rapid
toggles, a round transition, death/respawn and game over.
3. Lock fires at the threshold and clears on game over/restart. HUD must be installed.
T5 semantics intentionally clear an existing password at startup.
4. Telemetry sends the initial occupied round and each new round to the existing
endpoint. Check server ID/player count, same-round joins and an unreachable API.
5. Repeat on the actual server maps, especially afterlife and special-enemy rounds.
+84
View File
@@ -0,0 +1,84 @@
# TWS T6 Zombies scripts
Plutonium T6 versions of the T5 server scripts. Defaults live in the scripts,
just like T5. There is no separate TWS config file or database setup.
| File | Role | Dependencies |
| --- | --- | --- |
| `tws_t6_common.gsc` | Shared HUD values, helpers and first-spawn dispatcher | Stock T6 |
| `tws_t6_hud.gsc` | Match/round timers, name, zombie count and health bar | 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_telemetry.gsc` | HTTP JSON round updates, enabled by default | t6-gsc-utils |
## Install
1. Copy these five `tws_t6_*.gsc` files into `Plutonium/storage/t6/scripts/zm/`.
2. For telemetry, install [t6-gsc-utils v3.1.1](https://github.com/alicealys/t6-gsc-utils/releases/tag/v3.1.1)
in `Plutonium/plugins/` and restart the server process. The other four scripts
do not require that plugin.
3. Restart the map to load script updates.
If updating an existing deployment, **remove `tws_t6_db.gsc` and
`tws_t6_bank.gsc` from the server script directory**. Copying the remaining files
will not remove those old scripts. Remove any `exec server.example.cfg` added for
the earlier version. Neither a database nor that config file is used anymore.
Source `.gsc` files load directly under Plutonium's
[documented T6 script layout](https://plutonium.pw/docs/modding/loading-mods/).
Keep the filenames and `scripts/zm` location so shared references resolve.
To omit telemetry, remove its script; disabling it at runtime does not remove
its plugin dependency during linking.
## Defaults, matching T5
- HUD values live in `tws_t6_common::common_config()`. That function and
`ensure_config()` came from T5; they initialize in-script values and do not
load a config file. The requested fullscreen corner alignment is retained.
- AFK uses T5's native `level waittill("say", message, player)` listener.
`.back` restores movement and weapons immediately, then ends protection after
two seconds. The T6 control/weapon fix remains in place.
- Lock uses T5's HUD round/game-over notifications. Default threshold is 20.
As in T5, it clears `g_password` at startup and game over.
- Telemetry is enabled by default and posts to
`http://192.168.1.199:3000/api/telemetry`. Set `tws_server_id` to the exact,
case-sensitive Electric Cherry registry name; missing or overlong names
disable reporting with a console diagnostic.
The optional dvars already present in T5 are still supported: `tws_lock_round`,
`tws_server_id`, `tws_telemetry_url`, and `tws_telemetry_enabled` (`0` disables
telemetry). None require a separate config file. If earlier settings are already
set on the server, they still override these defaults.
## Round telemetry
Telemetry runs directly in GSC using t6-gsc-utils. Configure the exact Electric
Cherry registry name and full endpoint in your existing server configuration:
```cfg
set tws_server_id "t6_zombie_town_1"
set tws_telemetry_url "http://192.168.1.199:3000/api/telemetry"
```
It sends the first occupied positive round and subsequent round changes:
```json
{"event":"round_change","server":"t6_zombie_town_1","round":17,"players":4}
```
Delivery is asynchronous, with one in-flight request and one replaceable pending
observation. Transport failures retry at most three times (1/2/5 seconds), retiring
snapshots older than 15 seconds or superseded by a newer round. Transport success
does not establish API acceptance. These reports update Fleet; they do not send
beta boot, session, feature or match-completion evidence.
## Validation
All five scripts compile for T6 PC with gsc-tool 1.4.10. To repeat:
```powershell
.\tools\check.ps1 -CompilerPath C:\path\to\gsc-tool.exe
```
Compilation does not verify engine/plugin linking or gameplay. T6 differences,
research sources and live checks are in [NOTES.md](NOTES.md).
+34
View File
@@ -0,0 +1,34 @@
param(
[Parameter(Mandatory = $true)]
[string]$CompilerPath
)
$ErrorActionPreference = 'Stop'
$compiler = (Resolve-Path -LiteralPath $CompilerPath).Path
$repo = Split-Path -Parent $PSScriptRoot
$files = @(Get-ChildItem -LiteralPath $repo -Filter 'tws_t6_*.gsc' | Sort-Object Name)
if ($files.Count -ne 5) { throw 'Expected the five TWS modules: common, HUD, AFK, lock, telemetry.' }
$common = Get-Content -LiteralPath (Join-Path $repo 'tws_t6_common.gsc') -Raw
$functions = @([regex]::Matches($common, '(?m)^([a-zA-Z_][a-zA-Z_0-9]*)\([^\r\n]*\)\s*\r?\n\{') | ForEach-Object { $_.Groups[1].Value })
foreach ($file in $files) {
$source = Get-Content -LiteralPath $file.FullName -Raw
foreach ($entry in @('main', 'init')) {
if ($source -notmatch "(?m)^$entry\(\)\s*\r?\n\{") {
throw "Missing $entry entry point in $($file.Name)"
}
}
if ($source -match 'scripts\\sp\\|maps\\_utility|#"zombiemode"') {
throw "T5-only reference in $($file.Name)"
}
foreach ($reference in [regex]::Matches($source, 'scripts\\zm\\tws_t6_common::([a-zA-Z_][a-zA-Z_0-9]*)')) {
if ($reference.Groups[1].Value -notin $functions) {
throw "Missing common function: $($reference.Groups[1].Value)"
}
}
$output = (& $compiler -m comp -g t6 -s pc -y $file.FullName 2>&1 | Out-String)
# gsc-tool can return zero even when compilation emits an error.
if ($LASTEXITCODE -ne 0 -or $output -match '(?i)error|failed' -or $output -notmatch 'compiled t6/') {
throw "Compile failed for $($file.Name):`n$output"
}
Write-Output "PASS $($file.Name)"
}
Write-Output 'All five T6 compile checks and common references passed. Runtime linking/gameplay require Plutonium.'
+142
View File
@@ -0,0 +1,142 @@
/* T5-style .afk / .back listener with T6 control and weapon blocking. */
#include maps\mp\_utility;
#include common_scripts\utility;
// Plutonium probes both entry points; runtime setup belongs in init.
main()
{
}
init()
{
scripts\zm\tws_t6_common::ensure_config();
afk_init();
scripts\zm\tws_t6_common::register_spawn_handler(::afk_spawn);
}
afk_init()
{
level.afk_grace_seconds = 2; // invuln tail on .back
level thread afk_chat_listener();
}
afk_chat_listener()
{
for (;;)
{
level waittill("say", message, player);
if (!isDefined(player) || !isDefined(message))
continue;
cmd = toLower(message);
if (cmd == ".afk")
player thread afk_enter();
else if (cmd == ".back")
player thread afk_exit();
}
}
afk_spawn()
{
self endon("disconnect");
self.tws_afk_generation = 0;
self.hud_afk_on = false;
self.tws_afk_protected = false;
self.tws_afk_controls_locked = false;
elem = scripts\zm\tws_t6_common::make_text_elem(self,
level.hud_x + 42, level.hud_row_match, "left",
level.hud_font_small, level.hud_c_afk);
elem SetText("[AFK]");
elem.alpha = 0;
self.hud_afk_elem = elem;
elems = [];
elems[0] = elem;
self thread scripts\zm\tws_t6_common::destroy_on_disconnect(elems);
for (;;)
{
if (self.tws_afk_protected && !scripts\zm\tws_t6_common::player_active(self))
self afk_release();
if (self.hud_afk_on)
self afk_lock_controls();
else if (self.tws_afk_controls_locked)
self afk_restore_controls();
wait 0.05;
}
}
afk_enter()
{
if (!isDefined(self.hud_afk_on) || self.hud_afk_on)
return;
if (!scripts\zm\tws_t6_common::player_active(self))
return;
// Do not take ownership of stock ignoreme (laststand, afterlife, etc.).
if (is_true(self.ignoreme) && !self.tws_afk_protected)
return;
self.tws_afk_generation++;
self.hud_afk_on = true;
self.tws_afk_protected = true;
self.ignoreme = true;
self enableInvulnerability();
self afk_lock_controls();
self.hud_afk_elem.alpha = 1;
self iPrintLnBold("AFK - type .back to return");
}
afk_exit()
{
self endon("disconnect");
if (!isDefined(self.hud_afk_on) || !self.hud_afk_on)
return;
self.hud_afk_on = false;
self afk_restore_controls();
self.hud_afk_elem.alpha = 0;
self.tws_afk_generation++;
generation = self.tws_afk_generation;
self iPrintLnBold("Back in the fight");
wait level.afk_grace_seconds;
if (generation == self.tws_afk_generation && self.tws_afk_protected)
self afk_release();
}
afk_release()
{
self.tws_afk_generation++;
self.hud_afk_on = false;
self.tws_afk_protected = false;
self afk_restore_controls();
self.hud_afk_elem.alpha = 0;
// Stock scripts own protection during laststand/intermission/death.
if (scripts\zm\tws_t6_common::player_active(self))
{
self.ignoreme = false;
self disableInvulnerability();
}
}
afk_lock_controls()
{
// Stock gameplay can clear the freeze flag after AFK entry. Maintain it
// while AFK and explicitly block guns and offhand weapons as well.
self.tws_afk_controls_locked = true;
self freezeControls(true);
self disableWeapons();
self disableOffhandWeapons();
}
afk_restore_controls()
{
if (!self.tws_afk_controls_locked)
return;
// Leave laststand/intermission controls to stock scripts. If the player
// respawns, the watcher restores our controls once they are active again.
if (!scripts\zm\tws_t6_common::player_active(self))
return;
self.tws_afk_controls_locked = false;
self freezeControls(false);
self enableWeapons();
self enableOffhandWeapons();
}
+190
View File
@@ -0,0 +1,190 @@
/* tws_t6_common.gsc — shared config, HUD helpers, spawn-handler registry, connect dispatcher. */
#include maps\mp\_utility;
#include common_scripts\utility;
main()
{
precacheShader("white");
}
init()
{
common_config();
level thread on_player_connect_dispatch();
}
common_config()
{
if (isDefined(level.hud_x))
return;
// Small inset from the physical screen corner (fullscreen HUD coordinates).
level.hud_x = 10;
level.hud_y = 10;
level.hud_row_match = level.hud_y;
level.hud_row_round = level.hud_y + 15;
level.hud_row_name = level.hud_y + 32;
level.hud_row_count = level.hud_y + 54;
level.hud_row_bar = level.hud_y + 84;
level.hud_font_small = 1.4;
level.hud_font_large = 2.0;
level.hud_font_mid = 1.6;
level.hud_bar_width = 120;
level.hud_bar_height = 8;
level.hud_col_count = level.hud_x + 90;
level.hud_col_name = level.hud_x + 60;
level.hud_col_health = level.hud_x + level.hud_bar_width + 12;
level.hud_rate = 0.1;
// green/amber/red ramp is health-only
level.hud_c_label = (0.85, 0.85, 0.85);
level.hud_c_dim = (0.55, 0.55, 0.55);
level.hud_c_count = (1, 1, 1);
level.hud_c_name = (0.4, 0.9, 0.5);
level.hud_c_high = (0.2, 1, 0.4);
level.hud_c_mid = (1, 0.8, 0);
level.hud_c_low = (1, 0.2, 0.2);
level.hud_c_afk = (1, 0.8, 0.2);
// numbers fade in below this fraction of max hp
level.hud_health_threshold = 1.0;
if (!isDefined(level.tws_spawn_handlers))
level.tws_spawn_handlers = [];
}
ensure_config()
{
if (!isDefined(level.hud_x))
common_config();
}
register_spawn_handler(fn)
{
if (!isDefined(level.tws_spawn_handlers))
level.tws_spawn_handlers = [];
level.tws_spawn_handlers[level.tws_spawn_handlers.size] = fn;
}
// Shared dispatcher: one HUD instance and account load per connection.
on_player_connect_dispatch()
{
for (;;)
{
level waittill("connected", player);
player thread on_player_spawned_dispatch();
}
}
// handlers own their own self endon("disconnect")
on_player_spawned_dispatch()
{
self endon("disconnect");
self waittill("spawned_player");
if (!isDefined(level.tws_spawn_handlers))
return;
for (i = 0; i < level.tws_spawn_handlers.size; i++)
self thread [[ level.tws_spawn_handlers[i] ]]();
}
make_text_elem(player, x, y, align_x, scale, colour)
{
elem = NewClientHudElem(player);
// Fullscreen bypasses the safe-area inset of left/top screen anchors.
elem.horzAlign = "fullscreen";
elem.vertAlign = "fullscreen";
elem.alignX = align_x;
elem.alignY = "top";
elem.x = x;
elem.y = y;
elem.foreground = 1;
elem.fontScale = scale;
elem.alpha = 1;
elem.color = colour;
// black glow reads as an outline against bright geometry
elem.glowColor = (0, 0, 0);
elem.glowAlpha = 0.6;
return elem;
}
make_shader_elem(player, x, y)
{
elem = NewClientHudElem(player);
elem.horzAlign = "fullscreen";
elem.vertAlign = "fullscreen";
elem.alignX = "left";
elem.alignY = "top";
elem.x = x;
elem.y = y;
elem.foreground = 1;
elem.alpha = 1;
return elem;
}
destroy_on_disconnect(elems)
{
self waittill("disconnect");
for (i = 0; i < elems.size; i++)
{
if (isDefined(elems[i]))
elems[i] Destroy();
}
}
int_dvar_or(name, fallback)
{
raw = GetDvar(name);
if (raw == "")
return fallback;
return int(raw);
}
format_mmss(elapsed_ms)
{
total_secs = int(elapsed_ms / 1000);
if (total_secs < 0)
total_secs = 0;
mins = int(total_secs / 60);
secs = total_secs - (mins * 60);
sec_str = "" + secs;
if (secs < 10)
sec_str = "0" + secs;
return "" + mins + ":" + sec_str;
}
// T6 state guard used by the AFK control fix.
player_active(player)
{
if (is_true(level.intermission) || is_true(level.gameended))
return false;
if (!isDefined(player) || !isAlive(player))
return false;
if (!isDefined(player.sessionstate) || player.sessionstate != "playing")
return false;
if (player maps\mp\zombies\_zm_laststand::player_is_in_laststand())
return false;
if (isDefined(player.is_zombie) && player.is_zombie)
return false;
return true;
}
+345
View File
@@ -0,0 +1,345 @@
/* tws_t6_hud.gsc - configurable HUD stack: timers, zombie count, health bar. */
#include maps\mp\_utility;
#include common_scripts\utility;
// Plutonium probes both entry points; runtime setup belongs in init.
main()
{
}
init()
{
scripts\zm\tws_t6_common::ensure_config();
level thread round_watcher();
level thread game_over_watcher();
scripts\zm\tws_t6_common::register_spawn_handler(::hud_start);
}
hud_start()
{
self endon("disconnect");
if (isDefined(level.tws_ended) && level.tws_ended)
return;
self thread hud_timer_row();
self thread hud_zombie_counter();
self thread hud_health_display();
}
// Same game-over notification fan-out as T5.
game_over_watcher()
{
level thread game_over_via("end_game");
level thread game_over_via("game_ended");
level thread game_over_via("intermission");
}
game_over_via(notify_name)
{
level endon("hud_game_over");
level waittill(notify_name);
level.tws_ended = true;
level notify("hud_game_over");
}
// Poll stock round state; the first player spawn seeds pre-set round 1.
round_watcher()
{
level endon("hud_game_over");
last = 0;
if (isDefined(level.round_number))
last = level.round_number;
for (;;)
{
if (isDefined(level.round_number) && level.round_number != last)
{
last = level.round_number;
level.round_start_time = getTime();
if (!isDefined(level.match_start_time) && level.round_number >= 1)
{
level.match_start_time = getTime();
level notify("hud_match_started");
}
level notify("hud_round_changed");
}
wait (0.5);
}
}
hud_timer_row()
{
self endon("disconnect");
// fallback seed for the round-1-already-active case (round_watcher misses it)
if (!isDefined(level.match_start_time) &&
isDefined(level.round_number) &&
level.round_number >= 1)
{
level.match_start_time = getTime();
level.round_start_time = getTime();
level notify("hud_match_started");
}
match = scripts\zm\tws_t6_common::make_text_elem(
self, level.hud_x, level.hud_row_match,
"left", level.hud_font_small, level.hud_c_dim);
match.alpha = 0; // revealed by match_timer_bind when round 1 opens
round = scripts\zm\tws_t6_common::make_text_elem(
self, level.hud_x, level.hud_row_round,
"left", level.hud_font_small, level.hud_c_dim);
round SetTimerUp(0);
if (isDefined(level.round_start_time))
round SetTimerUp(max(0, (getTime() - level.round_start_time) / 1000.0));
// name only shows in co-op — your own name is noise in solo
name = scripts\zm\tws_t6_common::make_text_elem(
self, level.hud_x, level.hud_row_name,
"left", level.hud_font_small, level.hud_c_name);
if (isDefined(self.name))
name SetText(self.name);
else
name SetText(self.playername);
name.alpha = 0;
elems = [];
elems[elems.size] = match;
elems[elems.size] = round;
elems[elems.size] = name;
self thread scripts\zm\tws_t6_common::destroy_on_disconnect(elems);
self thread reset_timer_each_round(round);
self thread match_timer_bind(match);
self thread freeze_timers_on_game_over(match, round);
coop = -1;
for (;;)
{
is_coop = 0;
players = getPlayers();
if (players.size > 1)
is_coop = 1;
if (is_coop != coop)
{
name.alpha = is_coop;
coop = is_coop;
}
wait (1);
}
}
reset_timer_each_round(timer)
{
level endon("hud_game_over");
self endon("disconnect");
for (;;)
{
level waittill("hud_round_changed");
if (isDefined(timer))
timer SetTimerUp(0);
}
}
// T6 timer seed includes elapsed time for late joiners.
match_timer_bind(elem)
{
self endon("disconnect");
if (!isDefined(level.match_start_time))
level waittill("hud_match_started");
if (isDefined(elem))
{
elem SetTimerUp(max(0, (getTime() - level.match_start_time) / 1000.0));
elem.alpha = 1;
}
}
/*
Level-scope trigger only — per-player death/health polling is intercepted
by the zombiemode scripts. Freeze value math relies on the timers being
anchored at first spawn / first round transition, not level init.
*/
freeze_timers_on_game_over(match_elem, round_elem)
{
self endon("disconnect");
level waittill("hud_game_over");
if (isDefined(match_elem) && isDefined(level.match_start_time))
match_elem SetText(scripts\zm\tws_t6_common::format_mmss(getTime() - level.match_start_time));
if (isDefined(round_elem) && isDefined(level.round_start_time))
round_elem SetText(scripts\zm\tws_t6_common::format_mmss(getTime() - level.round_start_time));
}
hud_zombie_counter()
{
self endon("disconnect");
label = scripts\zm\tws_t6_common::make_text_elem(
self, level.hud_x, level.hud_row_count,
"left", level.hud_font_large, level.hud_c_label);
label SetText("Zombies");
count = scripts\zm\tws_t6_common::make_text_elem(
self, level.hud_col_count, level.hud_row_count,
"left", level.hud_font_large, level.hud_c_count);
count SetValue(0);
elems = [];
elems[elems.size] = label;
elems[elems.size] = count;
self thread scripts\zm\tws_t6_common::destroy_on_disconnect(elems);
last = -1;
for (;;)
{
remaining = 0;
if (isDefined(level.zombie_team))
remaining = maps\mp\zombies\_zm_utility::get_current_zombie_count();
if (isDefined(level.zombie_total))
remaining += max(0, level.zombie_total);
if (remaining != last)
{
count SetValue(remaining);
last = remaining;
}
wait (level.hud_rate);
}
}
hud_health_display()
{
self endon("disconnect");
bar_width = level.hud_bar_width;
// -2 on both axes so the back frames the bar with 2px padding
back = scripts\zm\tws_t6_common::make_shader_elem(self, level.hud_x - 2, level.hud_row_bar - 2);
back.alpha = 0.5;
back.color = (0, 0, 0);
back SetShader("white", bar_width + 4, level.hud_bar_height + 4);
bar = scripts\zm\tws_t6_common::make_shader_elem(self, level.hud_x, level.hud_row_bar);
bar.color = level.hud_c_high;
bar SetShader("white", bar_width, level.hud_bar_height);
/*
"87/160" is three elements, not a rebuilt string. SetText allocates
a config string per call; per-tick rebuilds crash the server.
cur is right-aligned so digits grow leftward and the slash + max
never shift.
*/
cur = scripts\zm\tws_t6_common::make_text_elem(
self, level.hud_col_health + 26, level.hud_row_bar + 3,
"right", level.hud_font_mid, level.hud_c_high);
cur SetValue(self.health);
slash = scripts\zm\tws_t6_common::make_text_elem(
self, level.hud_col_health + 30, level.hud_row_bar + 3,
"center", level.hud_font_mid, level.hud_c_high);
slash SetText("/");
max_hp = scripts\zm\tws_t6_common::make_text_elem(
self, level.hud_col_health + 34, level.hud_row_bar + 3,
"left", level.hud_font_mid, level.hud_c_high);
max_hp SetValue(100);
cur.alpha = 0;
slash.alpha = 0;
max_hp.alpha = 0;
elems = [];
elems[elems.size] = back;
elems[elems.size] = bar;
elems[elems.size] = cur;
elems[elems.size] = slash;
elems[elems.size] = max_hp;
self thread scripts\zm\tws_t6_common::destroy_on_disconnect(elems);
last_width = bar_width;
last_health = -1;
last_max = -1;
last_shown = -1;
for (;;)
{
maximum = 100;
if (isDefined(self.maxhealth) && self.maxhealth > 0)
maximum = self.maxhealth;
frac = self.health / (maximum * 1.0);
if (frac > 1)
frac = 1;
if (frac < 0)
frac = 0;
width = int(max(frac * bar_width, 1));
if (width != last_width)
{
bar SetShader("white", width, level.hud_bar_height);
last_width = width;
}
colour = level.hud_c_high;
if (frac <= 0.25)
colour = level.hud_c_low;
else if (frac <= 0.5)
colour = level.hud_c_mid;
bar.color = colour;
cur.color = colour;
slash.color = colour;
max_hp.color = colour;
// numbers only show once you're hurt
show = 0;
if (frac < level.hud_health_threshold)
show = 1;
if (show != last_shown)
{
cur.alpha = show;
slash.alpha = show;
max_hp.alpha = show;
last_shown = show;
}
if (self.health != last_health)
{
cur SetValue(self.health);
last_health = self.health;
}
if (maximum != last_max)
{
max_hp SetValue(maximum);
last_max = maximum;
}
wait (level.hud_rate);
}
}
+69
View File
@@ -0,0 +1,69 @@
/*
tws_t6_lock.gsc — lobby g_password lock at milestone round. Existing players
are unaffected; g_password is only checked at connect. Cleared on
hud_game_over so the next lobby opens unlocked.
*/
#include maps\mp\_utility;
#include common_scripts\utility;
// T6 loader probes main and init; initialize only once.
main()
{
}
init()
{
scripts\zm\tws_t6_common::ensure_config();
lock_init();
}
lock_init()
{
level.lock_round = scripts\zm\tws_t6_common::int_dvar_or("tws_lock_round", 20);
level.lock_active = false;
// recover from a prior lobby that crashed before clearing
setDvar("g_password", "");
level thread lock_round_watcher();
level thread lock_cleanup_watcher();
}
lock_round_watcher()
{
level endon("hud_game_over");
for (;;)
{
level waittill("hud_round_changed");
if (level.lock_active)
continue;
if (!isDefined(level.round_number) || level.round_number < level.lock_round)
continue;
// random per activation so the password can't be inferred from source
password = "lock_" + getTime() + "_" + randomInt(1000000);
setDvar("g_password", password);
level.lock_active = true;
lock_announce();
}
}
lock_cleanup_watcher()
{
level waittill("hud_game_over");
setDvar("g_password", "");
level.lock_active = false;
}
lock_announce()
{
players = getPlayers();
for (i = 0; i < players.size; i++)
players[i] iPrintLnBold("^3Server locked");
}
+163
View File
@@ -0,0 +1,163 @@
/* tws_t6_telemetry.gsc - direct HTTP round telemetry via t6-gsc-utils. */
#include maps\mp\_utility;
#include common_scripts\utility;
// Plutonium probes both entry points; runtime setup belongs in init.
main()
{
println("[tws_t6_telemetry] main reached (HTTP API target: t6-gsc-utils v3.1.1)");
}
init()
{
println("[tws_t6_telemetry] init reached");
// Loaded only from scripts/zm; no shared HUD configuration is needed.
// Enabled when unset; explicitly set 0 to opt out.
if (GetDvar("tws_telemetry_enabled") == "0")
{
println("[tws_t6_telemetry] disabled");
return;
}
// Repeated init in the same VM must not create competing senders.
if (isDefined(level.telemetry_initialized))
return;
level.telemetry_url = GetDvar("tws_telemetry_url");
if (level.telemetry_url == "")
level.telemetry_url = "http://192.168.1.199:3000/api/telemetry";
level.telemetry_server = GetDvar("tws_server_id");
if (level.telemetry_server == "" || level.telemetry_server.size > 255)
{
println("[tws_t6_telemetry] Set tws_server_id to the exact registered server name (1-255 characters)");
return;
}
level.telemetry_initialized = true;
level.telemetry_pending = undefined;
println("[tws_t6_telemetry] watching for an occupied positive round");
level thread telemetry_delivery_worker();
level thread telemetry_round_watcher();
}
// Independent of HUD installation. Report the first occupied positive round,
// including round 1 when the map pre-seeds it, then each observed transition.
telemetry_round_watcher()
{
level endon("end_game");
level endon("game_ended");
level endon("intermission");
last_round = -1;
for (;;)
{
if (isDefined(level.round_number) && level.round_number >= 1 &&
level.round_number != last_round)
{
players = getPlayers();
if (players.size > 0)
{
last_round = level.round_number;
// Snapshot values before handing off to the HTTP thread.
data = [];
data["event"] = "round_change";
data["round"] = int(last_round);
data["server"] = level.telemetry_server;
data["players"] = players.size;
// No runId field (including null/empty), boot, session or
// feature events in Fleet-only mode. Keep just the newest
// pending observation so an outage cannot build a backlog.
pending = spawnStruct();
println("[tws_t6_telemetry] serializing round " + last_round);
pending.body = jsonSerialize(data);
pending.observed_at = getTime();
level.telemetry_pending = pending;
}
}
wait (0.5);
}
}
telemetry_delivery_worker()
{
level endon("end_game");
level endon("game_ended");
level endon("intermission");
backoff = [];
backoff[0] = 1;
backoff[1] = 2;
backoff[2] = 5;
for (;;)
{
if (!isDefined(level.telemetry_pending))
{
wait (0.5);
continue;
}
pending = level.telemetry_pending;
level.telemetry_pending = undefined;
// One in-flight request plus one replaceable pending observation.
// A newer observation always retires retries of an older round.
for (attempt = 0; attempt < 4; attempt++)
{
if (isDefined(level.telemetry_pending) ||
getTime() - pending.observed_at > 15000)
break;
if (telemetry_send(pending.body))
break;
if (attempt < 3)
wait (backoff[attempt]);
}
}
}
// Returns transport completion only, NEVER beta/API acceptance. The released
// plugin exposes body/success/error but no HTTP status or cancellation. Do not
// start a replacement request on a script timeout: the old one could still
// commit later and overwrite Fleet with a stale round. Gameplay never waits
// on this worker. Malformed/HTML bodies are not passed to throwing jsonParse.
telemetry_send(body)
{
headers = [];
headers["Content-Type"] = "application/json";
options = [];
options["method"] = "POST";
options["headers"] = headers;
options["body"] = body;
// v3.1.1 global alias: options array, NOT positional body/headers.
// Avoid the raw loader treating the http namespace as http.gsc.
println("[tws_t6_telemetry] calling httpPost");
request = httpPost(level.telemetry_url, options);
println("[tws_t6_telemetry] HTTP handle returned; waiting for completion");
request waittill("done", response, success, error);
println("[tws_t6_telemetry] HTTP completion received");
// The plugin's success flag reports transport completion, not HTTP status.
// Unscoped round reports replace Fleet state; retrying the same snapshot
// cannot duplicate beta evidence. HTTP errors cannot be classified here,
// so only reported transport failures receive bounded retries.
if (!isDefined(success) || !success)
{
if (!isDefined(error))
error = "unknown transport error";
print("^1[tws_t6_telemetry] Round telemetry failed: " + error);
return false;
}
println("[tws_t6_telemetry] Transport completed; API acceptance is not verified by this plugin");
return true;
}