BOT 智能行为插件解析

作者: un1 powered by Gemini
一个为CS:GO新手玩家编写的插件代码详解

写给新手的特别提示:

如果你只会基础编译,那这份文档就是为你准备的!这里不会有太多深奥的废话,我们会用最简洁的方式告诉你:这个 SourcePawn 插件的代码长什么样,以及它到底做了什么神奇的事情,让你的服务器里的 BOT 变得更聪明、更像真人!

前一份文档我们介绍了 BOT 的皮肤,这份文档则深入到 BOT 的行为逻辑。它会改善 BOT 的买枪策略、投掷物使用、走位甚至瞄准方式,让它们在游戏中表现得更出色,甚至能模仿职业哥的打法!

一、插件概览:它是什么?

这个名为 "BOT Improvement" (BOT 改进) 的 SourcePawn 插件旨在大幅度提升 CS:GO 服务器中 BOT(机器人)的智能行为和拟真度。它不仅仅是给 BOT 换身好看的衣服,更深入地修改了 BOT 的内置 AI 逻辑,包括:

如果你的服务器经常和 BOT 玩耍,或者你想要你的 BOT 不再是“傻大个”,那么这个插件将带来质的飞跃。

二、代码构成详解

2.1 引入头文件(#include):插件的“高级工具箱”

与上一个插件类似,这里也引入了许多现成的代码模块,但这次引入了更多专门用于底层游戏交互和 AI 操控的库。

点击展开代码
#pragma semicolon 1

#include <sourcemod>
#include <sdkhooks>
#include <sdktools>
#include <cstrike>
#include <eItems>
#include <smlib>
#include <navmesh>
#include <dhooks>
#include <botmimic>
#include <PTaH>
#include <ripext>

2.2 全局变量(g_):插件的“记忆中枢”

这个插件的全局变量非常多,因为要存储和追踪大量关于 BOT 行为状态、游戏状态和特定属性偏移量的数据。为了更好地理解,我们将其分为几类:

点击展开代码
char g_szMap[128]; // 当前地图名称
char g_szPlayerNames[5][MAX_NAME_LENGTH]; // 用于存储团队指令中的玩家名字
char g_szCrosshairCode[MAXPLAYERS+1][35], g_szPreviousBuy[MAXPLAYERS+1][128]; // 准星代码,上次购买的武器名
bool g_bIsBombScenario, g_bIsHostageScenario, g_bFreezetimeEnd, g_bBombPlanted, g_bEveryoneDead, g_bHalftimeSwitch, g_bIsCompetitive; // 游戏场景标志
bool g_bUseCZ75[MAXPLAYERS+1], g_bUseUSP[MAXPLAYERS+1], g_bUseM4A1S[MAXPLAYERS+1], g_bDontSwitch[MAXPLAYERS+1], g_bDropWeapon[MAXPLAYERS+1], g_bHasGottenDrop[MAXPLAYERS+1]; // BOT武器选择和掉枪状态
bool g_bIsProBot[MAXPLAYERS+1], g_bThrowGrenade[MAXPLAYERS+1], g_bUncrouch[MAXPLAYERS+1]; // Pro Bot 标志,投掷物状态,取消蹲伏状态

int g_iProfileRank[MAXPLAYERS+1], g_iPlayerColor[MAXPLAYERS+1], g_iTarget[MAXPLAYERS+1], g_iPrevTarget[MAXPLAYERS+1], g_iDoingSmokeNum[MAXPLAYERS+1], g_iActiveWeapon[MAXPLAYERS+1]; // BOT个人资料排位,玩家颜色,当前目标,上次目标,烟雾弹编号,当前武器
int g_iCurrentRound, g_iRoundsPlayed, g_iCTScore, g_iTScore, g_iMaxNades; // 回合信息,比分,最大投掷物数量
int g_iProfileRankOffset, g_iPlayerColorOffset; // 玩家资源类中的属性偏移量
int g_iBotTargetSpotOffset, g_iBotNearbyEnemiesOffset, g_iFireWeaponOffset, g_iEnemyVisibleOffset, g_iBotProfileOffset, g_iBotSafeTimeOffset, g_iBotEnemyOffset, g_iBotLookAtSpotStateOffset, g_iBotMoraleOffset, g_iBotTaskOffset, g_iBotDispositionOffset; // BOT内部C++对象属性的偏移量(非常关键)
float g_fBotOrigin[MAXPLAYERS+1][3], g_fTargetPos[MAXPLAYERS+1][3], g_fNadeTarget[MAXPLAYERS+1][3]; // BOT坐标,目标坐标,投掷物目标坐标
float g_fRoundStart, g_fFreezeTimeEnd; // 回合开始时间,冻结时间结束时间
float g_fLookAngleMaxAccel[MAXPLAYERS+1], g_fReactionTime[MAXPLAYERS+1], g_fAggression[MAXPLAYERS+1], g_fShootTimestamp[MAXPLAYERS+1], g_fThrowNadeTimestamp[MAXPLAYERS+1], g_fCrouchTimestamp[MAXPLAYERS+1]; // BOT瞄准速度,反应时间,攻击性,射击时间戳,投掷物时间戳,蹲伏时间戳
ConVar g_cvBotEcoLimit; // 经济限制控制台变量

Handle g_hBotMoveTo; // BOT移动函数句柄
Handle g_hLookupBone; // 查找骨骼函数句柄
Handle g_hGetBonePosition; // 获取骨骼位置函数句柄
Handle g_hBotIsVisible; // BOT是否可见函数句柄
Handle g_hBotIsHiding; // BOT是否在藏身函数句柄
Handle g_hBotEquipBestWeapon; // BOT装备最佳武器函数句柄
Handle g_hBotSetLookAt; // BOT看向某点函数句柄
Handle g_hSetCrosshairCode; // 设置准星代码函数句柄
Handle g_hSwitchWeaponCall; // 切换武器函数句柄
Handle g_hIsLineBlockedBySmoke; // 线是否被烟雾阻挡函数句柄
Handle g_hBotBendLineOfSight; // BOT弯曲视线函数句柄
Handle g_hBotThrowGrenade; // BOT投掷手雷函数句柄
Handle g_hAddMoney; // 添加金钱函数句柄
Address g_pTheBots; // BOT管理器全局地址

CNavArea g_pCurrArea[MAXPLAYERS+1]; // BOT当前导航区域

//BOT Nades Variables (BOT投掷物数据)
float g_fNadePos[128][3], g_fNadeLook[128][3]; // 投掷物位置,看向点
int g_iNadeDefIndex[128]; // 投掷物DefIndex
char g_szReplay[128][128]; // 录制好的投掷动作回放文件路径
float g_fNadeTimestamp[128]; // 投掷物冷却时间戳
int g_iNadeTeam[128]; // 投掷物所属队伍

static char g_szBoneNames[][] =  { /* 骨骼名称列表 */ }; // 用于通用骨骼查找

enum RouteType { /* 导航路径类型 */ }
enum PriorityType { /* 优先级类型 */ }
enum LookAtSpotState { /* BOT看向点状态 */ }
enum GrenadeTossState { /* 投掷手雷状态 */ }
enum TaskType { /* BOT任务类型 */ }
enum DispositionType { /* BOT行为倾向类型 */ }
enum GamePhase { /* 游戏阶段 */ }

2.3 枚举(enum):定义行为的“标签”

枚举是一系列有名字的常量,让代码更易读。在这里,它们定义了 BOT 可能的各种行为模式、状态和游戏阶段。

点击展开代码
enum RouteType // 导航路径类型
{
	DEFAULT_ROUTE = 0, // 默认路径
	FASTEST_ROUTE,     // 最快路径
	SAFEST_ROUTE,      // 最安全路径
	RETREAT_ROUTE      // 撤退路径
}

enum PriorityType // 任务优先级
{
	PRIORITY_LOWEST = -1,
	PRIORITY_LOW, 
	PRIORITY_MEDIUM, 
	PRIORITY_HIGH, 
	PRIORITY_UNINTERRUPTABLE // 不可中断
}

enum LookAtSpotState // BOT看向某个点的状态
{
	NOT_LOOKING_AT_SPOT, // 没看
	LOOK_TOWARDS_SPOT,   // 正在看过去
	LOOK_AT_SPOT,        // 正在看
	NUM_LOOK_AT_SPOT_STATES
}

enum GrenadeTossState // 投掷手雷状态
{
	NOT_THROWING,    // 未投掷
	START_THROW,     // 开始投掷
	THROW_LINED_UP,  // 瞄准完毕
	FINISH_THROW     // 投掷完成
}

enum TaskType // BOT当前执行的任务类型
{
	SEEK_AND_DESTROY, // 搜寻并摧毁
	PLANT_BOMB,       // 安放炸弹
	FIND_TICKING_BOMB,// 找到正在计时炸弹
	DEFUSE_BOMB,      // 拆除炸弹
	// ... 更多任务类型,如守点、跟随、营救人质等
	NUM_TASKS
}

enum DispositionType // BOT行为倾向(攻击性、保守性)
{
	ENGAGE_AND_INVESTIGATE, // 交战并调查(积极)
	OPPORTUNITY_FIRE,       // 伺机开火(较积极)
	SELF_DEFENSE,           // 自我防卫(保守)
	IGNORE_ENEMIES,         // 忽略敌人(极度保守,用于逃跑或特定行为)
	NUM_DISPOSITIONS
}

enum GamePhase // 游戏阶段
{
	GAMEPHASE_WARMUP_ROUND,    // 热身赛
	GAMEPHASE_PLAYING_STANDARD, // 标准比赛
	GAMEPHASE_PLAYING_FIRST_HALF, // 上半场
	GAMEPHASE_PLAYING_SECOND_HALF, // 下半场
	GAMEPHASE_HALFTIME,         // 半场休息
	GAMEPHASE_MATCH_ENDED,      // 比赛结束
	GAMEPHASE_MAX
}

这些枚举让插件能够以更清晰和结构化的方式控制 BOT 的复杂行为,例如:设置 BOT 走哪种路径(最快还是最安全),让它进入哪种攻击状态(积极交战还是自我防卫),以及当前在执行什么任务(下包还是找敌人)。

2.4 插件信息(Plugin myinfo

标准插件信息,指示了插件的名称、作者、版本和描述。

点击展开代码
public Plugin myinfo = 
{
	name = "BOT Improvement", 
	author = "manico", 
	description = "Improves bots and does other things.", 
	version = "1.2.0", 
	url = "http://steamcommunity.com/id/manico001"
};

2.5 插件加载与初始化

public void OnPluginStart():插件启动时!

这是插件加载时执行的第一个函数。它负责配置基础设置、钩子游戏事件和加载所有必要的底层 SDK 函数与代码段替换(Detours)。

点击展开代码
public void OnPluginStart()
{	
	// 判断是否是竞技模式
	g_bIsCompetitive = FindConVar("game_mode").IntValue == 1 && FindConVar("game_type").IntValue == 0 ? true : false;

	// 钩子各种游戏事件:当事件发生时,会调用我们定义的函数
	HookEventEx("player_spawn", OnPlayerSpawn); // 玩家出生
	HookEventEx("round_prestart", OnRoundPreStart); // 回合开始前
	HookEventEx("round_start", OnRoundStart); // 回合开始
	HookEventEx("round_end", OnRoundEnd); // 回合结束
	HookEventEx("round_freeze_end", OnFreezetimeEnd); // 冻结时间结束
	HookEventEx("weapon_zoom", OnWeaponZoom); // 武器瞄准
	HookEventEx("weapon_fire", OnWeaponFire); // 武器开火
	
	LoadSDK();     // 加载SDK(游戏内部C++函数)的地址和偏移量
	LoadDetours(); // 加载Detours(函数劫持)来修改游戏行为
	
	g_cvBotEcoLimit = FindConVar("bot_eco_limit"); // 获取bot经济限制的控制台变量
	
	RegConsoleCmd("team", Command_Team); // 注册一个自定义控制台命令
}

public Action Command_Team(...):新的“团队”指令

这是一个自定义的控制台命令 team,允许直接在服务器中通过配置好的 BOT 名单和队伍 Logo 来管理 BOT。它适用于希望快速设置自定义 BOT 团队来与玩家对战的场景。

点击展开代码
public Action Command_Team(int client, int iArgs)
{
    // ... 参数检查和文件加载
	BuildPath(Path_SM, szPath, sizeof(szPath), "configs/bot_rosters.txt"); // 构建bot名单配置文件路径
	// ... 导入KeyValues文件并解析队伍信息
    
    // 从配置文件中获取玩家名字列表和队伍Logo
    char szPlayers[256], szLogo[64];
    kv.GetString("players", szPlayers, sizeof(szPlayers));
    kv.GetString("logo", szLogo, sizeof(szLogo));

    ServerCommand("bot_kick %s all", szSide); // 先踢掉所有该阵营的BOT

    char szPlayerName[MAX_NAME_LENGTH];
    int iIndex, iCount = ExplodeString(szPlayers, ",", g_szPlayerNames, sizeof(g_szPlayerNames), sizeof(szPlayerName));

    for (iIndex = 0; iIndex < iCount; iIndex++)
    {
        Format(szPlayerName, sizeof(szPlayerName), "%s", g_szPlayerNames[iIndex]);
        ServerCommand("bot_add_%s %s", szSide, szPlayerName); // 逐个添加BOT
    }

    // 设置队伍Logo
    if (StrEqual(szSide, "ct", false))
        ServerCommand("mp_teamlogo_1 %s", szLogo);
    else if (StrEqual(szSide, "t", false))
        ServerCommand("mp_teamlogo_2 %s", szLogo);

	delete kv;
    return Plugin_Handled;
}

public void OnMapStart():地图加载时!

每当新地图加载时,这个函数会执行。它初始化和刷新插件跟踪的游戏状态和数据。

点击展开代码
public void OnMapStart()
{
	g_iProfileRankOffset = FindSendPropInfo("CCSPlayerResource", "m_nPersonaDataPublicLevel"); // 查找个人排位属性偏移
	g_iPlayerColorOffset = FindSendPropInfo("CCSPlayerResource", "m_iCompTeammateColor"); // 查找队友颜色属性偏移

	GetCurrentMap(g_szMap, sizeof(g_szMap)); // 获取当前地图名
	GetMapDisplayName(g_szMap, g_szMap, sizeof(g_szMap)); // 获取地图显示名
	ParseMapNades(g_szMap); // 解析地图特定的投掷物数据

	g_bIsBombScenario = IsValidEntity(FindEntityByClassname(-1, "func_bomb_target")); // 判断是否是爆破模式地图
	g_bIsHostageScenario = IsValidEntity(FindEntityByClassname(-1, "func_hostage_rescue")); // 判断是否是人质模式地图

	// 创建定时器,以固定间隔执行某些检查和行为
	CreateTimer(1.0, Timer_CheckPlayer, _, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE);
	CreateTimer(0.1, Timer_MoveToBomb, _, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE);

	SDKHook(GetPlayerResourceEntity(), SDKHook_ThinkPost, OnThinkPost); // 钩子玩家资源实体的ThinkPost事件

	Array_Fill(g_iPlayerColor, MaxClients + 1, -1); // 初始化玩家颜色数组
}

public Action Timer_CheckPlayer(...):BOT 购买和随机行为逻辑

这个定时器每秒运行一次,负责处理 BOT 在购买区的购买行为,以及一些随机的非战斗行为。

点击展开代码
public Action Timer_CheckPlayer(Handle hTimer, any data)
{
	for (int i = 1; i <= MaxClients; i++)
	{
		if (IsValidClient(i) && IsFakeClient(i) && IsPlayerAlive(i)) // 确保是活着的有效BOT
		{
			// ... 获取BOT的经济、武器、是否在购买区等信息
			
			if (IsItMyChance(2.0)) // 2%几率随机进行武器检视
			{
				FakeClientCommand(i, "+lookatweapon");
				FakeClientCommand(i, "-lookatweapon");
			}
			
			if(!bInBuyZone) // 不在购买区则跳过后续购买逻辑
				continue;
			
			// 如果有主武器或队友有主武器且自己不是手枪,则购买护甲和拆弹器
			if (IsValidEntity(iPrimary) || (GetFriendsWithPrimary(i) >= 1 && !IsDefaultPistol(szCurrentWeapon)))
			{
				if (GetEntProp(i, Prop_Data, "m_ArmorValue") < 50 || GetEntProp(i, Prop_Send, "m_bHasHelmet") == 0)
					FakeClientCommand(i, "buy vesthelm"); // 买甲
				
				if (iTeam == CS_TEAM_CT && !bHasDefuser)
					FakeClientCommand(i, "buy defuser"); // CT买拆弹器
				
				if(GetGameTime() - g_fRoundStart > 6.0 && !g_bFreezetimeEnd) // 冻结时间还没结束,过了一段时间才买投掷物
				{
					// 随机购买投掷物组合,如烟雾弹、闪光弹、HE弹、燃烧弹
				}
			}
			
			// 经济局购买手枪或护甲
			if ((iAccount < g_cvBotEcoLimit.IntValue && iAccount > 2000 && !IsValidEntity(iPrimary)) || GetFriendsWithPrimary(i) >= 1)
			{
				if(IsDefaultPistol(szCurrentWeapon))
				{
					// 随机购买P250, Tec9, Deagle
				}
				else
				{
					// 1/15 几率买护甲
				}
			}
			
			if (g_iCurrentRound == 0 || g_iCurrentRound == 12) // 起步局和关键局的特殊购买
			{
				// 随机购买护甲或手枪
			}
		}
	}
	return Plugin_Continue;
}

public Action Timer_MoveToBomb(...):BOT 寻找 C4 逻辑

这个定时器每 0.1 秒运行一次,主要处理 CT 方 BOT 在炸弹被安放后,如何行动去寻找并处理 C4。

点击展开代码
public Action Timer_MoveToBomb(Handle hTimer, any data)
{
	for (int i = 1; i <= MaxClients; i++)
	{
		if (!IsValidClient(i) || !IsFakeClient(i) || !IsPlayerAlive(i))
			continue;

		if (!g_bBombPlanted) // 如果炸弹没被安放,则跳过
			continue;

		int iPlantedC4 = FindEntityByClassname(-1, "planted_c4"); // 找到已安放的C4实体
		if (!IsValidEntity(iPlantedC4) || GetClientTeam(i) != CS_TEAM_CT) // 确保C4存在且是CT方BOT
			continue;

		float fC4Pos[3];
		GetEntPropVector(iPlantedC4, Prop_Send, "m_vecOrigin", fC4Pos); // 获取C4位置

		float fDistanceToBomb = GetVectorDistance(g_fBotOrigin[i], fC4Pos); // 计算BOT到C4的距离
		int iCTCount = GetAliveTeamCount(CS_TEAM_CT);
		int iTCount = GetAliveTeamCount(CS_TEAM_T);
		// 判断是否为残局情况:T方0人且CT方1人(包点远处)
		bool bLastMan = (iTCount == 0 && iCTCount == 1 && fDistanceToBomb > 30.0 && GetTask(i) != ESCAPE_FROM_BOMB);

		// 如果是残局,或者C4距离很远(超过2000单位),且附近没敌人,也没被禁用切换武器
		if ((bLastMan || fDistanceToBomb > 2000.0) && GetEntData(i, g_iBotNearbyEnemiesOffset) == 0 && !g_bDontSwitch[i])
		{
			SDKCall(g_hSwitchWeaponCall, i, GetPlayerWeaponSlot(i, CS_SLOT_KNIFE), 0); // 切换到刀
			BotMoveTo(i, fC4Pos, FASTEST_ROUTE); // 跑到C4位置
		}
	}
	return Plugin_Continue;
}

public Action Timer_DropWeapons(...):BOT 掉枪(Eco局)逻辑

这个定时器处理 BOT 在经济局为队友掉落武器的行为,模拟了竞技游戏中常见的起步策略。

点击展开代码
public Action Timer_DropWeapons(Handle hTimer, any data)
{
	if (GetGameTime() - g_fRoundStart <= 3.0) // 回合开始3秒内不掉枪
		return Plugin_Continue;

	for (int i = 1; i <= MaxClients; i++) // 遍历所有玩家(可能是要枪的BOT)
	{
		if (g_bHasGottenDrop[i] || !IsValidClient(i) || !IsPlayerAlive(i))
			continue; // 如果已经收到掉落或无效BOT,跳过

		if (g_bFreezetimeEnd) // 冻结时间结束不掉枪
			continue;

		bool bInBuyZone = !!GetEntProp(i, Prop_Send, "m_bInBuyZone");
		if (!bInBuyZone) // 不在购买区不掉枪
			continue;

		int iPrimary = GetPlayerWeaponSlot(i, CS_SLOT_PRIMARY);
		int iAccount = GetEntProp(i, Prop_Send, "m_iAccount");

		if (IsValidEntity(iPrimary) || iAccount >= g_cvBotEcoLimit.IntValue)
			continue; // 如果有主武器或钱够,不掉枪

		int iTeam = GetClientTeam(i);

		for (int j = 1; j <= MaxClients; j++) // 遍历所有玩家(可能是要掉枪的BOT)
		{
			if (g_bDropWeapon[j] || !IsValidClient(j) || !IsFakeClient(j) || !IsPlayerAlive(j))
				continue; // 如果已经掉过枪或无效BOT,跳过

			if (GetClientTeam(j) != iTeam)
				continue; // 不同队伍跳过

			int iOtherPrimary = GetPlayerWeaponSlot(j, CS_SLOT_PRIMARY);
			if (!IsValidEntity(iOtherPrimary))
				continue; // 掉枪者没有主武器,跳过

			// ... 检查掉枪者是否有足够经济买下当前武器
			
			float fEyes[3];
			GetClientEyePosition(i, fEyes); // 获取要枪者眼睛位置

            // 让掉枪者看向要枪者,准备掉武器
			BotSetLookAt(j, "Use entity", fEyes, PRIORITY_HIGH, 3.0, false, 5.0, false);
			g_bDropWeapon[j] = true; // 标记已掉枪
			g_bHasGottenDrop[i] = true; // 标记已收到枪
			break;
		}
	}
	return g_bFreezetimeEnd ? Plugin_Stop : Plugin_Continue; // 冻结时间结束则停止定时器
}

public void OnMapEnd():地图结束和清理

当地图结束时,移除之前注册的 OnThinkPost 钩子,防止在地图切换时出现问题。

点击展开代码
public void OnMapEnd()
{
	SDKUnhook(GetPlayerResourceEntity(), SDKHook_ThinkPost, OnThinkPost);
}

public void OnClientPostAdminCheck(...):客户端加载完毕时

当一个玩家(包括 BOT)完全加载到服务器并准备好进入游戏时,这个函数会被调用。它是 BOT 智能行为初始化的关键。

点击展开代码
public void OnClientPostAdminCheck(int client)
{
	g_iProfileRank[client] = Math_GetRandomInt(1, 40); // 随机分配BOT的个人资料排位

	if (!IsFakeClient(client)) // 如果是真实玩家
	{
		char szColor[64];
		GetClientInfo(client, "cl_color", szColor, sizeof(szColor)); // 获取玩家的队友颜色
		g_iPlayerColor[client] = StringToInt(szColor);
		return;
	}

	char szBotName[MAX_NAME_LENGTH];
	GetClientName(client, szBotName, sizeof(szBotName));
	g_bIsProBot[client] = false;

	if (IsProBot(szBotName, g_szCrosshairCode[client], 35)) // 判断是否是“职业BOT”
	{
		// 根据特定的职业哥名字设置极端的AI属性
		if (strcmp(szBotName, "s1mple") == 0 || strcmp(szBotName, "ZywOo") == 0 || strcmp(szBotName, "NiKo") == 0 ||
		    strcmp(szBotName, "sh1ro") == 0 || strcmp(szBotName, "jL") == 0 || strcmp(szBotName, "donk") == 0 ||
		    strcmp(szBotName, "m0NESY") == 0)
		{
			g_fLookAngleMaxAccel[client] = 20000.0; // 极快的瞄准速度
			g_fReactionTime[client] = 0.0;         // 即时反应时间
			g_fAggression[client] = 1.0;          // 极高攻击性
		}
		else // 其他职业BOT随机属性
		{
			g_fLookAngleMaxAccel[client] = Math_GetRandomFloat(4000.0, 7000.0);
			g_fReactionTime[client] = Math_GetRandomFloat(0.165, 0.325);
			g_fAggression[client] = Math_GetRandomFloat(0.0, 1.0);
		}

		g_bIsProBot[client] = true;
	}

	// 随机决定BOT使用哪些默认武器的改版
	g_bUseUSP[client] = IsItMyChance(75.0); // 75%几率CT方用USP
	g_bUseM4A1S[client] = IsItMyChance(50.0); // 50%几率CT方用M4A1-S
	g_bUseCZ75[client] = IsItMyChance(20.0); // 20%几率用CZ75
	g_pCurrArea[client] = INVALID_NAV_AREA; // 初始化导航区域
}

public void OnRoundPreStart(...):回合开始前

在回合正式开始前,更新当前回合数,并根据竞技模式和半场情况调整 BOT 的经济限制。

点击展开代码
public void OnRoundPreStart(Event eEvent, char[] szName, bool bDontBroadcast)
{
	g_iCurrentRound = GameRules_GetProp("m_totalRoundsPlayed"); // 获取当前总回合数
	
	g_cvBotEcoLimit.IntValue = ShouldForce() ? 0 : 3000; // 如果是关键局(半场、赛点、最终回合),则bot经济限制为0(强制全起),否则为3000
}

public void OnRoundStart(...):回合开始时

每个回合开始时,重置 BOT 的临时状态,并根据游戏模式更新 BOT 的士气等属性。

点击展开代码
public void OnRoundStart(Event eEvent, char[] szName, bool bDontBroadcast)
{
	// 根据是爆破还是人质模式,决定T/CT队伍的角色(进攻/防守方)
	int iTeam = g_bIsBombScenario ? CS_TEAM_CT : CS_TEAM_T;
	int iOppositeTeam = g_bIsBombScenario ? CS_TEAM_T : CS_TEAM_CT;

	g_bFreezetimeEnd = false; // 重置冻结时间结束标志
	g_bEveryoneDead = false; // 重置所有人都死亡标志
	g_fRoundStart = GetGameTime(); // 记录回合开始时间

	for (int i = 1; i <= MaxClients; i++)
	{
		// ... 重置各种BOT状态标志(如是否蹲伏、是否掉枪、目标等)
		// 根据游戏模式和半场切换,调整BOT的m_morale(士气/自信心),影响其行为倾向
		if (g_bIsBombScenario || g_bIsHostageScenario)
		{
			int iClientTeam = GetClientTeam(i);
			if (iClientTeam == iTeam) // 比如,爆破模式下CT方士气-3(被动)
				SetEntData(i, g_iBotMoraleOffset, -3);
			if (g_bHalftimeSwitch && iClientTeam == iOppositeTeam) // 半场切换后,士气+1
				SetEntData(i, g_iBotMoraleOffset, 1);
		}
	}

	g_bHalftimeSwitch = false; // 重置半场切换标志

	if (g_bIsCompetitive)
		CreateTimer(0.2, Timer_DropWeapons, _, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); // 竞技模式启动掉枪定时器
}

public void OnRoundEnd(...):回合结束时

回合结束时,更新比分,并确保所有 BOT 停止模仿动作。

点击展开代码
public void OnRoundEnd(Event eEvent, char[] szName, bool bDontBroadcast)
{
	// 遍历并获取当前团队比分
	int iEnt = -1;
	while ((iEnt = FindEntityByClassname(iEnt, "cs_team_manager")) != -1)
	{
		int iTeamNum = GetEntProp(iEnt, Prop_Send, "m_iTeamNum");
		if (iTeamNum == CS_TEAM_CT)
			g_iCTScore = GetEntProp(iEnt, Prop_Send, "m_scoreTotal");
		else if (iTeamNum == CS_TEAM_T)
			g_iTScore = GetEntProp(iEnt, Prop_Send, "m_scoreTotal");
	}

	for (int i = 1; i <= MaxClients; i++)
	{
		if (IsValidClient(i) && IsFakeClient(i) && BotMimic_IsPlayerMimicing(i))
			BotMimic_StopPlayerMimic(i); // 停止所有BOT的模仿动作
	}

	g_iRoundsPlayed = g_iCTScore + g_iTScore; // 更新总回合数

	// 重置所有预设投掷物的冷却时间
	for (int i = 0; i < g_iMaxNades; i++)
		g_fNadeTimestamp[i] = 0.0;
}

public void OnFreezetimeEnd(...):冻结时间结束时

当回合的冻结时间结束(玩家可以自由移动和射击)时,更新相关标志。

点击展开代码
public void OnFreezetimeEnd(Event eEvent, char[] szName, bool bDontBroadcast)
{
	g_bFreezetimeEnd = true; // 设置冻结时间结束标志
	g_fFreezeTimeEnd = GetGameTime(); // 记录冻结时间结束的时刻
}

public void OnWeaponZoom(...):武器瞄准时

当玩家或 BOT 使用狙击枪(或 AUG/SG)进行瞄准时,记录下时间戳。

点击展开代码
public void OnWeaponZoom(Event eEvent, const char[] szName, bool bDontBroadcast)
{
	int client = GetClientOfUserId(eEvent.GetInt("userid"));
	if (!IsValidClient(client) || !IsFakeClient(client) || !IsPlayerAlive(client))
		return;

	g_fShootTimestamp[client] = GetGameTime(); // 记录瞄准操作发生的时间
}

这个时间戳可能用于后续的 BOT 行为判断,例如瞄准后开枪的延迟。

public void OnWeaponFire(...):武器开火时

当玩家或 BOT 开火时,这个函数会进行一些调整,特别是针对狙击枪的“快切”。

点击展开代码
public void OnWeaponFire(Event eEvent, const char[] szName, bool bDontBroadcast)
{
	int client = GetClientOfUserId(eEvent.GetInt("userid"));
	if (!IsValidClient(client) || !IsFakeClient(client) || !IsPlayerAlive(client))
		return;

	char szWeaponName[64];
	eEvent.GetString("weapon", szWeaponName, sizeof(szWeaponName));

	if (IsValidClient(g_iTarget[client])) // 如果BOT有目标敌人
	{
		float fTargetLoc[3];
		GetClientAbsOrigin(g_iTarget[client], fTargetLoc);

		float fRangeToEnemy = GetVectorDistance(g_fBotOrigin[client], fTargetLoc);
		if (strcmp(szWeaponName, "weapon_deagle") == 0 && fRangeToEnemy > 100.0) // 如果是沙鹰且距离超过100单位
		{
			// 增加沙鹰开火后的冷却时间,模拟现实中的沙鹰难用
			float currentOffset = GetEntDataFloat(client, g_iFireWeaponOffset);
			SetEntDataFloat(client, g_iFireWeaponOffset, currentOffset + Math_GetRandomFloat(0.20, 0.40));
		}
	}

	if ((strcmp(szWeaponName, "weapon_awp") == 0 || strcmp(szWeaponName, "weapon_ssg08") == 0) && IsItMyChance(50.0))
		RequestFrame(BeginQuickSwitch, GetClientUserId(client)); // 狙击枪有50%几率执行快切
}

public void OnThinkPost(...):每帧更新玩家资源

这个函数在游戏每一帧逻辑处理之后运行,用于更新玩家的排位、队友颜色和准星代码等可视化数据。

点击展开代码
public void OnThinkPost(int iEnt)
{
	// 将每个玩家的预设排位数据写入到CCSPlayerResource实体中,以便客户端可见
	SetEntDataArray(iEnt, g_iProfileRankOffset, g_iProfileRank, MAXPLAYERS + 1);
	// 将每个玩家的预设队友颜色数据写入到CCSPlayerResource实体中
	SetEntDataArray(iEnt, g_iPlayerColorOffset, g_iPlayerColor, MAXPLAYERS + 1);

	for (int i = 1; i <= MaxClients; i++)
		if (IsValidClient(i) && IsFakeClient(i))
			SetCrosshairCode(GetEntityAddress(iEnt), i, g_szCrosshairCode[i]); // 设置BOT的准星代码
}

这个函数非常巧妙,它通过修改所有玩家共用的 CCSPlayerResource 实体的数据,来间接实现了为每个 BOT 显示自定义的排位、队友颜色和准星效果。这些都是玩家客户端会读取来显示在记分板和游戏 HUD 上的信息。

public Action CS_OnBuyCommand(...):改变 BOT 购买武器的逻辑

这个钩子允许插件修改 BOT 执行购买命令时的实际行为,例如强制他们购买特定武器的改版或升级。

点击展开代码
public Action CS_OnBuyCommand(int client, const char[] szWeapon)
{
	if (IsValidClient(client) && IsFakeClient(client) && IsPlayerAlive(client))
	{
		// 投掷物和护甲跳过,它们有独立的购买逻辑
		if (strcmp(szWeapon, "molotov") == 0 || /* ... */ || strcmp(szWeapon, "defuser") == 0)
			return Plugin_Continue;
		// 如果BOT已经有主武器,且不是手枪,则阻止他们再次购买主武器(避免重复购买)
		else if (GetPlayerWeaponSlot(client, CS_SLOT_PRIMARY) != -1 && (strcmp(szWeapon, "galilar") == 0 || /* ... */))
			return Plugin_Handled; // 阻止这次购买
		
		int iAccount = GetEntProp(client, Prop_Send, "m_iAccount"); // 获取BOT金钱
		
		if (strcmp(szWeapon, "m4a1") == 0)
		{
			// 如果BOT被设置为使用M4A1-S且钱够,强制购买M4A1-S
			if (g_bUseM4A1S[client] && iAccount >= CS_GetWeaponPrice(client, CSWeapon_M4A1_SILENCER))
			{
				AddMoney(client, -CS_GetWeaponPrice(client, CSWeapon_M4A1_SILENCER), true, true, "weapon_m4a1_silencer"); // 扣钱
				CSGO_ReplaceWeapon(client, CS_SLOT_PRIMARY, "weapon_m4a1_silencer"); // 给予M4A1-S
				return Plugin_Changed; // 改变了原有行为
			}
			// 5%几率购买AUG
			if (IsItMyChance(5.0) && iAccount >= CS_GetWeaponPrice(client, CSWeapon_AUG)) { /* ... */ }
		}
		// 类似逻辑处理Mac10、MP9、Tec9/FiveSeven等武器,根据随机或配置强制购买升级版或替换武器
	}
	return Plugin_Continue;
}

BOT 底层行为 Detours (动态钩子)

这些函数是利用 dhooks 扩展实现的高级功能,它们绕过了 SourceMod 的标准钩子,直接修改了游戏内部 BOT AI 模块的 C++ 函数行为。这提供了对 BOT 行为最直接、最细致的控制。

public Action OnPlayerRunCmd(...):每帧执行的核心逻辑

这个函数在每个玩家(包括 BOT)每帧执行命令时被调用。它是插件中处理 BOT 步法、瞄准、开火、投掷物和拾取武器等核心行为的“大脑”。

点击展开代码
public Action OnPlayerRunCmd(int client, int &iButtons, int &iImpulse, float fVel[3], float fAngles[3], int &iWeapon, int &iSubtype, int &iCmdNum, int &iTickCount, int &iSeed, int iMouse[2])
{
	g_bBombPlanted = !!GameRules_GetProp("m_bBombPlanted"); // 更新炸弹是否安放状态

	if (IsValidClient(client) && IsPlayerAlive(client) && IsFakeClient(client)) // 确保是活着的有效BOT
	{
		if(!g_bFreezetimeEnd && g_bDropWeapon[client] && view_as<LookAtSpotState>(GetEntData(client, g_iBotLookAtSpotStateOffset)) == LOOK_AT_SPOT)
		{
            // 如果BOT被标记为掉枪,且正在看目标,则执行掉枪并购买上次的武器
			CS_DropWeapon(client, GetPlayerWeaponSlot(client, CS_SLOT_PRIMARY), true);
			FakeClientCommand(client, "buy %s", g_szPreviousBuy[client]);
			g_bDropWeapon[client] = false;
		}
		
		GetClientAbsOrigin(client, g_fBotOrigin[client]); // 获取BOT当前位置
		g_iActiveWeapon[client] = GetEntPropEnt(client, Prop_Send, "m_hActiveWeapon"); // 获取BOT当前手持武器
		// ...

		if(g_bFreezetimeEnd) // 冻结时间结束后才执行以下逻辑
		{			
			int iDefIndex = GetEntProp(g_iActiveWeapon[client], Prop_Send, "m_iItemDefinitionIndex");
			// ... 获取移动速度、当前导航区域等
			
			// 如果所有人都死亡 (残局情况),切换到刀,跑路
			if ((GetAliveTeamCount(CS_TEAM_T) == 0 || GetAliveTeamCount(CS_TEAM_CT) == 0) && !g_bDontSwitch[client])
			{
				SDKCall(g_hSwitchWeaponCall, client, GetPlayerWeaponSlot(client, CS_SLOT_KNIFE), 0);
				g_bEveryoneDead = true;
			}
				
            // 随机决定是否要去扔预设手雷
			if(IsItMyChance(0.2) && g_iDoingSmokeNum[client] == -1)
				g_iDoingSmokeNum[client] = GetNearestGrenade(client);
			
			if(GetDisposition(client) == SELF_DEFENSE) // 如果BOT处于自我防卫状态(被动),转为积极交战
				SetDisposition(client, ENGAGE_AND_INVESTIGATE);
			
            // 根据NavMesh区域属性调整BOT的移动模式(走路/跑步)
			if(g_pCurrArea[client] != INVALID_NAV_AREA) { /* ... */ }
			
            // 处理预设手雷投掷逻辑
			if(g_iDoingSmokeNum[client] != -1 && !BotMimic_IsPlayerMimicing(client))
			{
				// 让BOT移动到投掷位置,看向目标,并播放模仿动作
			}
			
            // 实际执行投掷手雷
			if(g_bThrowGrenade[client] && eItems_GetWeaponSlotByDefIndex(iDefIndex) == CS_SLOT_GRENADE)
			{
				BotThrowGrenade(client, g_fNadeTarget[client]);
				g_fThrowNadeTimestamp[client] = GetGameTime();
			}
			
            // 如果BOT认为自己安全或残局,则停止疾跑
			if(IsSafe(client) || g_bEveryoneDead)
				iButtons &= ~IN_SPEED;
				
			if (g_bIsProBot[client] && !g_bBombPlanted && GetTask(client) != COLLECT_HOSTAGES && GetTask(client) != RESCUE_HOSTAGES && /* ... */)
			{
				float fClientEyes[3];
				GetClientEyePosition(client, fClientEyes);
			
				// “职业BOT”寻找并拾取地图上的主武器和手枪
				// ... (复杂且重复的GetNearestEntity和BotMoveTo逻辑)
			}
			
			if (g_bIsProBot[client] && GetDisposition(client) != IGNORE_ENEMIES) // “职业BOT”的战斗瞄准与开火逻辑
			{		
				g_iTarget[client] = BotGetEnemy(client); // 获取当前目标
				
				float fTargetDistance;
				int iZoomLevel;
				bool bIsEnemyVisible = !!GetEntData(client, g_iEnemyVisibleOffset); // 敌人是否可见
				bool bIsHiding = BotIsHiding(client); // 是否在藏身处
				bool bIsDucking = !!(GetEntityFlags(client) & FL_DUCKING); // 是否在蹲伏
				bool bIsReloading = IsPlayerReloading(client); // 是否在换弹
				// ...
				
				if(bIsHiding && (iDefIndex == 8 || iDefIndex == 39) && iZoomLevel == 0) //如果是狙击枪且在藏身处,未开镜则开镜
					iButtons |= IN_ATTACK2;
				else if(!bIsHiding && (iDefIndex == 8 || iDefIndex == 39) && iZoomLevel == 1) //如果是狙击枪且不在藏身处,开镜则取消开镜
					iButtons |= IN_ATTACK2;
				
				if (bIsHiding && g_bUncrouch[client]) // 如果在藏身处且允许取消蹲伏
					iButtons &= ~IN_DUCK; // 取消蹲伏
					
				if (!IsValidClient(g_iTarget[client]) || !IsPlayerAlive(g_iTarget[client]) || g_fTargetPos[client][2] == 0)
				{
					g_iPrevTarget[client] = g_iTarget[client];
					return Plugin_Continue;
				}

                // 如果BOT在模仿,停止模仿,确保战斗优先
				if(BotMimic_IsPlayerMimicing(client)) { /* ... */ }
				
                // 如果当前是刀或投掷物,切换到最佳武器
				if ((eItems_GetWeaponSlotByDefIndex(iDefIndex) == CS_SLOT_KNIFE || eItems_GetWeaponSlotByDefIndex(iDefIndex) == CS_SLOT_GRENADE) && GetTask(client) != ESCAPE_FROM_BOMB && GetTask(client) != ESCAPE_FROM_FLAMES)
						BotEquipBestWeapon(client, true);
				
				if (bIsEnemyVisible && GetEntityMoveType(client) != MOVETYPE_LADDER) // 敌人可见且不在梯子上
				{
                    // 设置蹲伏时间戳,用于控制蹲伏频率
                    // ... 计算目标命中容忍度
						
					switch(iDefIndex) // 根据当前武器DefIndex调整开火和停止逻辑
					{
						case 7, 8, /* ... 步枪/冲锋枪/散弹枪 */ 60: // 大部分枪
						{
                            // 如果瞄准命中且距离近,或者距离远且正在开火,则执行急停
							if (fOnTarget > fAimTolerance && fTargetDistance < 2000.0 && iDefIndex != 17 /* 狙击枪 */ && iDefIndex != 19 /* AK */ && /* ... */)
								AutoStop(client, fVel, fAngles);
							else if (fTargetDistance > 2000.0 && GetEntDataFloat(client, g_iFireWeaponOffset) == GetGameTime())
								AutoStop(client, fVel, fAngles);
						
							if (fOnTarget > fAimTolerance && fTargetDistance < 2000.0) // 瞄准命中且距离近
							{
								iButtons &= ~IN_ATTACK; // 先解除攻击按钮
							
                                // 如果不在换弹且速度慢或蹲伏,则开火
								if(!bIsReloading && (fSpeed < 50.0 || bIsDucking || iDefIndex == 17 || iDefIndex == 19 || /* ... */))
								{
									iButtons |= IN_ATTACK;
									SetEntDataFloat(client, g_iFireWeaponOffset, GetGameTime()); // 记录开火时间
								}
							}
						}
						case 1: // Desert Eagle (沙鹰)
						{
                            // 短暂延迟开火后进行急停
							if (GetGameTime() - GetEntDataFloat(client, g_iFireWeaponOffset) < 0.15 && !bIsDucking && !bIsReloading)
								AutoStop(client, fVel, fAngles);
						}
						case 9, 40: // AWP/SSG08 (狙击枪)
						{
                            // 确保开镜、不在换弹、开火冷却结束、且准确瞄准目标后才开火
							if (fTargetDistance < 2750.0 && !bIsReloading && GetEntProp(client, Prop_Send, "m_bIsScoped") && GetGameTime() - g_fShootTimestamp[client] > 0.4 && GetClientAimTarget(client, true) == g_iTarget[client])
							{
								iButtons |= IN_ATTACK;
								SetEntDataFloat(client, g_iFireWeaponOffset, GetGameTime());
							}	
						}
					}
					
					float fClientLoc[3];
					Array_Copy(g_fBotOrigin[client], fClientLoc, 3);
					fClientLoc[2] += HalfHumanHeight; // 客户端眼睛高度
					
                    // 在有效瞄准目标时(步枪/冲锋枪/散弹枪有效武器DefIndex),有条件进行蹲伏射击
					if (GetGameTime() >= g_fCrouchTimestamp[client] && !GetEntProp(g_iActiveWeapon[client], Prop_Data, "m_bInReload") && IsPointVisible(fClientLoc, g_fTargetPos[client]) && fOnTarget > fAimTolerance && fTargetDistance < 2000.0 && (iDefIndex == 7 || iDefIndex == 8 || /* ... */ || iDefIndex == 28))
						iButtons |= IN_DUCK;
						
					g_iPrevTarget[client] = g_iTarget[client]; // 记录上一个目标
				}
			}
			return Plugin_Changed; // 告诉游戏我们修改了BOT的命令
		}
	}
	return Plugin_Continue;
}

public void OnPlayerSpawn(...):玩家出生动作

当玩家或 BOT 出生时,除了在 OnClientPostAdminCheck 中设置的属性外,还会调整 BOT 的内部 AI 参数和手枪类型。

点击展开代码
public void OnPlayerSpawn(Event eEvent, const char[] szName, bool bDontBroadcast)
{
	int client = GetClientOfUserId(eEvent.GetInt("userid"));
	
	SetPlayerTeammateColor(client); // 设置队友颜色(在地图开始时会设置,这里可能是再次确保)

	if (IsValidClient(client) && IsFakeClient(client))
	{
		if(g_bIsProBot[client]) // 如果是“职业BOT”
		{
			Address pLocalProfile = view_as<Address>(GetEntData(client, g_iBotProfileOffset)); // 获取BOT的个人资料对象内存地址
			
			// 直接通过偏移量修改BOT的内部AI参数:瞄准最大加速度、反应时间、攻击性
			StoreToAddress(pLocalProfile + view_as<Address>(104), view_as<int>(g_fLookAngleMaxAccel[client]), NumberType_Int32);
			StoreToAddress(pLocalProfile + view_as<Address>(116), view_as<int>(g_fLookAngleMaxAccel[client]), NumberType_Int32);
			StoreToAddress(pLocalProfile + view_as<Address>(84), view_as<int>(g_fReactionTime[client]), NumberType_Int32);
			StoreToAddress(pLocalProfile + view_as<Address>(4), view_as<int>(g_fAggression[client]), NumberType_Int32);
		}
		
		if (g_bUseUSP[client] && GetClientTeam(client) == CS_TEAM_CT) // 如果CT方BOT偏好使用USP-S
		{
			char szUSP[32];
			GetClientWeapon(client, szUSP, sizeof(szUSP));
			if (strcmp(szUSP, "weapon_hkp2000") == 0) // 如果默认手枪是P2000
				CSGO_ReplaceWeapon(client, CS_SLOT_SECONDARY, "weapon_usp_silencer"); // 替换为USP-S
		}
	}
}

public void BotMimic_OnPlayerStopsMimicing(...):停止模仿

当 BOT 停止播放模仿动作时,重置其预设投掷物相关的状态。

点击展开代码
public void BotMimic_OnPlayerStopsMimicing(int client, char[] szName, char[] szCategory, char[] szPath)
{
	g_iDoingSmokeNum[client] = -1; // 重置正在进行的烟雾弹投掷编号
}

public void OnClientDisconnect(...):客户端断开连接

当客户端(包括 BOT)断开连接时,清理 BOT 的排位信息。

点击展开代码
public void OnClientDisconnect(int client)
{
	if (IsValidClient(client) && IsFakeClient(client))
		g_iProfileRank[client] = 0; // 重置BOT的排位信息
}

public void eItems_OnItemsSynced():物品同步后

这个函数在 eItems 扩展完全同步游戏物品数据后被调用。这里的作用是强制服务器更换地图,以此触发所有插件的重新加载和初始化,确保 eItems 提供的物品数据在插件启动时能够完全就绪。

点击展开代码
public void eItems_OnItemsSynced()
{
	ServerCommand("changelevel %s", g_szMap); // 强制服务器更换到当前地图,触发全面刷新
}

这是一个小小的“技巧”,确保所有依赖 eItems 数据的插件(包括本插件和上一个 BOT Inventory 插件)都能在数据完整加载后,在一个干净的环境中启动。

2.6 辅助函数:让 BOT 更“聪明”的工具

void ParseMapNades(const char[] szMap):解析地图投掷物

这个函数负责从配置文件 configs/bot_nades.txt 中读取特定地图的预设投掷物信息。

点击展开代码
void ParseMapNades(const char[] szMap)
{
	char szPath[PLATFORM_MAX_PATH];
	BuildPath(Path_SM, szPath, sizeof(szPath), "configs/bot_nades.txt"); // 构建配置文件路径
	
	if (!FileExists(szPath)) { /* ... */ return; } // 文件不存在报错
	
	KeyValues kv = new KeyValues("Nades"); // 创建KeyValues对象
	if (!kv.ImportFromFile(szPath)) { /* ... */ return; } // 载入文件失败报错
	
	if(!kv.JumpToKey(szMap)) { /* ... */ return; } // 跳到当前地图的配置段
	if(!kv.GotoFirstSubKey()) { /* ... */ return; } // 跳到第一个投掷物配置
	
	int i = 0;
	do
	{
		// 读取投掷物的位置、看向点、物品DefIndex、回放文件路径、冷却时间戳、所属队伍
		kv.GetVector("position", g_fNadePos[i]);
		kv.GetVector("lookat", g_fNadeLook[i]);
		g_iNadeDefIndex[i] = kv.GetNum("nadedefindex");
		kv.GetString("replay", g_szReplay[i], 128);
		g_fNadeTimestamp[i] = kv.GetFloat("timestamp");
		// ... 判断队伍
		i++;
	} while (kv.GotoNextKey()); // 遍历所有投掷物配置
	
	delete kv;	
	g_iMaxNades = i; // 记录投掷物数量
}

这个函数是 BOT 能够精确投掷预设投掷物的基础。它读取的 bot_nades.txt 文件需要管理员手动配置,指明每个烟雾、火等的投掷点、目标点以及相关的 BOT 模仿回放文件。

bool IsProBot(...):判断是否是“职业BOT”

检查 BOT 的名字是否在 data/bot_info.json 中。如果是,则读取其对应的准星代码。

点击展开代码
bool IsProBot(const char[] szName, char[] szCrosshairCode, int iSize)
{
	char szPath[PLATFORM_MAX_PATH];
	BuildPath(Path_SM, szPath, sizeof(szPath), "data/bot_info.json"); // 构建配置文件路径
	
	if (!FileExists(szPath)) { /* ... */ return false; } // 文件不存在报错
	
	JSONObject jData = JSONObject.FromFile(szPath); // 从JSON文件读取数据
	if(jData.HasKey(szName)) // 检查JSON中是否有该BOT的名字
	{
		JSONObject jInfoObj = view_as<JSONObject>(jData.Get(szName));
		jInfoObj.GetString("crosshair_code", szCrosshairCode, iSize); // 读取准星代码
		delete jInfoObj;
		delete jData;
		return true; // 是职业BOT
	}
	
	delete jData;
	return false; // 不是职业BOT
}

这个函数让你能够为特定名字的 BOT 设置独特的行为和准星,比如名为“s1mple”的 BOT 可以拥有 s1mple 的准星和更强的 AI 属性。

void LoadSDK():加载 SDK 函数地址和偏移量

这个函数通过读取 botstuff.games 配置文件(通常由 SourceHook 或其他工具生成),来获取游戏内部 C++ 函数(如 BOT 移动、瞄准、骨骼查找等)在内存中的地址和 BOT 对象的属性偏移量。这是实现高级 BOT AI 最底层的技术。

点击展开代码
public void LoadSDK()
{
	GameData hGameConfig = new GameData("botstuff.games"); // 载入游戏数据配置文件
	if (hGameConfig == null) { /* ... */ }
	
	// 获取BOT管理器TheBots的全局地址
	if(!(g_pTheBots = hGameConfig.GetAddress("TheBots"))) { /* ... */ }
	
	// 获取一系列CCSBot(BOT核心AI对象)内部属性的偏移量
	if ((g_iBotTargetSpotOffset = hGameConfig.GetOffset("CCSBot::m_targetSpot")) == -1) { /* ... */ }
	// ... (省略了大量类似的代码,每行都在获取一个BOT内部属性的偏移量)
	
	// 准备SDK调用,获取游戏内部C++函数的句柄
	StartPrepSDKCall(SDKCall_Player); // 准备对玩家实体进行SDK调用
	PrepSDKCall_SetFromConf(hGameConfig, SDKConf_Signature, "CCSBot::MoveTo"); // 设置要调用的函数签名
	// ... (添加函数参数和返回信息)
	if ((g_hBotMoveTo = EndPrepSDKCall()) == null) { /* ... */ } // 获取函数句柄
	
	// ... (省略了大量类似的代码,为LookupBone, GetBonePosition, BotIsVisible等获取函数句柄)
	
	delete hGameConfig;
}

如果你不进行游戏引擎的逆向工程,你不需要完全理解这些偏移量和签名的含义。你只需要知道,它们是插件能够直接告诉游戏“做某事”(例如“让 BOT 移动到哪里”、“让 BOT 看向哪里”)以及“获取某信息”(例如“BOT 的目标位置是什么”)的关键。这个文件 botstuff.games 通常需要额外下载或根据你的服务器游戏版本生成。

void LoadDetours():加载动态钩子 (Detours)

这个函数负责设置动态钩子(Detours),它比标准 SDK 钩子更强大,允许插件在游戏内部的 C++ 函数执行前或执行后插入自己的代码,甚至完全替换原函数的功能。这对于彻底改变 BOT 的核心 AI 行为至关重要。

点击展开代码
public void LoadDetours()
{
	GameData hGameData = new GameData("botstuff.games"); // 再次载入游戏数据配置文件
	if (hGameData == null) { /* ... */ return; }
	
	// 设置CCSBot::SetLookAt函数的Detour
	DynamicDetour hBotSetLookAtDetour = DynamicDetour.FromConf(hGameData, "CCSBot::SetLookAt");
	if(!hBotSetLookAtDetour.Enable(Hook_Pre, CCSBot_SetLookAt)) // 在原函数执行前钩子CCSBot_SetLookAt函数
		SetFailState("Failed to setup detour for CCSBot::SetLookAt");
	
	// 设置CCSBot::PickNewAimSpot函数的Detour
	DynamicDetour hBotPickNewAimSpotDetour = DynamicDetour.FromConf(hGameData, "CCSBot::PickNewAimSpot");
	if(!hBotPickNewAimSpotDetour.Enable(Hook_Post, CCSBot_PickNewAimSpot)) // 在原函数执行后钩子CCSBot_PickNewAimSpot函数
		SetFailState("Failed to setup detour for CCSBot::PickNewAimSpot");
	
	// 设置BotCOS、BotSIN、CCSBot::GetPartPosition函数的Detour
	// ... (类似的代码,钩子了上面提到过的BotCOS, BotSIN, GetPartPosition)
	
	delete hGameData;
}

通过这些 Detours,插件能够“劫持”游戏内部 BOT AI 的关键决策函数,例如 BOT 决定看向哪里、选择哪个瞄准点、甚至底层的感知方式。然后,插件运行自己定制的逻辑来替代或补充原有的游戏 AI 行为,从而实现高度定制化的 BOT 智能。

SDK Wrapper 函数:简化调用

这些函数是对前面通过 LoadSDK() 获取的句柄的封装。它们让插件开发人员能够像调用普通函数一样,轻松地调用游戏内部的 C++ 函数。

点击展开代码
public int LookupBone(int iEntity, const char[] szName) { /* ... */ } // 查找实体骨骼ID
public void GetBonePosition(int iEntity, int iBone, float fOrigin[3], float fAngles[3]) { /* ... */ } // 获取骨骼位置
public void BotMoveTo(int client, float fOrigin[3], RouteType routeType) { /* ... */ } // 让BOT移动到指定位置
public bool BotIsVisible(int client, float fPos[3], bool bTestFOV, int iIgnore = -1) { /* ... */ } // BOT是否能看到某点
public bool BotIsHiding(int client) { /* ... */ } // BOT是否在藏身处
public void BotEquipBestWeapon(int client, bool bMustEquip) { /* ... */ } // 让BOT装备最佳武器
public void BotSetLookAt(int client, const char[] szDesc, const float fPos[3], PriorityType pri, float fDuration, bool bClearIfClose, float fAngleTolerance, bool bAttack) { /* ... */ } // 让BOT看向指定位置
public bool BotBendLineOfSight(int client, const float fEye[3], const float fTarget[3], float fBend[3], float fAngleLimit) { /* ... */ } // 让BOT的视线绕过障碍物
public void BotThrowGrenade(int client, const float fTarget[3]) { /* ... */ } // 让BOT投掷手雷
public int BotGetEnemy(int client) { /* ... */ } // 获取BOT当前锁定的敌人
public void SetCrosshairCode(Address pCCSPlayerResource, int client, const char[] szCode) { /* ... */ } // 设置准星代码
public void AddMoney(int client, int iAmount, bool bTrackChange, bool bItemBought, const char[] szItemName) { /* ... */ } // 给玩家增减金钱

这些辅助函数大大简化了代码,使得在插件内部调用复杂的游戏引擎函数变得像普通函数调用一样简单,提高了代码的可读性和开发效率。

其他实用辅助函数

这些是插件中用于各种判断、计算和操作的通用函数。

点击展开代码
bool IsDefaultPistol(const char[] szWeapon) { /* ... */ } // 判断是否是默认手枪(P2000/USP-S/Glock)
int GetFriendsWithPrimary(int client) { /* ... */ } // 计算同一队伍中有主武器的队友数量
public int GetNearestGrenade(int client) { /* ... */ } // 找到距离BOT最近的未使用的预设投掷物
stock int GetNearestEntity(int client, char[] szClassname) { /* ... */ } // 找到距离BOT最近的某个类名的实体(如武器)
stock int CSGO_ReplaceWeapon(int client, int iSlot, const char[] szClass) { /* ... */ } // 替换玩家武器槽位中的武器
bool IsPlayerReloading(int client) { /* ... */ } // 判断玩家是否正在换弹
public void BeginQuickSwitch(int client) { /* ... */ } // 开始狙击枪快切
public void FinishQuickSwitch(int client) { /* ... */ } // 完成狙击枪快切
public Action Timer_EnableSwitch(Handle hTimer, any client) { /* ... */ } // 定时器,用于解除BOT的武器切换限制
public Action Timer_DontForceThrow(Handle hTimer, any client) { /* ... */ } // 定时器,用于解除BOT的强制投掷物状态
public void DelayThrow(any client) { /* ... */ } // 延迟投掷手雷的辅助函数
public void SelectBestTargetPos(int client, float fTargetPos[3]) { /* ... */ } // 选择最佳瞄准目标点(头部或身体)
stock void GetViewVector(float fVecAngle[3], float fOutPut[3]) { /* ... */ } // 将角度转换为方向向量
stock float AngleNormalize(float fAngle) { /* ... */ } // 规范化角度到-180到180度之间
stock bool IsPointVisible(float fStart[3], float fEnd[3]) { /* ... */ } // 判断两点之间是否可见(无障碍物)
public bool TraceEntityFilterStuff(int iEntity, int iMask) { /* ... */ } // 射线追踪的实体过滤回调
public void ProcessGrenadeThrow(int client, float fTarget[3]) { /* ... */ } // 处理BOT手雷投掷的完整流程
stock bool GetGrenadeToss(int client, float fTossTarget[3]) { /* ... */ } // 计算手雷投掷的最佳抛物线
stock bool LineGoesThroughSmoke(float fFrom[3], float fTo[3]) { /* ... */ } // 判断两点之间是否有烟雾阻挡
stock int GetAliveTeamCount(int iTeam) { /* ... */ } // 计算队伍中存活的玩家数量
stock bool IsSafe(int client) { /* ... */ } // 判断BOT是否处于“安全”状态(冻结时间结束后一段时间内)
stock TaskType GetTask(int client) { /* ... */ } // 获取BOT当前任务
stock DispositionType GetDisposition(int client) { /* ... */ } // 获取BOT当前行为倾向
stock void SetDisposition(int client, DispositionType iDisposition) { /* ... */ } // 设置BOT行为倾向
stock void SetPlayerTeammateColor(int client) { /* ... */ } // 设置玩家的队友颜色
public void AutoStop(int client, float fVel[3], float fAngles[3]) { /* ... */ } // 模拟玩家急停动作
stock bool ShouldForce() { /* ... */ } // 判断是否处于关键局(半场、赛点、加时)
stock int GetNumWinsToClinch() { /* ... */ } // 获取胜利所需的回合数
stock bool IsItMyChance(float fChance = 0.0) { /* ... */ } // 根据几率判断是否发生某事
stock bool IsValidClient(int client) { /* ... */ } // 判断客户端是否有效

这份文档由 un1 创作,并由 Gemini 提供技术辅助。
希望这份详细的解析能帮助你更好地理解这个 SourcePawn 插件,让你的 CS:GO BOT 变得更加强大和智能!