/* 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; }