Skip to content

Commit

Permalink
Match Restore Coach Fix (#754)
Browse files Browse the repository at this point in the history
Add support for coaches locked via steam ID.
  • Loading branch information
PhlexPlexico authored Jul 15, 2022
1 parent 4460f01 commit f1e27ad
Show file tree
Hide file tree
Showing 11 changed files with 255 additions and 16 deletions.
1 change: 1 addition & 0 deletions configs/get5/example_match.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
}

"players_per_team" "5"
"coaches_per_team" "2"
"min_players_to_ready" "1" // Minimum # of players a team must have to ready
"min_spectators_to_ready" "0" // How many spectators must be ready to begin.

Expand Down
3 changes: 2 additions & 1 deletion configs/get5/example_match.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"matchid": "example_match",
"num_maps": 3,
"players_per_team": 1,
"players_per_team": 5,
"coaches_per_team": 2,
"min_players_to_ready": 1,
"min_spectators_to_ready": 0,
"skip_veto": false,
Expand Down
1 change: 1 addition & 0 deletions configs/get5/scrim_template.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"scrim" "1"
"side_type" "never_knife"
"players_per_team" "5"
"coaches_per_team" "2"
"num_maps" "1"
"skip_veto" "1"

Expand Down
3 changes: 3 additions & 0 deletions documentation/docs/match_schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ match.
backup method, defaults to `0`.
- `matchtext`: Wraps `mp_teammatchstat_1`, you probably don't want to set this, in BoX series `mp_teamscore` cvars are
automatically set and take the place of the `mp_teammatchstat` cvars.
- `coaches`: Identical to the `players` tag, it's an optional list of Steam ID's for users who wish to coach a team.
You may also force player names here. This field is optional.

## Optional Values

