1
0

tf_afkbot.sp 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. /**
  2. * [TF2] AFK TFBot
  3. *
  4. * Performs some dirty hacks so clients have an underlying TFBot AI that can be toggled.
  5. */
  6. #pragma semicolon 1
  7. #include <sourcemod>
  8. #include <sdkhooks>
  9. #include <sdktools>
  10. #include <dhooks>
  11. #pragma newdecls required
  12. #include <stocksoup/color_literals>
  13. Handle g_AllocatePlayerBotEntity;
  14. Handle g_SDKCallPEntityOfEntIndex, g_SDKCallNextBotFakeClient;
  15. Handle g_DHookProcessUsercmd, g_DHookPlayerIsBot, g_DHookNextBotFakeClient;
  16. static bool g_bInBotPhysicsSimulate[MAXPLAYERS + 1];
  17. static bool g_bIsIdleBot[MAXPLAYERS + 1];
  18. public void OnPluginStart() {
  19. Handle hGameConf = LoadGameConfigFile("tf2.afkbot");
  20. if (!hGameConf) {
  21. SetFailState("Failed to load gamedata (tf2.afkbot).");
  22. }
  23. StartPrepSDKCall(SDKCall_Static);
  24. PrepSDKCall_SetFromConf(hGameConf, SDKConf_Signature, "CTFBot::AllocatePlayerEntity()");
  25. PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain);
  26. PrepSDKCall_AddParameter(SDKType_String, SDKPass_Pointer);
  27. g_AllocatePlayerBotEntity = EndPrepSDKCall();
  28. if (!g_AllocatePlayerBotEntity) {
  29. SetFailState("Failed to create SDKCall for function CTFBot::AllocatePlayerEntity()");
  30. }
  31. StartPrepSDKCall(SDKCall_Static);
  32. PrepSDKCall_SetFromConf(hGameConf, SDKConf_Signature, "EDICT_NUM()");
  33. PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain);
  34. PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain);
  35. g_SDKCallPEntityOfEntIndex = EndPrepSDKCall();
  36. Handle dt_ClientPutInServer = DHookCreateFromConf(hGameConf, "ClientPutInServer()");
  37. DHookEnableDetour(dt_ClientPutInServer, false, OnClientPutInServerPre);
  38. Handle dt_BotUpdate = DHookCreateFromConf(hGameConf, "NextBotPlayer<CTFPlayer>::Update()");
  39. DHookEnableDetour(dt_BotUpdate, false, OnBotUpdate);
  40. Handle dt_BotEntPhysicsSimulate = DHookCreateFromConf(hGameConf, "CTFBot::PhysicsSimulate()");
  41. DHookEnableDetour(dt_BotEntPhysicsSimulate, false, OnBotEntPhysicsSimulate);
  42. DHookEnableDetour(dt_BotEntPhysicsSimulate, true, OnBotEntPhysicsSimulatePost);
  43. g_DHookProcessUsercmd = DHookCreateFromConf(hGameConf, "CBasePlayer::ProcessUsercmds()");
  44. g_DHookPlayerIsBot = DHookCreateFromConf(hGameConf, "CBasePlayer::IsBot()");
  45. g_DHookNextBotFakeClient = DHookCreateFromConf(hGameConf, "NextBotPlayer<CTFPlayer>::IsFakeClient()");
  46. StartPrepSDKCall(SDKCall_Player);
  47. PrepSDKCall_SetFromConf(hGameConf, SDKConf_Virtual, "NextBotPlayer<CTFPlayer>::IsFakeClient()");
  48. PrepSDKCall_SetReturnInfo(SDKType_Bool, SDKPass_Plain);
  49. g_SDKCallNextBotFakeClient = EndPrepSDKCall();
  50. delete hGameConf;
  51. RegConsoleCmd("sm_afkbot", ToggleAFKBot);
  52. }
  53. public void OnPluginEnd() {
  54. // re-enable client prediction
  55. for (int i = 1; i <= MaxClients; i++) {
  56. if (IsClientInGame(i) && !IsFakeClient(i)) {
  57. SetClientPrediction(i, true);
  58. }
  59. }
  60. }
  61. public Action ToggleAFKBot(int client, int argc) {
  62. if (!IsPlayerNextBot(client)) {
  63. ReplyToCommand(client, "No underlying bot logic available. Reconnecting...");
  64. ClientCommand(client, "retry");
  65. return Plugin_Handled;
  66. }
  67. g_bIsIdleBot[client] = !g_bIsIdleBot[client];
  68. SetClientPrediction(client, !g_bIsIdleBot[client]);
  69. PrintColoredChatEx(client, CHAT_SOURCE_SELF, COLOR_TEAM ... "Beep boop (%d).",
  70. g_bIsIdleBot[client]);
  71. return Plugin_Handled;
  72. }
  73. public void OnMapStart() {
  74. for (int i = 1; i <= MaxClients; i++) {
  75. if (IsClientInGame(i)) {
  76. OnClientPutInServer(i);
  77. }
  78. }
  79. }
  80. /**
  81. * Intercepts the default player entity allocation step and uses the TFBot allocator instead.
  82. */
  83. public MRESReturn OnClientPutInServerPre(Handle hParams) {
  84. int client = DHookGetParam(hParams, 1);
  85. char name[MAX_NAME_LENGTH];
  86. DHookGetParamString(hParams, 2, name, sizeof(name));
  87. AllocatePlayerBotEntity(client, name);
  88. // equivalent to CBasePlayer::SetPlayerName()
  89. SetEntPropString(client, Prop_Data, "m_szNetname", name);
  90. SetEntityFlags(client, GetEntityFlags(client) & ~FL_FAKECLIENT);
  91. PrintToServer("[afkbot] ocpis");
  92. return MRES_Supercede;
  93. }
  94. public void OnClientPutInServer(int client) {
  95. if (!IsFakeClient(client)) {
  96. DHookEntity(g_DHookProcessUsercmd, false, client, .callback = OnPlayerProcessUsercmds);
  97. DHookEntity(g_DHookPlayerIsBot, false, client, .callback = OnPlayerIsBot);
  98. DHookEntity(g_DHookNextBotFakeClient, false, client, .callback = OnPlayerIsFakeClient);
  99. g_bIsIdleBot[client] = false;
  100. }
  101. }
  102. /**
  103. * Disables NextBot updates on human players that aren't configured to be AFK.
  104. */
  105. public MRESReturn OnBotUpdate(int client) {
  106. if (!IsFakeClient(client) && !g_bIsIdleBot[client]) {
  107. return MRES_Supercede;
  108. }
  109. return MRES_Ignored;
  110. }
  111. /**
  112. * Scoped check to determine if CBasePlayer::ProcessUsercmds() is called from within the context
  113. * of a bot or if it is not.
  114. *
  115. * Because of the filthy, filthy hacks we're doing to make this work, both bot and player
  116. * usercmds are called, and without this check, the player gets updated twice as quickly
  117. * (having interesting side effects in that the player moves much faster).
  118. */
  119. public MRESReturn OnBotEntPhysicsSimulate(int client) {
  120. g_bInBotPhysicsSimulate[client] = true;
  121. return MRES_Ignored;
  122. }
  123. public MRESReturn OnBotEntPhysicsSimulatePost(int client) {
  124. g_bInBotPhysicsSimulate[client] = false;
  125. return MRES_Ignored;
  126. }
  127. /**
  128. * Only perform the usercommand processing for bots or not bots, depending on if the player is
  129. * being controlled by a bot or not.
  130. *
  131. * Dead players always have their usercommands processed so freezecams and respawns work.
  132. *
  133. * Precondition: OnClientPutInServer() only hooks this on human players.
  134. */
  135. public MRESReturn OnPlayerProcessUsercmds(int client, Handle hParams) {
  136. /**
  137. * drop inputs that occur inside / outside of CTFBot::PhysicsSimulate()
  138. * dependent on whether the player is idle or not
  139. */
  140. return (g_bInBotPhysicsSimulate[client] == g_bIsIdleBot[client]) || !IsPlayerAlive(client)?
  141. MRES_Ignored : MRES_Supercede;
  142. }
  143. static bool s_bGetTrueFakeClientResult;
  144. stock bool IsPlayerNextBot(int client) {
  145. s_bGetTrueFakeClientResult = true;
  146. bool result = SDKCall(g_SDKCallNextBotFakeClient, client);
  147. s_bGetTrueFakeClientResult = false;
  148. return result;
  149. }
  150. /**
  151. * Overrides the result of NextBotPlayer<CTFPlayer>::IsFakeClient(). This controls the result
  152. * of `tf_bot_count` and seemingly deals with loadouts.
  153. *
  154. * NextBot logic will only run when this returns `true`.
  155. * Loadout caching (?) will only occur when this returns `false`, though we can set it to true
  156. * afterwards without any known consequences.
  157. *
  158. * Precondition: OnClientPutInServer() only hooks this on human players.
  159. */
  160. public MRESReturn OnPlayerIsFakeClient(int client, Handle hReturn) {
  161. if (s_bGetTrueFakeClientResult) {
  162. return MRES_Ignored;
  163. }
  164. DHookSetReturn(hReturn, g_bIsIdleBot[client]);
  165. return MRES_Supercede;
  166. }
  167. /**
  168. * Overrides the result of CBasePlayer::IsBot().
  169. * This is checked when updating m_iPing during `CPlayerResource::UpdatePlayerData`.
  170. */
  171. public MRESReturn OnPlayerIsBot(int client, Handle hReturn) {
  172. DHookSetReturn(hReturn, false);
  173. return MRES_Supercede;
  174. }
  175. /**
  176. * Allocates a bot to the player entity.
  177. *
  178. * Uses a semi-filthy hack to get the edict_t* from an entity index, as sdktools/vdecoder.cpp
  179. * won't let us act on a freed edict (so we pass the edict_t pointer as POD instead).
  180. * A proper version would actually SDKCall on `server->PEntityOfEntIndex()` or reverse-engineer
  181. * the function, but ain't nobody got time for that.
  182. */
  183. void AllocatePlayerBotEntity(int client, const char[] name) {
  184. Address pEdict = SDKCall(g_SDKCallPEntityOfEntIndex, client);
  185. SDKCall(g_AllocatePlayerBotEntity, pEdict, name);
  186. }
  187. /**
  188. * Sets client-side prediction on a client.
  189. */
  190. void SetClientPrediction(int client, bool bPrediction) {
  191. // https://github.com/Pelipoika/TF2_Idlebot/blob/master/idlebot.sp
  192. FindConVar("sv_client_predict").ReplicateToClient(client, bPrediction? "-1" : "0");
  193. SetEntProp(client, Prop_Data, "m_bLagCompensation", bPrediction);
  194. SetEntProp(client, Prop_Data, "m_bPredictWeapons", bPrediction);
  195. }