botmanager.sp 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. #pragma semicolon 1
  2. #include <sourcemod>
  3. #include <sdktools>
  4. #include <tf2>
  5. #include <tf2_stocks>
  6. #define PLUGIN_VERSION "1.2.1"
  7. public Plugin:myinfo = {
  8. name = "[TF2] Bot Manager",
  9. author = "Dr. McKay",
  10. description = "Allows for customization of TFBots",
  11. version = PLUGIN_VERSION,
  12. url = "http://www.doctormckay.com"
  13. };
  14. new Handle:cvarBotQuota;
  15. new Handle:cvarBotJoinAfterPlayer;
  16. new Handle:cvarGameLogic;
  17. new Handle:cvarSupportedMap;
  18. new Handle:cvarOnTeamsOnly;
  19. new Handle:tf_bot_quota;
  20. new Handle:joiningBots;
  21. new Handle:fwdBotAdd;
  22. new Handle:fwdBotKick;
  23. #define UPDATE_FILE "botmanager.txt"
  24. #define CONVAR_PREFIX "bot_manager"
  25. #include "mckayupdater.sp"
  26. public APLRes:AskPluginLoad2(Handle:myself, bool:late, String:error[], err_max) {
  27. decl String:game[64];
  28. GetGameFolderName(game, sizeof(game));
  29. if(!StrEqual(game, "tf")) {
  30. strcopy(error, err_max, "Bot Manager only works on Team Fortress 2");
  31. return APLRes_Failure;
  32. }
  33. RegPluginLibrary("botmanager");
  34. return APLRes_Success;
  35. }
  36. public OnPluginStart() {
  37. cvarBotQuota = CreateConVar("sm_bot_quota", "0", "Number of players to keep in the server");
  38. cvarBotJoinAfterPlayer = CreateConVar("sm_bot_join_after_player", "1", "If nonzero, bots wait until a player joins before entering the game.");
  39. cvarGameLogic = CreateConVar("sm_bot_game_logic", "1", "0 = use plugin logic when assigning bots, 1 = use game logic");
  40. cvarSupportedMap = CreateConVar("sm_bot_supported_map", "1", "If nonzero, bots will only be added on maps that have nav files");
  41. 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");
  42. tf_bot_quota = FindConVar("tf_bot_quota");
  43. HookEvent("player_connect_client", Event_PlayerConnect, EventHookMode_Pre);
  44. HookEvent("player_disconnect", Event_PlayerDisconnect, EventHookMode_Pre);
  45. HookEvent("player_team", Event_PlayerTeam, EventHookMode_Pre);
  46. joiningBots = CreateArray();
  47. fwdBotAdd = CreateGlobalForward("Bot_OnBotAdd", ET_Single, Param_CellByRef, Param_CellByRef, Param_CellByRef, Param_String);
  48. fwdBotKick = CreateGlobalForward("Bot_OnBotKick", ET_Single, Param_CellByRef);
  49. new Handle:buffer = FindConVar("tf_bot_quota_mode");
  50. SetConVarString(buffer, "normal");
  51. HookConVarChange(buffer, OnConVarChange);
  52. buffer = FindConVar("tf_bot_join_after_player");
  53. SetConVarInt(buffer, 0);
  54. HookConVarChange(buffer, OnConVarChange);
  55. }
  56. public OnConVarChange(Handle:convar, const String:oldValue[], const String:newValue[]) {
  57. decl String:name[64];
  58. GetConVarName(convar, name, sizeof(name));
  59. if(StrEqual(name, "tf_bot_quota_mode")) {
  60. LogMessage("tf_bot_quota_mode cannot be changed while Bot Manager is running. tf_bot_quota_mode set to \"normal\".");
  61. SetConVarString(convar, "normal");
  62. } else if(StrEqual(name, "tf_bot_join_after_player")) {
  63. 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.");
  64. SetConVarInt(convar, 0);
  65. }
  66. }
  67. public OnConfigsExecuted() {
  68. decl String:buffer[64];
  69. GetCurrentMap(buffer, sizeof(buffer));
  70. Format(buffer, sizeof(buffer), "maps/%s.nav", buffer);
  71. if(FileExists(buffer, true) || !GetConVarBool(cvarSupportedMap)) {
  72. CreateTimer(0.1, Timer_CheckBotNum, _, TIMER_REPEAT|TIMER_FLAG_NO_MAPCHANGE);
  73. } else {
  74. LogMessage("Bots are not supported on this map. Bot Manager disabled.");
  75. }
  76. }
  77. public OnMapEnd() {
  78. SetConVarInt(tf_bot_quota, 0); // Prevents an issue that happens at mapchange
  79. }
  80. public Action:Timer_CheckBotNum(Handle:timer) {
  81. new clients = GetValidClientCount();
  82. new actual = GetValidClientCount(false);
  83. new bots = GetBotCount();
  84. new realClients = clients - bots;
  85. if(realClients == 0 && GetConVarBool(cvarBotJoinAfterPlayer)) {
  86. if(bots > 0) {
  87. RemoveBot();
  88. }
  89. return;
  90. }
  91. if(GetConVarInt(cvarBotQuota) >= MaxClients) {
  92. LogMessage("sm_bot_quota cannot be greater than or equal to maxplayers. Setting sm_bot_quota to \"%d\".", MaxClients - 1);
  93. SetConVarInt(cvarBotQuota, MaxClients - 1);
  94. }
  95. if(clients < GetConVarInt(cvarBotQuota) && actual < (MaxClients - 1)) {
  96. AddBot();
  97. } else if(clients > GetConVarInt(cvarBotQuota) && bots > 0) {
  98. RemoveBot();
  99. }
  100. }
  101. GetValidClientCount(bool:excludeTeamsOnly = true) {
  102. new count = 0;
  103. for(new i = 1; i <= MaxClients; i++) {
  104. if(!IsClientInGame(i) || IsClientSourceTV(i) || IsClientReplay(i)) {
  105. continue;
  106. }
  107. if(excludeTeamsOnly && GetConVarBool(cvarOnTeamsOnly) && GetClientTeam(i) <= 1) {
  108. continue;
  109. }
  110. count++;
  111. }
  112. return count;
  113. }
  114. GetBotCount() {
  115. new count = 0;
  116. for(new i = 1; i <= MaxClients; i++) {
  117. if(IsClientInGame(i) && !IsClientSourceTV(i) && !IsClientReplay(i) && IsFakeClient(i)) {
  118. count++;
  119. }
  120. }
  121. return count;
  122. }
  123. AddBot() {
  124. new TFTeam:team = TFTeam_Unassigned;
  125. if(!GetConVarBool(cvarGameLogic)) {
  126. if(GetTeamClientCount(2) < GetTeamClientCount(3)) {
  127. team = TFTeam_Red;
  128. } else {
  129. team = TFTeam_Blue;
  130. }
  131. }
  132. new TFClassType:class = TFClass_Unknown;
  133. if(!GetConVarBool(cvarGameLogic)) {
  134. new scout, soldier, pyro, demoman, heavy, engineer, medic, sniper, spy;
  135. GetClassCounts(team, scout, soldier, pyro, demoman, heavy, engineer, medic, sniper, spy);
  136. if(medic == 0) {
  137. class = TFClass_Medic;
  138. } else {
  139. new least = scout;
  140. class = TFClass_Scout;
  141. if(soldier < least) {
  142. least = soldier;
  143. class = TFClass_Soldier;
  144. }
  145. if(pyro < least) {
  146. least = pyro;
  147. class = TFClass_Pyro;
  148. }
  149. if(demoman < least) {
  150. least = demoman;
  151. class = TFClass_DemoMan;
  152. }
  153. if(heavy < least) {
  154. least = heavy;
  155. class = TFClass_Heavy;
  156. }
  157. if(engineer < least) {
  158. least = engineer;
  159. class = TFClass_Engineer;
  160. }
  161. if(sniper < least) {
  162. least = sniper;
  163. class = TFClass_Sniper;
  164. }
  165. if(spy < least) {
  166. least = spy;
  167. class = TFClass_Spy;
  168. }
  169. }
  170. }
  171. new difficulty = -1;
  172. new String:name[MAX_NAME_LENGTH];
  173. Call_StartForward(fwdBotAdd);
  174. Call_PushCellRef(class);
  175. Call_PushCellRef(team);
  176. Call_PushCellRef(difficulty);
  177. Call_PushStringEx(name, sizeof(name), SM_PARAM_STRING_UTF8|SM_PARAM_STRING_COPY, SM_PARAM_COPYBACK);
  178. Call_Finish();
  179. decl String:strDifficulty[16], String:strTeam[16], String:strClass[16];
  180. switch(difficulty) {
  181. case 0: Format(strDifficulty, sizeof(strDifficulty), "easy");
  182. case 1: Format(strDifficulty, sizeof(strDifficulty), "normal");
  183. case 2: Format(strDifficulty, sizeof(strDifficulty), "hard");
  184. case 3: Format(strDifficulty, sizeof(strDifficulty), "expert");
  185. default: Format(strDifficulty, sizeof(strDifficulty), "");
  186. }
  187. switch(team) {
  188. case TFTeam_Red: Format(strTeam, sizeof(strTeam), "red");
  189. case TFTeam_Blue: Format(strTeam, sizeof(strTeam), "blue");
  190. default: Format(strTeam, sizeof(strTeam), "");
  191. }
  192. switch(class) {
  193. case TFClass_Scout: Format(strClass, sizeof(strClass), "Scout");
  194. case TFClass_Soldier: Format(strClass, sizeof(strClass), "Soldier");
  195. case TFClass_Pyro: Format(strClass, sizeof(strClass), "Pyro");
  196. case TFClass_DemoMan: Format(strClass, sizeof(strClass), "Demoman");
  197. case TFClass_Heavy: Format(strClass, sizeof(strClass), "HeavyWeapons");
  198. case TFClass_Engineer: Format(strClass, sizeof(strClass), "Engineer");
  199. case TFClass_Medic: Format(strClass, sizeof(strClass), "Medic");
  200. case TFClass_Sniper: Format(strClass, sizeof(strClass), "Sniper");
  201. case TFClass_Spy: Format(strClass, sizeof(strClass), "Spy");
  202. default: Format(strClass, sizeof(strClass), "");
  203. }
  204. ReplaceString(name, sizeof(name), "\"", "");
  205. if(strlen(name) > 0) {
  206. Format(name, sizeof(name), "\"%s\"", name);
  207. }
  208. ServerCommand("tf_bot_add %s %s %s %s", strDifficulty, strTeam, strClass, name); // count class team difficulty name (any order)
  209. }
  210. GetClassCounts(TFTeam:team, &scout, &soldier, &pyro, &demoman, &heavy, &engineer, &medic, &sniper, &spy) {
  211. for(new i = 1; i <= MaxClients; i++) {
  212. if(!IsClientInGame(i) || TFTeam:GetClientTeam(i) != team) {
  213. continue;
  214. }
  215. switch(TF2_GetPlayerClass(i)) {
  216. case TFClass_Scout: scout++;
  217. case TFClass_Soldier: soldier++;
  218. case TFClass_Pyro: pyro++;
  219. case TFClass_DemoMan: demoman++;
  220. case TFClass_Heavy: heavy++;
  221. case TFClass_Engineer: engineer++;
  222. case TFClass_Medic: medic++;
  223. case TFClass_Sniper: sniper++;
  224. case TFClass_Spy: spy++;
  225. }
  226. }
  227. }
  228. RemoveBot() {
  229. new teamToKick;
  230. if(GetTeamClientCount(2) > GetTeamClientCount(3)) {
  231. teamToKick = 2;
  232. } else if(GetTeamClientCount(2) < GetTeamClientCount(3)) {
  233. teamToKick = 3;
  234. } else {
  235. teamToKick = GetRandomInt(2, 3);
  236. }
  237. new Handle:bots = CreateArray();
  238. for(new i = 1; i <= MaxClients; i++) {
  239. if(IsClientConnected(i) && !IsClientSourceTV(i) && !IsClientReplay(i) && IsFakeClient(i) && GetClientTeam(i) == teamToKick) {
  240. PushArrayCell(bots, i);
  241. }
  242. }
  243. if(GetArraySize(bots) == 0) {
  244. CloseHandle(bots);
  245. return;
  246. }
  247. new bot = GetArrayCell(bots, GetRandomInt(0, GetArraySize(bots) - 1));
  248. CloseHandle(bots);
  249. Call_StartForward(fwdBotKick);
  250. Call_PushCellRef(bot);
  251. Call_Finish();
  252. ServerCommand("tf_bot_kick \"%N\"", bot);
  253. }
  254. public Event_PlayerConnect(Handle:event, const String:name[], bool:dontBroadcast) {
  255. if(GetEventBool(event, "bot")) {
  256. PushArrayCell(joiningBots, GetEventInt(event, "userid"));
  257. SetEventBroadcast(event, true);
  258. }
  259. }
  260. public Event_PlayerDisconnect(Handle:event, const String:name[], bool:dontBroadcast) {
  261. new client = GetClientOfUserId(GetEventInt(event, "userid"));
  262. if(client == 0) {
  263. return;
  264. }
  265. if(IsFakeClient(client)) {
  266. SetEventBroadcast(event, true);
  267. PrintToChatAll("\x01BOT \x07%06X%N \x01has left the game", GetTeamColor(GetClientTeam(client)), client);
  268. }
  269. }
  270. public Event_PlayerTeam(Handle:event, const String:name[], bool:dontBroadcast) {
  271. new client = GetClientOfUserId(GetEventInt(event, "userid"));
  272. if(client == 0) {
  273. return;
  274. }
  275. if(IsFakeClient(client)) {
  276. SetEventBroadcast(event, true);
  277. new pos;
  278. if((pos = FindValueInArray(joiningBots, GetClientUserId(client))) != -1) {
  279. RemoveFromArray(joiningBots, pos);
  280. PrintToServer("BOT %N has joined the game", client);
  281. PrintToChatAll("\x01BOT \x07%06X%N \x01has joined the game", GetTeamColor(GetEventInt(event, "team")), client);
  282. }
  283. }
  284. }
  285. GetTeamColor(team) {
  286. new value;
  287. switch(team) {
  288. case 1: {
  289. value = 0xCCCCCC;
  290. }
  291. case 2: {
  292. value = 0xFF4040;
  293. }
  294. case 3: {
  295. value = 0x99CCFF;
  296. }
  297. default: {
  298. value = 0x3EFF3E;
  299. }
  300. }
  301. return value;
  302. }