/* tws_t5_lock.gsc — automatic lobby lock plus majority .lock / .unlock votes. Existing players are unaffected; g_password is only checked at connect. A successful unlock overrides automatic locking for the rest of the match. */ #include maps\_utility; #include common_scripts\utility; init() { if (GetDvar(#"zombiemode") != "1") return; scripts\sp\tws_t5_common::ensure_config(); scripts\sp\tws_t5_common::register_help_command(".lock", "Vote to lock (6+)"); scripts\sp\tws_t5_common::register_help_command(".unlock", "Vote to unlock"); lock_init(); } lock_init() { level.lock_round = scripts\sp\tws_t5_common::int_dvar_or("tws_lock_round", 20); level.lock_active = false; level.lock_auto_done = false; level.lock_ended = false; level.lock_vote = undefined; level.lock_vote_serial = 0; // recover from a prior lobby that crashed before clearing setDvar("g_password", ""); level thread lock_round_watcher(); level thread lock_chat_listener(); level thread lock_vote_watcher(); level thread lock_cleanup_watcher("hud_game_over"); level thread lock_cleanup_watcher("end_game"); level thread lock_cleanup_watcher("game_ended"); level thread lock_cleanup_watcher("intermission"); } lock_round_watcher() { level endon("tws_lock_game_over"); for (;;) { level waittill("hud_round_changed"); lock_check_round(); } } lock_check_round() { if (lock_game_ended() || level.lock_auto_done) return; if (!isDefined(level.round_number) || level.round_number < level.lock_round) return; // Consume the milestone once, even if players already locked early. level.lock_auto_done = true; if (!level.lock_active) lock_set_state(true); } lock_chat_listener() { level endon("tws_lock_game_over"); for (;;) { level waittill("say", message, player); if (!isDefined(player) || !isDefined(message)) continue; command = toLower(message); if (command == ".lock" || command == ".unlock") lock_request(player, command); } } lock_game_ended() { return level.lock_ended || is_true(level.intermission) || is_true(level.gameended) || is_true(level.tws_ended); } // This path never yields: each chat message casts at most one vote, and a // successful vote changes the password before another request can run. lock_request(player, command) { if (lock_game_ended()) return; players = getPlayers(); present = false; for (i = 0; i < players.size; i++) { if (players[i] == player) present = true; } if (!present) return; lock_update_vote(); target = command == ".lock"; if (target == level.lock_active) { if (target) player iPrintLn("^3Server is already locked"); else player iPrintLn("^3Server is already unlocked"); return; } if (target && (!isDefined(level.round_number) || level.round_number <= 5)) { player iPrintLn("^3.lock is available from round 6"); return; } if (!isDefined(level.lock_vote)) { level.lock_vote_serial++; vote = spawnStruct(); vote.id = level.lock_vote_serial; vote.command = command; vote.target = target; vote.deadline = getTime() + 60000; level.lock_vote = vote; } if (isDefined(player.tws_lock_vote_id) && player.tws_lock_vote_id == level.lock_vote.id) { player iPrintLn("^3You already voted " + command); return; } // Connection-local fields cannot carry a vote across a reconnect. player.tws_lock_vote_id = level.lock_vote.id; if (lock_update_vote()) { tally = lock_vote_tally(); lock_announce("^3" + command + ": " + tally.yes + "/" + tally.required + " votes. Type " + command + " to vote (60s window)"); } } lock_vote_tally() { tally = spawnStruct(); tally.yes = 0; players = getPlayers(); tally.required = int(players.size / 2) + 1; for (i = 0; i < players.size; i++) { if (isDefined(players[i].tws_lock_vote_id) && players[i].tws_lock_vote_id == level.lock_vote.id) tally.yes++; } return tally; } // Recount the live roster so disconnected votes are dropped and newcomers // count toward the majority. Expiry is checked before accepting any result. lock_update_vote() { if (lock_game_ended() || !isDefined(level.lock_vote)) return false; if (getTime() >= level.lock_vote.deadline) { command = level.lock_vote.command; level.lock_vote = undefined; lock_announce("^3" + command + " vote expired"); return false; } tally = lock_vote_tally(); if (tally.yes >= tally.required) { lock_set_state(level.lock_vote.target); return false; } return true; } lock_vote_watcher() { level endon("tws_lock_game_over"); for (;;) { wait (0.5); lock_update_vote(); } } lock_set_state(locked) { level.lock_vote = undefined; if (locked) { // Random per activation so the password cannot be inferred from source. password = "lock_" + getTime() + "_" + randomInt(1000000); setDvar("g_password", password); } else { setDvar("g_password", ""); level.lock_auto_done = true; } level.lock_active = locked; if (locked) lock_announce("^3Server locked"); else lock_announce("^3Server unlocked"); } lock_cleanup_watcher(event) { level endon("tws_lock_game_over"); level waittill(event); level.lock_ended = true; level.lock_vote = undefined; setDvar("g_password", ""); level.lock_active = false; level notify("tws_lock_game_over"); } lock_announce(message) { players = getPlayers(); for (i = 0; i < players.size; i++) players[i] iPrintLn(message); }