/** * [TF2] AFK TFBot * * Performs some dirty hacks so clients have an underlying TFBot AI that can be toggled. */ #pragma semicolon 1 #include #include #include #include #pragma newdecls required #include #define PLUGIN_VERSION "1.0.0" public Plugin myinfo = { name = "[TF2] TFAFKBot", author = "nosoop", description = "TFBots conjoined to human players.", version = PLUGIN_VERSION, url = "localhost" } Handle g_AllocatePlayerBotEntity; Handle g_SDKCallPEntityOfEntIndex, g_SDKCallNextBotFakeClient; Handle g_DHookProcessUsercmd, g_DHookPlayerIsBot, g_DHookNextBotFakeClient; static bool g_bInBotPhysicsSimulate[MAXPLAYERS + 1]; static bool g_bIsIdleBot[MAXPLAYERS + 1]; public void OnPluginStart() { Handle hGameConf = LoadGameConfigFile("tf2.afkbot"); if (!hGameConf) { SetFailState("Failed to load gamedata (tf2.afkbot)."); } StartPrepSDKCall(SDKCall_Static); PrepSDKCall_SetFromConf(hGameConf, SDKConf_Signature, "CTFBot::AllocatePlayerEntity()"); PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); PrepSDKCall_AddParameter(SDKType_String, SDKPass_Pointer); g_AllocatePlayerBotEntity = EndPrepSDKCall(); if (!g_AllocatePlayerBotEntity) { SetFailState("Failed to create SDKCall for function CTFBot::AllocatePlayerEntity()"); } StartPrepSDKCall(SDKCall_Static); PrepSDKCall_SetFromConf(hGameConf, SDKConf_Signature, "EDICT_NUM()"); PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain); g_SDKCallPEntityOfEntIndex = EndPrepSDKCall(); Handle dt_ClientPutInServer = DHookCreateFromConf(hGameConf, "ClientPutInServer()"); DHookEnableDetour(dt_ClientPutInServer, false, OnClientPutInServerPre); Handle dt_BotUpdate = DHookCreateFromConf(hGameConf, "NextBotPlayer::Update()"); DHookEnableDetour(dt_BotUpdate, false, OnBotUpdate); Handle dt_BotEntPhysicsSimulate = DHookCreateFromConf(hGameConf, "CTFBot::PhysicsSimulate()"); DHookEnableDetour(dt_BotEntPhysicsSimulate, false, OnBotEntPhysicsSimulate); DHookEnableDetour(dt_BotEntPhysicsSimulate, true, OnBotEntPhysicsSimulatePost); g_DHookProcessUsercmd = DHookCreateFromConf(hGameConf, "CBasePlayer::ProcessUsercmds()"); g_DHookPlayerIsBot = DHookCreateFromConf(hGameConf, "CBasePlayer::IsBot()"); g_DHookNextBotFakeClient = DHookCreateFromConf(hGameConf, "NextBotPlayer::IsFakeClient()"); StartPrepSDKCall(SDKCall_Player); PrepSDKCall_SetFromConf(hGameConf, SDKConf_Virtual, "NextBotPlayer::IsFakeClient()"); PrepSDKCall_SetReturnInfo(SDKType_Bool, SDKPass_Plain); g_SDKCallNextBotFakeClient = EndPrepSDKCall(); delete hGameConf; RegConsoleCmd("sm_afkbot", ToggleAFKBot); } public void OnPluginEnd() { // re-enable client prediction for (int i = 1; i <= MaxClients; i++) { if (IsClientInGame(i) && !IsFakeClient(i)) { SetClientPrediction(i, true); } } } public Action ToggleAFKBot(int client, int argc) { if (!IsPlayerNextBot(client)) { ReplyToCommand(client, "No underlying bot logic available. Reconnecting..."); ClientCommand(client, "retry"); return Plugin_Handled; } g_bIsIdleBot[client] = !g_bIsIdleBot[client]; SetClientPrediction(client, !g_bIsIdleBot[client]); PrintColoredChatEx(client, CHAT_SOURCE_SELF, COLOR_TEAM ... "Beep boop (%d).", g_bIsIdleBot[client]); return Plugin_Handled; } public void OnMapStart() { for (int i = 1; i <= MaxClients; i++) { if (IsClientInGame(i)) { OnClientPutInServer(i); } } } /** * Intercepts the default player entity allocation step and uses the TFBot allocator instead. */ public MRESReturn OnClientPutInServerPre(Handle hParams) { int client = DHookGetParam(hParams, 1); char name[MAX_NAME_LENGTH]; DHookGetParamString(hParams, 2, name, sizeof(name)); AllocatePlayerBotEntity(client, name); // equivalent to CBasePlayer::SetPlayerName() SetEntPropString(client, Prop_Data, "m_szNetname", name); SetEntityFlags(client, GetEntityFlags(client) & ~FL_FAKECLIENT); PrintToServer("[afkbot] ocpis"); return MRES_Supercede; } public void OnClientPutInServer(int client) { if (!IsFakeClient(client)) { DHookEntity(g_DHookProcessUsercmd, false, client, .callback = OnPlayerProcessUsercmds); DHookEntity(g_DHookPlayerIsBot, false, client, .callback = OnPlayerIsBot); DHookEntity(g_DHookNextBotFakeClient, false, client, .callback = OnPlayerIsFakeClient); g_bIsIdleBot[client] = false; } } /** * Disables NextBot updates on human players that aren't configured to be AFK. */ public MRESReturn OnBotUpdate(int client) { if (!IsFakeClient(client) && !g_bIsIdleBot[client]) { return MRES_Supercede; } return MRES_Ignored; } /** * Scoped check to determine if CBasePlayer::ProcessUsercmds() is called from within the context * of a bot or if it is not. * * Because of the filthy, filthy hacks we're doing to make this work, both bot and player * usercmds are called, and without this check, the player gets updated twice as quickly * (having interesting side effects in that the player moves much faster). */ public MRESReturn OnBotEntPhysicsSimulate(int client) { g_bInBotPhysicsSimulate[client] = true; return MRES_Ignored; } public MRESReturn OnBotEntPhysicsSimulatePost(int client) { g_bInBotPhysicsSimulate[client] = false; return MRES_Ignored; } /** * Only perform the usercommand processing for bots or not bots, depending on if the player is * being controlled by a bot or not. * * Dead players always have their usercommands processed so freezecams and respawns work. * * Precondition: OnClientPutInServer() only hooks this on human players. */ public MRESReturn OnPlayerProcessUsercmds(int client, Handle hParams) { /** * drop inputs that occur inside / outside of CTFBot::PhysicsSimulate() * dependent on whether the player is idle or not */ return (g_bInBotPhysicsSimulate[client] == g_bIsIdleBot[client]) || !IsPlayerAlive(client)? MRES_Ignored : MRES_Supercede; } static bool s_bGetTrueFakeClientResult; stock bool IsPlayerNextBot(int client) { s_bGetTrueFakeClientResult = true; bool result = SDKCall(g_SDKCallNextBotFakeClient, client); s_bGetTrueFakeClientResult = false; return result; } /** * Overrides the result of NextBotPlayer::IsFakeClient(). This controls the result * of `tf_bot_count` and seemingly deals with loadouts. * * NextBot logic will only run when this returns `true`. * Loadout caching (?) will only occur when this returns `false`, though we can set it to true * afterwards without any known consequences. * * Precondition: OnClientPutInServer() only hooks this on human players. */ public MRESReturn OnPlayerIsFakeClient(int client, Handle hReturn) { if (s_bGetTrueFakeClientResult) { return MRES_Ignored; } DHookSetReturn(hReturn, g_bIsIdleBot[client]); return MRES_Supercede; } /** * Overrides the result of CBasePlayer::IsBot(). * This is checked when updating m_iPing during `CPlayerResource::UpdatePlayerData`. */ public MRESReturn OnPlayerIsBot(int client, Handle hReturn) { DHookSetReturn(hReturn, false); return MRES_Supercede; } /** * Allocates a bot to the player entity. * * Uses a semi-filthy hack to get the edict_t* from an entity index, as sdktools/vdecoder.cpp * won't let us act on a freed edict (so we pass the edict_t pointer as POD instead). * A proper version would actually SDKCall on `server->PEntityOfEntIndex()` or reverse-engineer * the function, but ain't nobody got time for that. */ void AllocatePlayerBotEntity(int client, const char[] name) { Address pEdict = SDKCall(g_SDKCallPEntityOfEntIndex, client); SDKCall(g_AllocatePlayerBotEntity, pEdict, name); } /** * Sets client-side prediction on a client. */ void SetClientPrediction(int client, bool bPrediction) { // https://github.com/Pelipoika/TF2_Idlebot/blob/master/idlebot.sp FindConVar("sv_client_predict").ReplicateToClient(client, bPrediction? "-1" : "0"); SetEntProp(client, Prop_Data, "m_bLagCompensation", bPrediction); SetEntProp(client, Prop_Data, "m_bPredictWeapons", bPrediction); }