【求生之路2】主动检视武器插件 LookAtWeapon
银靴
编辑于 2025年03月21日 16:14

看到有人提及了给求生加入一个主动检视武器的插件思路,想了想觉得蛮有意思,恰好我在社区内检索不到这样子的插件,所以自己动手写了下

Description:

    该插件能够让客户端随时随地地检视武器,选择一个方式以开启/关闭检视,需要武器Mod本身带有检视动作。具体原理为:在客户端正前方生成一个透明实体,触发武器的检视动作。或者使用菜单命令,强迫武器重复播放某段动画。

Requirements:

    sourcemod1.10 or higher (sourcemod1.9未测试)

Installation:

    1.将插件源码保存为 l4d2_lookatweapon.sp ,并编译成插件

    2.将编译好的插件放入 ../addons/sourcemod/plugins

Cvars:

    l4d2_lookatweapon_allow - 插件开关

    l4d2_lookatweapon_hints  - 是否显示信息(0=不显示,1=显示在聊天框,2=显示在提示框)

    l4d2_lookatweapon_distance - 透明实体距离玩家的距离

    l4d2_lookatweapon_way - 启用方法(位运算: 1=双击静步, 2=输入指令, 4=双击手电筒, 8=满仓时双击换弹)

    l4d2_lookatweapon_way_multishift - 短时间内连续敲击SHIFT多少次可以开关检视,优先级高于 l4d2_lookatweapon_way 参数,值需大于等于2

    l4d2_lookatweapon_way_sensitive - 检视时玩家移动是否会打断检视

    l4d2_lookatweapon_menu - 是否启用菜单播放动画

    l4d2_lookatweapon_afk - 是否在闲置时自动检视(仅开启AFKCHECK编译时生效)

Cmd:

    sm_lookatweapon - 开关检视动作

    sm_lookatmenu - 检视菜单(仅开启 l4d2_lookatweapon_menu 参数时生效)

Questions : 

Q还可以添加其他方法切换吗?

A你可以自行替换按键,具体替换位置已于文件中标注,或者替换命令的名称。

Q这个插件会影响性能吗?

A因为这个插件创造了新实体,可能会对服务器和客户端的渲染带来压力,不过一般情况下在可接受范围内。事实上,影响最大的还是这个插件为了更好的体验,实时计算更新实体的位置,这可能会给服务器的CPU带来压力。未来可能会推出没有实时更新的选项。

Q如何开启闲置时自动检视的功能呢?

A请在源码开头37行处找到AFKCHECK,将0改为1,重新编译一次即可。同时,你可以修改33行AFK_DURATION的浮点数值来定义触发的时间。

Q这个插件有bug吗?

A我不敢保证,但是我已经尽我所能去测试了,如果发现了bug,请向我反馈,我会在能力和时间允许的前提下去修复它。

Q你的代码里看起来多了许多莫名的符号

A请切换到旧版本专栏视图,新版存在显示bug。

Changes

代码块
C++
自动换行
复制代码
1.4.1 (21-March-2025)
	- 优化代码部分逻辑

1.4 (12-March-2025)
	- 修复开启 AFKCHECK 时的编译错误

1.3.1 (7-Oct-2023)
	- 修复菜单不能正确显示状态的bug

1.3 (7-Oct-2023)
	- 修复运行时后台会提示报错信息的bug
	- 增加新参数 l4d2_lookatweapon_way_sensitive ,开启后移动将会打断检视
	- 增加新参数 l4d2_lookatweapon_way_multishift ,可以指定SHIFT键敲击计数来开关检视,
		该参数优先于 l4d2_lookatweapon_way 参数中的 1 值设定。
 
1.2 (5-May-2023)
	- 增加了对播放静态动画的支持,可通过菜单播放指定动画
	- 增加了新参数 l4d2_lookatweapon_menu ,用于开关静态动画的支持
	- 增加了一个编译选项 AFKCHECK ,用于选择是否编译新增的闲置自动触发检视
	- 在开启 AFKCHECK 编译后,增加了新参数 l4d2_lookatweapon_afk ,用于开关新增的闲置自动触发检视

1.1 (12-April-2023)
	- 增加了更多触发检视的按键,现支持4种方式

1.0 (16-March-2023)
	- Initial release.
复制成功

Special Thanks:

    感谢 Liszt101 提供的插件思路     感谢 源樱 提供的改进思路

Code: 

