238 lines
5.9 KiB
Plaintext
238 lines
5.9 KiB
Plaintext
/*
|
|
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);
|
|
}
|