/* 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 ", "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(); } 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 "); } } 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 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 , then .afk buy 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; } 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 ."); 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_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; } // 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; }