#pragma semicolon 1 #include #include #include #include #define PLUGIN_VERSION "1.3.4" 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" }; new Handle:cvarBotQuota; new Handle:cvarBotJoinAfterPlayer; new Handle:cvarGameLogic; new Handle:cvarSupportedMap; new Handle:cvarOnTeamsOnly; new Handle:tf_bot_quota; new Handle:joiningBots; new Handle:fwdBotAdd; new Handle:fwdBotKick; public APLRes:AskPluginLoad2(Handle:myself, bool:late, String:error[], err_max) { decl String: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 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 = CreateArray(); fwdBotAdd = CreateGlobalForward("Bot_OnBotAdd", ET_Single, Param_CellByRef, Param_CellByRef, Param_CellByRef, Param_String); fwdBotKick = CreateGlobalForward("Bot_OnBotKick", ET_Single, Param_CellByRef); new Handle:buffer = FindConVar("tf_bot_quota_mode"); SetConVarString(buffer, "normal"); HookConVarChange(buffer, OnConVarChange); buffer = FindConVar("tf_bot_join_after_player"); SetConVarInt(buffer, 0); HookConVarChange(buffer, OnConVarChange); } public OnConVarChange(Handle:convar, const String:oldValue[], const String:newValue[]) { decl String:name[64]; GetConVarName(convar, 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 OnConfigsExecuted() { decl String:buffer[64]; GetCurrentMap(buffer, sizeof(buffer)); Format(buffer, sizeof(buffer), "maps/%s.nav", buffer); if(FileExists(buffer, true) || !GetConVarBool(cvarSupportedMap)) { CreateTimer(0.1, Timer_CheckBotNum, _, TIMER_REPEAT|TIMER_FLAG_NO_MAPCHANGE); } else { LogMessage("Bots are not supported on this map. Bot Manager disabled."); } } public OnMapEnd() { SetConVarInt(tf_bot_quota, 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; } } } new clients = GetValidClientCount(); new actual = GetValidClientCount(false); new bots = GetBotCount(); new realClients = clients - bots; if(realClients == 0 && GetConVarBool(cvarBotJoinAfterPlayer)) { if(bots > 0) { RemoveBot(); } return Plugin_Continue; } if(GetConVarInt(cvarBotQuota) >= MaxClients) { LogMessage("sm_bot_quota cannot be greater than or equal to maxplayers. Setting sm_bot_quota to \"%d\".", MaxClients - 1); SetConVarInt(cvarBotQuota, MaxClients - 1); } if(clients < GetConVarInt(cvarBotQuota) && actual < (MaxClients - 1)) { AddBot(); } else if(clients > GetConVarInt(cvarBotQuota) && bots > 0) { RemoveBot(); } return Plugin_Continue; } GetValidClientCount(bool:excludeTeamsOnly = true) { new count = 0; for(new i = 1; i <= MaxClients; i++) { if(!IsClientInGame(i) || IsClientSourceTV(i) || IsClientReplay(i)) { continue; } // arena mode hacks if(excludeTeamsOnly && GetConVarBool(cvarOnTeamsOnly) && (GetClientTeam(i) <= 1 || (GameRules_GetProp("m_nGameType") == 4 && GetEntProp(i, Prop_Send, "m_bArenaSpectator")) )) { continue; } count++; } return count; } GetBotCount() { new count = 0; for(new i = 1; i <= MaxClients; i++) { if(IsClientInGame(i) && !IsClientSourceTV(i) && !IsClientReplay(i) && IsFakeClient(i)) { count++; } } return count; } AddBot() { new TFTeam:team = TFTeam_Unassigned; if(!GetConVarBool(cvarGameLogic)) { if(GetTeamClientCount(2) < GetTeamClientCount(3)) { team = TFTeam_Red; } else { team = TFTeam_Blue; } } new TFClassType:class = TFClass_Unknown; if(!GetConVarBool(cvarGameLogic)) { 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; } } new difficulty = -1; new String: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(); decl String:strDifficulty[16], String:strTeam[16], String:strClass[16]; switch(difficulty) { case 0: Format(strDifficulty, sizeof(strDifficulty), "easy"); case 1: Format(strDifficulty, sizeof(strDifficulty), "normal"); case 2: Format(strDifficulty, sizeof(strDifficulty), "hard"); case 3: Format(strDifficulty, sizeof(strDifficulty), "expert"); default: Format(strDifficulty, sizeof(strDifficulty), ""); } switch(team) { case TFTeam_Red: Format(strTeam, sizeof(strTeam), "red"); case TFTeam_Blue: Format(strTeam, sizeof(strTeam), "blue"); default: Format(strTeam, sizeof(strTeam), ""); } switch(class) { case TFClass_Scout: Format(strClass, sizeof(strClass), "Scout"); case TFClass_Soldier: Format(strClass, sizeof(strClass), "Soldier"); case TFClass_Pyro: Format(strClass, sizeof(strClass), "Pyro"); case TFClass_DemoMan: Format(strClass, sizeof(strClass), "Demoman"); case TFClass_Heavy: Format(strClass, sizeof(strClass), "HeavyWeapons"); case TFClass_Engineer: Format(strClass, sizeof(strClass), "Engineer"); case TFClass_Medic: Format(strClass, sizeof(strClass), "Medic"); case TFClass_Sniper: Format(strClass, sizeof(strClass), "Sniper"); case TFClass_Spy: Format(strClass, sizeof(strClass), "Spy"); default: Format(strClass, sizeof(strClass), ""); } 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) } GetClassCounts(TFTeam:team, int numClass[TFClassType]) { for(new i = 1; i <= MaxClients; i++) { if(!IsClientInGame(i) || TFTeam:GetClientTeam(i) != team) { continue; } numClass[ TF2_GetPlayerClass(i) ]++; } } RemoveBot() { new teamToKick; if(GetTeamClientCount(2) > GetTeamClientCount(3)) { teamToKick = 2; } else if(GetTeamClientCount(2) < GetTeamClientCount(3)) { teamToKick = 3; } else { teamToKick = GetRandomInt(2, 3); } new Handle:bots = CreateArray(); for(new i = 1; i <= MaxClients; i++) { if(IsClientConnected(i) && !IsClientSourceTV(i) && !IsClientReplay(i) && IsFakeClient(i) && GetClientTeam(i) == teamToKick) { PushArrayCell(bots, i); } } if(GetArraySize(bots) == 0) { CloseHandle(bots); return; } new bot = GetArrayCell(bots, GetRandomInt(0, GetArraySize(bots) - 1)); CloseHandle(bots); Call_StartForward(fwdBotKick); Call_PushCellRef(bot); Call_Finish(); ServerCommand("tf_bot_kick \"%N\"", bot); } public Event_PlayerConnect(Handle:event, const String:name[], bool:dontBroadcast) { if(GetEventBool(event, "bot")) { PushArrayCell(joiningBots, GetEventInt(event, "userid")); SetEventBroadcast(event, true); // more arena hacks ServerCommand("namelockid %d 1", GetEventInt(event, "userid")); } } public Event_PlayerDisconnect(Handle:event, const String:name[], bool:dontBroadcast) { new client = GetClientOfUserId(GetEventInt(event, "userid")); if(client == 0) { return; } if(IsFakeClient(client)) { SetEventBroadcast(event, true); PrintToChatAll("\x01BOT \x07%06X%N \x01has left the game", GetTeamColor(GetClientTeam(client)), client); } } public Event_PlayerTeam(Handle:event, const String:name[], bool:dontBroadcast) { new client = GetClientOfUserId(GetEventInt(event, "userid")); if(client == 0) { return; } if(IsFakeClient(client)) { SetEventBroadcast(event, true); new pos; if((pos = FindValueInArray(joiningBots, GetClientUserId(client))) != -1) { RemoveFromArray(joiningBots, pos); PrintToServer("BOT %N has joined the game", client); PrintToChatAll("\x01BOT \x07%06X%N \x01has joined the game", GetTeamColor(GetEventInt(event, "team")), client); } } } GetTeamColor(team) { new value; switch(team) { case 1: { value = 0xCCCCCC; } case 2: { value = 0xFF4040; } case 3: { value = 0x99CCFF; } default: { value = 0x3EFF3E; } } return value; }