代码块
C++
自动换行
复制代码
/*========================================================================================
	Plugin Info:

*	Name	:	[L4D2] Look At Weapon Yourself
*	Author	:	Mengsk
*	Descrp	:	Make it possible to look at weapon anywhere in l4d2.
*	Plugins	:	https://www.bilibili.com/read/cv22443300

========================================================================================
	Change Log:
1.4.1 (21-March-2025)
	- 优化代码部分逻辑
1.4 (12-March-2025)
	- 修复开启 AFKCHECK 时的编译错误
1.3.1 (7-Oct-2023)
	- 修复菜单不能正确显示状态的bug
1.3 (7-Oct-2023)
	- 修复运行时后台会提示报错信息的bug
	- 增加新参数 l4d2_lookatweapon_way_sensitive ,开启后移动将会打断检视
	- 增加新参数 l4d2_lookatweapon_way_multishift ,可以指定SHIFT键敲击计数来开关检视,
		该参数优先于 l4d2_lookatweapon_way 参数中的 1 值设定。
1.2 (5-May-2023)
	- 增加了对播放静态动画的支持,可通过菜单播放指定动画
	- 增加了新参数 l4d2_lookatweapon_menu ,用于开关静态动画的支持
	- 增加了新编译选项 AFKCHECK ,用于选择是否编译新增的闲置自动触发检视
	- 在开启 AFKCHECK 编译后,增加了新参数 l4d2_lookatweapon_afk ,用于开关新增的闲置自动触发检视

1.1 (12-April-2023)
	- 增加了更多触发检视的按键,现支持4种方式

1.0 (16-March-2023)
	- Initial release.

==========================================================================================*/

#pragma semicolon 1
#pragma newdecls required

#include <sourcemod>
#include <sdktools>
#include <sdkhooks>

#define AFKCHECK 1	// 是否编译闲置自动触发检视
#define AFK_DURATION 10.0	// 在开启AFKCHECK的情况下,玩家静止几秒后认定处于闲置状态(浮点数)
#define SENSITIVE_DURATION 2.0 // 在开启 l4d2_lookatweapon_way_sensitive 的情况下,允许检视中保持移动多久

enum
{
	ENUM_KEYSHIFT	= (1 << 0),
	ENUM_COMMAND	= (1 << 1),
	ENUM_KEYF		= (1 << 2),
	ENUM_KEYR		= (1 << 3),
}

// Cvar Handles/Variables
static int iEntRef[MAXPLAYERS+1] = {INVALID_ENT_REFERENCE, ...};
static int iEntOwner[2048+1];
static bool g_bEntityState[MAXPLAYERS+1];
static int IMPULSE_FLASHLIGHT = 100;

#if AFKCHECK
	static float buttonTime[MAXPLAYERS+1];
	Handle AFK_Timer[MAXPLAYERS + 1];
	ConVar g_hCvarAfk;
	bool g_bCvarAfk;
#endif

ConVar g_hCvarAllow, g_hCvarHints, g_hCvarDist, g_hCvarWay, g_hCvarMenu, g_hCvarShiftCount, g_hCvarSensitive;
bool g_bCvarAllow, g_bCvarShiftCount, g_bCvarSensitive;
int g_iCvarHints, g_iCvarDist, g_iCvarWay, g_iCvarMenu, g_iCvarShiftCount;

int g_iLastButton[MAXPLAYERS+1], g_iLastSelect[MAXPLAYERS+1], g_iLastSequence[MAXPLAYERS+1], g_iSHIFTClickCount[MAXPLAYERS+1];
float g_fSHIFTTime[MAXPLAYERS+1],g_fSHIFTTime2[MAXPLAYERS+1],g_fKEYFTime[MAXPLAYERS+1],g_fKEYRTime[MAXPLAYERS+1];
bool g_bSensitiveDelet[MAXPLAYERS+1], g_bIsPlayingAnim[MAXPLAYERS+1];
Handle SHIFTClear_Timer[MAXPLAYERS+1];
Handle Sensitive_Timer[MAXPLAYERS+1];
Handle hTrie_Ammo = INVALID_HANDLE;
Handle hTrie_Sequence = INVALID_HANDLE;


// ====================================================================================================
//					PLUGIN INFO / START / END
// ====================================================================================================

public Plugin myinfo =
{
	name = "[L4D2] Look At Weapon Yourself",
	author = "Mengsk",
	description = "让你可以主动检视武器",
	version = "1.4.1",
	url = "https://space.bilibili.com/24447721"
}

public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max)
{
	EngineVersion test = GetEngineVersion();
	if( test != Engine_Left4Dead2 )
	{
		strcopy(error, err_max, "Plugin only supports Left 4 Dead 2.");
		return APLRes_SilentFailure;
	}
	return APLRes_Success;
}

