Add resetmatch command to soft restart the map without the need to respawn the server [restart] (fixes #2609)
Closes #2609
See merge request xonotic/xonotic-data.pk3dir!940
alias printstats "qc_cmd_sv printstats ${* ?}" // Dump eventlog player stats and other score information
alias radarmap "qc_cmd_sv radarmap ${* ?}" // Generate a radar image of the map
alias reducematchtime "qc_cmd_sv reducematchtime ${* ?}" // Decrease the timelimit value incrementally
+alias resetmatch "qc_cmd_sv resetmatch ${* ?}" // Soft restart the map
alias setbots "qc_cmd_sv setbots ${* ?}" // Adjust how many bots are in the match
alias shuffleteams "qc_cmd_sv shuffleteams ${* ?}" // Randomly move players to different teams
alias stuffto "qc_cmd_sv stuffto ${* ?}" // Send a command to be executed on a client
set sv_vote_command_restriction_reducematchtime "0"
set sv_vote_command_restriction_extendmatchtime "0"
set sv_vote_command_restriction_allready "0"
+set sv_vote_command_restriction_resetmatch "0"
set sv_vote_command_restriction_kick "1;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;" // enough space for ban reason
set sv_vote_command_restriction_kickban "1;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;" // enough space for ban reason
set sv_vote_command_restriction_cointoss "0"
// =================================
set sv_vote_call 1 "Allow users to call a vote for the commands in sv_vote_commands"
set sv_vote_change 1 "Allow voters to change their mind after already voting"
-set sv_vote_commands "restart fraglimit gotomap nextmap endmatch reducematchtime extendmatchtime allready kick cointoss movetoauto shuffleteams bots nobots" "these commands can be voted by players or used directly by masters (vdo) in addition to sv_vote_master_commands"
+set sv_vote_commands "restart fraglimit gotomap nextmap endmatch reducematchtime extendmatchtime allready resetmatch kick cointoss movetoauto shuffleteams bots nobots" "these commands can be voted by players or used directly by masters (vdo) in addition to sv_vote_master_commands"
set sv_vote_only_commands ""
set sv_vote_limit 160 "Maximum allowed length of a vote command"
set sv_vote_master_commands "movetored movetoblue movetoyellow movetopink movetospec" "Extra commands which vote masters can execute by themselves, along with the normal sv_vote_commands." // maybe add kickban here (but then sv_vote_master 0)
// rcon server commands
// ======================
rcon_secure 1
-set rcon_restricted_commands "restart fraglimit chmap gotomap nextmap endmatch reducematchtime extendmatchtime allready kick cointoss movetoauto shuffleteams bots nobots movetored movetoblue movetoyellow movetopink movetospec kickban \"sv_cmd bans\" \"sv_cmd unban *\" status \"sv_cmd teamstatus\""
+set rcon_restricted_commands "restart fraglimit chmap gotomap nextmap endmatch reducematchtime extendmatchtime allready resetmatch kick cointoss movetoauto shuffleteams bots nobots movetored movetoblue movetoyellow movetopink movetospec kickban \"sv_cmd bans\" \"sv_cmd unban *\" status \"sv_cmd teamstatus\""
{
float warmup_timelimit = STAT(WARMUP_TIMELIMIT);
if(warmup_timelimit > 0)
- warmup_timeleft = max(0, warmup_timelimit - time);
+ warmup_timeleft = max(0, warmup_timelimit - time + STAT(GAMESTARTTIME));
else if(warmup_timelimit == 0)
warmup_timeleft = timeleft;
warmup_timeleft = ceil(warmup_timeleft);
{
if (IS_CLIENT(caller))
{
- if (warmup_stage || autocvar_sv_ready_restart || g_race_qualifying == 2)
+ if (warmup_stage || g_race_qualifying == 2)
{
- if (!readyrestart_happened || autocvar_sv_ready_restart_repeatable)
+ if (time < game_starttime) // game is already restarting
+ return;
+ if (caller.ready) // toggle
{
- if (time < game_starttime) // game is already restarting
- return;
- if (caller.ready) // toggle
- {
- caller.ready = false;
- if(IS_PLAYER(caller) || caller.caplayer == 1)
- bprint(playername(caller.netname, caller.team, false), "^2 is ^1NOT^2 ready\n");
- }
- else
- {
- caller.ready = true;
- if(IS_PLAYER(caller) || caller.caplayer == 1)
- bprint(playername(caller.netname, caller.team, false), "^2 is ready\n");
- }
-
- // cannot reset the game while a timeout is active!
- if (!timeout_status) ReadyCount();
+ caller.ready = false;
+ if(IS_PLAYER(caller) || caller.caplayer == 1)
+ bprint(playername(caller.netname, caller.team, false), "^2 is ^1NOT^2 ready\n");
}
else
{
- sprint(caller, "^1Game has already been restarted\n");
+ caller.ready = true;
+ if(IS_PLAYER(caller) || caller.caplayer == 1)
+ bprint(playername(caller.netname, caller.team, false), "^2 is ready\n");
}
+
+ // cannot reset the game while a timeout is active!
+ if (!timeout_status) ReadyCount();
}
}
return; // never fall through to usage
float autocvar_sv_clientcommand_antispam_time;
int autocvar_sv_clientcommand_antispam_count;
-bool autocvar_sv_ready_restart;
-bool autocvar_sv_ready_restart_repeatable;
.float cmd_floodtime;
.float cmd_floodcount;
{
case CMD_REQUEST_COMMAND:
{
- ReadyRestart();
+ if(warmup_stage)
+ {
+ ReadyRestart(true);
+ }
+ else
+ LOG_INFO("Not in warmup.");
+
return;
}
}
}
+void GameCommand_resetmatch(int request)
+{
+ switch (request)
+ {
+ case CMD_REQUEST_COMMAND:
+ {
+ ReadyRestart(false);
+ return;
+ }
+
+ default:
+ case CMD_REQUEST_USAGE:
+ {
+ LOG_HELP("Usage:^3 sv_cmd resetmatch");
+ LOG_HELP(" No arguments required.");
+ return;
+ }
+ }
+}
+
void GameCommand_stuffto(int request, int argc)
{
// This... is a fairly dangerous and powerful command... - It allows any arguments to be sent to a client via rcon.
// Do not hard code aliases for these, instead create them in commands.cfg... also: keep in alphabetical order, please ;)
SERVER_COMMAND(adminmsg, "Send an admin message to a client directly") { GameCommand_adminmsg(request, arguments); }
-SERVER_COMMAND(allready, "Restart the server and reset the players") { GameCommand_allready(request); }
+SERVER_COMMAND(allready, "Ends warmup and starts the match") { GameCommand_allready(request); }
SERVER_COMMAND(allspec, "Force all players to spectate") { GameCommand_allspec(request, arguments); }
SERVER_COMMAND(anticheat, "Create an anticheat report for a client") { GameCommand_anticheat(request, arguments); }
SERVER_COMMAND(animbench, "Benchmark model animation (LAGS)") { GameCommand_animbench(request, arguments); }
SERVER_COMMAND(printstats, "Dump eventlog player stats and other score information") { GameCommand_printstats(request); }
SERVER_COMMAND(radarmap, "Generate a radar image of the map") { GameCommand_radarmap(request, arguments); }
SERVER_COMMAND(reducematchtime, "Decrease the timelimit value incrementally") { GameCommand_reducematchtime(request); }
+SERVER_COMMAND(resetmatch, "Soft restart the game without changing teams; goes back to warmup if enabled") { GameCommand_resetmatch(request); }
SERVER_COMMAND(setbots, "Adjust how many bots are in the match") { GameCommand_setbots(request, arguments); }
SERVER_COMMAND(shuffleteams, "Randomly move players to different teams") { GameCommand_shuffleteams(request); }
SERVER_COMMAND(stuffto, "Send a command to be executed on a client") { GameCommand_stuffto(request, arguments); }
{
if (!MUTATOR_CALLHOOK(reset_map_players))
{
- if (restart_mapalreadyrestarted || (time < game_starttime))
+ FOREACH_CLIENT(IS_PLAYER(it),
{
- FOREACH_CLIENT(IS_PLAYER(it),
- {
- /*
- only reset players if a restart countdown is active
- this can either be due to cvar sv_ready_restart_after_countdown having set
- restart_mapalreadyrestarted to 1 after the countdown ended or when
- sv_ready_restart_after_countdown is not used and countdown is still running
- */
- // PlayerScore_Clear(it);
- CS(it).killcount = 0;
- // stop the player from moving so that he stands still once he gets respawned
- it.velocity = '0 0 0';
- it.avelocity = '0 0 0';
- CS(it).movement = '0 0 0';
- PutClientInServer(it);
- });
- }
+ // PlayerScore_Clear(it);
+ CS(it).killcount = 0;
+ // stop the player from moving so that he stands still once he gets respawned
+ it.velocity = '0 0 0';
+ it.avelocity = '0 0 0';
+ CS(it).movement = '0 0 0';
+ PutClientInServer(it);
+ });
}
}
}
// Restarts the map after the countdown is over (and cvar sv_ready_restart_after_countdown is set)
void ReadyRestart_think(entity this)
{
- restart_mapalreadyrestarted = true;
reset_map(true);
Score_ClearAll();
delete(this);
if (time <= game_starttime && game_stopped)
return;
- bprint("^1Server is restarting...\n");
+ bprint("^1Match is restarting...\n");
VoteReset();
if (checkrules_overtimesadded > 0 && g_race_qualifying != 2)
cvar_set("timelimit", ftos(autocvar_timelimit - (checkrules_overtimesadded * autocvar_timelimit_overtime)));
checkrules_suddendeathend = checkrules_overtimesadded = checkrules_suddendeathwarning = 0;
-
- readyrestart_happened = true;
- game_starttime = time + RESTART_COUNTDOWN;
+
+ if(warmup_stage)
+ game_starttime = time; // Warmup: No countdown in warmup
+ else
+ game_starttime = time + RESTART_COUNTDOWN; // Go into match mode
// clear player attributes
FOREACH_CLIENT(IS_PLAYER(it), {
CS(it).killcount = 0;
});
- restart_mapalreadyrestarted = false; // reset this var, needed when cvar sv_ready_restart_repeatable is in use
-
- // disable the warmup global for the server
- if(warmup_stage)
+ // if we're ending the warmup stage call the corresponding hook
+ if(!warmup_stage)
localcmd("\nsv_hook_warmupend\n");
- warmup_stage = 0; // once the game is restarted the game is in match stage
// reset the .ready status of all players (also spectators)
FOREACH_CLIENT(IS_REAL_CLIENT(it), { it.ready = false; });
// lock teams with lockonrestart
if (autocvar_teamplay_lockonrestart && teamplay)
- {
- lockteams = true;
- bprint("^1The teams are now locked.\n");
- }
+ lockteams = !warmup_stage;
// initiate the restart-countdown-announcer entity
- if (sv_ready_restart_after_countdown)
+ if (sv_ready_restart_after_countdown && !warmup_stage)
{
entity restart_timer = new_pure(restart_timer);
setthink(restart_timer, ReadyRestart_think);
FOREACH_CLIENT(IS_PLAYER(it) && IS_REAL_CLIENT(it), { CS(it).allowed_timeouts = autocvar_sv_timeout_number; });
}
- if (!sv_ready_restart_after_countdown) reset_map(true);
+ if (!sv_ready_restart_after_countdown || warmup_stage) reset_map(true);
if (autocvar_sv_eventlog) GameLogEcho(":restart");
}
-void ReadyRestart()
+void ReadyRestart(bool forceWarmupEnd)
{
if (MUTATOR_CALLHOOK(ReadyRestart_Deny) || intermission_running || race_completing) localcmd("restart\n");
else localcmd("\nsv_hook_readyrestart\n");
// Reset ALL scores, but only do that at the beginning of the countdown if sv_ready_restart_after_countdown is off!
// Otherwise scores could be manipulated during the countdown.
if (!sv_ready_restart_after_countdown) Score_ClearAll();
+
+ if(forceWarmupEnd)
+ warmup_stage = 0; // forcefully end warmup and go to match stage
+ else
+ warmup_stage = cvar("g_warmup"); // go into warmup if it's enabled, otherwise restart into match stage
+
ReadyRestart_force();
}
ready_needed_factor = bound(0.5, cvar("g_warmup_majority_factor"), 0.999);
ready_needed_count = floor(t_players * ready_needed_factor) + 1;
- if (readycount >= ready_needed_count) ReadyRestart();
+ if (readycount >= ready_needed_count) ReadyRestart(true);
}
}
case "restart":
+ case "resetmatch": // re-direct all match restarting to resetmatch
{
- // add a delay so that vote result can be seen and announcer can be heard
- // if the vote is accepted
- vote_parsed_command = strcat("defer 1 ", vote_command);
- vote_parsed_display = strzone(strcat("^1", vote_command));
+ vote_parsed_command = "resetmatch";
+ vote_parsed_display = strzone("^1resetmatch");
+
+ break;
+ }
+ case "allready":
+ {
+ if(!warmup_stage) {
+ print_to(caller, "Game already started. Use the resetmatch command to restart the match.");
+ return -1;
+ }
+
+ vote_parsed_command = vote_command;
+ vote_parsed_display = strzone(strcat("^1", vote_command));
break;
}
const float RESTART_COUNTDOWN = 10;
entity nagger;
float readycount; // amount of players who are ready
-float readyrestart_happened; // keeps track of whether a restart has already happened
-float restart_mapalreadyrestarted; // bool, indicates whether reset_map() was already executed
.float ready; // flag for if a player is ready
.int team_saved; // team number to restore upon map reset
.void(entity this) reset; // if set, an entity is reset using this
CreatureFrame_All();
CheckRules_World();
- if (warmup_stage && !game_stopped && warmup_limit > 0 && time >= warmup_limit) {
- ReadyRestart();
+ if (warmup_stage && !game_stopped && warmup_limit > 0 && time - game_starttime >= warmup_limit) {
+ ReadyRestart(true);
return;
}
BADCVAR("sv_maxrate");
BADCVAR("sv_motd");
BADCVAR("sv_public");
- BADCVAR("sv_ready_restart");
BADCVAR("sv_showfps");
BADCVAR("sv_status_privacy");
BADCVAR("sv_taunt");
if(readyplayers || playerswithlaps >= 2)
{
checkrules_suddendeathend = 0;
- ReadyRestart(); // go to race
+ ReadyRestart(true); // go to race
return;
}
else
float WinningCondition_Scores(float limit, float leadlimit);
void SetWinners(.float field, float value);
-void ReadyRestart();
+void ReadyRestart(bool endWarmup);
void DumpStats(float final);
set minplayers_per_team 0 "fill server with bots to reach this number of players per team (if bot_number is not enough)"
// restart server if all players hit "ready"-button
-set sv_ready_restart 0 "allow a map to be restarted once all players pressed the \"ready\" button"
set sv_ready_restart_after_countdown 0 "reset players and map items after the countdown ended, instead of at the beginning of the countdown"
-set sv_ready_restart_repeatable 0 "allows the players to restart the game as often as needed"
alias sv_hook_readyrestart