#pragma semicolon 1 #include #include #include #include #include #pragma newdecls required #define PLUGIN_VERSION "1.3.8" public Plugin myinfo = { name = "[TF2] Bot Manager", author = "nosoop (forked from Dr. McKay)", description = "Allows for customization of TFBots", version = PLUGIN_VERSION, url = "http://csrd.science/" }; ConVar cvarBotQuota; ConVar cvarBotJoinAfterPlayer; ConVar cvarGameLogic; ConVar cvarSupportedMap; ConVar cvarOnTeamsOnly; ConVar tf_bot_quota; ArrayList joiningBots; // userid Handle fwdBotAdd, fwdBotKick; public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) { char game[64]; GetGameFolderName(game, sizeof(game)); if(!StrEqual(game, "tf")) { strcopy(error, err_max, "Bot Manager only works on Team Fortress 2"); return APLRes_Failure; } RegPluginLibrary("botmanager"); return APLRes_Success; } public void OnPluginStart() { cvarBotQuota = CreateConVar("sm_bot_quota", "0", "Number of players to keep in the server"); cvarBotJoinAfterPlayer = CreateConVar("sm_bot_join_after_player", "1", "If nonzero, bots wait until a player joins before entering the game."); cvarGameLogic = CreateConVar("sm_bot_game_logic", "1", "0 = use plugin logic when assigning bots, 1 = use game logic"); cvarSupportedMap = CreateConVar("sm_bot_supported_map", "1", "If nonzero, bots will only be added on maps that have nav files"); cvarOnTeamsOnly = CreateConVar("sm_bot_on_team_only", "1", "If nonzero, players will only be considered \"in-game\" if they're on a team for " ... "purposes of determining the bot count"); tf_bot_quota = FindConVar("tf_bot_quota"); HookEvent("player_connect_client", Event_PlayerConnect, EventHookMode_Pre); HookEvent("player_disconnect", Event_PlayerDisconnect, EventHookMode_Pre); HookEvent("player_team", Event_PlayerTeam, EventHookMode_Pre); joiningBots = new ArrayList(); fwdBotAdd = CreateGlobalForward("Bot_OnBotAdd", ET_Single, Param_CellByRef, Param_CellByRef, Param_CellByRef, Param_String); fwdBotKick = CreateGlobalForward("Bot_OnBotKick", ET_Single, Param_CellByRef); ConVar cvar = FindConVar("tf_bot_quota_mode"); cvar.SetString("normal"); cvar.AddChangeHook(OnConVarChange); cvar = FindConVar("tf_bot_join_after_player"); cvar.IntValue = 0; cvar.AddChangeHook(OnConVarChange); } public void OnConVarChange(ConVar convar, const char[] oldValue, const char[] newValue) { char name[64]; convar.GetName(name, sizeof(name)); if (StrEqual(name, "tf_bot_quota_mode")) { LogMessage("tf_bot_quota_mode cannot be changed while Bot Manager is running. tf_bot_quota_mode set to \"normal\"."); SetConVarString(convar, "normal"); } else if (StrEqual(name, "tf_bot_join_after_player")) { LogMessage("tf_bot_join_after_player cannot be changed while Bot Manager is running. tf_bot_join_after_player set to \"0\". Use sm_bot_join_after_player for similar functionality."); SetConVarInt(convar, 0); } } public void OnConfigsExecuted() { char buffer[64]; GetCurrentMap(buffer, sizeof(buffer)); Format(buffer, sizeof(buffer), "maps/%s.nav", buffer); if (FileExists(buffer, true) || !cvarSupportedMap.BoolValue) { CreateTimer(0.1, Timer_CheckBotNum, _, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); } else { LogMessage("Bots are not supported on this map. Bot Manager disabled."); } } public void OnMapEnd() { tf_bot_quota.IntValue = 0; // Prevents an issue that happens at mapchange } public Action Timer_CheckBotNum(Handle timer) { if (GameRules_GetProp("m_nGameType") == 4) { // suspend bot checks during an active arena round switch (GameRules_GetRoundState()) { case RoundState_Stalemate, RoundState_TeamWin: { return Plugin_Continue; } } } int clients = GetValidClientCount(); int actual = GetValidClientCount(false); int bots = GetBotCount(); int realClients = clients - bots; if (realClients == 0 && cvarBotJoinAfterPlayer.BoolValue) { if(bots > 0) { RemoveBot(); } return Plugin_Continue; } if (cvarBotQuota.IntValue >= MaxClients) { LogMessage("sm_bot_quota cannot be greater than or equal to maxplayers. Setting sm_bot_quota to \"%d\".", MaxClients - 1); cvarBotQuota.IntValue = MaxClients - 1; } if (clients < cvarBotQuota.IntValue && actual < (MaxClients - 1)) { AddBot(); } else if (clients > cvarBotQuota.IntValue && bots > 0) { RemoveBot(); } return Plugin_Continue; } int GetValidClientCount(bool excludeTeamsOnly = true) { int count = 0; for (int i = 1; i <= MaxClients; i++) { if (!IsClientInGame(i) || IsClientSourceTV(i) || IsClientReplay(i)) { continue; } // arena mode hacks if (excludeTeamsOnly && cvarOnTeamsOnly.BoolValue && (GetClientTeam(i) <= 1 || (GameRules_GetProp("m_nGameType") == 4 && GetEntProp(i, Prop_Send, "m_bArenaSpectator")) )) { continue; } count++; } return count; } int GetBotCount() { int count = 0; for (int i = 1; i <= MaxClients; i++) { if (IsClientInGame(i) && !IsClientSourceTV(i) && !IsClientReplay(i) && IsFakeClient(i)) { count++; } } return count; } void AddBot() { TFTeam team = TFTeam_Unassigned; if (!cvarGameLogic.BoolValue) { if (GetTeamClientCount(2) < GetTeamClientCount(3)) { team = TFTeam_Red; } else { team = TFTeam_Blue; } } TFClassType class = TFClass_Unknown; if (!cvarGameLogic.BoolValue) { int numClass[TFClassType]; GetClassCounts(team, numClass); if(!numClass[TFClass_Medic]) { class = TFClass_Medic; } else { static TFClassType iter[] = { TFClass_Scout, TFClass_Soldier, TFClass_Pyro, TFClass_DemoMan, TFClass_Heavy, TFClass_Engineer, TFClass_Sniper, TFClass_Spy }; TFClassType lowest = TFClass_Scout; for (int i = 1; i < sizeof(iter); i++) { if (numClass[ iter[i] ] < numClass[lowest]) { lowest = iter[i]; } } class = lowest; } } int difficulty = -1; char name[MAX_NAME_LENGTH]; Call_StartForward(fwdBotAdd); Call_PushCellRef(class); Call_PushCellRef(team); Call_PushCellRef(difficulty); Call_PushStringEx(name, sizeof(name), SM_PARAM_STRING_UTF8|SM_PARAM_STRING_COPY, SM_PARAM_COPYBACK); Call_Finish(); char strDifficulty[16], strTeam[16], strClass[16]; switch (difficulty) { case 0: strDifficulty = "easy"; case 1: strDifficulty = "normal"; case 2: strDifficulty = "hard"; case 3: strDifficulty = "expert"; } switch (team) { case TFTeam_Red: strTeam = "red"; case TFTeam_Blue: strTeam = "blue"; } switch (class) { case TFClass_Scout: strClass = "Scout"; case TFClass_Soldier: strClass = "Soldier"; case TFClass_Pyro: strClass = "Pyro"; case TFClass_DemoMan: strClass = "Demoman"; case TFClass_Heavy: strClass = "HeavyWeapons"; case TFClass_Engineer: strClass = "Engineer"; case TFClass_Medic: strClass = "Medic"; case TFClass_Sniper: strClass = "Sniper"; case TFClass_Spy: strClass = "Spy"; } char quotedName[MAX_NAME_LENGTH + 2]; ReplaceString(name, sizeof(name), "\"", ""); if (strlen(name)) { Format(quotedName, sizeof(quotedName), "\"%s\"", name); } ServerCommand("tf_bot_add %s %s %s %s", strDifficulty, strTeam, strClass, quotedName); // count class team difficulty name (any order) } void GetClassCounts(TFTeam team, int numClass[TFClassType]) { for (int i = 1; i <= MaxClients; i++) { if (!IsClientInGame(i) || TF2_GetClientTeam(i) != team) { continue; } numClass[ TF2_GetPlayerClass(i) ]++; } } void RemoveBot() { int teamToKick; if (GetTeamClientCount(2) > GetTeamClientCount(3)) { teamToKick = 2; } else if (GetTeamClientCount(2) < GetTeamClientCount(3)) { teamToKick = 3; } else { teamToKick = GetRandomInt(2, 3); } int botKickCandidates[MAXPLAYERS + 1], numBots; for (int i = 1; i <= MaxClients; i++) { if (IsClientConnected(i) && !IsClientSourceTV(i) && !IsClientReplay(i) && IsFakeClient(i) && GetClientTeam(i) == teamToKick) { botKickCandidates[numBots++] = i; } } if (!numBots) { return; } int bot = botKickCandidates[ GetRandomInt(0, numBots - 1) ]; Call_StartForward(fwdBotKick); Call_PushCellRef(bot); Call_Finish(); ServerCommand("tf_bot_kick \"%N\"", bot); } public Action Event_PlayerConnect(Event event, const char[] name, bool dontBroadcast) { if (event.GetBool("bot")) { joiningBots.Push(event.GetInt("userid")); event.BroadcastDisabled = true; // more arena hacks ServerCommand("namelockid %d 1", event.GetInt("userid")); } return Plugin_Continue; } public Action Event_PlayerDisconnect(Event event, const char[] name, bool dontBroadcast) { int client = GetClientOfUserId(event.GetInt("userid")); if (!client) { return Plugin_Continue; } if (IsFakeClient(client)) { event.BroadcastDisabled = true; char botDisplay[128]; Format(botDisplay, sizeof(botDisplay), "\x07%06X%N\x01", GetTeamColor(GetClientTeam(client)), client); PrintValveTranslationToAll(Destination_Chat, "game_player_left_game", botDisplay, "TF_Scoreboard_Bot"); } return Plugin_Continue; } public Action Event_PlayerTeam(Event event, const char[] name, bool dontBroadcast) { int client = GetClientOfUserId(event.GetInt("userid")); if (!client) { return Plugin_Continue; } if (IsFakeClient(client)) { event.BroadcastDisabled = true; int pos; if ((pos = joiningBots.FindValue(GetClientUserId(client))) != -1) { joiningBots.Erase(pos); PrintToServer("%N (BOT) has joined the game", client); char botDisplay[128]; Format(botDisplay, sizeof(botDisplay), "\x07%06X%N\x01", GetTeamColor(event.GetInt("team")), client); PrintValveTranslationToAll(Destination_Chat, "game_player_joined_game", botDisplay); } } return Plugin_Continue; } int GetTeamColor(int team) { switch(team) { case 1: { return 0xCCCCCC; } case 2: { return 0xFF4040; } case 3: { return 0x99CCFF; } } return 0x3EFF3E; }