Skip to content

Commit cf55b1c

Browse files
committed
[visualise_impacts] Improved code for sending temporary entities.
1) Change "BSP Decal" to "World Decal" allows you to use command `r_cleardecal` for clearing. 2) Now when sending temporary entities, cvar `sv_multiplayer_maxtempentities` is taken into account; previously, the code ignored this and created a bunch of timers. 3) Attempting to add code to automatically clean up temporary entities.
1 parent 2df6ccc commit cf55b1c

2 files changed

Lines changed: 267 additions & 54 deletions

File tree

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"Games"
2+
{
3+
"left4dead2"
4+
{
5+
"Signatures"
6+
{
7+
/** @A1m:
8+
* [Windows]
9+
* To locate the `CBaseEntity::RemoveAllDecals` function, follow these steps:
10+
* 1. Find the `CC_Ent_Create` function — it can be identified by the string "Cannot ent_create players!\n".
11+
* 2. Near the end of `CC_Ent_Create`, there is a call to `UTIL_DropToFloor`. Check the xrefs to this function.
12+
* 3. Among the cross-references, look for `CItem::Respawn` (usually the second-to-last one).
13+
* 4. Inside `CItem::Respawn`, you will find a call to `CBaseEntity::RemoveAllDecals`.
14+
*
15+
* Signature:
16+
* void CBaseEntity::RemoveAllDecals( void )
17+
*/
18+
"CBaseEntity::RemoveAllDecals"
19+
{
20+
"library" "server"
21+
22+
"linux" "@_ZN11CBaseEntity15RemoveAllDecalsEv"
23+
/* 6A 00 51 E8 ? ? ? ? 6A 01 */
24+
"windows" "\x6A\x00\x51\xE8\x2A\x2A\x2A\x2A\x6A\x01"
25+
}
26+
}
27+
}
28+
}
Lines changed: 239 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,101 +1,286 @@
1-
/* @A1m`:
2-
* We cannot send to the client temporary objects larger than specified in cvar 'sv_multiplayer_maxtempentities'.
3-
* A large number of decals will not be displayed if you do not set a delay in sending,
4-
* or we need to increase the cvar 'sv_multiplayer_maxtempentities' value, by default it is 32 (we can set 255).
1+
/** @A1m`:
2+
* The engine does not allow sending temporary entities larger than the value set in the cvar 'sv_multiplayer_maxtempentities'.
3+
* If too many decals are sent in a single tick, some will not be displayed unless we add a delay,
4+
* or increase the cvar value (default is 32, can be raised up to 255).
55
*
6-
* TE_SendToClient with the set delay does not fix this issue.
7-
* Now the plugin shows all impacts correctly.
8-
* The plugin also correctly resets this delay with some time, so we don't get high delay.
9-
* Fix plugin not working after loading the map, it was necessary to constantly reload it.
10-
*/
6+
* Note: Using TE_SendToClient with a delay alone does not fix this issue.
7+
*
8+
* This plugin solves the problem by properly queuing decals, so all bullet impacts are displayed.
9+
* The delay is cleared automatically after a short period, so it won’t accumulate.
10+
* Additionally, the plugin fixes an issue where it would stop working after map load (previously it required a manual reload)
11+
* (Add PrecacheDecal in `OnMapStart`).
12+
*
13+
* The plugin now supports automatic decal removal after the configured time period.
14+
*
15+
* Original code & Notes (Author Jahze): https://github.com/Jahze/l4d2_plugins/tree/master/spread_patch
16+
*
17+
* Note: For some reason, calling function `CBaseEntity::RemoveAllDecals` for the client doesn't work to clear decals.
18+
* Note: Use command `r_removedecals` for client to clean old decals.
19+
*
20+
**/
1121

1222
#pragma semicolon 1
1323
#pragma newdecls required
1424

1525
#include <sourcemod>
1626
#include <sdktools>
1727

