Files
tws-zombies-scripts/t6/scripts/tws_t6_afk.gsc
T

1019 lines
33 KiB
Plaintext

/* Persistent AFK reserve. Requires the TWS native AFK journal API and EC /api/afk. */
#include maps\mp\_utility;
#include common_scripts\utility;
main()
{
}
init()
{
scripts\zm\tws_t6_common::ensure_config();
level.afk_url = "https://ec.thewrightserver.net/api/afk";
level.afk_server = GetDvar("tws_server_id");
level.afk_token = GetDvar("tws_bank_token");
level.afk_ready = false;
level.afk_ended = false;
level.afk_players = [];
level.afk_requests = [];
level.afk_stops = [];
level.afk_grace_seconds = 2;
level.afk_cooldown_seconds = 15;
level.afk_warning_sound = "zmb_perks_packa_ready";
if (GetDvar("tws_afk_warning_sound") != "")
level.afk_warning_sound = GetDvar("tws_afk_warning_sound");
if (GetDvar("tws_afk_cooldown_seconds") != "")
level.afk_cooldown_seconds = max(2, min(300, GetDvarInt("tws_afk_cooldown_seconds")));
scripts\zm\tws_t6_common::register_help_command(".afk", "Protected AFK using your time reserve");
scripts\zm\tws_t6_common::register_help_command(".afk balance", "Free allowance and saved time");
scripts\zm\tws_t6_common::register_help_command(".afk buy <minutes>", "Quote a bank purchase; confirmation required");
scripts\zm\tws_t6_common::register_help_command(".back", "Return from AFK");
scripts\zm\tws_t6_common::register_spawn_handler(::afk_spawn);
// Use the stock damage-callback chain, not the shared engine god-mode bit.
// At expiry this hook immediately falls through, preserving stock protection.
callbacks = [];
callbacks[0] = ::afk_damage;
if (isDefined(level.player_damage_callbacks))
for (i = 0; i < level.player_damage_callbacks.size; i++)
callbacks[callbacks.size] = level.player_damage_callbacks[i];
level.player_damage_callbacks = callbacks;
level thread afk_chat_listener();
level thread afk_startup();
level thread afk_end_watcher();
level thread afk_round_watcher();
level thread afk_team_watcher();
}
afk_startup()
{
if (GetDvar("tws_afk_enabled") == "0" || level.afk_server == "" ||
level.afk_token.size < 40 || getSubStr(level.afk_token, 0, 8) != "ec_bank_")
{
println("[tws_afk] Disabled: configure tws_server_id and private tws_bank_token");
return;
}
while (!is_true(level.telemetry_started) || !isDefined(level.telemetry_match))
wait 0.1;
level.afk_match = level.telemetry_match;
if (!afkRecover(level.afk_server, level.afk_url, level.afk_match))
{
println("[tws_afk] Journal recovery failed; protected AFK disabled");
return;
}
level.afk_ready = true;
println("[tws_afk] Ready native=" + afkVersion());
level thread afk_sender();
}
afk_guid(player)
{
if (!isDefined(player))
return undefined;
guid = player.guid;
if (!isDefined(guid) || (typeof(guid) != "string" && typeof(guid) != "int"))
return undefined;
guid = "" + guid;
if (guid.size < 1 || guid.size > 64)
return undefined;
allowed = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_.:-";
for (i = 0; i < guid.size; i++)
{
found = false;
for (j = 0; j < allowed.size; j++)
if (guid[i] == allowed[j])
found = true;
if (!found)
return undefined;
}
return guid;
}
afk_spawn()
{
self endon("disconnect");
if (isDefined(self.tws_afk_state))
return;
guid = afk_guid(self);
if (!isDefined(guid))
return;
if (!isDefined(level.afk_players[guid]))
{
state = spawnStruct();
state.guid = guid;
state.seq = 0;
state.active_ms = 0;
state.kills = 0;
state.headshots = 0;
state.revives = 0;
state.rounds = 0;
state.last_round_awarded = 0;
state.round_active_ms = 0;
state.round_contribution = false;
level.afk_players[guid] = state;
}
progress = level.afk_players[guid];
if (isDefined(progress.owner) && progress.owner.connected)
return;
// Connection state is never reused: an old HTTP completion cannot mutate a
// reconnect's session or confirmation. Only cumulative match counters persist.
state = spawnStruct();
state.guid = guid;
state.progress = progress;
progress.owner = state;
state.player = self;
state.connected = true;
state.pending = false;
state.cancelled = false;
state.session = undefined;
state.quote = undefined;
state.last_exit = -300000;
state.last_input = getTime() - 60000;
state.last_engaged = getTime() - 60000;
state.last_flush = getTime();
self.tws_afk_state = state;
self.hud_afk_on = false;
self.tws_afk_protected = false;
self.tws_afk_controls_locked = false;
self.tws_afk_ignore_owned = false;
self.tws_afk_grace_until = 0;
self.tws_afk_authorized = 0;
self.tws_afk_last_second = -1;
self afk_hud_create();
self thread afk_disconnect_watcher(state);
self thread afk_lifecycle_watcher();
self thread afk_kill_watcher();
self thread afk_revive_watcher();
self thread afk_activity_watcher();
self thread afk_tick();
}
afk_hud_create()
{
// Separate foreground elements stay visible when the optional core HUD hides.
timer = NewClientHudElem(self);
timer.horzAlign = "center";
timer.vertAlign = "middle";
timer.alignX = "center";
timer.alignY = "middle";
timer.x = 0;
timer.y = -30;
timer.font = "default";
timer.fontScale = 3.0;
timer.foreground = true;
timer.sort = 100;
timer.alpha = 0;
timer SetText("AFK");
self.hud_afk_elem = timer;
warning = NewClientHudElem(self);
warning.horzAlign = "center";
warning.vertAlign = "middle";
warning.alignX = "center";
warning.alignY = "middle";
warning.x = 0;
warning.y = 5;
warning.font = "default";
warning.fontScale = 1.3;
warning.foreground = true;
warning.sort = 100;
warning.alpha = 0;
warning SetText("Protection ends when this timer reaches zero.");
self.tws_afk_warning_elem = warning;
elems = [];
elems[0] = timer;
elems[1] = warning;
self thread scripts\zm\tws_t6_common::destroy_on_disconnect(elems);
}
afk_playing(player)
{
return isDefined(player) && !level.afk_ended && !is_true(level.intermission) &&
!is_true(level.gameended) && isAlive(player) &&
isDefined(player.sessionstate) && player.sessionstate == "playing" &&
!(player maps\mp\zombies\_zm_laststand::player_is_in_laststand()) &&
!is_true(player.is_zombie) && !is_true(player.afterlife);
}
afk_stock_controls(player)
{
return !afk_playing(player) || is_true(level.player_movement_suppressed) ||
isDefined(level.hostmigrationtimer) || is_true(level.host_ended_game) ||
is_true(player.hostmigrationcontrolsfrozen) ||
is_true(player.disabledweapon) || is_true(player.disabledusability) ||
is_true(player.tws_afk_external_controls);
}
afk_can_enter(player)
{
if (!afk_playing(player) || afk_stock_controls(player))
return false;
return !is_true(player.ignoreme) && !isGodMode(player);
}
afk_can_earn(player)
{
// Manual .back restores play immediately; its separate protection tail
// must not discard successful revives or other contributions after return.
return afk_playing(player) && isDefined(player.tws_afk_state) &&
!is_true(player.hud_afk_on);
}
afk_chat_listener()
{
for (;;)
{
level waittill("say", message, player);
if (!isDefined(player) || !isDefined(message))
continue;
args = strTok(toLower(message), " ");
if (args.size == 0)
continue;
if (args[0] == ".back" && args.size == 1)
{
if (isDefined(player.tws_afk_state))
{
player.tws_afk_state.cancelled = true;
player thread afk_exit();
}
continue;
}
if (args[0] != ".afk")
continue;
if (!level.afk_ready || !isDefined(player.tws_afk_state) || level.afk_ended)
{
player iPrintLnBold("AFK reserve unavailable. No bank points have been spent.");
continue;
}
state = player.tws_afk_state;
if (state.pending)
{
player iPrintLnBold("AFK request is still pending. Use .back to cancel entry.");
continue;
}
if (args.size == 1)
{
if (!is_true(player.hud_afk_on))
level thread afk_enter(player, state);
}
else if ((args.size == 2 && args[1] == "balance") ||
(args.size >= 3 && args[1] == "buy"))
{
state.pending = true;
level thread afk_account_command(player, state, args);
}
else
player iPrintLnBold("Usage: .afk | .back | .afk balance | .afk buy <minutes>");
}
}
afk_base(state, action)
{
data = [];
data["action"] = action;
data["server"] = level.afk_server;
data["guid"] = state.guid;
data["match"] = level.afk_match;
if (action != "balance")
data["request_id"] = telemetryNewMatch();
return data;
}
afk_say(state, message)
{
if (state.connected && isDefined(state.player))
state.player iPrintLnBold(message);
}
afk_minutes(text)
{
if (text.size < 1 || text.size > 4)
return 0;
digits = "0123456789";
for (i = 0; i < text.size; i++)
{
found = false;
for (j = 0; j < digits.size; j++)
if (text[i] == digits[j])
found = true;
if (!found)
return 0;
}
return int(text);
}
afk_account_command(player, state, args)
{
afk_account_request(player, state, args);
state.pending = false;
}
afk_account_request(player, state, args)
{
if (args[1] == "buy" && (is_true(player.hud_afk_on) ||
is_true(player.tws_afk_protected) || isDefined(state.session)))
{
state.quote = undefined;
afk_say(state, "Use .back before buying time. Purchases apply to your next AFK session.");
return;
}
if (args[1] == "buy" && args.size == 4 && args[3] == "confirm")
{
quote = state.quote;
state.quote = undefined;
if (!isDefined(quote) || getTime() > quote.expires ||
afk_minutes(args[2]) != quote.minutes || !state.connected)
{
afk_say(state, "Purchase confirmation expired. Request .afk buy <minutes> again.");
return;
}
data = afk_base(state, "purchase");
data["packs"] = quote.packs;
data["confirmed"] = true;
data["expected_purchase_points"] = quote.points;
data["expected_purchase_seconds"] = quote.seconds;
// Persist the same ID before sending. Retries never become a new charge.
if (!afk_enqueue(data, state))
afk_say(state, "AFK storage unavailable. No purchase was sent.");
else
afk_say(state, "Purchase submitted; checking your bank and reserve capacity.");
return;
}
if (args[1] == "buy" && args.size != 3)
{
afk_say(state, "Usage: .afk buy <minutes>, then .afk buy <minutes> confirm");
return;
}
data = afk_base(state, "balance");
reply = afk_http("balance", jsonSerialize(data), "");
if (!state.connected)
return;
if (!isDefined(reply) || reply["state"] != "balance")
{
afk_say(state, "AFK account unavailable. Existing authorized protection keeps counting down.");
return;
}
if (args[1] == "balance")
{
afk_say(state, "AFK free " + afk_format(reply["free_seconds"]) +
" | saved " + afk_format(reply["saved_seconds"]) +
" | reserved " + afk_format(reply["reserved_seconds"]));
return;
}
minutes = afk_minutes(args[2]);
seconds = minutes * 60;
if (minutes < 1 || reply["purchase_seconds"] < 1 || seconds % reply["purchase_seconds"] != 0)
{
afk_say(state, "Buy whole packs of " + afk_format(reply["purchase_seconds"]) + " each.");
return;
}
packs = int(seconds / reply["purchase_seconds"]);
if (packs > reply["max_purchase_packs"])
{
afk_say(state, "Purchase would exceed reserve capacity. No points spent.");
return;
}
quote = spawnStruct();
quote.minutes = minutes;
quote.packs = packs;
quote.points = reply["purchase_points"];
quote.seconds = reply["purchase_seconds"];
quote.expires = getTime() + 30000;
state.quote = quote;
afk_say(state, "Buy " + minutes + " minutes for " + (packs * reply["purchase_points"]) +
" bank points? Type .afk buy " + minutes + " confirm within 30 seconds.");
}
afk_enter(player, state)
{
state.pending = true;
state.cancelled = false;
afk_activate(player, state);
state.pending = false;
}
afk_activate(player, state)
{
if (!state.connected || !afk_can_enter(player))
{
afk_say(state, "AFK requires a living, standing player with normal controls.");
return;
}
if (getTime() - state.last_exit < level.afk_cooldown_seconds * 1000 ||
is_true(player.tws_afk_protected))
{
afk_say(state, "Wait briefly after returning before entering AFK again.");
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["session_id"] = telemetryNewMatch();
data["seconds"] = 604800;
session = data["session_id"];
state.session = session;
body = jsonSerialize(data);
if (!afkPrepare(level.afk_server, level.afk_url, body))
{
state.session = undefined;
afk_say(state, "AFK journal unavailable. Protection was not started.");
return;
}
reply = afk_http("reserve", body, data["request_id"]);
if (!isDefined(reply) || reply["state"] != "reserved" || reply["authorized_seconds"] <= 0 ||
reply["session_id"] != session)
{
afk_stop_state(state, "never_started");
if (isDefined(reply) && reply["code"] == "empty")
afk_say(state, "No AFK time available. Earn time through play or use .afk buy <minutes>.");
else
afk_say(state, "AFK authorization unavailable or already reserved elsewhere. Protection was not started.");
return;
}
if (!state.connected || state.cancelled || !isDefined(state.session) || state.session != session)
{
afk_stop_state(state, "never_started");
return;
}
authorized = reply["authorized_seconds"];
if (!afkAuthorize(level.afk_server, level.afk_url, state.session, authorized))
{
afk_stop_state(state, "never_started");
return;
}
afkRemove(level.afk_server, level.afk_url, data["request_id"]);
start = afk_base(state, "start");
start["session_id"] = state.session;
start_body = jsonSerialize(start);
if (!afkStore(level.afk_server, level.afk_url, start_body))
{
afk_stop_state(state, "never_started");
return;
}
reply = afk_http("start", start_body, start["request_id"]);
if (!isDefined(reply) || reply["state"] != "started" || reply["session_id"] != session ||
reply["authorized_seconds"] != authorized || !state.connected ||
state.cancelled || !afk_can_enter(player) || afk_other_active(player) == 0)
{
afk_stop_state(state, "never_started");
afk_say(state, "AFK entry cancelled. No protected time used.");
return;
}
afkRemove(level.afk_server, level.afk_url, start["request_id"]);
// No wait between the durable native start clock and applying protection.
if (!afkBegin(level.afk_server, level.afk_url, state.session))
{
afk_stop_state(state, "never_started");
return;
}
player.tws_afk_authorized = authorized;
player.tws_afk_next_checkpoint = getTime() + 1000;
player.tws_afk_ignore_before = player.ignoreme;
player.tws_afk_ignore_owned = true;
player.ignoreme = true;
player.hud_afk_on = true;
player.tws_afk_protected = true;
player.tws_afk_grace_until = 0;
player.tws_afk_last_second = -1;
player afk_lock_controls();
player afk_countdown();
afk_say(state, "AFK - type .back to return. Protection ends when this timer reaches zero.");
}
afk_damage(inflictor, attacker, damage, flags, mod, weapon, point, direction, hitloc, offset)
{
if (is_true(self.tws_afk_protected) && afk_playing(self) &&
((is_true(self.hud_afk_on) && self afk_remaining() > 0) ||
(!is_true(self.hud_afk_on) && getTime() < self.tws_afk_grace_until)))
return 0;
if (damage > 0 && afk_can_earn(self))
self.tws_afk_state.last_engaged = getTime();
return -1;
}
afk_tick()
{
self endon("disconnect");
for (;;)
{
if (is_true(self.hud_afk_on))
{
if (self afk_remaining() <= 0)
{
self afk_finish("expired", false);
self iPrintLnBold("AFK time expired. Protection ended - you are vulnerable now.");
}
else if (!afk_playing(self) || afk_stock_controls(self))
self afk_finish("death", false);
else
{
// Backend connectivity is irrelevant to this local deadline.
if (getTime() >= self.tws_afk_next_checkpoint)
{
elapsed = afkCheckpoint(level.afk_server, level.afk_url, self.tws_afk_state.session);
self.tws_afk_next_checkpoint = getTime() + 1000;
if (elapsed < 0)
{
self afk_finish("manual", false);
self iPrintLnBold("AFK journal unavailable. Protection ended; unused time will reconcile.");
}
}
if (self.hud_afk_on)
{
self afk_lock_controls();
self afk_countdown();
}
}
}
else if (is_true(self.tws_afk_protected) && getTime() >= self.tws_afk_grace_until)
self afk_release();
else if (self.tws_afk_controls_locked)
self afk_restore_controls();
if (!self.tws_afk_protected && self.tws_afk_ignore_owned)
self afk_restore_targeting();
wait 0.05;
}
}
afk_remaining()
{
if (!isDefined(self.tws_afk_state.session))
return 0;
return afkRemaining(level.afk_server, level.afk_url, self.tws_afk_state.session);
}
afk_countdown()
{
remaining = int((max(0, self afk_remaining()) + 999) / 1000);
if (remaining == self.tws_afk_last_second)
return;
self.tws_afk_last_second = remaining;
self.hud_afk_elem SetTextUnlimited("AFK " + afk_format(remaining));
self.hud_afk_elem.alpha = 1;
self.tws_afk_warning_elem.alpha = 1;
self.hud_afk_elem.fontScale = 3.0;
self.hud_afk_elem.color = (1, 1, 1);
if (remaining <= 60)
{
self.hud_afk_elem.color = (1, 0.25, 0.1);
self.hud_afk_elem.fontScale = 3.5;
if (remaining == 60 || remaining == 30 || remaining <= 10)
self playLocalSound(level.afk_warning_sound);
}
}
afk_format(seconds)
{
minutes = int(seconds / 60);
seconds = int(seconds % 60);
tail = "" + seconds;
if (seconds < 10)
tail = "0" + seconds;
return minutes + ":" + tail;
}
afk_exit()
{
if (!is_true(self.hud_afk_on))
return;
if (self afk_remaining() <= 0)
{
self afk_finish("expired", false);
self iPrintLnBold("AFK time expired. Protection ended - you are vulnerable now.");
return;
}
self afk_finish("manual", true);
self iPrintLnBold("Back in the fight");
}
afk_finish(reason, manual_grace)
{
state = self.tws_afk_state;
self.hud_afk_on = false;
state.last_exit = getTime();
self.hud_afk_elem.alpha = 0;
self.tws_afk_warning_elem.alpha = 0;
self afk_restore_controls();
if (manual_grace && afk_playing(self))
self.tws_afk_grace_until = getTime() + level.afk_grace_seconds * 1000;
else
self afk_release();
// Accepted transition, never inferred from the chat text. Manual grace is
// separate from paid AFK; its two-second tail cannot be renewed by re-entry.
afk_stop_state(state, reason);
afk_flush(state);
}
afk_release()
{
self.hud_afk_on = false;
self.tws_afk_protected = false;
self.tws_afk_grace_until = 0;
self afk_restore_controls();
if (isDefined(self.hud_afk_elem))
self.hud_afk_elem.alpha = 0;
if (isDefined(self.tws_afk_warning_elem))
self.tws_afk_warning_elem.alpha = 0;
// Stock laststand/afterlife/intermission takes over ignoreme and controls.
// Our damage hook has already relinquished protection even in those states.
self afk_restore_targeting();
}
afk_restore_targeting()
{
if (self.tws_afk_ignore_owned && afk_playing(self) && !afk_stock_controls(self))
{
self.ignoreme = self.tws_afk_ignore_before;
self.tws_afk_ignore_owned = false;
}
}
afk_lock_controls()
{
self.tws_afk_controls_locked = true;
self freezeControls(true);
self disableWeapons();
self disableOffhandWeapons();
}
afk_restore_controls()
{
if (!self.tws_afk_controls_locked || afk_stock_controls(self))
return;
self.tws_afk_controls_locked = false;
self freezeControls(false);
self enableWeapons();
self enableOffhandWeapons();
}
afk_stop_state(state, reason)
{
if (!isDefined(state.session))
return;
stop = spawnStruct();
stop.session = state.session;
stop.reason = reason;
state.session = undefined;
if (!afkStop(level.afk_server, level.afk_url, stop.session, stop.reason))
level.afk_stops[level.afk_stops.size] = stop;
}
afk_disconnect_watcher(state)
{
self waittill("disconnect");
// No endon: checkpoint/stop while the departing entity is still valid.
if (is_true(self.hud_afk_on))
self afk_finish("disconnect", false);
else
afk_stop_state(state, "never_started");
state.connected = false;
state.cancelled = true;
afk_flush(state);
}
afk_lifecycle_watcher()
{
self endon("disconnect");
for (;;)
{
self waittill_any("killed_player", "spawned_player");
if (is_true(self.hud_afk_on))
self afk_finish("death", false);
else if (is_true(self.tws_afk_protected))
self afk_release();
self.tws_afk_state.cancelled = true;
}
}
afk_end_watcher()
{
level waittill_any("end_game", "game_ended", "intermission");
// Stop without yielding before stock intermission takes control.
players = getPlayers();
for (i = 0; i < players.size; i++)
{
if (!isDefined(players[i].tws_afk_state))
continue;
if (is_true(players[i].hud_afk_on))
players[i] afk_finish("match_end", false);
else if (is_true(players[i].tws_afk_protected))
players[i] afk_release();
players[i].tws_afk_state.cancelled = true;
afk_flush(players[i].tws_afk_state);
}
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.
// Never infer contributions from spendable score, score_total, or bank actions.
afk_kill_watcher()
{
self endon("disconnect");
for (;;)
{
self waittill("zom_kill", zombie);
afk_credit_kill(self, zombie);
}
}
afk_credit_kill(player, zombie)
{
if (!afk_can_earn(player) || !isDefined(zombie) ||
!isDefined(zombie.attacker) || zombie.attacker != player ||
is_true(zombie.tws_afk_rewarded))
return;
zombie.tws_afk_rewarded = true;
state = player.tws_afk_state;
state.progress.kills++;
if (afk_headshot(zombie))
state.progress.headshots++;
state.progress.round_contribution = true;
state.last_engaged = getTime();
// Cumulative kill/headshot totals are journaled by the 30-second flush.
}
afk_headshot(zombie)
{
if (!isDefined(zombie.damagelocation) || !isDefined(zombie.damagemod) ||
!isDefined(zombie.damageweapon))
return false;
return maps\mp\zombies\_zm_utility::is_headshot(zombie.damageweapon,
zombie.damagelocation, zombie.damagemod);
}
// The revived teammate owns this notification. A successful repeat revive is a
// new event every time; no teammate cache, cooldown, or reward limiter belongs here.
afk_revive_watcher()
{
self endon("disconnect");
for (;;)
{
self waittill("player_revived", reviver);
if (!isDefined(reviver) || reviver == self || !afk_can_earn(reviver))
continue;
state = reviver.tws_afk_state;
state.progress.revives++;
state.progress.round_contribution = true;
state.last_engaged = getTime();
afk_flush(state);
}
}
afk_round_watcher()
{
for (;;)
{
level waittill("end_of_round");
players = getPlayers();
for (i = 0; i < players.size; i++)
{
player = players[i];
if (!isDefined(player.tws_afk_state))
continue;
state = player.tws_afk_state;
if (afk_can_earn(player) && state.progress.last_round_awarded != level.round_number &&
(state.progress.round_contribution || state.progress.round_active_ms >= 30000))
{
state.progress.rounds++;
state.progress.last_round_awarded = level.round_number;
afk_flush(state);
}
}
identities = getArrayKeys(level.afk_players);
for (i = 0; i < identities.size; i++)
{
progress = level.afk_players[identities[i]];
progress.round_active_ms = 0;
progress.round_contribution = false;
}
}
}
afk_activity_watcher()
{
self endon("disconnect");
state = self.tws_afk_state;
previous_origin = self.origin;
previous_angles = self getPlayerAngles();
previous_buttons = self afk_buttons();
previous_time = getTime();
for (;;)
{
wait 1;
now = getTime();
elapsed = min(1000, max(0, now - previous_time));
angles = self getPlayerAngles();
buttons = self afk_buttons();
if (afk_can_earn(self))
{
changed = distanceSquared(previous_origin, self.origin) > 16 ||
distanceSquared(previous_angles, angles) > 4 || buttons != previous_buttons;
if (changed)
{
state.last_input = now;
state.last_engaged = now;
}
// A stationary revive is real support. Holding a zombie also counts
// with recent deliberate input; simply being targeted indefinitely
// or leaving a button held does not constitute active play.
if (self afk_supporting() ||
(now - state.last_input <= 60000 && self afk_holding_zombie()))
state.last_engaged = now;
if (now - state.last_engaged <= 15000)
{
state.progress.active_ms += elapsed;
state.progress.round_active_ms += elapsed;
}
}
previous_origin = self.origin;
previous_angles = angles;
previous_buttons = buttons;
previous_time = now;
if (now - state.last_flush >= 30000)
afk_flush(state);
}
}
afk_buttons()
{
return (self useButtonPressed()) + 2 * (self attackButtonPressed()) +
4 * (self adsButtonPressed()) + 8 * (self meleeButtonPressed());
}
afk_supporting()
{
if (!(self useButtonPressed()))
return false;
players = getPlayers();
for (i = 0; i < players.size; i++)
if (players[i] != self &&
(players[i] maps\mp\zombies\_zm_laststand::player_is_in_laststand()) &&
isDefined(players[i].revivetrigger) &&
(self maps\mp\zombies\_zm_laststand::is_reviving(players[i])))
return true;
return false;
}
afk_holding_zombie()
{
if (!isDefined(level.zombie_team))
return false;
zombies = GetAiSpeciesArray(level.zombie_team, "all");
for (i = 0; i < zombies.size; i++)
if (isAlive(zombies[i]) && isDefined(zombies[i].enemy) && zombies[i].enemy == self &&
distanceSquared(self.origin, zombies[i].origin) <= 262144)
return true;
return false;
}
afk_flush(state)
{
if (!level.afk_ready || !isDefined(level.afk_match))
return;
data = afk_base(state, "event");
state.progress.seq++;
data["seq"] = state.progress.seq;
data["active_seconds"] = int(state.progress.active_ms / 1000);
data["kills"] = state.progress.kills;
data["headshots"] = state.progress.headshots;
data["rounds"] = state.progress.rounds;
data["revives"] = state.progress.revives;
data["activity"] = "playing";
if (afk_enqueue(data, undefined))
state.last_flush = getTime();
}
afk_enqueue(data, state)
{
if (!isDefined(data["request_id"]))
return false;
if (!afkStore(level.afk_server, level.afk_url, jsonSerialize(data)))
return false;
if (isDefined(state))
level.afk_requests[data["request_id"]] = state;
return true;
}
afk_sender()
{
for (;;)
{
pending_stops = [];
for (i = 0; i < level.afk_stops.size; i++)
{
stop = level.afk_stops[i];
if (!afkStop(level.afk_server, level.afk_url, stop.session, stop.reason))
pending_stops[pending_stops.size] = stop;
}
level.afk_stops = pending_stops;
entries = afkLoad(level.afk_server, level.afk_url);
if (isDefined(entries))
for (i = 0; i < entries.size; i++)
{
entry = entries[i];
// Never activate orphaned protection from a background retry.
if (entry["action"] == "reserve" || entry["action"] == "start")
continue;
reply = afk_http(entry["action"], entry["body"], entry["id"]);
if (!isDefined(reply))
break;
if (isDefined(level.afk_requests[entry["id"]]))
{
state = level.afk_requests[entry["id"]];
if (reply["state"] == "purchased")
afk_say(state, "Purchased AFK reserve. Saved: " + afk_format(reply["saved_seconds"]));
else
afk_say(state, "AFK request: " + reply["code"] + ". Use .afk balance to check your account.");
level.afk_requests[entry["id"]] = undefined;
}
if (!afkRemove(level.afk_server, level.afk_url, entry["id"]))
break;
}
wait 1;
}
}
afk_http(action, body, request_id)
{
headers = [];
headers["Content-Type"] = "application/json";
headers["Authorization"] = "Bearer " + level.afk_token;
options = [];
options["method"] = "POST";
options["headers"] = headers;
options["body"] = body;
options["connect_timeout_ms"] = 5000;
options["timeout_ms"] = 15000;
request = httpPost(level.afk_url + "/" + action, options);
if (!isDefined(request))
return undefined;
request waittill("done", response, success, error, diagnostics);
if (!isDefined(success) || !success || !isDefined(response) ||
!isDefined(diagnostics) || !isDefined(diagnostics["http_status"]))
return undefined;
status = diagnostics["http_status"];
if (status < 200 || status >= 500 || status == 429)
return undefined;
reply = afkResponse(response);
if (!isDefined(reply) || !reply["valid"] || reply["request_id"] != request_id ||
(status == 409 && reply["code"] == "not_in_match"))
return undefined;
return reply;
}