public void OnPluginStart()
{
	g_hCvarAllow 		= CreateConVar("l4d2_lookatweapon_allow", "1", "0=Plugin off, 1=Plugin on.");
	g_hCvarHints 		= CreateConVar("l4d2_lookatweapon_hints", "0", "0=No message, 1=Print to chat, 2=Hint box.");
	g_hCvarDist 		= CreateConVar("l4d2_lookatweapon_distance", "30", "Distance the entity forward player.");
	g_hCvarWay 			= CreateConVar("l4d2_lookatweapon_way", "2", "Bitwise: 0=no way, 1=double SHIFT, 2=chat command, 4=double F, 8=double R when full ammo");
	g_hCvarShiftCount 	= CreateConVar("l4d2_lookatweapon_way_multishift", "2", "How much time SHIFT need hit to switch, should greater than 2, 0=close");
	g_hCvarSensitive 	= CreateConVar("l4d2_lookatweapon_way_sensitive", "0", "0=No sensitive, 1=ONLY ONE touch can disable lookatweapon state.");
	g_hCvarMenu 		= CreateConVar("l4d2_lookatweapon_menu", "1", "0=Menu disalbe, 1=Menu enable.");


	g_hCvarAllow.AddChangeHook(ConVarChanged_Allow);
	g_hCvarHints.AddChangeHook(ConVarChanged_Cvars);
	g_hCvarDist.AddChangeHook(ConVarChanged_Cvars);
	g_hCvarWay.AddChangeHook(ConVarChanged_Cvars);
	g_hCvarMenu.AddChangeHook(ConVarChanged_Cvars);
	g_hCvarShiftCount.AddChangeHook(ConVarChanged_Cvars);
	g_hCvarSensitive.AddChangeHook(ConVarChanged_Cvars);

#if AFKCHECK
	g_hCvarAfk = CreateConVar("l4d2_lookatweapon_afk", "0", "0=Afk check disalbe, 1=Afk check enable.(AVAILABLE ONLY WHEN AFKCHECK COMPILED)");
	g_hCvarAfk.AddChangeHook(ConVarChanged_Cvars);
#endif

  	// 生成cfg文件
	AutoExecConfig(true, "l4d2_lookatweapon");

	HookEvent("player_death", Event_PlayerDeath, EventHookMode_Pre);
	HookEvent("player_team", Event_TeamChange, EventHookMode_Post);
	HookEvent("round_start", Event_RoundStart, EventHookMode_Post);

	HookEvent("player_bot_replace", Event_PlayerBotReplace, EventHookMode_Pre);
	HookEvent("jockey_ride", Event_Interrupt, EventHookMode_Pre);
	HookEvent("charger_carry_start", Event_Interrupt, EventHookMode_Pre);
	HookEvent("charger_pummel_start", Event_Interrupt, EventHookMode_Pre);
	HookEvent("tongue_grab", Event_Interrupt, EventHookMode_Pre);
	HookEvent("player_ledge_grab", Event_Interrupt, EventHookMode_Pre);
	HookEvent("lunge_pounce", Event_Interrupt, EventHookMode_Pre);
	HookEvent("player_incapacitated_start", Event_Interrupt, EventHookMode_Pre);

 	hTrie_Ammo = CreateTrie();
	hTrie_Sequence = CreateTrie();

 	// 注册命令,可自行修改名称
 	RegConsoleCmd("sm_lookatweapon", cmdLookAtWeapon, "allow client to toggle LookAtWeapon status");
  	RegConsoleCmd("sm_lookatmenu", cmdLookAtWeaponMenu, "Show menu to set weapon sequence");
}

public void OnPluginEnd()
{
	for( int i = 1; i <= MaxClients; i++ )
		DeleteFakeEntity(i);
}

public void OnMapEnd()
{
	for( int i = 1; i <= MaxClients; i++ )
		DeleteFakeEntity(i);

#if AFKCHECK
	AllClientStopAFKTimer();
#endif
}

public void OnClientConnected(int client)
{
	g_iLastSelect[client] = -1;
	g_iLastSequence[client] = -1;
	g_iSHIFTClickCount[client] = 0;
	ShutdownTimer(SHIFTClear_Timer[client]);
	ShutdownTimer(Sensitive_Timer[client]);
}

public void OnClientPutInServer(int client)
{
	SDKHook(client, SDKHook_WeaponCanUse, Hook_WeaponCanUse);
	SDKHook(client, SDKHook_WeaponSwitch, Hook_WeaponSwitch);
}

public void OnClientDisconnect(int client)
{
	SDKUnhook(client, SDKHook_WeaponCanUse, Hook_WeaponCanUse);
	SDKUnhook(client, SDKHook_WeaponSwitch, Hook_WeaponSwitch);
	DeleteFakeEntity(client);
}
// ====================================================================================================
//					CVARS
// ====================================================================================================

public void OnConfigsExecuted()
{
	IsAllowed();
}

public void ConVarChanged_Allow(Handle convar, const char[] oldValue, const char[] newValue)
{
	IsAllowed();
}

public void ConVarChanged_Cvars(Handle convar, const char[] oldValue, const char[] newValue)
{
	GetCvars();
}

