Import T5-AIO and T6 into combined repo
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
.research/
|
||||
compiled/
|
||||
__pycache__/
|
||||
+98
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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.'
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user