|
@@ -0,0 +1,240 @@
|
|
|
+/**
|
|
|
+ * [TF2] AFK TFBot
|
|
|
+ *
|
|
|
+ * Performs some dirty hacks so clients have an underlying TFBot AI that can be toggled.
|
|
|
+ */
|
|
|
+#pragma semicolon 1
|
|
|
+#include <sourcemod>
|
|
|
+
|
|
|
+#include <sdkhooks>
|
|
|
+#include <sdktools>
|
|
|
+#include <dhooks>
|
|
|
+
|
|
|
+#pragma newdecls required
|
|
|
+
|
|
|
+#include <stocksoup/color_literals>
|
|
|
+
|
|
|
+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<CTFPlayer>::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<CTFPlayer>::IsFakeClient()");
|
|
|
+
|
|
|
+ StartPrepSDKCall(SDKCall_Player);
|
|
|
+ PrepSDKCall_SetFromConf(hGameConf, SDKConf_Virtual, "NextBotPlayer<CTFPlayer>::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<CTFPlayer>::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);
|
|
|
+}
|