diff --git a/scripting/get5.sp b/scripting/get5.sp index 49adf6899..e40f0ea96 100644 --- a/scripting/get5.sp +++ b/scripting/get5.sp @@ -134,6 +134,9 @@ ArrayList g_CvarNames = null; ArrayList g_CvarValues = null; bool g_InScrimMode = false; bool g_HasKnifeRoundStarted = false; +bool g_IsChangingPauseState = false; // Used to prevent mp_pause_match and mp_unpause_match from being called directly. +float g_FixedPauseUnpauseTimerTime = 0.0; // on round start, this creates the unpause hook if the game has fixed-duration pause. +Get5Team g_PausingTeam = Get5Team_None; // The team that last called for a pause. /** Other state **/ Get5State g_GameState = Get5State_None; @@ -190,7 +193,6 @@ int g_TeamPausesUsed[MATCHTEAM_COUNT]; int g_TeamTechPausesUsed[MATCHTEAM_COUNT]; int g_TechPausedTimeOverride[MATCHTEAM_COUNT]; int g_TeamGivenTechPauseCommand[MATCHTEAM_COUNT]; -int g_PauseTimeUsed = 0; int g_ReadyTimeWaitingUsed = 0; char g_DefaultTeamColors[][] = { TEAM1_COLOR, @@ -258,6 +260,7 @@ Handle g_OnRoundStatsUpdated = INVALID_HANDLE; Handle g_OnSeriesInit = INVALID_HANDLE; Handle g_OnSeriesResult = INVALID_HANDLE; Handle g_OnSidePicked = INVALID_HANDLE; +Handle g_FixedPauseTimer = INVALID_HANDLE; #include "get5/util.sp" #include "get5/version.sp" @@ -516,6 +519,8 @@ public void OnPluginStart() { AddCommandListener(Command_Coach, "coach"); AddCommandListener(Command_JoinTeam, "jointeam"); AddCommandListener(Command_JoinGame, "joingame"); + AddCommandListener(Command_PauseOrUnpauseMatch, "mp_pause_match"); + AddCommandListener(Command_PauseOrUnpauseMatch, "mp_unpause_match"); /** Setup data structures **/ g_MapPoolList = new ArrayList(PLATFORM_MAX_PATH); @@ -1301,6 +1306,12 @@ public Action Event_RoundPreStart(Event event, const char[] name, bool dontBroad public Action Event_FreezeEnd(Event event, const char[] name, bool dontBroadcast) { LogDebug("Event_FreezeEnd"); + // If there's a changelevel command during a pause, we need to ensure that this + // handle gets reset. FreezeEnd is called immediately as the map loads, and the timer is always set with + // TIMER_FLAG_NO_MAPCHANGE, so its callback will not fire. + g_FixedPauseTimer = INVALID_HANDLE; + g_PausingTeam = Get5Team_None; + // We always want this to be correct, regardless of game state. g_RoundStartedTime = GetEngineTime(); if (g_GameState == Get5State_Live) { @@ -1346,6 +1357,10 @@ public Action Event_RoundStart(Event event, const char[] name, bool dontBroadcas g_BombPlantedTime = 0.0; g_BombSiteLastPlanted = Get5BombSite_Unknown; + // If a pause was pending with a fixed timer, begin counting down as the round starts. + // This function does nothing if no pending timer value was set. + StartFixedPauseUnpauseTimer(); + if (g_GameState != Get5State_Live) { g_RoundNumber = -1; // Round number always -1 if not yet live. return; diff --git a/scripting/get5/backups.sp b/scripting/get5/backups.sp index 4f416b91a..18a703d80 100644 --- a/scripting/get5/backups.sp +++ b/scripting/get5/backups.sp @@ -355,7 +355,7 @@ public void RestoreGet5Backup() { EndWarmup(); EndWarmup(); ServerCommand("mp_restartgame 5"); - PauseGame(Get5Team_None, Get5PauseType_Backup, 1); + PauseGame(Get5Team_None, Get5PauseType_Backup); if (g_CoachingEnabledCvar.BoolValue) { CreateTimer(6.0, Timer_SwapCoaches); } @@ -377,7 +377,7 @@ public Action Timer_SwapCoaches(Handle timer) { } public Action Time_StartRestore(Handle timer) { - PauseGame(Get5Team_None, Get5PauseType_Backup, 1); + PauseGame(Get5Team_None, Get5PauseType_Backup); char tempValveBackup[PLATFORM_MAX_PATH]; GetTempFilePath(tempValveBackup, sizeof(tempValveBackup), TEMP_VALVE_BACKUP_PATTERN); diff --git a/scripting/get5/mapveto.sp b/scripting/get5/mapveto.sp index b76ba84b6..d86800f69 100644 --- a/scripting/get5/mapveto.sp +++ b/scripting/get5/mapveto.sp @@ -15,11 +15,7 @@ public void CreateVeto() { g_VetoCaptains[Get5Team_2] = GetTeamCaptain(Get5Team_2); ResetReadyStatus(); if (g_PauseOnVetoCvar.BoolValue) { - if (g_PausingEnabledCvar.BoolValue) { - PauseGame(Get5Team_None, Get5PauseType_Admin, 1); - } else { - ServerCommand("mp_pause_match"); - } + PauseGame(Get5Team_None, Get5PauseType_Admin); } CreateTimer(1.0, Timer_VetoCountdown, _, TIMER_REPEAT); } @@ -50,7 +46,7 @@ public void VetoFinished() { Get5_MessageToAll("%t", "MapDecidedInfoMessage"); if (IsPaused()) { - ServerCommand("mp_unpause_match"); + UnpauseGame(Get5Team_None); } // Use total series score as starting point, to not print skipped maps diff --git a/scripting/get5/pausing.sp b/scripting/get5/pausing.sp index bc2302acb..9719e1d08 100644 --- a/scripting/get5/pausing.sp +++ b/scripting/get5/pausing.sp @@ -2,7 +2,13 @@ public bool Pauseable() { return g_GameState >= Get5State_KnifeRound && g_PausingEnabledCvar.BoolValue; } -public bool PauseGame(Get5Team team, Get5PauseType type, int pausesLeft) { +stock void PauseGame(Get5Team team, Get5PauseType type, int pausesLeft = 1, float fixedPauseTime = 0.0) { + if (type == Get5PauseType_None) { + LogError("PauseGame() called with Get5PauseType_None. Please call UnpauseGame() instead."); + UnpauseGame(team); + return; + } + Get5MatchPausedEvent event = new Get5MatchPausedEvent(g_MatchID, g_MapNumber, team, type); LogDebug("Calling Get5_OnMatchPaused()"); @@ -13,10 +19,35 @@ public bool PauseGame(Get5Team team, Get5PauseType type, int pausesLeft) { EventLogger_LogAndDeleteEvent(event); - return Pause(type, g_FixedPauseTimeCvar.IntValue, Get5TeamToCSTeam(team), pausesLeft); + g_PauseType = type; + g_PausingTeam = team; + g_IsChangingPauseState = true; + ServerCommand("mp_pause_match"); + CreateTimer(0.1, Timer_ResetPauseRestriction); + + // Setting props for anything other than fixed-time tactical pauses causes a bunch of issues. + // We did try messing around with m_bTechnicalTimeOut for tech pauses, but to no avail. + if (fixedPauseTime < 1 || type != Get5PauseType_Tactical) { + return; + } + + Get5Side side = view_as(Get5TeamToCSTeam(team)); + if (side == Get5Side_T) { + GameRules_SetProp("m_bTerroristTimeOutActive", true); + GameRules_SetPropFloat("m_flTerroristTimeOutRemaining", fixedPauseTime); + GameRules_SetProp("m_nTerroristTimeOuts", pausesLeft); + } else if (side == Get5Side_CT) { + GameRules_SetProp("m_bCTTimeOutActive", true); + GameRules_SetPropFloat("m_flCTTimeOutRemaining", fixedPauseTime); + GameRules_SetProp("m_nCTTimeOuts", pausesLeft); + } +} + +public Action Timer_ResetPauseRestriction(Handle timer, int data) { + g_IsChangingPauseState = false; } -public void UnpauseGame(Get5Team team) { +stock void UnpauseGame(Get5Team team) { Get5MatchUnpausedEvent event = new Get5MatchUnpausedEvent(g_MatchID, g_MapNumber, team); LogDebug("Calling Get5_OnMatchUnpaused()"); @@ -27,17 +58,34 @@ public void UnpauseGame(Get5Team team) { EventLogger_LogAndDeleteEvent(event); - Unpause(); + g_PauseType = Get5PauseType_None; + g_PausingTeam = Get5Team_None; + delete g_FixedPauseTimer; + g_IsChangingPauseState = true; + ServerCommand("mp_unpause_match"); + CreateTimer(0.1, Timer_ResetPauseRestriction); +} + +public Action Command_PauseOrUnpauseMatch(int client, const char[] command, int argc) { + if (g_GameState == Get5State_None || g_IsChangingPauseState) { + return Plugin_Continue; + } + ReplyToCommand(client, "Get5 prevents calls to %s. Administrators should use sm_pause/sm_unpause.", command); + return Plugin_Stop; } public Action Command_TechPause(int client, int args) { - if (!g_AllowTechPauseCvar.BoolValue || !Pauseable() || IsPaused()) { + if (client == 0) { + // Redirect admin use of sm_tech to regular pause. We only have one type of admin pause. + return Command_Pause(client, args); + } + + if (!g_AllowTechPauseCvar.BoolValue) { + Get5_MessageToAll("%t", "TechPausesNotEnabled"); return Plugin_Handled; } - if (client == 0) { - PauseGame(Get5Team_None, Get5PauseType_Admin, 1); - Get5_MessageToAll("%t", "AdminForceTechPauseInfoMessage"); + if (!Pauseable() || IsPaused()) { return Plugin_Handled; } @@ -75,7 +123,7 @@ public Action Command_TechPause(int client, int args) { } } - PauseGame(team, Get5PauseType_Tech, 1); + PauseGame(team, Get5PauseType_Tech); Get5_MessageToAll("%t", "MatchTechPausedByTeamMessage", client); return Plugin_Handled; @@ -87,7 +135,7 @@ public Action Command_Pause(int client, int args) { } if (client == 0) { - PauseGame(Get5Team_None, Get5PauseType_Admin, 1); + PauseGame(Get5Team_None, Get5PauseType_Admin); Get5_MessageToAll("%t", "AdminForcePauseInfoMessage"); return Plugin_Handled; } @@ -104,10 +152,12 @@ public Action Command_Pause(int client, int args) { return Plugin_Handled; } - int maxPauseTime = g_MaxPauseTimeCvar.IntValue; - if (maxPauseTime > 0 && g_TeamPauseTimeUsed[team] >= maxPauseTime && IsPlayerTeam(team)) { - Get5_Message(client, "%t", "MaxPausesTimeUsedInfoMessage", maxPauseTime, pausePeriodString); - return Plugin_Handled; + if (!g_FixedPauseTimeCvar.BoolValue) { + int maxPauseTime = g_MaxPauseTimeCvar.IntValue; + if (maxPauseTime > 0 && g_TeamPauseTimeUsed[team] >= maxPauseTime && IsPlayerTeam(team)) { + Get5_Message(client, "%t", "MaxPausesTimeUsedInfoMessage", maxPauseTime, pausePeriodString); + return Plugin_Handled; + } } g_TeamReadyForUnpause[Get5Team_1] = false; @@ -120,20 +170,32 @@ public Action Command_Pause(int client, int args) { pausesLeft = g_MaxPausesCvar.IntValue - g_TeamPausesUsed[team] - 1; } - // If the pause will need explicit resuming, we will create a timer to poll the pause status. - bool need_resume = PauseGame(team, Get5PauseType_Tactical, pausesLeft); + float fixedPauseTime = g_FixedPauseTimeCvar.FloatValue; + if (fixedPauseTime > 0.0 && fixedPauseTime < 15.0) { + fixedPauseTime = 15.0; // Don't allow less than 15 second pauses. + } + + PauseGame(team, Get5PauseType_Tactical, pausesLeft, fixedPauseTime); if (IsPlayer(client)) { Get5_MessageToAll("%t", "MatchPausedByTeamMessage", client); } if (IsPlayerTeam(team)) { - if (need_resume) { - g_PauseTimeUsed = g_PauseTimeUsed + g_FixedPauseTimeCvar.IntValue - 1; + if (fixedPauseTime > 0) { + // If the pause time is fixed, we create a timer that fires 100ms before the in-game countdown (set by PauseGame) + // would have unpaused the game. This gives us the correct countdown but prevents firing the unpause command twice. + // The global handle is reset when the game unpauses and mp_pause_match and mp_unpause_match are both blocked to + // prevent users from messing up this logic. + g_FixedPauseUnpauseTimerTime = fixedPauseTime - 0.1; + if (InFreezeTime()) { // Don't start counting until freeze time begins. + StartFixedPauseUnpauseTimer(); + } // else the counter starts on round start + } else if (g_MaxPauseTimeCvar.IntValue > 0) { + // Else, if we have a maximum pause time (sum of seconds), we will not use the in-game pause timer, but count + // down manually and unpause when the team runs out of pause time. CreateTimer(1.0, Timer_PauseTimeCheck, team, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - // This timer is used to only fire off the Unpause event. - CreateTimer(1.0, Timer_UnpauseEventCheck, team, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - } + } // else pauses are unlimited un duration and must be unpaused explicitly by both teams; no timer required. g_TeamPausesUsed[team]++; @@ -146,7 +208,7 @@ public Action Command_Pause(int client, int args) { if (pausesLeft == 1 && g_MaxPausesCvar.IntValue > 0) { Get5_MessageToAll("%t", "OnePauseLeftInfoMessage", g_FormattedTeamNames[team], pausesLeft, pausePeriodString); - } else if (g_MaxPausesCvar.IntValue > 0) { + } else { Get5_MessageToAll("%t", "PausesLeftInfoMessage", g_FormattedTeamNames[team], pausesLeft, pausePeriodString); } @@ -156,6 +218,17 @@ public Action Command_Pause(int client, int args) { return Plugin_Handled; } +stock void StartFixedPauseUnpauseTimer() { + if (g_FixedPauseUnpauseTimerTime == 0) { + LogDebug("Ignoring StartFixedPauseUnpauseTimer() call because g_FixedPauseUnpauseTimerTime was zero."); + return; + } + LogDebug("Starting fixed pause countdown with %f seconds.", g_FixedPauseUnpauseTimerTime); + delete g_FixedPauseTimer; // Never run the same timer twice; logically this shouldn't happen, but just to be sure. + g_FixedPauseTimer = CreateTimer(g_FixedPauseUnpauseTimerTime, Timer_UnpauseBeforeGameUnpause, g_PausingTeam, TIMER_FLAG_NO_MAPCHANGE); + g_FixedPauseUnpauseTimerTime = 0.0; // Reset +} + public Action Timer_TechPauseOverrideCheck(Handle timer, int data) { Get5Team team = view_as(data); int maxTechPauseTime = g_MaxTechPauseTime.IntValue; @@ -203,44 +276,29 @@ public Action Timer_TechPauseOverrideCheck(Handle timer, int data) { } // Someone can call pause during a round and will set this timer. - // Keep running timer until we are paused. + // Keep running timer until we are unpaused. return Plugin_Continue; } -public Action Timer_UnpauseEventCheck(Handle timer, int data) { - if (!Pauseable()) { - g_PauseTimeUsed = 0; - return Plugin_Stop; - } - - // Unlimited pause time. - if (g_MaxPauseTimeCvar.IntValue <= 0) { - // Reset state. - g_PauseTimeUsed = 0; - return Plugin_Stop; - } - - if (!InFreezeTime()) { - // Someone can call pause during a round and will set this timer. - // Keep running timer until we are paused. - return Plugin_Continue; - } else { - if (g_PauseTimeUsed <= 0) { - Get5Team team = view_as(data); - UnpauseGame(team); - // Reset state - g_PauseTimeUsed = 0; - return Plugin_Stop; - } - g_PauseTimeUsed--; - LogDebug("Subtracting time used. Current time = %d", g_PauseTimeUsed); +public Action Timer_UnpauseBeforeGameUnpause(Handle timer, Get5Team team) { + g_FixedPauseTimer = INVALID_HANDLE; // Always remove global reference to the timer. + if (!Pauseable() || !IsPaused()) { + LogError("Fixed pause timer unexpectedly found no paused state when it expired."); + return Plugin_Handled; } + LogDebug("Unpausing tactical pause before in-game timer runs out."); + // We have to do this as UnpauseGame() kills the timer that calls this function, and SourceMod will error if we kill + // a timer inside its own callback. + RequestFrame(UnpauseInsideTimer, team); + return Plugin_Handled; +} - return Plugin_Continue; +public void UnpauseInsideTimer(Get5Team team) { + UnpauseGame(team); } -public Action Timer_PauseTimeCheck(Handle timer, int data) { - if (!Pauseable() || !IsPaused() || g_FixedPauseTimeCvar.BoolValue) { +public Action Timer_PauseTimeCheck(Handle timer, Get5Team team) { + if (!Pauseable() || !IsPaused()) { return Plugin_Stop; } int maxPauseTime = g_MaxPauseTimeCvar.IntValue; @@ -254,10 +312,10 @@ public Action Timer_PauseTimeCheck(Handle timer, int data) { Format(pausePeriodString, sizeof(pausePeriodString), " %t", "PausePeriodSuffix"); } - Get5Team team = view_as(data); int timeLeft = maxPauseTime - g_TeamPauseTimeUsed[team]; - // Only count against the team's pause time if we're actually in the freezetime - // pause and they haven't requested an unpause yet. + // If the team that called the pause has indicated they are ready, no more time should be subtracted from their + // maximum pause time, but the timer must keep running as they could go back to not-ready-for-unpause before the + // other team unpauses, in which case we would keep counting their seconds used. if (InFreezeTime() && !g_TeamReadyForUnpause[team]) { g_TeamPauseTimeUsed[team]++; @@ -281,6 +339,11 @@ public Action Command_Unpause(int client, int args) { if (!IsPaused()) return Plugin_Handled; + if (g_FixedPauseTimer != INVALID_HANDLE) { + Get5_MessageToAll("%t", "FixedPauseCannotBeUnpaused"); + return Plugin_Handled; + } + if (g_PauseType == Get5PauseType_Admin && client != 0) { Get5_MessageToAll("%t", "UserCannotUnpauseAdmin"); return Plugin_Handled; diff --git a/scripting/get5/util.sp b/scripting/get5/util.sp index 4c7b90582..7739bdbf0 100644 --- a/scripting/get5/util.sp +++ b/scripting/get5/util.sp @@ -293,38 +293,6 @@ stock bool IsPaused() { return GameRules_GetProp("m_bMatchWaitingForResume") != 0; } -// Pauses and returns if the match will automatically unpause after the duration ends. -stock bool Pause(Get5PauseType pauseType, int pauseTime, int csTeam, int pausesLeft) { - if (pauseType == Get5PauseType_None) { - LogMessage("Pause() called with Get5PauseType_None. Please call Unpause() instead."); - Unpause(); - return false; - } - - g_PauseType = pauseType; - ServerCommand("mp_pause_match"); - if (pauseType == Get5PauseType_Tech || pauseTime == 0 || csTeam == CS_TEAM_SPECTATOR || - csTeam == CS_TEAM_NONE) { - return false; - } else { - if (csTeam == CS_TEAM_T) { - GameRules_SetProp("m_bTerroristTimeOutActive", true); - GameRules_SetPropFloat("m_flTerroristTimeOutRemaining", float(pauseTime)); - GameRules_SetProp("m_nTerroristTimeOuts", pausesLeft); - } else if (csTeam == CS_TEAM_CT) { - GameRules_SetProp("m_bCTTimeOutActive", true); - GameRules_SetPropFloat("m_flCTTimeOutRemaining", float(pauseTime)); - GameRules_SetProp("m_nCTTimeOuts", pausesLeft); - } - return true; - } -} - -stock void Unpause() { - g_PauseType = Get5PauseType_None; - ServerCommand("mp_unpause_match"); -} - stock void RestartGame(int delay) { ServerCommand("mp_restartgame %d", delay); } diff --git a/translations/chi/get5.phrases.txt b/translations/chi/get5.phrases.txt index fbbdbe623..dc14e0c80 100644 --- a/translations/chi/get5.phrases.txt +++ b/translations/chi/get5.phrases.txt @@ -84,10 +84,6 @@ { "chi" "{1}要求发起一个技术暂停。" } - "AdminForceTechPauseInfoMessage" - { - "chi" "一位管理员发起了技术暂停。" - } "PauseTimeExpiration10SecInfoMessage" { "chi" "{1}的暂停时间即将耗尽,将在10秒后解除暂停。" diff --git a/translations/da/get5.phrases.txt b/translations/da/get5.phrases.txt index 09f75497f..96a540bbe 100644 --- a/translations/da/get5.phrases.txt +++ b/translations/da/get5.phrases.txt @@ -92,9 +92,17 @@ { "da" "{1} har bedt om teknisk pause." } - "AdminForceTechPauseInfoMessage" + "TechPausesNotEnabled" { - "da" "En admin har bedt om teknisk pause." + "da" "Tekniske pause er ikke aktiveret." + } + "UserCannotUnpauseAdmin" + { + "da" "Pauser startet af administratorer kan kun afbrydes af administratorer." + } + "FixedPauseCannotBeUnpaused" + { + "da" "Pauser med fast længde kan ikke afbrydes. I skal vente på, at pausen udløber." } "PauseTimeExpiration10SecInfoMessage" { diff --git a/translations/es/get5.phrases.txt b/translations/es/get5.phrases.txt index 72ab92c19..75c693439 100644 --- a/translations/es/get5.phrases.txt +++ b/translations/es/get5.phrases.txt @@ -88,10 +88,6 @@ { "es" "{1} pidió una pausa tecnica." } - "AdminForceTechPauseInfoMessage" - { - "es" "Un administrador ha pedido una pausa tecnica." - } "PauseTimeExpiration10SecInfoMessage" { "es" "{1} pronto se termina la pausa, termino en 10 segundos." diff --git a/translations/fr/get5.phrases.txt b/translations/fr/get5.phrases.txt index ae163dbad..373401ca7 100644 --- a/translations/fr/get5.phrases.txt +++ b/translations/fr/get5.phrases.txt @@ -88,10 +88,6 @@ { "fr" "{1} a demandé une pause technique." } - "AdminForceTechPauseInfoMessage" - { - "fr" "Un admin a demandé une pause technique." - } "PauseTimeExpiration10SecInfoMessage" { "fr" "{1} terminera sa pause dans 10 secondes." diff --git a/translations/get5.phrases.txt b/translations/get5.phrases.txt index d6013f2df..cc776bb1d 100644 --- a/translations/get5.phrases.txt +++ b/translations/get5.phrases.txt @@ -102,14 +102,18 @@ "#format" "{1:N}" "en" "{1} has called for a technical pause." } - "AdminForceTechPauseInfoMessage" + "TechPausesNotEnabled" { - "en" "An admin has called for a technical pause." + "en" "Technical pauses are not enabled." } "UserCannotUnpauseAdmin" { "en" "As an admin has called for this pause, it must also be unpaused by an admin." } + "FixedPauseCannotBeUnpaused" + { + "en" "A fixed-duration pause cannot be unpaused. You must wait for it to end." + } "PauseTimeExpiration10SecInfoMessage" { "#format" "{1:s}"