18-
#define DECAL_NAME "materials/decals/metal/metal01b.vtf"
28+
#define GAMEDATA_FILE "visualise_impacts"
29+
#define REMOVEALLDECALS_SIGN "CBaseEntity::RemoveAllDecals"
30+
#define DECAL_NAME "materials/decals/metal/metal01b.vtf"
1931

2032
int
21-
decalThisTick = 0,
22-
iLastTick = 0,
2333
g_iPrecacheDecal = 0;
24-
25-
public Plugin myinfo =
34+
35+
float
36+
g_hRemoveDecalsTime = 0.0;
37+
38+
ConVar
39+
g_hCvarMultiplayerMaxTempEnts = null,
40+
g_hCvarRemoveDecalsTime = null;
41+
42+
Handle
43+
g_hCallRemoveAllDecals = null;
44+
45+
ArrayList
46+
g_DecalQueue = null;
47+
48+
public Plugin myinfo =
2649
{
2750
name = "Visualise impacts",
28-
author = "Jahze?, A1m`",
29-
version = "1.3",
30-
description = "See name",
51+
author = "A1m`",
52+
version = "1.6",
53+
description = "Shows bullet impacts (based on the original by Jahze, fully rewritten and improved)",
3154
url = "https://github.com/SirPlease/L4D2-Competitive-Rework"
3255
};
3356

3457
public void OnPluginStart()
3558
{
59+
InitGameData();
60+
InitPlugin();
61+
62+
g_hCvarRemoveDecalsTime = CreateConVar("l4d_remove_decals_time", "20.0", "After what time will the decals be removed? (0 for disable)", _, true, 0.0, true, 320.0);
63+
64+
RegConsoleCmd("sm_remove_decals", Cmd_RemoveDecals);
65+
}
66+
67+
void InitGameData()
68+
{
69+
GameData hGameData = new GameData(GAMEDATA_FILE);
70+
if (hGameData == null) {
71+
SetFailState("Could not load gamedata/%s.txt", GAMEDATA_FILE);
72+
}
73+
74+
StartPrepSDKCall(SDKCall_Entity);
75+
76+
if (!PrepSDKCall_SetFromConf(hGameData, SDKConf_Signature, REMOVEALLDECALS_SIGN)) {
77+
SetFailState("Function '%s' not found", REMOVEALLDECALS_SIGN);
78+
}
79+
80+
g_hCallRemoveAllDecals = EndPrepSDKCall();
81+
if (g_hCallRemoveAllDecals == null) {
82+
SetFailState("Function '%s' found, but something went wrong", REMOVEALLDECALS_SIGN);
83+
}
84+
85+
delete hGameData;
86+
}
87+
88+
void InitPlugin()
89+
{
90+
g_hCvarMultiplayerMaxTempEnts = FindConVar("sv_multiplayer_maxtempentities");
91+
92+
g_DecalQueue = new ArrayList();
93+
3694
g_iPrecacheDecal = PrecacheDecal(DECAL_NAME, true);
37-
38-
HookEvent("bullet_impact", BulletImpactEvent, EventHookMode_Post);
39-
HookEvent("round_start", EventRoundReset, EventHookMode_PostNoCopy);
40-
HookEvent("round_end", EventRoundReset, EventHookMode_PostNoCopy);
95+
96+
HookEvent("bullet_impact", Event_BulletImpact, EventHookMode_Post);
97+
98+
HookEvent("round_start", Event_RoundChangeState, EventHookMode_PostNoCopy);
99+
HookEvent("round_end", Event_RoundChangeState, EventHookMode_PostNoCopy);
100+
}
101+
102+
public void OnPluginEnd()
103+
{
104+
ClearAllData();
41105
}
42106

43107
public void OnMapStart()
44108
{
109+
ClearAllData();
110+
45111
if (!IsDecalPrecached(DECAL_NAME)) {
46112
g_iPrecacheDecal = PrecacheDecal(DECAL_NAME, true); //true or false?
47113
}
48114
}
49115