void GetCvars()
{
	g_iCvarHints = g_hCvarHints.IntValue;
	g_iCvarDist = g_hCvarDist.IntValue;
	g_iCvarWay = g_hCvarWay.IntValue;
	g_iCvarMenu = g_hCvarMenu.IntValue;
	g_iCvarShiftCount = g_hCvarShiftCount.IntValue > 1 ? g_hCvarShiftCount.IntValue - 1 : 1;
	g_bCvarShiftCount = g_hCvarShiftCount.IntValue > 0 ? true : false;
	g_bCvarSensitive = g_hCvarSensitive.BoolValue;

#if AFKCHECK
	g_bCvarAfk = g_hCvarAfk.BoolValue;
	if(g_bCvarAfk)
	{
		AllClientInitAFKTimer();
	}else{
		AllClientStopAFKTimer();
	}
#endif
}

void IsAllowed()
{
	bool bAllowCvar = g_hCvarAllow.BoolValue;
	GetCvars();

	if( g_bCvarAllow == false && bAllowCvar == true)
	{
		g_bCvarAllow = true;
	}
	else if( g_bCvarAllow == true && bAllowCvar == false)
	{
		g_bCvarAllow = false;
		for( int i = 1; i <= MaxClients; i++ )
		{
			DeleteFakeEntity(i);
		}
	}
}

// ====================================================================================================
//					ACTION
// ====================================================================================================

public Action Hook_WeaponCanUse(int client, int weapon)
{
	if(!g_bCvarAllow || GetClientTeam(client) != 2 || !IsValidEntity(weapon))	return Plugin_Continue;
	for (int i = 1; i <= MaxClients; i++) {
		if(IsValidEntRef(iEntRef[i]) && (EntRefToEntIndex(weapon) == EntRefToEntIndex(iEntRef[i])))
			return Plugin_Handled;
	}
	return Plugin_Continue;
}

public Action Hook_WeaponSwitch(int client, int weapon)
{
	if(!g_bCvarAllow || !g_iCvarMenu || GetClientTeam(client) != 2 || !IsValidEntity(weapon) || IsFakeClient(client))	return Plugin_Continue;

	char classname[32];
	int weaponid = GetEntPropEnt(client, Prop_Send, "m_hActiveWeapon");
	int Sequence = GetEntProp(GetEntPropEnt(client, Prop_Send, "m_hViewModel"), Prop_Send, "m_nSequence");

	if(weaponid == INVALID_ENT_REFERENCE) return Plugin_Continue;
	if(!GetEntityClassname(weaponid, classname, sizeof(classname))) return Plugin_Continue;
	if(hTrie_Sequence == INVALID_HANDLE) hTrie_Sequence = CreateTrie();
	SetTrieValue(hTrie_Sequence, classname, Sequence);

	// PrintToChat(client, "记录武器: %s 动画序列:%d", classname, Sequence);
	return Plugin_Continue;
}

public Action cmdLookAtWeapon(int client, int args)
{
	if (!client || !g_bCvarAllow || !IsClientInGame(client) || IsFakeClient(client) || GetClientTeam(client) != 2)	 return Plugin_Handled;
	if (g_iCvarWay & ENUM_COMMAND)	SwitchEntityState(client);
	return Plugin_Handled;
}

public Action cmdLookAtWeaponMenu(int client, int args)
{
	if (!client || !g_bCvarAllow || !g_iCvarMenu || !IsClientInGame(client) || IsFakeClient(client) || GetClientTeam(client) != 2)	 return Plugin_Handled;
	char classname[32];
	char text[64];
	char num[64];
	char status[64];
	// int ViewModel = GetEntPropEnt(client, Prop_Send, "m_hViewModel");
	int weapon = GetEntPropEnt(client, Prop_Send, "m_hActiveWeapon");
	if(!GetEntityClassname(weapon, classname, sizeof(classname))) return Plugin_Handled;

	Menu menu = new Menu(iLookAtWeaponMenuHandler);
	menu.SetTitle(classname);
	FormatEx(status, sizeof(status), "当前检视状态:%s", g_bEntityState[client] ? "☑" : "☐");
	menu.AddItem("w", status);
	menu.AddItem("q", "上次选择");
	for (int i = 1; i <= 35; i++) {
		Format(text, sizeof(text), "动画序列 %d", i-1);
		IntToString(i-1, num, sizeof(num));
		menu.AddItem(num, text);
	}

	menu.ExitBackButton = true;
	menu.Display(client, 20);
	return Plugin_Handled;
}

