Add majority lock and unlock voting for T5 and T6
This commit is contained in:
+19
-12
@@ -10,7 +10,7 @@ private server token.
|
||||
| `tws_t6_common.gsc` | Shared HUD values, helpers, paginated `.help`, service window, connection and first-spawn dispatchers | Stock T6 |
|
||||
| `tws_t6_hud.gsc` | Unboxed timers, branding, zombies/health and per-player `.hud` visibility toggle | Common 1.4.0+, Plutonium r5334+ |
|
||||
| `tws_t6_afk.gsc` | Persistent AFK reserve, rewards and central countdown | Common 1.4.0+, telemetry 1.0.0+, plugin 1.4.0+, EC #151 |
|
||||
| `tws_t6_lock.gsc` | Lock at round 20; clear password on game over | Common, HUD |
|
||||
| `tws_t6_lock.gsc` | Auto-lock at round 20 plus majority `.lock` / `.unlock` votes; clear on game over | Common, HUD |
|
||||
| `tws_t6_telemetry.gsc` | Async queued game reports (game_start/heartbeat/round_change/game_end) | tws-t6-gsc-utils |
|
||||
| `tws_t6_join.gsc` | `.join` chat command lets a spectator newcomer enter mid-match once | Common |
|
||||
| `tws_t6_leaderboard.gsc` | Private map record on connection, `.leaders` replay and adaptive round-start reminders | Common 1.5.0+, tws-t6-gsc-utils |
|
||||
@@ -154,9 +154,9 @@ apply to both versions. All T6 script filenames use the `tws_t6_` prefix.
|
||||
Type `.help` or `.commands` in chat to see the commands supplied by the loaded TWS modules and
|
||||
a short description of each. The list is shown only to the requesting player,
|
||||
including spectators who have not spawned. Commands are case-insensitive.
|
||||
Help uses the unboxed upper-right service area. All nine installed commands
|
||||
fit in one list with command and description columns. Additional registered
|
||||
commands use pages of nine rows, with a counter only for multi-page lists.
|
||||
Help uses the unboxed upper-right service area with command and description
|
||||
columns. Registered commands use pages of nine rows, with a counter only for
|
||||
multi-page lists.
|
||||
Pages advance every 12 seconds and close after the last page. Typing either
|
||||
command again returns to page one with a fresh timer. Both engines use
|
||||
explicit row positions so T5 does not concatenate newline-separated text.
|
||||
@@ -172,15 +172,17 @@ explicit row positions so T5 does not concatenate newline-separated text.
|
||||
| `.b` | Check bank balance (bank module required) |
|
||||
| `.d <amount\|all>` | Deposit points (bank module required) |
|
||||
| `.w <amount\|all>` | Withdraw points (bank module required) |
|
||||
| `.lock` | Vote to lock the lobby from round 6 (lock module required) |
|
||||
| `.unlock` | Vote to unlock the lobby (lock module required) |
|
||||
|
||||
Command modules register their commands during `init()` through
|
||||
`tws_t6_common::register_help_command(command, description)`. If a module is
|
||||
omitted, its commands are omitted from `.help`. Commands follow the order above,
|
||||
independent of initialization order. Register new commands alongside their
|
||||
module's initialization so the list stays current.
|
||||
With all command modules installed, help shows one page of nine commands,
|
||||
including the full `.d <amount|all>` / `.w <amount|all>` syntax. Removing HUD
|
||||
removes `.hud` from the list.
|
||||
omitted, its commands are omitted from `.help`. The first nine commands follow
|
||||
the order above; additional entries follow registration order. Register new
|
||||
commands alongside their module's initialization so the list stays current.
|
||||
Lock and extended AFK commands appear on additional pages. Bank entries retain
|
||||
the full `.d <amount|all>` / `.w <amount|all>` syntax. Removing HUD removes `.hud`
|
||||
from the list.
|
||||
The enabled leaderboard module registers `.leaders`, which replays the latest
|
||||
cached map record privately. Repeating it restarts the record from the first page;
|
||||
help pauses the record's remaining page time and resumes it after the final
|
||||
@@ -195,8 +197,13 @@ help page. `.leaders` during help replaces the pending record replay from page o
|
||||
- AFK uses T5's native `level waittill("say", message, player)` listener.
|
||||
`.back` restores movement and weapons immediately, then ends protection after
|
||||
two seconds. The T6 control/weapon fix remains in place.
|
||||
- Lock uses T5's HUD round/game-over notifications. Default threshold is 20.
|
||||
As in T5, it clears `g_password` at startup and game over.
|
||||
- Lock uses T5's HUD round notifications with a default automatic threshold of
|
||||
20. `.lock` is available from round 6; `.unlock` whenever locked. Both use a
|
||||
60-second strict-majority vote (1/1, 2/2, 2/3, 3/4). A successful unlock
|
||||
overrides auto-lock for the rest of the match. There is no round-6 prompt.
|
||||
As in T5, it clears `g_password` and votes at startup and HUD/stock game over.
|
||||
Lock `1.1.0` adds no dependency; copy the script and restart the map, without
|
||||
a full server-process restart. See [voting behavior and validation](../../README.md#lobby-lock-voting-t5-and-t6).
|
||||
- Telemetry is enabled by default and posts to
|
||||
`https://ec.thewrightserver.net/api/telemetry`. Set `tws_server_id` to the exact,
|
||||
case-sensitive Electric Cherry registry name; missing or overlong names
|
||||
|
||||
+194
-24
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
tws_t6_lock.gsc — lobby g_password lock at milestone round. Existing players
|
||||
are unaffected; g_password is only checked at connect. Cleared on
|
||||
hud_game_over so the next lobby opens unlocked.
|
||||
tws_t6_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\mp\_utility;
|
||||
@@ -15,6 +15,8 @@ main()
|
||||
init()
|
||||
{
|
||||
scripts\zm\tws_t6_common::ensure_config();
|
||||
scripts\zm\tws_t6_common::register_help_command(".lock", "Vote to lock (6+)");
|
||||
scripts\zm\tws_t6_common::register_help_command(".unlock", "Vote to unlock");
|
||||
|
||||
lock_init();
|
||||
}
|
||||
@@ -23,47 +25,215 @@ lock_init()
|
||||
{
|
||||
level.lock_round = scripts\zm\tws_t6_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_cleanup_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("hud_game_over");
|
||||
level endon("tws_lock_game_over");
|
||||
|
||||
for (;;)
|
||||
{
|
||||
level waittill("hud_round_changed");
|
||||
|
||||
if (level.lock_active)
|
||||
continue;
|
||||
|
||||
if (!isDefined(level.round_number) || level.round_number < level.lock_round)
|
||||
continue;
|
||||
|
||||
// random per activation so the password can't be inferred from source
|
||||
password = "lock_" + getTime() + "_" + randomInt(1000000);
|
||||
setDvar("g_password", password);
|
||||
level.lock_active = true;
|
||||
|
||||
lock_announce();
|
||||
lock_check_round();
|
||||
}
|
||||
}
|
||||
|
||||
lock_cleanup_watcher()
|
||||
lock_check_round()
|
||||
{
|
||||
level waittill("hud_game_over");
|
||||
setDvar("g_password", "");
|
||||
level.lock_active = false;
|
||||
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_announce()
|
||||
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] iPrintLnBold("^3Server locked");
|
||||
players[i] iPrintLn(message);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user