diff --git a/configs/get5/example_match.cfg b/configs/get5/example_match.cfg index d26168304..b4b538161 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 bd4c2df7e..bb1f7e9e4 100644 --- a/documentation/docs/match_schema.md +++ b/documentation/docs/match_schema.md @@ -11,6 +11,7 @@ There are quite a few values that are also optional within the team schema, but - `players`: A list of Steam ID's for users on the team (not used if `get5_check_auths` is set to `0`). You can also force player names in here; in JSON you may use either an array of steamids or a dictionary of Steam IDs to names. Both ways are shown in the above example. - `series_score`: The current score in the series, this can be used to give a team a map advantage or used as a manual 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 - `matchid`: A string matchid used to identify the match. @@ -20,6 +21,7 @@ There are quite a few values that are also optional within the team schema, but - `veto_first`: Either "team1", or "team2". If not set, or set to any other value, "team1" will veto first. - `side_type`: Either "standard", "never_knife", or "always_knife"; "standard" means the team that doesn't pick a map 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 7a7e1997b..1e4092763 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]; @@ -109,11 +110,11 @@ char g_FormattedTeamNames[MATCHTEAM_COUNT][MAX_CVAR_LENGTH]; char g_TeamFlags[MATCHTEAM_COUNT][MAX_CVAR_LENGTH]; char g_TeamLogos[MATCHTEAM_COUNT][MAX_CVAR_LENGTH]; char g_TeamMatchTexts[MATCHTEAM_COUNT][MAX_CVAR_LENGTH]; -char g_TeamCoaches[MATCHTEAM_COUNT][64]; 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; @@ -486,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(); diff --git a/scripting/get5/debug.sp b/scripting/get5/debug.sp index e2fac7bb1..e2914aabd 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,7 +138,7 @@ 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_TeamCoaches = %s", g_TeamCoaches[team]); + WriteArrayList(f, "g_TeamCoaches", g_TeamCoaches[team]); } } diff --git a/scripting/get5/matchconfig.sp b/scripting/get5/matchconfig.sp index 701b79fe5..138e08a82 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" @@ -27,7 +28,7 @@ stock bool LoadMatchConfig(const char[] config, bool restoreBackup = false) { g_TeamTechPausesUsed[team] = 0; g_TechPausedTimeOverride[team] = 0; g_TeamGivenTechPauseCommand[team] = false; - g_TeamCoaches[team] = ""; + ClearArray(GetTeamCoaches(team)); ClearArray(GetTeamAuths(team)); } @@ -270,6 +271,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); @@ -331,10 +333,13 @@ 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]); - if (!StrEqual(g_TeamCoaches[team], "")) { - kv.JumpToKey("coach", true); - kv.SetString(g_TeamCoaches[team], KEYVALUE_STRING_PLACEHOLDER); - kv.GoBack(); + 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); } } } @@ -344,6 +349,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); @@ -455,6 +461,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", @@ -575,16 +583,16 @@ 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 - char tmpCoach[AUTH_LENGTH]; + 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); json_object_get_string_safe(json, "logo", g_TeamLogos[matchTeam], MAX_CVAR_LENGTH); json_object_get_string_safe(json, "matchtext", g_TeamMatchTexts[matchTeam], MAX_CVAR_LENGTH); - // Since we can have any steam auth come in, we need to convert this as well. - json_object_get_string_safe(json, "coach", tmpCoach, AUTH_LENGTH); - ConvertAuthToSteam64(tmpCoach, g_TeamCoaches[matchTeam]); } else { JSON_Object fromfileJson = json_load_file(fromfile); if (fromfileJson == null) { @@ -607,21 +615,16 @@ static void LoadTeamData(KeyValues kv, MatchTeam matchTeam) { kv.GetString("fromfile", fromfile, sizeof(fromfile)); if (StrEqual(fromfile, "")) { - char tmpCoach[AUTH_LENGTH]; 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, ""); kv.GetString("logo", g_TeamLogos[matchTeam], MAX_CVAR_LENGTH, ""); kv.GetString("matchtext", g_TeamMatchTexts[matchTeam], MAX_CVAR_LENGTH, ""); - if (kv.JumpToKey("coach")) { - if(kv.GotoFirstSubKey(false)) { - kv.GetSectionName(tmpCoach, AUTH_LENGTH); - ConvertAuthToSteam64(tmpCoach, g_TeamCoaches[matchTeam]); - kv.GoBack(); - } - kv.GoBack(); - } } else { KeyValues fromfilekv = new KeyValues("team"); if (fromfilekv.ImportFromFile(fromfile)) { @@ -858,19 +861,19 @@ public Action Command_AddCoach(int client, int args) { return Plugin_Handled; } - if (!StrEqual(g_TeamCoaches[team], "")) { - ReplyToCommand(client, "There is already a coach on that team."); + if (GetTeamCoaches(team).Length == g_CoachesPerTeam) { + ReplyToCommand(client, "Coach Spots are full for team %s", teamString); return Plugin_Handled; } - ConvertAuthToSteam64(auth, steam64); + if(!ConvertAuthToSteam64(auth, steam64)) { + return Plugin_Handled; + } - if (AddPlayerToTeam(auth, team, name)) { - strcopy(g_TeamCoaches[team], AUTH_LENGTH, steam64); + if (AddCoachToTeam(auth, team, name)) { ReplyToCommand(client, "Successfully added player %s to coach team %s", auth, teamString); } else { - ReplyToCommand(client, "Player %s is already on a match team, setting them as coach.", auth); - strcopy(g_TeamCoaches[team], AUTH_LENGTH, steam64); + ReplyToCommand(client, "Player %s is already in a coaching position on a team.", auth); } } else { ReplyToCommand(client, "Usage: get5_addcoach [name]"); diff --git a/scripting/get5/teamlogic.sp b/scripting/get5/teamlogic.sp index 379adbdc3..6e2cddbe6 100644 --- a/scripting/get5/teamlogic.sp +++ b/scripting/get5/teamlogic.sp @@ -24,7 +24,7 @@ public void CheckClientTeam(int client) { } else if (GetAuth(client, auth, sizeof(auth))) { char steam64[AUTH_LENGTH]; ConvertAuthToSteam64(auth, steam64); - if (StrEqual(g_TeamCoaches[csTeam], steam64)) { + if (IsAuthOnTeamCoach(steam64, correctTeam)) { UpdateCoachTarget(client, csTeam); } } @@ -85,8 +85,8 @@ public Action Command_JoinTeam(int client, const char[] command, int argc) { if (!g_CoachingEnabledCvar.BoolValue) { KickClient(client, "%t", "TeamIsFullInfoMessage"); } else { - // Only attempt swapping if the coach slot is empty. - if (StrEqual(g_TeamCoaches[correctTeam], "")) { + // 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"); @@ -110,7 +110,7 @@ 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 && strcmp(clientAuth64, g_TeamCoaches[team]) == 0) { + if (g_CoachingEnabledCvar.BoolValue && IsAuthOnTeamCoach(clientAuth64, team)) { LogDebug("Forcing player %N to coach as they were previously.", client); MoveClientToCoach(client); return true; @@ -139,8 +139,13 @@ public void MoveClientToCoach(int client) { } char teamString[4]; + char clientAuth[64]; CSTeamString(csTeam, teamString, sizeof(teamString)); - GetAuth(client, g_TeamCoaches[matchTeam], AUTH_LENGTH); + 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. @@ -171,9 +176,8 @@ public Action Command_SmCoach(int client, int args) { GetAuth(client, auth, sizeof(auth)); MatchTeam matchTeam = GetClientMatchTeam(client); - // Don't allow a new coach if one is already selected. - LogDebug("Attempting to get auth %s to coach when coach is %s", auth, g_TeamCoaches[matchTeam]); - if (!StrEqual(g_TeamCoaches[matchTeam], auth)) { + // Don't allow a new coach if spots are full. + if (GetTeamCoaches(matchTeam).Length > g_CoachesPerTeam) { return Plugin_Stop; } @@ -263,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++) { @@ -334,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) { @@ -394,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);