int iLookAtWeaponMenuHandler(Menu menu, MenuAction action, int client, int param2)
{
	char classname[32];
	int ViewModel = GetEntPropEnt(client, Prop_Send, "m_hViewModel");
	int weapon = GetEntPropEnt(client, Prop_Send, "m_hActiveWeapon");
	if(!GetEntityClassname(weapon, classname, sizeof(classname))) return 0;
	switch (action) {
		case MenuAction_Select:
		{
			char sParam[64];
			if(menu.GetItem(param2, sParam, sizeof(sParam)))
			{
				if(strcmp(sParam[0], "q") == 0)
				{
					if(g_iLastSelect[client] < 0){
						PrintToChat(client, "未获取到上次播放的序列", g_iLastSelect[client]);
						return 0;
					}
				}
				else if(strcmp(sParam[0], "w") == 0)
				{
					SwitchEntityState(client);
					return 0;
				}
				else
				{
					g_iLastSelect[client] = StringToInt(sParam);
				}
				if(g_iLastSelect[client] >= 0 && g_iLastSelect[client] <= 35)
				{
					int Sequence = GetEntProp(ViewModel, Prop_Send, "m_nSequence");
					if (Sequence > 4) {
						PrintToChat(client, "正在播放其他动画中");
						return 0;
					}
					g_iLastSequence[client] = Sequence;
					g_bIsPlayingAnim[client] = true;
					SetEntProp(ViewModel, Prop_Send, "m_nSequence", g_iLastSelect[client]);
					PrintToChat(client, "当前播放动画序列:%d", g_iLastSelect[client]);
				}
			}
		}
		case MenuAction_End:
			delete menu;
	}
	return 0;
}

public Action HideModel(int iEntity, int iClient)
{
	static int iOwner;
	iOwner = GetClientOfUserId(iEntOwner[iEntity]);

	if(iOwner < 1 || !IsClientInGame(iOwner))
		return Plugin_Handled;

 	// 仅玩家自身可见,这是为了避免服务器与客户端同步时去反复渲染此实体
	if(iOwner == iClient)
		return Plugin_Continue;

	return Plugin_Handled;
}

public Action Timer_SHIFTClear(Handle Timer, int client)
{
	g_iSHIFTClickCount[client] = 0;
	SHIFTClear_Timer[client] = INVALID_HANDLE;
	return Plugin_Stop;
}

public Action Timer_Sensitive(Handle Timer, int client)
{
	g_bSensitiveDelet[client] = true;
	Sensitive_Timer[client] = INVALID_HANDLE;
	return Plugin_Stop;
}

// ====================================================================================================
//					EVENTS
// ====================================================================================================

public void Event_PlayerDeath(Handle hEvent, const char[] sEventName, bool bDontBroadcast)
{
	int client = GetClientOfUserId(GetEventInt(hEvent, "userid"));

	if(!g_bCvarAllow || client < 1 || client > MaxClients || !IsClientInGame(client) || IsFakeClient(client) || GetClientTeam(client) != 2)
		return;

	DeleteFakeEntity(client);
}

public void Event_TeamChange(Handle hEvent, const char[] sEventName, bool bDontBroadcast)
{
	int client = GetClientOfUserId(GetEventInt(hEvent, "userid"));

	if(!g_bCvarAllow || client < 1 || client > MaxClients || !IsClientInGame(client))
		return;

	DeleteFakeEntity(client);

#if AFKCHECK
	if (g_bCvarAfk && !IsFakeClient(client))
	{
   		int team = GetEventInt(hEvent, "team");
		if (team == 2){	// 仅生还
			InitAFKTimer(client);
		}else{
			StopAFKTimer(client);
		}
	}
#endif
}

public void TP_OnThirdPersonChanged(int client, bool bIsThirdPerson)
{
	if(g_bCvarAllow && client)
		DeleteFakeEntity(client);
}

public void Event_RoundStart(Handle hEvent, const char[] sEventName, bool bDontBroadcast)
{
	for(int i = 1; i <= MaxClients; i++)
		DeleteFakeEntity(i);
}

public void Event_Interrupt(Handle event, const char[] name, bool dontBroadcast)
{
	int client = GetClientOfUserId(GetEventInt(event, name));

	if(g_bCvarAllow)
		DeleteFakeEntity(client);
}

public void Event_PlayerBotReplace(Handle event, const char[] name, bool dontBroadcast)
{
	int client = GetClientOfUserId(GetEventInt(event, "player"));
	int bot = GetClientOfUserId(GetEventInt(event, "bot"));

	if(g_bCvarAllow){
		DeleteFakeEntity(client);
		DeleteFakeEntity(bot);
	}
}

// ====================================================================================================
//					DYNAMIC ENTITY ON/OFF
// ====================================================================================================

