Kaynağa Gözat

Initial plugin commit

This was a fun experiment.
nosoop 6 yıl önce
ebeveyn
işleme
4c914cdfbe
2 değiştirilmiş dosya ile 384 ekleme ve 0 silme
  1. 144 0
      gamedata/tf2.afkbot.txt
  2. 240 0
      scripting/tf_afkbot.sp

+ 144 - 0
gamedata/tf2.afkbot.txt

@@ -0,0 +1,144 @@
+"Games"
+{
+	"tf"
+	{
+		"Functions"
+		{
+			"ClientPutInServer()"
+			{
+				"signature"		"ClientPutInServer()"
+				"callconv"		"cdecl"
+				"return"		"void"
+				"arguments"
+				{
+					"edict"
+					{
+						"type"	"edict"
+					}
+					"name"
+					{
+						"type"	"charptr"
+					}
+				}
+			}
+			"CTFBot::PhysicsSimulate()"
+			{
+				"signature"		"CTFBot::PhysicsSimulate()"
+				"callconv"		"thiscall"
+				"return"		"void"
+				"this"			"entity"
+			}
+			"NextBotPlayer<CTFPlayer>::Update()"
+			{
+				"signature"		"NextBotPlayer<CTFPlayer>::Update()"
+				"callconv"		"thiscall"
+				"return"		"void"
+				"this"			"entity"
+			}
+			"NextBotPlayer<CTFPlayer>::IsFakeClient()"
+			{
+				"offset"		"NextBotPlayer<CTFPlayer>::IsFakeClient()"
+				"hooktype"		"entity"
+				"this"			"entity"
+				"return"		"bool"
+			}
+			"CBaseEntity::IsNetClient()"
+			{
+				"offset"		"CBaseEntity::IsNetClient()"
+				"hooktype"		"entity"
+				"this"			"entity"
+				"return"		"bool"
+			}
+			"CBasePlayer::IsBot()"
+			{
+				"offset"		"CBasePlayer::IsBot()"
+				"hooktype"		"entity"
+				"this"			"entity"
+				"return"		"bool"
+			}
+			"CBasePlayer::ProcessUsercmds()"
+			{
+				"offset"		"CBasePlayer::ProcessUsercmds()"
+				"hooktype"		"entity"
+				"this"			"entity"
+				"return"		"void"
+				"arguments"
+				{
+					"cmds"
+					{
+						"type"	"objectptr"
+					}
+					"num_cmds"
+					{
+						"type"	"int"
+					}
+					"total_cmds"
+					{
+						"type"	"int"
+					}
+					"num_dropped_packets"
+					{
+						"type"	"int"
+					}
+					"paused"
+					{
+						"type"	"bool"
+					}
+				}
+			}
+		}
+		
+		"Signatures"
+		{
+			"CTFBot::AllocatePlayerEntity()"
+			{
+				"library"		"server"
+				"linux"			"@_ZN6CTFBot20AllocatePlayerEntityEP7edict_tPKc"
+			}
+			"ClientPutInServer()"
+			{
+				"library"		"server"
+				"linux"			"@_Z17ClientPutInServerP7edict_tPKc"
+			}
+			"CTFBot::PhysicsSimulate()"
+			{
+				"library"		"server"
+				"linux"			"@_ZN6CTFBot15PhysicsSimulateEv"
+			}
+			"NextBotPlayer<CTFPlayer>::Update()"
+			{
+				"library"		"server"
+				"linux"			"@_ZN13NextBotPlayerI9CTFPlayerE6UpdateEv"
+			}
+			
+			"EDICT_NUM()"
+			{
+				"library"		"engine"
+				"linux"			"@_Z9EDICT_NUMi"
+			}
+		}
+		
+		"Offsets"
+		{
+			"CBasePlayer::ProcessUsercmds()"
+			{
+				"linux"			"422"
+			}
+			"CBaseEntity::IsNetClient()"
+			{
+				"windows"	"81"
+				"linux"		"82"
+			}
+			"CBasePlayer::IsBot()"
+			{
+				"windows"	"446"
+				"linux"		"447"
+			}
+			"NextBotPlayer<CTFPlayer>::IsFakeClient()"
+			{
+				"windows"	"340"
+				"linux"		"342"
+			}
+		}
+	}
+}

+ 240 - 0
scripting/tf_afkbot.sp

@@ -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);
+}