botmanager.sp 10.0 KB

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