public Action OnPlayerRunCmd(int client, int& buttons, int& impulse, float vel[3], float angles[3], int& weapon, int& subtype, int& cmdnum, int& tickcount, int& seed, int mouse[2])
{
	if( g_bCvarAllow && IsClientInGame(client) && !IsFakeClient(client))
	{
		int ref = iEntRef[client];

#if AFKCHECK
		static int iLastMouse[MAXPLAYERS+1][2];
		if(g_bCvarAfk)
		{
			if (mouse[0] != iLastMouse[client][0] || mouse[1] != iLastMouse[client][1])
			{
				iLastMouse[client][0] = mouse[0];
				iLastMouse[client][1] = mouse[1];
				SetButtonTime(client);
			}
			else if (buttons || impulse) SetButtonTime(client);
		}
#endif

		if (g_iCvarMenu && g_bIsPlayingAnim[client])
		{
			int ViewModel = GetEntPropEnt(client, Prop_Send, "m_hViewModel");
			if( buttons & (IN_ATTACK | IN_RELOAD | IN_ATTACK2 | IN_USE) ){
				SetEntProp(ViewModel, Prop_Send, "m_nSequence", GetOriginalSequence(client));
				g_bIsPlayingAnim[client] = false;
			}
		}

		if ((buttons & IN_ATTACK) == IN_ATTACK)
		{
			DeleteFakeEntity(client);
			buttons |= IN_ATTACK;
			return Plugin_Continue;
		}

		if (GetClientTeam(client) != 2 || !IsPlayerAlive(client) || (buttons & (IN_USE | IN_ATTACK2)))
		{
			DeleteFakeEntity(client);
			return Plugin_Continue;
		}

		if(g_bCvarSensitive)
		{
			if(g_bEntityState[client] && (buttons & (IN_FORWARD | IN_BACK | IN_LEFT | IN_RIGHT | IN_JUMP | IN_DUCK)))
			{
				if(Sensitive_Timer[client] == INVALID_HANDLE)
				{
					Sensitive_Timer[client] = CreateTimer(SENSITIVE_DURATION, Timer_Sensitive, client);
				}

				if(g_bSensitiveDelet[client])
				{
					g_bSensitiveDelet[client] = false;
					DeleteFakeEntity(client);
				}

				return Plugin_Continue;
			}
		}

		if((!g_bCvarShiftCount) && (g_iCvarWay & ENUM_KEYSHIFT))
		{
			int iButton = IN_SPEED;
			if ((buttons & iButton) && !(g_iLastButton[client] & iButton))	// 摁下时触发
			{
				float time = GetGameTime();
				if(time - g_fSHIFTTime[client] <= 0.5) SwitchEntityState(client);
				g_fSHIFTTime[client] = time;
			}
			// if ((g_iLastButton[client] & iButton) && !(buttons & iButton)){}	//松开时触发
		}

		if(g_iCvarWay & ENUM_KEYR)
		{
			int iButton2 = IN_RELOAD;
			if ((buttons & iButton2) && !(g_iLastButton[client] & iButton2))
			{
				if(IsActiveWeaponFullAmmo(client)){
					float time = GetGameTime();
					if(time - g_fKEYRTime[client] <= 0.5) SwitchEntityState(client);
					g_fKEYRTime[client] = time;
				}
			}
			// if ((g_iLastButton[client] & iButton2) && !(buttons & iButton2)){}	//松开时触发
		}

		if(g_bCvarShiftCount)
		{
			int iButton3 = IN_SPEED;
			if ((buttons & iButton3) && !(g_iLastButton[client] & iButton3))	// 摁下时触发
			{
				float time = GetGameTime();
				if(time - g_fSHIFTTime2[client] <= 0.3)	SHIFTClickCount(client); // 多击SHIFT时,每次敲击间隔限定0.3s
				g_fSHIFTTime2[client] = time;
			}
			// if ((g_iLastButton[client] & iButton3) && !(buttons & iButton3)){}	//松开时触发
		}

		g_iLastButton[client] = buttons;

		if(g_iCvarWay & ENUM_KEYF)
		{
			if(impulse == IMPULSE_FLASHLIGHT)
			{
				float time = GetGameTime();
				if(time - g_fKEYFTime[client] <= 0.5) SwitchEntityState(client);
				g_fKEYFTime[client] = time;
			}
		}

		if(IsValidEntRef(ref)){
			TeleportDynamicLight(client, EntRefToEntIndex(ref));
		}
	}

	return Plugin_Continue;
}

void DeleteFakeEntity(int client)
{
	g_bEntityState[client] = false;	// 泄露就泄露吧

	if(IsValidEntRef(iEntRef[client]))
	{
		DisplayHint(client, false);
		AcceptEntityInput(EntRefToEntIndex(iEntRef[client]), "kill");
	}

	iEntRef[client] = INVALID_ENT_REFERENCE;
}