50-
void EventRoundReset(Event hEvent, const char[] name, bool dontBroadcast)
116+
public void OnMapEnd()
51117
{
52-
decalThisTick = 0;
53-
iLastTick = 0;
118+
ClearAllData();
54119
}
55120

56-
void BulletImpactEvent(Event hEvent, const char[] name, bool dontBroadcast)
121+
public void OnGameFrame()
57122
{
58-
float pos[3];
59-
int userid = hEvent.GetInt("userid");
60-
//int client = GetClientOfUserId(userid);
123+
/** @A1m`:
124+
* We only use half the possible value for reliability if any other decals were sent.
125+
* We use a function `OnGameFrame` instead of creating a bunch of timers,
126+
* and no longer ignore cvar `sv_multiplayer_maxtempentities`.
127+
**/
61128

62-
pos[0] = hEvent.GetFloat("x");
63-
pos[1] = hEvent.GetFloat("y");
64-
pos[2] = hEvent.GetFloat("z");
65-
66-
int iTick = GetGameTickCount();
129+
SendSendQueueDecals();
130+
ShouldRemoveAllDecals();
131+
}
67132

68-
if (iTick != iLastTick) {
69-
decalThisTick = 0;
70-
iLastTick = iTick;
133+
void ShouldRemoveAllDecals()
134+
{
135+
if (g_hRemoveDecalsTime <= 0.5 || GetGameTime() < g_hRemoveDecalsTime) {
136+
return;
71137
}
72138

73-
ArrayStack hStack = new ArrayStack(sizeof(pos));
74-
hStack.PushArray(pos[0], sizeof(pos));
75-
hStack.Push(userid);
76-
77-
CreateTimer(++decalThisTick * GetTickInterval(), TimerDelayShowDecal, hStack, TIMER_FLAG_NO_MAPCHANGE | TIMER_HNDL_CLOSE);
139+
RemoveAllDecalsForAll();
140+
g_hRemoveDecalsTime = 0.0;
78141
}
79142

80-
Action TimerDelayShowDecal(Handle hTimer, ArrayStack hStack)
143+
void SendSendQueueDecals()
81144
{
82-
if (!hStack.Empty) {
83-
int client = GetClientOfUserId(hStack.Pop());
84-
if (client > 0) {
85-
float pos[3];
86-
hStack.PopArray(pos[0], sizeof(pos));
87-
SendDecal(client, pos);
145+
if (g_DecalQueue.Length <= 0) {
146+
return;
147+
}
148+
149+
int iMaxPerTick = 32 / 2; // 32 - default value
150+
151+
if (g_hCvarMultiplayerMaxTempEnts != null) {
152+
int iCvarValue = g_hCvarMultiplayerMaxTempEnts.IntValue;
153+
154+
// Disabled?
155+
// We protect against division by zero and guarantee that at least one decal will be send.
156+
if (iCvarValue < 1) {
157+
return;
158+
}
159+
160+
if (iCvarValue < 2) {
161+
iCvarValue = 2;
88162
}
163+
164+
iMaxPerTick = iCvarValue / 2;
89165
}
90166

91-
return Plugin_Stop;
167+
int iProcessed = 0;
168+
169+
while (g_DecalQueue.Length > 0 && iProcessed < iMaxPerTick) {
170+
DataPack hDp = g_DecalQueue.Get(0);
171+
172+
if (hDp != null) {
173+
hDp.Reset();
174+
175+
int iClient = GetClientOfUserId(hDp.ReadCell());
176+
if (iClient > 0) {
177+
float fPos[3];
178+
hDp.ReadFloatArray(fPos, sizeof(fPos));
179+
180+
SendDecal(iClient, fPos);
181+
}
182+
}
183+
184+
CloseHandle(hDp);
185+
g_DecalQueue.Erase(0);
186+
iProcessed++;
187+
}
188+
}
189+
190+
Action Cmd_RemoveDecals(int iClient, int iArgs)
191+
{
192+
RemoveAllDecalsForAll();
193+
194+
ReplyToCommand(iClient, "All decals removed successfully!");
195+
return Plugin_Handled;
196+
}
197+
198+
void Event_RoundChangeState(Event hEvent, const char[] sEventName, bool bDontBroadcast)
199+
{
200+
ClearAllData();
92201
}
93202

94-
void SendDecal(int client, float pos[3])
203+
void Event_BulletImpact(Event hEvent, const char[] sEventName, bool bDontBroadcast)
95204
{
96-
TE_Start("BSP Decal");
97-
TE_WriteVector("m_vecOrigin", pos);
98-
TE_WriteNum("m_nEntity", 0);
205+
int iUserId = hEvent.GetInt("userid");
206+
207+
float fPos[3];
208+
fPos[0] = hEvent.GetFloat("x");
209+
fPos[1] = hEvent.GetFloat("y");
210+
fPos[2] = hEvent.GetFloat("z");
211+
212+
DataPack hDp = new DataPack();
213+
hDp.WriteCell(iUserId);
214+
hDp.WriteFloatArray(fPos, sizeof(fPos), false);
215+
216+
g_DecalQueue.Push(hDp);
217+
218+
g_hRemoveDecalsTime = GetGameTime() + g_hCvarRemoveDecalsTime.FloatValue;
219+
}
220+
221+
void SendDecal(int iClient, float fPos[3])
222+
{
223+
/** @A1m`:
224+
* "World Decal" instead of "BSP Decal" allows you to use command `r_cleardecal` for clearing.
225+
* Command `r_cleardecal` cannot be executed by the server only by the client. =(
226+
* But it seems like it's impossible to clean "BSP Decal" at all.
227+
**/
228+
229+
TE_Start("World Decal");
230+
231+
TE_WriteVector("m_vecOrigin", fPos);
99232
TE_WriteNum("m_nIndex", g_iPrecacheDecal);
100-
TE_SendToClient(client, 0.0);
233+
234+
TE_SendToClient(iClient, 0.0);
235+
236+
g_hRemoveDecalsTime = GetGameTime() + g_hCvarRemoveDecalsTime.FloatValue;
237+
}
238+
239+
void RemoveAllDecalsForAll()
240+
{
241+
for (int iIter = 1; iIter <= MaxClients; iIter++) {
242+
if (!IsClientInGame(iIter) || IsFakeClient(iIter)) {
243+
continue;
244+
}
245+
246+
RemoveAllDecals(iIter);
247+
}
248+
}
249+
250+
/** @A1m`:
251+
* I don't know how to implement this code in sourcemod, it seems impossible,
252+
* it takes the index of the entity class instead of the usermessage index.
253+
*
254+
#define BASEENTITY_MSG_REMOVE_DECALS 1
255+
256+
void CBaseEntity::RemoveAllDecals( void )
257+
{
258+
EntityMessageBegin( this );
259+
MessageWriteByte( BASEENTITY_MSG_REMOVE_DECALS );
260+
MessageEnd();
261+
}
262+
**/
263+
264+
void RemoveAllDecals(int iClient)
265+
{
266+
PrintToChat(iClient, "[Note] Use command `r_removedecals` for client to clean old decals.");
267+
268+
SDKCall(g_hCallRemoveAllDecals, iClient);
269+
}
270+
271+
void ClearAllData()
272+
{
273+
g_hRemoveDecalsTime = 0.0;
274+
275+
for (int iIter = 0; iIter < g_DecalQueue.Length; iIter++) {
276+
DataPack hDp = g_DecalQueue.Get(0);
277+
278+
if (hDp != null) {
279+
CloseHandle(hDp);
280+
}
281+
282+
g_DecalQueue.Erase(0);
283+
}
284+
285+
g_DecalQueue.Clear();
101286
}

0 commit comments

Comments
 (0)