Expand All @@ -39,6 +41,7 @@ match.
gets the side choice, "never_knife" means "team1" is always on CT first, and "always_knife" means there is always a
knife round.
- `players_per_team`: Maximum players per team (doesn't include a coach spot, default: 5).
- `coaches_per_team`: Maximum coaches per team (default: 2).
- `min_players_to_ready`: Minimum players a team needs to be able to ready up (default: 1).
- `favored_percentage_team1`: Wrapper for the servers `mp_teamprediction_pct`.
- `favored_percentage_text` Wrapper for the servers `mp_teamprediction_txt`.
Expand Down
20 changes: 15 additions & 5 deletions scripting/get5.sp
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ bool g_BO2Match = false;
char g_MatchID[MATCH_ID_LENGTH];
ArrayList g_MapPoolList = null;
ArrayList g_TeamAuths[MATCHTEAM_COUNT];
ArrayList g_TeamCoaches[MATCHTEAM_COUNT];
StringMap g_PlayerNames;
char g_TeamNames[MATCHTEAM_COUNT][MAX_CVAR_LENGTH];
char g_TeamTags[MATCHTEAM_COUNT][MAX_CVAR_LENGTH];
Expand All @@ -113,6 +114,7 @@ char g_MatchTitle[MAX_CVAR_LENGTH];
int g_FavoredTeamPercentage = 0;
char g_FavoredTeamText[MAX_CVAR_LENGTH];
int g_PlayersPerTeam = 5;
int g_CoachesPerTeam = 2;
int g_MinPlayersToReady = 1;
int g_MinSpectatorsToReady = 0;
bool g_SkipVeto = false;
Expand Down Expand Up @@ -414,6 +416,8 @@ public void OnPluginStart() {
RegAdminCmd("get5_endmatch", Command_EndMatch, ADMFLAG_CHANGEMAP, "Force ends the current match");
RegAdminCmd("get5_addplayer", Command_AddPlayer, ADMFLAG_CHANGEMAP,
"Adds a steamid to a match team");
RegAdminCmd("get5_addcoach", Command_AddCoach, ADMFLAG_CHANGEMAP,
"Adds a steamid to a match teams coach slot");
RegAdminCmd("get5_removeplayer", Command_RemovePlayer, ADMFLAG_CHANGEMAP,
"Removes a steamid from a match team");
RegAdminCmd("get5_addkickedplayer", Command_AddKickedPlayer, ADMFLAG_CHANGEMAP,
Expand Down Expand Up @@ -483,6 +487,8 @@ public void OnPluginStart() {

for (int i = 0; i < sizeof(g_TeamAuths); i++) {
g_TeamAuths[i] = new ArrayList(AUTH_LENGTH);
// Same length.
g_TeamCoaches[i] = new ArrayList(AUTH_LENGTH);
}
g_PlayerNames = new StringMap();

Expand Down Expand Up @@ -959,12 +965,16 @@ public void RestoreLastRound(int client) {

char lastBackup[PLATFORM_MAX_PATH];
g_LastGet5BackupCvar.GetString(lastBackup, sizeof(lastBackup));
if (RestoreFromBackup(lastBackup)) {
Get5_MessageToAll("%t", "BackupLoadedInfoMessage", lastBackup);
// Fix the last backup cvar since it gets reset.
g_LastGet5BackupCvar.SetString(lastBackup);
if (!StrEqual(lastBackup, "")) {
if (RestoreFromBackup(lastBackup)) {
Get5_MessageToAll("%t", "BackupLoadedInfoMessage", lastBackup);
// Fix the last backup cvar since it gets reset.
g_LastGet5BackupCvar.SetString(lastBackup);
} else {
ReplyToCommand(client, "Failed to load backup %s - check error logs", lastBackup);
}
} else {
ReplyToCommand(client, "Failed to load backup %s - check error logs", lastBackup);
ReplyToCommand(client, "Failed to load backup, as previous round backup does not exist.");
}
}

Expand Down
22 changes: 22 additions & 0 deletions scripting/get5/backups.sp
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,9 @@ public void RestoreGet5Backup() {
EndWarmup();
ServerCommand("mp_restartgame 5");
Pause(PauseType_Backup);
if (g_CoachingEnabledCvar.BoolValue) {
CreateTimer(6.0, Timer_SwapCoaches);
}
} else {
EnsurePausedWarmup();
}
Expand All @@ -359,6 +362,16 @@ public void RestoreGet5Backup() {
}
}

public Action Timer_SwapCoaches(Handle timer) {
for (int i = 1; i <= MaxClients; i++) {
if (IsAuthedPlayer(i)) {
CheckIfClientCoaching(i, MatchTeam_Team1);
CheckIfClientCoaching(i, MatchTeam_Team2);
}

}
}

public Action Time_StartRestore(Handle timer) {
Pause(PauseType_Backup);

Expand All @@ -369,6 +382,15 @@ public Action Time_StartRestore(Handle timer) {
}

public Action Timer_FinishBackup(Handle timer) {
if (g_CoachingEnabledCvar.BoolValue) {
// If we are coaching we want to ensure our
// coaches get moved back onto the team.
// We cannot trust Valve's system as a disconnected
// player will count as a "player" and not be placed
// in the coach slot. So, we cannot enable warmup during
// the round restore process if using a Valve backup.
CreateTimer(0.5, Timer_SwapCoaches);
}
g_DoingBackupRestoreNow = false;
}

Expand Down
4 changes: 4 additions & 0 deletions scripting/get5/debug.sp
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ static void AddGlobalStateInfo(File f) {

f.WriteLine("g_MatchTitle = %s", g_MatchTitle);
f.WriteLine("g_PlayersPerTeam = %d", g_PlayersPerTeam);
f.WriteLine("g_CoachesPerTeam = %d", g_CoachesPerTeam);
f.WriteLine("g_MinPlayersToReady = %d", g_MinPlayersToReady);
f.WriteLine("g_MinSpectatorsToReady = %d", g_MinSpectatorsToReady);
f.WriteLine("g_SkipVeto = %d", g_SkipVeto);
Expand Down Expand Up @@ -137,6 +138,8 @@ static void AddGlobalStateInfo(File f) {
f.WriteLine("g_TeamPausesUsed = %d", g_TeamPausesUsed[team]);
f.WriteLine("g_TeamTechPausesUsed = %d", g_TeamTechPausesUsed[team]);
f.WriteLine("g_TeamGivenTechPauseCommand = %d", g_TeamGivenTechPauseCommand[team]);
f.WriteLine("g_TeamGivenStopCommand = %d", g_TeamGivenStopCommand[team]);
WriteArrayList(f, "g_TeamCoaches", g_TeamCoaches[team]);
}
}

Expand All @@ -155,6 +158,7 @@ static void AddInterestingCvars(File f) {
WriteCvarString(f, "get5_pausing_enabled");
WriteCvarString(f, "get5_reset_pauses_each_half");
WriteCvarString(f, "get5_web_api_url");
WriteCvarString(f, "get5_last_backup_file");
WriteCvarString(f, "mp_freezetime");
WriteCvarString(f, "mp_halftime");
WriteCvarString(f, "mp_halftime_duration");
Expand Down
77 changes: 77 additions & 0 deletions scripting/get5/matchconfig.sp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#define CONFIG_MATCHID_DEFAULT "matchid"
#define CONFIG_MATCHTITLE_DEFAULT "Map {MAPNUMBER} of {MAXMAPS}"
#define CONFIG_PLAYERSPERTEAM_DEFAULT 5
#define CONFIG_COACHESPERTEAM_DEFAULT 2
#define CONFIG_MINPLAYERSTOREADY_DEFAULT 0
#define CONFIG_MINSPECTATORSTOREADY_DEFAULT 0
#define CONFIG_SPECTATORSNAME_DEFAULT "casters"
Expand Down Expand Up @@ -32,6 +33,7 @@ stock bool LoadMatchConfig(const char[] config, bool restoreBackup = false) {
}
g_TechPausedTimeOverride[team] = 0;
g_TeamGivenTechPauseCommand[team] = false;
ClearArray(GetTeamCoaches(team));
ClearArray(GetTeamAuths(team));
}

Expand Down Expand Up @@ -274,6 +276,7 @@ public void WriteMatchToKv(KeyValues kv) {
kv.SetNum("bo2_series", g_BO2Match);
kv.SetNum("skip_veto", g_SkipVeto);
kv.SetNum("players_per_team", g_PlayersPerTeam);
kv.SetNum("coaches_per_team", g_CoachesPerTeam);
kv.SetNum("min_players_to_ready", g_MinPlayersToReady);
kv.SetNum("min_spectators_to_ready", g_MinSpectatorsToReady);
kv.SetString("match_title", g_MatchTitle);
Expand Down Expand Up @@ -335,6 +338,15 @@ static void AddTeamBackupData(KeyValues kv, MatchTeam team) {
kv.SetString("flag", g_TeamFlags[team]);
kv.SetString("logo", g_TeamLogos[team]);
kv.SetString("matchtext", g_TeamMatchTexts[team]);
kv.JumpToKey("coaches", true);
for (int i = 0; i < GetTeamCoaches(team).Length; i++) {
GetTeamCoaches(team).GetString(i, auth, sizeof(auth));
if (!g_PlayerNames.GetString(auth, name, sizeof(name))) {
strcopy(name, sizeof(name), KEYVALUE_STRING_PLACEHOLDER);
}
kv.SetString(auth, KEYVALUE_STRING_PLACEHOLDER);
}
kv.GoBack();
}
}

Expand All @@ -343,6 +355,7 @@ static bool LoadMatchFromKv(KeyValues kv) {
g_InScrimMode = kv.GetNum("scrim") != 0;
kv.GetString("match_title", g_MatchTitle, sizeof(g_MatchTitle), CONFIG_MATCHTITLE_DEFAULT);
g_PlayersPerTeam = kv.GetNum("players_per_team", CONFIG_PLAYERSPERTEAM_DEFAULT);
g_CoachesPerTeam = kv.GetNum("coaches_per_team", CONFIG_COACHESPERTEAM_DEFAULT);
g_MinPlayersToReady = kv.GetNum("min_players_to_ready", CONFIG_MINPLAYERSTOREADY_DEFAULT);
g_MinSpectatorsToReady =
kv.GetNum("min_spectators_to_ready", CONFIG_MINSPECTATORSTOREADY_DEFAULT);
Expand Down Expand Up @@ -454,6 +467,8 @@ static bool LoadMatchFromJson(JSON_Object json) {

g_PlayersPerTeam =
json_object_get_int_safe(json, "players_per_team", CONFIG_PLAYERSPERTEAM_DEFAULT);
g_CoachesPerTeam =
json_object_get_int_safe(json, "coaches_per_team", CONFIG_COACHESPERTEAM_DEFAULT);
g_MinPlayersToReady =
json_object_get_int_safe(json, "min_players_to_ready", CONFIG_MINPLAYERSTOREADY_DEFAULT);
g_MinSpectatorsToReady = json_object_get_int_safe(json, "min_spectators_to_ready",
Expand Down Expand Up @@ -574,7 +589,11 @@ static void LoadTeamDataJson(JSON_Object json, MatchTeam matchTeam) {
if (StrEqual(fromfile, "")) {
// TODO: this needs to support both an array and a dictionary
// For now, it only supports an array
JSON_Object coaches = json.GetObject("coaches");
AddJsonAuthsToList(json, "players", GetTeamAuths(matchTeam), AUTH_LENGTH);
if (coaches != null) {
AddJsonAuthsToList(json, "coaches", GetTeamCoaches(matchTeam), AUTH_LENGTH);
}
json_object_get_string_safe(json, "name", g_TeamNames[matchTeam], MAX_CVAR_LENGTH);
json_object_get_string_safe(json, "tag", g_TeamTags[matchTeam], MAX_CVAR_LENGTH);
json_object_get_string_safe(json, "flag", g_TeamFlags[matchTeam], MAX_CVAR_LENGTH);
Expand Down Expand Up @@ -603,6 +622,7 @@ static void LoadTeamData(KeyValues kv, MatchTeam matchTeam) {

if (StrEqual(fromfile, "")) {
AddSubsectionAuthsToList(kv, "players", GetTeamAuths(matchTeam), AUTH_LENGTH);
AddSubsectionAuthsToList(kv, "coaches", GetTeamCoaches(matchTeam), AUTH_LENGTH);
kv.GetString("name", g_TeamNames[matchTeam], MAX_CVAR_LENGTH, "");
kv.GetString("tag", g_TeamTags[matchTeam], MAX_CVAR_LENGTH, "");
kv.GetString("flag", g_TeamFlags[matchTeam], MAX_CVAR_LENGTH, "");
Expand Down Expand Up @@ -815,6 +835,63 @@ public Action Command_AddPlayer(int client, int args) {
return Plugin_Handled;
}

public Action Command_AddCoach(int client, int args) {
if (g_GameState == Get5State_None) {
ReplyToCommand(client, "Cannot change coach targets when there is no match to modify");
return Plugin_Handled;
} else if (!g_CoachingEnabledCvar.BoolValue) {
ReplyToCommand(client, "Cannot change coach targets if coaching is disabled.");
return Plugin_Handled;
}

char auth[AUTH_LENGTH];
char steam64[AUTH_LENGTH];
char teamString[32];
char name[MAX_NAME_LENGTH];
if (args >= 2 && GetCmdArg(1, auth, sizeof(auth)) &&
GetCmdArg(2, teamString, sizeof(teamString))) {
if (args >= 3) {
GetCmdArg(3, name, sizeof(name));
}

MatchTeam team = MatchTeam_TeamNone;
if (StrEqual(teamString, "team1")) {
team = MatchTeam_Team1;
} else if (StrEqual(teamString, "team2")) {
team = MatchTeam_Team2;
} else {
ReplyToCommand(client, "Unknown team: must be one of team1 or team2");
return Plugin_Handled;
}

if (GetTeamCoaches(team).Length == g_CoachesPerTeam) {
ReplyToCommand(client, "Coach Spots are full for team %s", teamString);
return Plugin_Handled;
}

if(!ConvertAuthToSteam64(auth, steam64)) {
return Plugin_Handled;
}

if (AddCoachToTeam(auth, team, name)) {
// Check if we are in the playerlist already and remove.
int index = GetTeamAuths(team).FindString(auth);
if (index >= 0) {
GetTeamAuths(team).Erase(index);
}
// Update the backup structure as well for round restores, covers edge
// case of users joining, coaching, stopping, and getting 16k cash as player.
WriteBackup();
ReplyToCommand(client, "Successfully added player %s to coach team %s", auth, teamString);
} else {
ReplyToCommand(client, "Player %s is already in a coaching position on a team.", auth);
}
} else {
ReplyToCommand(client, "Usage: get5_addcoach <auth> <team1|team2> [name]");
}
return Plugin_Handled;
}

public Action Command_AddKickedPlayer(int client, int args) {
if (g_GameState == Get5State_None) {
ReplyToCommand(client, "Cannot change player lists when there is no match to modify");
Expand Down
2 changes: 2 additions & 0 deletions scripting/get5/pausing.sp
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,8 @@ public Action Command_Unpause(int client, int args) {
return Plugin_Handled;
}

// Check to see if we have a timeout that is timed. Otherwise, we need to
// continue for unpausing. New pause type to avoid match restores failing.
if (g_FixedPauseTimeCvar.BoolValue && g_PauseType == PauseType_Tactical) {
return Plugin_Handled;
}
Expand Down
Loading

0 comments on commit f1e27ad

Please sign in to comment.