Skip to content

Commit

Permalink
Added surrender system
Browse files Browse the repository at this point in the history
Add leave-surrender logic and fix unintended match end on empty server
Add message informing players to stay to win
  • Loading branch information
nickdnk committed Aug 29, 2022
1 parent 7c12eae commit 39c8d99
Show file tree
Hide file tree
Showing 10 changed files with 446 additions and 28 deletions.
8 changes: 8 additions & 0 deletions documentation/docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,14 @@ the [get5_stop_command_enabled](../configuration/#get5_stop_command_enabled) is

: Alias for [`get5_scrim`](#get5_scrim).

####`!surrender` or `!gg` {: #surrender }

: If the surrender features is [enabled](../configuration/#get5_surrender_enabled), this initiates a vote to surrender
the **current map**. After the first vote is cast,
a [minimum number of votes](../configuration/#get5_surrender_required_votes) must be cast be other team members
within [the defined time limit](../configuration/#get5_surrender_time_limit). You can only vote to surrender if you
are [sufficiently behind on points](../configuration/#get5_surrender_minimum_round_deficit).

####`!get5`

: Opens a menu that wraps some common commands. It's mostly intended for people using scrim settings, and has
Expand Down
28 changes: 28 additions & 0 deletions documentation/docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,34 @@ must confirm. **`Default: 0`**
: Whether [tactical pause](../pausing/#tactical) limits (time used and count) are reset each halftime period.
[Technical pauses](../pausing/#technical) are not reset. **`Default: 1`**

## Surrender

####`get5_surrender_enabled`
: Whether the [`!surrender`](../commands/#surrender) command is available. **`Default: 0`**

####`get5_surrender_minimum_round_deficit`
: The minimum number of rounds a team must be behind in order to initiate a vote to surrender. This cannot be set
lower than `1`. **`Default: 8`**

####`get5_surrender_required_votes`
: The number of votes required to surrender as a team. If set to `1` or below, any attempt to surrender will
immediately succeed. **`Default: 3`**

####`get5_surrender_time_limit`
: The number of seconds a team has to vote to surrender after the first vote is cast. This cannot be set lower
than `10`. **`Default: 15`**

####`get5_surrender_cooldown`
: The minimum number of seconds a team must wait before they can initiate a surrender vote following a failed
vote. Set to zero to disable. **`Default: 60`**

####`get5_surrender_time_to_rejoin`
: If a full team disconnects and [`get5_end_match_on_empty_server`](#get5_end_match_on_empty_server) is set, this
determines the number of seconds a player from the disconnecting team has to rejoin the server before they forfeit the
match. If both teams disconnect, the determines how long any player from any team has to rejoin the match before it is
ended in a tie. Cannot be set lower than 30 seconds. This applies even
if [`get5_surrender_enabled`](#get5_surrender_enabled) is disabled. **`Default: 60`**

## Formats

**Note: for these, setting the cvar to an empty string ("") will disable the file writing entirely.**
Expand Down
11 changes: 11 additions & 0 deletions documentation/docs/translations.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,17 @@ end with a full stop as this is added automatically.
| `VetoCountdown` | Veto commencing in _3_ seconds. | Chat |
| `NewVersionAvailable` | A newer version of Get5 is available. Please visit _splewis.github.io/get5_ to update. | Chat |
| `PrereleaseVersionWarning` | You are running an unofficial version of Get5 (_0.9.0-c7af39a_) intended for development and testing only. This message can be disabled with _get5_print_update_notice_. | Chat |
| `SurrenderCommandNotEnabled` | The surrender command is not enabled. | Chat |
| `SurrenderMinimumRoundDeficit` | You must be behind by at least _3_ round(s) in order to surrender. | Chat |
| `SurrenderInitiated` | A vote to surrender was initiated by _PlayerName_. Your team must reach _3_ votes within _15_ seconds. | Chat |
| `SurrenderVoteStatus` | _2_ of _3_ required surrender votes have been cast. | Chat |
| `SurrenderSuccessful` | _Team A_ has surrendered. | Chat |
| `SurrenderVoteFailed` | Not enough players on your team voted to surrender. | Chat |
| `SurrenderOnCooldown` | You must wait _1:30_ to initiate a new vote to surrender. | Chat |
| `SurrenderTeamMustRejoin` | _Team A_ left the server and must rejoin within _60_ seconds or _Team 2_ will win. | Chat |
| `SurrenderRejoinCountdownCanceled` | Surrender countdown was canceled. | Chat |
| `AllPlayersLeftTieCountdown` | Both teams have left the server. At least one player must rejoin within _60_ seconds or the match will end in a tie. | Chat |
| `SurrenderTeamAppearsToLeaveWarning` | _Team A_ appears to be leaving the server. Nobody from _Team B_ can leave before the surrender timer has started, or the match will end in a tie. | Chat |

## Supported Languages {: #supported-languages }

Expand Down
115 changes: 89 additions & 26 deletions scripting/get5.sp
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,12 @@ ConVar g_PhaseAnnouncementCountCvar;
ConVar g_Team1NameColorCvar;
ConVar g_Team2NameColorCvar;
ConVar g_SpecNameColorCvar;
ConVar g_SurrenderEnabledCvar;
ConVar g_MinimumRoundDeficitForSurrenderCvar;
ConVar g_VotesRequiredForSurrenderCvar;
ConVar g_SurrenderVoteTimeLimitCvar;
ConVar g_SurrenderCooldownCvar;
ConVar g_SurrenderTimeToRejoinCvar;

// Autoset convars (not meant for users to set)
ConVar g_GameStateCvar;
Expand Down Expand Up @@ -168,6 +174,14 @@ int g_TacticalPauseTimeUsed[MATCHTEAM_COUNT];
int g_TacticalPausesUsed[MATCHTEAM_COUNT];
int g_TechnicalPausesUsed[MATCHTEAM_COUNT];

/** Surrender **/
int g_SurrenderVotes[MATCHTEAM_COUNT];
float g_SurrenderFailedAt[MATCHTEAM_COUNT];
bool g_SurrenderedPlayers[MAXPLAYERS + 1];
Handle g_SurrenderTimers[MATCHTEAM_COUNT];
Get5Team g_PendingSurrenderTeam = Get5Team_None;
Handle g_EndMatchOnEmptyServerTimer = INVALID_HANDLE;

/** Other state **/
Get5State g_GameState = Get5State_None;
ArrayList g_MapsToPlay = null;
Expand Down Expand Up @@ -290,6 +304,7 @@ Handle g_OnSidePicked = INVALID_HANDLE;
#include "get5/readysystem.sp"
#include "get5/recording.sp"
#include "get5/stats.sp"
#include "get5/surrender.sp"
#include "get5/teamlogic.sp"
#include "get5/tests.sp"

Expand Down Expand Up @@ -446,6 +461,19 @@ public void OnPluginStart() {
"The color used for the name of team 2 in chat messages.");
g_SpecNameColorCvar = CreateConVar("get5_spec_color", "{NORMAL}",
"The color used for the name of spectators in chat messages.");
g_SurrenderEnabledCvar =
CreateConVar("get5_surrender_enabled", "0", "Whether the surrender command is enabled.");
g_MinimumRoundDeficitForSurrenderCvar =
CreateConVar("get5_surrender_minimum_round_deficit", "8", "The minimum number of rounds a team must be behind in order to surrender.");
g_VotesRequiredForSurrenderCvar =
CreateConVar("get5_surrender_required_votes", "3", "The number of votes required for a team to surrender.");
g_SurrenderVoteTimeLimitCvar =
CreateConVar("get5_surrender_time_limit", "15", "The number of seconds before a vote to surrender fails.");
g_SurrenderCooldownCvar =
CreateConVar("get5_surrender_cooldown", "60", "The number of seconds before a vote to surrender can be retried if it fails.");
g_SurrenderTimeToRejoinCvar = CreateConVar(
"get5_surrender_time_to_rejoin", "60",
"If get5_end_match_on_empty_server is set, this determines how many seconds a team has to rejoin the game before they surrender the match. Cannot be set lower than 30 seconds.");

/** Create and exec plugin's configuration file **/
AutoExecConfig(true, "get5");
Expand Down Expand Up @@ -483,6 +511,8 @@ public void OnPluginStart() {
AddAliasedCommand("t", Command_T, "Elects to start on T side after winning a knife round");
AddAliasedCommand("ct", Command_Ct, "Elects to start on CT side after winning a knife round");
AddAliasedCommand("stop", Command_Stop, "Elects to stop the game to reload a backup file");
AddAliasedCommand("surrender", Command_Surrender, "Starts a vote for surrendering for your team.");
AddAliasedCommand("gg", Command_Surrender, "Alias for surrender.");

/** Admin/server commands **/
RegAdminCmd(
Expand Down Expand Up @@ -763,54 +793,64 @@ public void OnClientSayCommand_Post(int client, const char[] command, const char
*/
public Action Event_PlayerConnectFull(Event event, const char[] name, bool dontBroadcast) {
int client = GetClientOfUserId(event.GetInt("userid"));
if (IsValidClient(client)) {
if (IsValidClient(client) && !IsFakeClient(client) && !IsClientSourceTV(client)) {
char ipAddress[32];
GetClientIP(client, ipAddress, sizeof(ipAddress));

Get5PlayerConnectedEvent connectEvent =
new Get5PlayerConnectedEvent(GetPlayerObject(client), ipAddress);

LogDebug("Calling Get5_OnPlayerConnected()");

Call_StartForward(g_OnPlayerConnected);
Call_PushCell(connectEvent);
Call_Finish();

EventLogger_LogAndDeleteEvent(connectEvent);

SetEntPropFloat(client, Prop_Send, "m_fForceTeam", 3600.0);

CheckSurrenderStateOnConnect(client);
}
}

public Action Event_PlayerDisconnect(Event event, const char[] name, bool dontBroadcast) {
int client = GetClientOfUserId(event.GetInt("userid"));

if (client > 0) {
if (client > 0 && !IsFakeClient(client) && !IsClientSourceTV(client)) {
Get5PlayerDisconnectedEvent disconnectEvent =
new Get5PlayerDisconnectedEvent(GetPlayerObject(client));

LogDebug("Calling Get5_OnPlayerDisconnected()");

Call_StartForward(g_OnPlayerDisconnected);
Call_PushCell(disconnectEvent);
Call_Finish();

EventLogger_LogAndDeleteEvent(disconnectEvent);
}

// TODO: consider adding a forfeit if a full team disconnects.
if (g_EndMatchOnEmptyServerCvar.BoolValue && g_GameState >= Get5State_Warmup &&
g_GameState < Get5State_PostGame && GetRealClientCount() == 0 && !g_MapChangePending) {
g_TeamSeriesScores[Get5Team_1] = 0;
g_TeamSeriesScores[Get5Team_2] = 0;
StopRecording();
EndSeries(Get5Team_None, false, 0.0, false);
// Because the disconnect event fires before the user leaves the server, we have to put this on a short callback
// to get the right "number of players per team" in CheckForSurrenderOnDisconnect().
CreateTimer(0.1, Timer_DisconnectCheck, _, TIMER_FLAG_NO_MAPCHANGE);
}
return Plugin_Continue;
}

public Action Timer_DisconnectCheck(Handle timer) {
CheckForSurrenderOnDisconnect();
}

// This runs every time a map starts *or* when the plugin is reloaded.
public void OnConfigsExecuted() {
LogDebug("OnConfigsExecuted");

// This is a defensive solution that ensures we don't have lingering surrender-timers. If everyone leaves and a player
// then joins the server again, the server may change the map, which triggers this. If this happens, we cannot
// recover the game state and must force the series to end.
if (g_EndMatchOnEmptyServerTimer != INVALID_HANDLE) {
if (g_GameState != Get5State_None) {
LogDebug("Triggering surrender timer immediately as map was changed.");
TriggerTimer(g_EndMatchOnEmptyServerTimer);
} else {
delete g_EndMatchOnEmptyServerTimer;
}
}

g_MapChangePending = false;
g_DoingBackupRestoreNow = false;
g_ReadyTimeWaitingUsed = 0;
Expand All @@ -821,6 +861,8 @@ public void OnConfigsExecuted() {
g_DemoFileName = "";
DeleteOldBackups();

EndSurrenderTimers();
g_PendingSurrenderTeam = Get5Team_None;
// Always reset ready status on map start
ResetReadyStatus();

Expand Down Expand Up @@ -1368,6 +1410,13 @@ void EndSeries(Get5Team winningTeam, bool printWinnerMessage, float restoreDelay
// has passed.
CreateTimer(restoreDelay, Timer_RestoreMatchCvars, _, TIMER_FLAG_NO_MAPCHANGE);
}

// If a forfeit by disconnect is counting down and the match ends, either by force or because a team leaves in the last
// round and loses the match, ensure that no timer is running so a new game won't be forfeited if it is started before
// the timer runs out.
if (g_EndMatchOnEmptyServerTimer != INVALID_HANDLE) {
delete g_EndMatchOnEmptyServerTimer;
}
}

public Action Timer_KickOnEnd(Handle timer) {
Expand Down Expand Up @@ -1559,14 +1608,20 @@ public Action Event_RoundStart(Event event, const char[] name, bool dontBroadcas
}
}

if (g_GameState == Get5State_Warmup || g_GameState == Get5State_KnifeRound || g_GameState == Get5State_Live) {
if ((g_GameState == Get5State_Warmup || g_GameState == Get5State_KnifeRound || g_GameState == Get5State_Live) && g_PendingSurrenderTeam == Get5Team_None) {
WriteBackup(); // Filters out backup states on its own
}

if (g_GameState != Get5State_Live) {
return;
}

if (g_PendingSurrenderTeam != Get5Team_None) {
SurrenderMap(g_PendingSurrenderTeam);
g_PendingSurrenderTeam = Get5Team_None;
return;
}

Get5RoundStartedEvent startEvent =
new Get5RoundStartedEvent(g_MatchID, g_MapNumber, g_RoundNumber);
LogDebug("Calling Get5_OnRoundStart()");
Expand Down Expand Up @@ -1666,10 +1721,19 @@ public Action Event_RoundEnd(Event event, const char[] name, bool dontBroadcast)
if (g_GameState == Get5State_Live) {
int csTeamWinner = event.GetInt("winner");

int team1Score = CS_GetTeamScore(Get5TeamToCSTeam(Get5Team_1));
int team2Score = CS_GetTeamScore(Get5TeamToCSTeam(Get5Team_2));

if (team1Score == team2Score) {
// If a vote is started and the game proceeds to a tie; stop the timers as surrender can now not be performed.
EndSurrenderTimers();
}

Get5_MessageToAll("%s {GREEN}%d {NORMAL}- {GREEN}%d %s", g_FormattedTeamNames[Get5Team_1],
CS_GetTeamScore(Get5TeamToCSTeam(Get5Team_1)),
CS_GetTeamScore(Get5TeamToCSTeam(Get5Team_2)),
g_FormattedTeamNames[Get5Team_2]);
team1Score,
team2Score,
g_FormattedTeamNames[Get5Team_2]
);

Stats_RoundEnd(csTeamWinner);

Expand Down Expand Up @@ -1722,18 +1786,17 @@ public Action Event_RoundEnd(Event event, const char[] name, bool dontBroadcast)
// https://github.com/alliedmodders/sourcemod/blob/master/plugins/include/cstrike.inc#L53-L77
// - which is why we subtract one.
Get5RoundEndedEvent roundEndEvent = new Get5RoundEndedEvent(
g_MatchID, g_MapNumber, g_RoundNumber, GetRoundTime(),
view_as<CSRoundEndReason>(event.GetInt("reason") - 1),
new Get5Winner(CSTeamToGet5Team(csTeamWinner), view_as<Get5Side>(csTeamWinner)),
CS_GetTeamScore(Get5TeamToCSTeam(Get5Team_1)),
CS_GetTeamScore(Get5TeamToCSTeam(Get5Team_2)));
g_MatchID, g_MapNumber, g_RoundNumber, GetRoundTime(),
view_as<CSRoundEndReason>(event.GetInt("reason") - 1),
new Get5Winner(CSTeamToGet5Team(csTeamWinner), view_as<Get5Side>(csTeamWinner)),
team1Score,
team2Score
);

LogDebug("Calling Get5_OnRoundEnd()");

Call_StartForward(g_OnRoundEnd);
Call_PushCell(roundEndEvent);
Call_Finish();

EventLogger_LogAndDeleteEvent(roundEndEvent);

// Reset this when a round ends, as voting has no reference to which round the teams wanted to restore to, so
Expand Down
5 changes: 5 additions & 0 deletions scripting/get5/backups.sp
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,11 @@ bool RestoreFromBackup(const char[] path, bool restartRecording = true) {
StopRecording();
}

if (g_EndMatchOnEmptyServerTimer != INVALID_HANDLE) {
LogDebug("Killing surrender/empty server timer as backup was requested.");
delete g_EndMatchOnEmptyServerTimer;
}

if (kv.JumpToKey("Match")) {
char tempBackupFile[PLATFORM_MAX_PATH];
GetTempFilePath(tempBackupFile, sizeof(tempBackupFile), TEMP_MATCHCONFIG_BACKUP_PATTERN);
Expand Down
2 changes: 2 additions & 0 deletions scripting/get5/debug.sp
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ static void AddGlobalStateInfo(File f) {
f.WriteLine("g_PausingTeam = %d", g_PausingTeam);
f.WriteLine("g_PauseType = %d", g_PauseType);
f.WriteLine("g_LatestPauseDuration = %d", g_LatestPauseDuration);
f.WriteLine("g_PendingSurrenderTeam = %d", g_PendingSurrenderTeam);

LOOP_TEAMS(team) {
GetTeamString(team, buffer, sizeof(buffer));
Expand All @@ -139,6 +140,7 @@ static void AddGlobalStateInfo(File f) {
f.WriteLine("g_TeamFlags = %s", g_TeamFlags[team]);
f.WriteLine("g_TeamLogos = %s", g_TeamLogos[team]);
f.WriteLine("g_TeamMatchTexts = %s", g_TeamMatchTexts[team]);
f.WriteLine("g_SurrenderVotes = %d", g_SurrenderVotes[team]);

CSTeamString(g_TeamSide[team], buffer, sizeof(buffer));
f.WriteLine("g_TeamSide = %s (%d)", buffer, g_TeamSide[team]);
Expand Down
3 changes: 3 additions & 0 deletions scripting/get5/matchconfig.sp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ bool LoadMatchConfig(const char[] config, bool restoreBackup = false) {
return false;
}

EndSurrenderTimers();
g_PendingSurrenderTeam = Get5Team_None;

ResetReadyStatus();
LOOP_TEAMS(team) {
g_TeamSeriesScores[team] = 0;
Expand Down
4 changes: 2 additions & 2 deletions scripting/get5/readysystem.sp
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,10 @@ public int GetTeamReadyCount(Get5Team team) {
return readyCount;
}

public int GetTeamPlayerCount(Get5Team team) {
int GetTeamPlayerCount(Get5Team team, bool includeCoaches = false) {
int playerCount = 0;
LOOP_CLIENTS(i) {
if (IsPlayer(i) && GetClientMatchTeam(i) == team && !IsClientCoaching(i)) {
if (IsPlayer(i) && GetClientMatchTeam(i) == team && (includeCoaches || !IsClientCoaching(i))) {
playerCount++;
}
}
Expand Down
Loading

0 comments on commit 39c8d99

Please sign in to comment.