void CreatFakeEntity(int client)
{
	// if(IsLimited(client)) return; // 非安全状态下无法开启检视,避免未知bug

	g_bEntityState[client] = true;

	int iEntity;

	if(IsValidEntRef(iEntRef[client]))
		return;

	iEntity = CreateEntityByName("weapon_propanetank");
  // 起初我想通过prop_dynamic_override实现,
  // 但无论如何没有实体的物体没办法触发检视,
  // 而Prop_physics_override又与本文的方法无本质差异,
  // 所以最后选择了煤气罐,
  // 因为是圆柱性,可以省去实体旋转的步骤,减少计算量。

	if(iEntity < 0)
		return;
	DisplayHint(client, true);

	DispatchKeyValue(iEntity, "solid", "0");	// 取消碰撞模式
	DispatchKeyValue(iEntity, "disableshadows", "1"); // 关闭阴影

	// 消除光圈
	SetEntProp(iEntity, Prop_Send, "m_iGlowType", 3);
	SetEntProp(iEntity, Prop_Send, "m_glowColorOverride", 1);
	SetEntProp(iEntity, Prop_Send, "m_nGlowRange", 0);

   // 设置碰撞组,仅与世界碰撞
	SetEntProp(iEntity, Prop_Send, "m_CollisionGroup", 1);
	SetEntProp(iEntity, Prop_Send, "m_nSolidType", 1);

	// 模型透明
	SetEntityRenderMode(iEntity, RENDER_NONE);
	SetEntityRenderColor(iEntity, 100, 100, 100, 100);

   // 生成并激活实体
	DispatchSpawn(iEntity);
	ActivateEntity(iEntity);

	// 固定模型
	SetEntityMoveType(iEntity, MOVETYPE_NONE);

	iEntRef[client] = EntIndexToEntRef(iEntity);
	iEntOwner[iEntity] = GetClientUserId(client);

	// 规避所有碰撞
	SDKHook(iEntity, SDKHook_ShouldCollide, ShouldCollide);

	// 模型对其他人不可见
	SDKHook(iEntity, SDKHook_SetTransmit, HideModel);
}

public bool ShouldCollide(int entity, int collisiongroup, int contentsmask, bool originalResult)
{
	return false;
}

void TeleportDynamicLight(int client, int entity)
{
	if (!IsValidEntity(entity))
		return;
	
	float vLoc[3], vPos[3], vAng[3];

	GetClientEyeAngles(client, vAng);
	GetClientEyePosition(client, vLoc);

	Handle trace;
	trace = TR_TraceRayFilterEx(vLoc, vAng, MASK_SHOT, RayType_Infinite, TraceFilter, client);

	if( TR_DidHit(trace) )
	{
		TR_GetEndPosition(vPos, trace);
		float fDist = GetVectorDistance(vLoc, vPos);

		if( fDist >= g_iCvarDist )
		{
			GetAngleVectors(vAng, vAng, NULL_VECTOR, NULL_VECTOR);
			vPos[0] = vLoc[0] + (vAng[0] * g_iCvarDist);
			vPos[1] = vLoc[1] + (vAng[1] * g_iCvarDist);
			vPos[2] = vLoc[2] + (vAng[2] * g_iCvarDist);
			TeleportEntity(entity, vPos, view_as<float>({-89.0, 0.0, 0.0}), NULL_VECTOR);
        // 把实体旋转89度后,在相同距离下,实际的体验效果会大大增加
		}
		else
		{
			TeleportEntity(entity, vPos, NULL_VECTOR, NULL_VECTOR);
		}
	}

	delete trace;
}

// ====================================================================================================
//					OTHERS
// ====================================================================================================

void DisplayHint(int client, bool type)
{
	switch ( g_iCvarHints )
	{
		case 1:		// Print To Chat
		{
			if(type)	PrintToChat(client, "[LookAtWeapon] On");
			if(!type)	PrintToChat(client, "[LookAtWeapon] Off");
		}
		case 2:		// Print Hint Text
		{
			if(type)	PrintHintText(client, "[LookAtWeapon] On");
			if(!type)	PrintHintText(client, "[LookAtWeapon] Off");
		}
	}
}

public void OnEntityCreated(int entity, const char[] classname)
{
    if(StrContains(classname, "weapon_", false) != -1) SDKHookEx(entity, SDKHook_SpawnPost, StoreMaxClip1);
}

public void StoreMaxClip1(int entity)
{
    if(hTrie_Ammo == INVALID_HANDLE) hTrie_Ammo = CreateTrie();
    if(entity <= MaxClients || !IsValidEntity(entity) || FindDataMapInfo(entity, "m_iClip1") == -1) return;

    char classname[30];
    if(!GetEntityClassname(entity, classname, sizeof(classname))) return;

    SetTrieValue(hTrie_Ammo, classname, GetEntProp(entity, Prop_Send, "m_iClip1"));
}

public void SHIFTClickCount(int client)
{
	g_iSHIFTClickCount[client]++;   // 计数+1

	if(g_iSHIFTClickCount[client] >= g_iCvarShiftCount)  // 计数达到预设值
	{
		SwitchEntityState(client);
		g_iSHIFTClickCount[client] = 0;
	}

	ShutdownTimer(SHIFTClear_Timer[client]);
	SHIFTClear_Timer[client] = CreateTimer(1.0, Timer_SHIFTClear, client);
}

void SwitchEntityState(int client)
{
	if(g_bEntityState[client]){
		DeleteFakeEntity(client);
	} else {
		CreatFakeEntity(client);

		if(g_bCvarSensitive)
		{
			ShutdownTimer(Sensitive_Timer[client]);
			Sensitive_Timer[client] = CreateTimer(SENSITIVE_DURATION, Timer_Sensitive, client);
		}
	}
}

