diff --git a/cfg/get5/knife.cfg b/cfg/get5/knife.cfg
index 2125eb366..a987f2192 100644
--- a/cfg/get5/knife.cfg
+++ b/cfg/get5/knife.cfg
@@ -10,3 +10,4 @@ mp_roundtime 1.92
mp_roundtime_defuse 1.92
mp_roundtime_hostage 1.92
mp_t_default_secondary ""
+mp_round_restart_delay 3
diff --git a/documentation/docs/configuration.md b/documentation/docs/configuration.md
index 0e6bb164c..08d70bafe 100644
--- a/documentation/docs/configuration.md
+++ b/documentation/docs/configuration.md
@@ -117,7 +117,8 @@ in [scrim mode](../getting_started/#scrims), put on `team2`. **`Default: 1`**
####`get5_time_to_make_knife_decision`
: Time (in seconds) a team has to make a [`!stay`](../commands/#stay) or [`!swap`](../commands/#swap)
-decision after winning knife round, 0 = unlimited. **`Default: 60`**
+decision after winning knife round. Cannot be set lower than 10 if non-zero. Set to zero to remove
+limit.
**`Default: 60`**
####`get5_veto_countdown`
: Time (in seconds) to countdown before veto process commences. **`Default: 5`**
diff --git a/scripting/get5.sp b/scripting/get5.sp
index 091a69d6c..f8f532145 100644
--- a/scripting/get5.sp
+++ b/scripting/get5.sp
@@ -33,7 +33,7 @@
#include
#define CHECK_READY_TIMER_INTERVAL 1.0
-#define INFO_MESSAGE_TIMER_INTERVAL 29.0
+#define INFO_MESSAGE_TIMER_INTERVAL 20.0
#define DEBUG_CVAR "get5_debug"
#define MATCH_ID_LENGTH 64
@@ -175,6 +175,7 @@ ArrayList g_MapSides = null;
ArrayList g_MapsLeftInVetoPool = null;
Get5Team g_LastVetoTeam;
Menu g_ActiveVetoMenu = null;
+Handle g_InfoTimer = INVALID_HANDLE;
/** Backup data **/
bool g_WaitingForRoundBackup = false;
@@ -623,8 +624,7 @@ public void OnPluginStart() {
/** Start any repeating timers **/
CreateTimer(CHECK_READY_TIMER_INTERVAL, Timer_CheckReady, _, TIMER_REPEAT);
- CreateTimer(INFO_MESSAGE_TIMER_INTERVAL, Timer_InfoMessages, _, TIMER_REPEAT);
-
+ RestartInfoTimer();
CheckForLatestVersion();
}
@@ -669,18 +669,7 @@ static Action Timer_InfoMessages(Handle timer) {
g_MapChangePending && GetTvDelay() > 0) {
Get5_MessageToAll("%t", "WaitingForGOTVVetoInfoMessage");
} else if (g_GameState == Get5State_WaitingForKnifeRoundDecision) {
- if (g_KnifeWinnerTeam == Get5Team_None) {
- return Plugin_Continue;
- }
- // Handle waiting for knife decision. Also check g_KnifeWinnerTeam as there is a small delay between
- // selecting a side and the game state changing, during which this message should not be printed.
- char formattedStayCommand[64];
- FormatChatCommand(formattedStayCommand, sizeof(formattedStayCommand), "!stay");
- char formattedSwapCommand[64];
- FormatChatCommand(formattedSwapCommand, sizeof(formattedSwapCommand), "!swap");
- Get5_MessageToAll("%t", "WaitingForEnemySwapInfoMessage",
- g_FormattedTeamNames[g_KnifeWinnerTeam], formattedStayCommand,
- formattedSwapCommand);
+ PromptForKnifeDecision();
} else if (g_GameState == Get5State_PostGame && GetTvDelay() > 0) {
// Handle postgame
Get5_MessageToAll("%t", "WaitingForGOTVBrodcastEndingInfoMessage");
@@ -826,6 +815,7 @@ static Action Timer_ConfigsExecutedCallback(Handle timer) {
g_MapChangePending = false;
g_DoingBackupRestoreNow = false;
g_ReadyTimeWaitingUsed = 0;
+ g_KnifeWinnerTeam = Get5Team_None;
g_HasKnifeRoundStarted = false;
// Recording is always automatically stopped on map change, and
// since there are no hooks to detect tv_stoprecord, we reset
@@ -1450,6 +1440,15 @@ static Action Event_FreezeEnd(Event event, const char[] name, bool dontBroadcast
}
}
+static void RestartInfoTimer() {
+ // We restart this on each round start to make sure we don't double-print info messages
+ // right on top of manually printed messages, such as "waiting for knife decision".
+ if (g_InfoTimer != INVALID_HANDLE) {
+ delete g_InfoTimer;
+ }
+ g_InfoTimer = CreateTimer(INFO_MESSAGE_TIMER_INTERVAL, Timer_InfoMessages, _, TIMER_REPEAT);
+}
+
static Action Event_RoundStart(Event event, const char[] name, bool dontBroadcast) {
LogDebug("Event_RoundStart");
@@ -1458,6 +1457,7 @@ static Action Event_RoundStart(Event event, const char[] name, bool dontBroadcas
g_RoundStartedTime = 0.0;
g_BombPlantedTime = 0.0;
g_BombSiteLastPlanted = Get5BombSite_Unknown;
+ RestartInfoTimer();
if (g_GameState == Get5State_None || IsDoingRestoreOrMapChange()) {
// Get5_OnRoundStart() is fired from within the backup event when loading the valve backup.
@@ -1467,13 +1467,26 @@ static Action Event_RoundStart(Event event, const char[] name, bool dontBroadcas
// We cannot do this during warmup, as sending users into warmup post-knife triggers a round start
// event.
if (!InWarmup()) {
- if (g_GameState == Get5State_WaitingForKnifeRoundDecision) {
+ if (g_GameState == Get5State_KnifeRound && g_HasKnifeRoundStarted) {
+ // Knife-round decision cannot be made until the round has completely ended and a new round starts.
+ // Letting players choose a side in after-round-time sometimes causes weird problems, such as going
+ // directly to live and missing the countdown if done at the *exact* wrong time.
+ g_HasKnifeRoundStarted = false;
+
// Ensures that round end after knife sends players directly into warmup.
// This immediately triggers another Event_RoundStart, so we can return here and avoid
// writing backup twice.
LogDebug("Changed to warmup post knife.");
+ if (g_KnifeChangedCvars != INVALID_HANDLE) {
+ RestoreCvars(g_KnifeChangedCvars, true);
+ }
ExecCfg(g_WarmupCfgCvar);
StartWarmup();
+
+ // Change state *after* starting the warmup just to reduce !swap/!stay race condition windows.
+ ChangeState(Get5State_WaitingForKnifeRoundDecision);
+ PromptForKnifeDecision();
+ StartKnifeTimer();
return;
}
if (g_GameState == Get5State_GoingLive) {
@@ -1519,13 +1532,6 @@ static Action Event_RoundStart(Event event, const char[] name, bool dontBroadcas
static Action Event_RoundWinPanel(Event event, const char[] name, bool dontBroadcast) {
LogDebug("Event_RoundWinPanel");
if (g_GameState == Get5State_KnifeRound && g_HasKnifeRoundStarted) {
- g_HasKnifeRoundStarted = false;
-
- ChangeState(Get5State_WaitingForKnifeRoundDecision);
- if (g_KnifeChangedCvars != INVALID_HANDLE) {
- RestoreCvars(g_KnifeChangedCvars, true);
- }
-
int ctAlive = CountAlivePlayersOnTeam(CS_TEAM_CT);
int tAlive = CountAlivePlayersOnTeam(CS_TEAM_T);
int winningCSTeam;
@@ -1545,44 +1551,15 @@ static Action Event_RoundWinPanel(Event event, const char[] name, bool dontBroad
LogDebug("Randomized knife winner to side %d", winningCSTeam);
}
}
-
g_KnifeWinnerTeam = CSTeamToGet5Team(winningCSTeam);
- char formattedStayCommand[64];
- FormatChatCommand(formattedStayCommand, sizeof(formattedStayCommand), "!stay");
- char formattedSwapCommand[64];
- FormatChatCommand(formattedSwapCommand, sizeof(formattedSwapCommand), "!swap");
- Get5_MessageToAll("%t", "WaitingForEnemySwapInfoMessage",
- g_FormattedTeamNames[g_KnifeWinnerTeam], formattedStayCommand,
- formattedSwapCommand);
-
- if (g_TeamTimeToKnifeDecisionCvar.FloatValue > 0) {
- g_KnifeDecisionTimer =
- CreateTimer(g_TeamTimeToKnifeDecisionCvar.FloatValue, Timer_ForceKnifeDecision);
- }
- // This ensures that the correct graphic is displayed in-game for the winning team, as CTs will
- // always win if the clock runs out. It also ensures that the fun fact displayed is correct;
- // overriding to number of players killed by knife and no "CT won by running down the clock".
- // MVP can still be on the losing team though. ran down".
- int maxFrags = 0;
- int topFragClient = 0;
- int frags;
- LOOP_CLIENTS(i) {
- if (IsValidClient(i)) {
- frags = GetClientFrags(i);
- if (frags >= maxFrags) {
- maxFrags = frags;
- topFragClient = i;
- }
- }
- }
- if (topFragClient > 0) {
- // Found here:
- // https://github.com/SteamDatabase/GameTracking-CSGO/blob/master/csgo/bin/server_client_strings.txt
- event.SetString("funfact_token", "#funfact_knife_kills");
- event.SetInt("funfact_player", topFragClient);
- event.SetInt("funfact_data1", maxFrags);
- }
+ // Adjust fun-fact to nothing and make sure the correct team is announced as winner.
+ // This prevents things like "CTs won by running down the clock"
+ event.SetString("funfact_token", "");
+ event.SetInt("funfact_player", 0);
+ event.SetInt("funfact_data1", 0);
+ event.SetInt("funfact_data2", 0);
+ event.SetInt("funfact_data3", 0);
event.SetInt("final_event", ConvertCSTeamToDefaultWinReason(winningCSTeam));
}
}
@@ -1593,7 +1570,7 @@ static Action Event_RoundEnd(Event event, const char[] name, bool dontBroadcast)
return;
}
- if (g_GameState == Get5State_WaitingForKnifeRoundDecision && g_KnifeWinnerTeam != Get5Team_None) {
+ if (g_GameState == Get5State_KnifeRound && g_KnifeWinnerTeam != Get5Team_None) {
int winningCSTeam = Get5TeamToCSTeam(g_KnifeWinnerTeam);
// Event_RoundWinPanel is called before Event_RoundEnd, so that event handles knife winner.
// We override this event only to have the correct audio callout in the game.
@@ -1718,16 +1695,10 @@ static void StartGame(bool knifeRound) {
LogDebug("StartGame");
if (knifeRound) {
- ExecCfg(g_LiveCfgCvar); // live first, then apply and save knife cvars below
+ ExecCfg(g_LiveCfgCvar); // live first, then apply and save knife cvars in callback
LogDebug("StartGame: about to begin knife round");
ChangeState(Get5State_KnifeRound);
- if (g_KnifeChangedCvars != INVALID_HANDLE) {
- CloseCvarStorage(g_KnifeChangedCvars);
- }
- char knifeConfig[PLATFORM_MAX_PATH];
- g_KnifeCfgCvar.GetString(knifeConfig, sizeof(knifeConfig));
- g_KnifeChangedCvars = ExecuteAndSaveCvars(knifeConfig);
- CreateTimer(1.0, StartKnifeRound);
+ CreateTimer(0.5, StartKnifeRound);
} else {
// If there is no knife round, we go directly to live, which loads the live config etc. on its
// own.
diff --git a/scripting/get5/kniferounds.sp b/scripting/get5/kniferounds.sp
index dfc028d1e..dca64c341 100644
--- a/scripting/get5/kniferounds.sp
+++ b/scripting/get5/kniferounds.sp
@@ -1,8 +1,11 @@
Action StartKnifeRound(Handle timer) {
g_HasKnifeRoundStarted = false;
-
- // Removes ready tags
- SetMatchTeamCvars();
+ if (g_KnifeChangedCvars != INVALID_HANDLE) {
+ CloseCvarStorage(g_KnifeChangedCvars);
+ }
+ char knifeConfig[PLATFORM_MAX_PATH];
+ g_KnifeCfgCvar.GetString(knifeConfig, sizeof(knifeConfig));
+ g_KnifeChangedCvars = ExecuteAndSaveCvars(knifeConfig);
Get5_MessageToAll("%t", "KnifeIn5SecInfoMessage");
if (InWarmup()) {
@@ -33,6 +36,30 @@ static Action Timer_AnnounceKnife(Handle timer) {
return Plugin_Handled;
}
+void StartKnifeTimer() {
+ float knifeDecisionTime = g_TeamTimeToKnifeDecisionCvar.FloatValue;
+ if (knifeDecisionTime > 0.0) {
+ if (knifeDecisionTime < 10.0) {
+ knifeDecisionTime = 10.0;
+ }
+ g_KnifeDecisionTimer = CreateTimer(knifeDecisionTime, Timer_ForceKnifeDecision);
+ }
+}
+
+void PromptForKnifeDecision() {
+ if (g_KnifeWinnerTeam == Get5Team_None) {
+ // Handle waiting for knife decision. Also check g_KnifeWinnerTeam as there is a small delay between
+ // selecting a side and the game state changing, during which this message should not be printed.
+ return;
+ }
+ char formattedStayCommand[64];
+ FormatChatCommand(formattedStayCommand, sizeof(formattedStayCommand), "!stay");
+ char formattedSwapCommand[64];
+ FormatChatCommand(formattedSwapCommand, sizeof(formattedSwapCommand), "!swap");
+ Get5_MessageToAll("%t", "WaitingForEnemySwapInfoMessage",
+ g_FormattedTeamNames[g_KnifeWinnerTeam], formattedStayCommand, formattedSwapCommand);
+}
+
static void PerformSideSwap(bool swap) {
if (swap) {
int tmp = g_TeamSide[Get5Team_2];
@@ -151,7 +178,7 @@ Action Command_T(int client, int args) {
return Plugin_Handled;
}
-Action Timer_ForceKnifeDecision(Handle timer) {
+static Action Timer_ForceKnifeDecision(Handle timer) {
g_KnifeDecisionTimer = INVALID_HANDLE;
if (g_GameState == Get5State_WaitingForKnifeRoundDecision && g_KnifeWinnerTeam != Get5Team_None) {
Get5_MessageToAll("%t", "TeamLostTimeToDecideInfoMessage",
diff --git a/scripting/get5/matchconfig.sp b/scripting/get5/matchconfig.sp
index 705548eb6..584a078b7 100644
--- a/scripting/get5/matchconfig.sp
+++ b/scripting/get5/matchconfig.sp
@@ -39,6 +39,7 @@ bool LoadMatchConfig(const char[] config, bool restoreBackup = false) {
g_MatchID = "";
g_ReadyTimeWaitingUsed = 0;
g_HasKnifeRoundStarted = false;
+ g_KnifeWinnerTeam = Get5Team_None;
g_MapChangePending = false;
g_MapNumber = 0;
g_NumberOfMapsInSeries = 0;