diff --git a/configs/get5/example_match.cfg b/configs/get5/example_match.cfg index 06c1ae6c0..794d71156 100644 --- a/configs/get5/example_match.cfg +++ b/configs/get5/example_match.cfg @@ -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. diff --git a/configs/get5/example_match.json b/configs/get5/example_match.json index 869a07348..a0f7078a4 100644 --- a/configs/get5/example_match.json +++ b/configs/get5/example_match.json @@ -2,6 +2,7 @@ "matchid": "example_match", "num_maps": 3, "players_per_team": 1, + "coaches_per_team": 2, "min_players_to_ready": 1, "min_spectators_to_ready": 0, "skip_veto": false, diff --git a/configs/get5/scrim_template.cfg b/configs/get5/scrim_template.cfg index afe3fdaaf..6cef0ba70 100644 --- a/configs/get5/scrim_template.cfg +++ b/configs/get5/scrim_template.cfg @@ -9,6 +9,7 @@ "scrim" "1" "side_type" "never_knife" "players_per_team" "5" + "coaches_per_team" "2" "num_maps" "1" "skip_veto" "1" diff --git a/documentation/docs/match_schema.md b/documentation/docs/match_schema.md index 408a2628d..9d3b7d18f 100644 --- a/documentation/docs/match_schema.md +++ b/documentation/docs/match_schema.md @@ -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 @@ -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`. diff --git a/scripting/get5.sp b/scripting/get5.sp index 5d3ef15c8..e26ac19b7 100644 --- a/scripting/get5.sp +++ b/scripting/get5.sp @@ -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]; @@ -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; @@ -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, @@ -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(); @@ -959,12 +965,17 @@ 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); + } + //ServerCommand("get5_loadbackup \"%s\"", 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."); } } diff --git a/scripting/get5/debug.sp b/scripting/get5/debug.sp index 466653bc7..2579d2f98 100644 --- a/scripting/get5/debug.sp +++ b/scripting/get5/debug.sp @@ -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); @@ -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]); } } @@ -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"); diff --git a/scripting/get5/matchconfig.sp b/scripting/get5/matchconfig.sp index 1f56b3013..dca2b3a1d 100644 --- a/scripting/get5/matchconfig.sp +++ b/scripting/get5/matchconfig.sp @@ -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" @@ -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)); } @@ -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); @@ -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(); } } @@ -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); @@ -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", @@ -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); @@ -603,6 +622,10 @@ static void LoadTeamData(KeyValues kv, MatchTeam matchTeam) { if (StrEqual(fromfile, "")) { AddSubsectionAuthsToList(kv, "players", GetTeamAuths(matchTeam), AUTH_LENGTH); + if (kv.JumpToKey("coaches")) { + AddSubsectionAuthsToList(kv, "coaches", GetTeamCoaches(matchTeam), AUTH_LENGTH); + kv.GoBack(); + } 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, ""); @@ -815,6 +838,55 @@ 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)) { + 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 [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"); diff --git a/scripting/get5/pausing.sp b/scripting/get5/pausing.sp index cd8a19ff3..d6bf79279 100644 --- a/scripting/get5/pausing.sp +++ b/scripting/get5/pausing.sp @@ -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; } diff --git a/scripting/get5/teamlogic.sp b/scripting/get5/teamlogic.sp index 81ef1f88d..6126b4f4b 100644 --- a/scripting/get5/teamlogic.sp +++ b/scripting/get5/teamlogic.sp @@ -14,12 +14,19 @@ public Action Command_JoinGame(int client, const char[] command, int argc) { public void CheckClientTeam(int client) { MatchTeam correctTeam = GetClientMatchTeam(client); + char auth[AUTH_LENGTH]; int csTeam = MatchTeamToCSTeam(correctTeam); int currentTeam = GetClientTeam(client); if (csTeam != currentTeam) { if (IsClientCoaching(client)) { UpdateCoachTarget(client, csTeam); + } else if (GetAuth(client, auth, sizeof(auth))) { + char steam64[AUTH_LENGTH]; + ConvertAuthToSteam64(auth, steam64); + if (IsAuthOnTeamCoach(steam64, correctTeam)) { + UpdateCoachTarget(client, csTeam); + } } SwitchPlayerTeam(client, csTeam); @@ -63,7 +70,11 @@ public Action Command_JoinTeam(int client, const char[] command, int argc) { } if (csTeam == team_to) { - return Plugin_Continue; + if(CheckIfClientCoaching(client, correctTeam)) { + return Plugin_Stop; + } else { + return Plugin_Continue; + } } if (csTeam != GetClientTeam(client)) { @@ -74,21 +85,39 @@ public Action Command_JoinTeam(int client, const char[] command, int argc) { if (!g_CoachingEnabledCvar.BoolValue) { KickClient(client, "%t", "TeamIsFullInfoMessage"); } else { - LogDebug("Forcing player %N to coach", client); - MoveClientToCoach(client); - Get5_Message(client, "%t", "MoveToCoachInfoMessage"); + // Only attempt to move to coach if we are not full on coaches already. + if (GetTeamCoaches(correctTeam).Length <= g_CoachesPerTeam) { + LogDebug("Forcing player %N to coach", client); + MoveClientToCoach(client); + Get5_Message(client, "%t", "MoveToCoachInfoMessage"); + } else { + KickClient(client, "%t", "TeamIsFullInfoMessage"); + } } } else { LogDebug("Forcing player %N onto %d", client, csTeam); FakeClientCommand(client, "jointeam %d", csTeam); } - + + CheckIfClientCoaching(client, correctTeam); return Plugin_Stop; } return Plugin_Stop; } +public bool CheckIfClientCoaching(int client, MatchTeam team) { + // Force user to join the coach if specified by config or reconnect. + char clientAuth64[AUTH_LENGTH]; + GetAuth(client, clientAuth64, AUTH_LENGTH); + if (g_CoachingEnabledCvar.BoolValue && IsAuthOnTeamCoach(clientAuth64, team)) { + LogDebug("Forcing player %N to coach as they were previously.", client); + MoveClientToCoach(client); + return true; + } + return false; +} + public void MoveClientToCoach(int client) { LogDebug("MoveClientToCoach %L", client); MatchTeam matchTeam = GetClientMatchTeam(client); @@ -110,12 +139,18 @@ public void MoveClientToCoach(int client) { } char teamString[4]; + char clientAuth[64]; CSTeamString(csTeam, teamString, sizeof(teamString)); - - // If we're in warmup or a freezetime we use the in-game + GetAuth(client, clientAuth, AUTH_LENGTH); + if (!IsAuthOnTeamCoach(clientAuth, matchTeam)) { + AddCoachToTeam(clientAuth, matchTeam, ""); + } + + // If we're in warmup we use the in-game // coaching command. Otherwise we manually move them to spec // and set the coaching target. - if (!InWarmup() && !InFreezeTime()) { + // If in freeze time, we have to manually move as well. + if (!InWarmup() && InFreezeTime()) { // TODO: this needs to be tested more thoroughly, // it might need to be done in reverse order (?) LogDebug("Moving %L directly to coach slot", client); @@ -130,6 +165,7 @@ public void MoveClientToCoach(int client) { } public Action Command_SmCoach(int client, int args) { + char auth[AUTH_LENGTH]; if (g_GameState == Get5State_None) { return Plugin_Continue; } @@ -138,11 +174,19 @@ public Action Command_SmCoach(int client, int args) { return Plugin_Handled; } + GetAuth(client, auth, sizeof(auth)); + MatchTeam matchTeam = GetClientMatchTeam(client); + // Don't allow a new coach if spots are full. + if (GetTeamCoaches(matchTeam).Length > g_CoachesPerTeam) { + return Plugin_Stop; + } + MoveClientToCoach(client); return Plugin_Handled; } public Action Command_Coach(int client, const char[] command, int argc) { + if (g_GameState == Get5State_None) { return Plugin_Continue; } @@ -223,6 +267,24 @@ public MatchTeam GetAuthMatchTeam(const char[] steam64) { return MatchTeam_TeamNone; } +public MatchTeam GetAuthMatchTeamCoach(const char[] steam64) { + if (g_GameState == Get5State_None) { + return MatchTeam_TeamNone; + } + + if (g_InScrimMode) { + return IsAuthOnTeamCoach(steam64, MatchTeam_Team1) ? MatchTeam_Team1 : MatchTeam_Team2; + } + + for (int i = 0; i < MATCHTEAM_COUNT; i++) { + MatchTeam team = view_as(i); + if (IsAuthOnTeamCoach(steam64, team)) { + return team; + } + } + return MatchTeam_TeamNone; +} + stock int CountPlayersOnCSTeam(int team, int exclude = -1) { int count = 0; for (int i = 1; i <= MaxClients; i++) { @@ -294,10 +356,18 @@ public ArrayList GetTeamAuths(MatchTeam team) { return g_TeamAuths[team]; } +public ArrayList GetTeamCoaches(MatchTeam team) { + return g_TeamCoaches[team]; +} + public bool IsAuthOnTeam(const char[] auth, MatchTeam team) { return GetTeamAuths(team).FindString(auth) >= 0; } +public bool IsAuthOnTeamCoach(const char[] auth, MatchTeam team) { + return GetTeamCoaches(team).FindString(auth) >= 0; +} + public void SetStartingTeams() { int mapNumber = GetMapNumber(); if (mapNumber >= g_MapSides.Length || g_MapSides.Get(mapNumber) == SideChoice_KnifeRound) { @@ -354,6 +424,23 @@ public bool AddPlayerToTeam(const char[] auth, MatchTeam team, const char[] name } } +public bool AddCoachToTeam(const char[] auth, MatchTeam team, const char[] name) { + char steam64[AUTH_LENGTH]; + ConvertAuthToSteam64(auth, steam64); + + if (team == MatchTeam_TeamSpec) { + LogDebug("Not allowed to coach a spectator team."); + return false; + } + if (GetAuthMatchTeamCoach(steam64) == MatchTeam_TeamNone) { + GetTeamCoaches(team).PushString(steam64); + Get5_SetPlayerName(auth, name); + return true; + } else { + return false; + } +} + public bool RemovePlayerFromTeams(const char[] auth) { char steam64[AUTH_LENGTH]; ConvertAuthToSteam64(auth, steam64); diff --git a/scripting/include/get5.inc b/scripting/include/get5.inc index ac29820ea..ef68c1637 100644 --- a/scripting/include/get5.inc +++ b/scripting/include/get5.inc @@ -42,7 +42,7 @@ enum PauseType { PauseType_None, PauseType_Tech, // Technical pause PauseType_Tactical, // Tactical Pause - PauseType_Admin, // Admin/RCON Pause + PauseType_Admin, // Admin/RCON Pause PauseType_Backup // Special type for match pausing during backups. };