int GetOriginalSequence(int client)
{
	int weapon = GetEntPropEnt(client, Prop_Send, "m_hActiveWeapon");
	int sequence = -1;
	char classname[32];
	if(IsValidEntity(weapon) && IsValidEdict(weapon) && GetEntityClassname(weapon, classname, sizeof(classname)))
	{
		if(GetTrieValue(hTrie_Sequence, classname, sequence)) {
			// PrintToChat(client, "GetOriginalSequence: 得到表数据 %d", sequence);
			return sequence;
		}
	}
	// PrintToChat(client, "GetOriginalSequence: 得到上次记录 %d", g_iLastSequence[client]);
	return g_iLastSequence[client];
}

static bool IsValidEntRef(int iRef)
{
	return (iRef != INVALID_ENT_REFERENCE && EntRefToEntIndex(iRef) != -1);
}

public bool TraceFilter(int entity, int contentsMask, any client)
{
	if( entity == client )
		return false;
	return true;
	// return (entity != client)
}

bool IsActiveWeaponFullAmmo(int client)
{
	int weapon = GetEntPropEnt(client, Prop_Send, "m_hActiveWeapon");
	int maxclip1 = -1;
	char classname[32];
	if (IsValidEntity(weapon) && IsValidEdict(weapon) && HasEntProp(weapon, Prop_Data, "m_iClip1"))
	{
		if(!GetEntityClassname(weapon, classname, sizeof(classname))) return false;
		if(!GetTrieValue(hTrie_Ammo, classname, maxclip1)) return false;
		return (GetEntProp(weapon, Prop_Send, "m_iClip1") >= maxclip1);
	}
	return false;
}

void ShutdownTimer(Handle& timer)
{
	if(timer != INVALID_HANDLE){
		KillTimer(timer);
		timer = INVALID_HANDLE;
	}
}

/*
bool IsLimited(int client)
{
	return (GetEntPropEnt(client, Prop_Send, "m_tongueVictim") > 0) // Smoker
		|| (GetEntPropEnt(client, Prop_Send, "m_pounceVictim") > 0) // Hunter
		|| (GetEntPropEnt(client, Prop_Send, "m_jockeyVictim") > 0) // Jockey
		|| (GetEntPropEnt(client, Prop_Send, "m_carryVictim") > 0) // Charger
		|| (GetEntPropEnt(client, Prop_Send, "m_pummelVictim") > 0) // Charger
		|| (GetEntProp(client, Prop_Send, "m_isHangingFromLedge") != 0 || GetEntProp(client, Prop_Send, "m_isFallingFromLedge") != 0) // 挂边
		|| (GetEntityMoveType(client) == MOVETYPE_LADDER) // 爬梯
		|| (GetEntProp(client, Prop_Send, "m_isIncapacitated", 1) == 1); // 倒地
}
*/

#if AFKCHECK
	public void AllClientInitAFKTimer()
	{
		for (int i = 1; i <= MaxClients; i++)
		{
			if(IsClientInGame(i) && GetClientTeam(i) == 2 && !IsFakeClient(i))
				InitAFKTimer(i);
		}
	}

	public void AllClientStopAFKTimer()
	{
		for (int i = 1; i <= MaxClients; i++)
		{
			StopAFKTimer(i);
		}
	}

	public void InitAFKTimer(int client)
	{
		if(AFK_Timer[client] != INVALID_HANDLE){
			KillTimer(AFK_Timer[client]);
			AFK_Timer[client] = INVALID_HANDLE;
		}
		SetButtonTime(client);
		AFK_Timer[client] = CreateTimer(1.0, Timer_AFK, client, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE);
	}

	public void StopAFKTimer(int client)
	{
		if(AFK_Timer[client] != INVALID_HANDLE){
			KillTimer(AFK_Timer[client]);
			AFK_Timer[client] = INVALID_HANDLE;
		}

		DeleteFakeEntity(client);
	}

	public Action Timer_AFK(Handle Timer, int client)
	{
		if (IsPlayerAfk(client)) {
			if (!g_bEntityState[client])
			{
				CreatFakeEntity(client);
			}
		} else if (!g_bCvarSensitive) {	// 仅当另一个检测没有生效时,才会退出检视
			if (g_bEntityState[client])
			{
				DeleteFakeEntity(client);
			}
		}
		return Plugin_Continue;
	}

	void SetButtonTime(int client)
	{
		buttonTime[client] = GetEngineTime();
	}

	bool IsPlayerAfk(int client)
	{
		return GetEngineTime() - buttonTime[client] > AFK_DURATION;
	}
#endif
复制成功

成品下载: 

本插件有几处编译选项可开关,此为其中一种方案的编译成品供参考https://github.com/MengskQAQ/L4D2-Plugins/releases

参考文档: 

[1] https://github.com/ValveSoftware/source-sdk-2013/blob/master/mp/src/public/const.h

[2] https://developer.valvesoftware.com/wiki/Prop_physics_override

[3] https://forums.alliedmods.net/showthread.php?t=186558

[4] https://forums.alliedmods.net/showthread.php?t=216894