6 Commits 6f98c30576 ... 9d987d9418

Author SHA1 Message Date
  nosoop 9d987d9418 Also suspend bot checks on Arena round win 5 years ago
  nosoop 561d03dec8 Disable name changes on bots 5 years ago
  nosoop cbcf24152e Suspend bot checks during a sudden death 5 years ago
  nosoop bd8ec8856d Correct counts for arena spectators 5 years ago
  nosoop ab05c537ab Allow full 31-character names 5 years ago
  nosoop 2907fb8dd7 Initial fork commit 5 years ago
1 changed files with 359 additions and 0 deletions
  1. 359 0
      scripting/botmanager.sp

+ 359 - 0
scripting/botmanager.sp

@@ -0,0 +1,359 @@
+#pragma semicolon 1
+
+#include <sourcemod>
+#include <sdktools>
+#include <tf2>
+#include <tf2_stocks>
+
+#define PLUGIN_VERSION			"1.3.3"
+
+public Plugin:myinfo = {
+	name		= "[TF2] Bot Manager",
+	author		= "Dr. McKay",
+	description	= "Allows for customization of TFBots",
+	version		= PLUGIN_VERSION,
+	url			= "http://www.doctormckay.com"
+};
+
+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)) {
+		new scout, soldier, pyro, demoman, heavy, engineer, medic, sniper, spy;
+		GetClassCounts(team, scout, soldier, pyro, demoman, heavy, engineer, medic, sniper, spy);
+		if(medic == 0) {
+			class = TFClass_Medic;
+		} else {
+			new least = scout;
+			class = TFClass_Scout;
+			if(soldier < least) {
+				least = soldier;
+				class = TFClass_Soldier;
+			}
+			if(pyro < least) {
+				least = pyro;
+				class = TFClass_Pyro;
+			}
+			if(demoman < least) {
+				least = demoman;
+				class = TFClass_DemoMan;
+			}
+			if(heavy < least) {
+				least = heavy;
+				class = TFClass_Heavy;
+			}
+			if(engineer < least) {
+				least = engineer;
+				class = TFClass_Engineer;
+			}
+			if(sniper < least) {
+				least = sniper;
+				class = TFClass_Sniper;
+			}
+			if(spy < least) {
+				least = spy;
+				class = TFClass_Spy;
+			}
+		}
+	}
+	
+	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, &scout, &soldier, &pyro, &demoman, &heavy, &engineer, &medic, &sniper, &spy) {
+	for(new i = 1; i <= MaxClients; i++) {
+		if(!IsClientInGame(i) || TFTeam:GetClientTeam(i) != team) {
+			continue;
+		}
+		switch(TF2_GetPlayerClass(i)) {
+			case TFClass_Scout: scout++;
+			case TFClass_Soldier: soldier++;
+			case TFClass_Pyro: pyro++;
+			case TFClass_DemoMan: demoman++;
+			case TFClass_Heavy: heavy++;
+			case TFClass_Engineer: engineer++;
+			case TFClass_Medic: medic++;
+			case TFClass_Sniper: sniper++;
+			case TFClass_Spy: spy++;
+		}
+	}
+}
+
+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;
+}