|
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). |
5 | 5 | * |
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 | +**/ |
11 | 21 |
|
12 | 22 | #pragma semicolon 1 |
13 | 23 | #pragma newdecls required |
14 | 24 |
|
15 | 25 | #include <sourcemod> |
16 | 26 | #include <sdktools> |
17 | 27 |
|
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" |
19 | 31 |
|
20 | 32 | int |
21 | | - decalThisTick = 0, |
22 | | - iLastTick = 0, |
23 | 33 | 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 = |
26 | 49 | { |
27 | 50 | 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)", |
31 | 54 | url = "https://github.com/SirPlease/L4D2-Competitive-Rework" |
32 | 55 | }; |
33 | 56 |
|
34 | 57 | public void OnPluginStart() |
35 | 58 | { |
| 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 | + |
36 | 94 | 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(); |
41 | 105 | } |
42 | 106 |
|
43 | 107 | public void OnMapStart() |
44 | 108 | { |
| 109 | + ClearAllData(); |
| 110 | + |
45 | 111 | if (!IsDecalPrecached(DECAL_NAME)) { |
46 | 112 | g_iPrecacheDecal = PrecacheDecal(DECAL_NAME, true); //true or false? |
47 | 113 | } |
48 | 114 | } |
49 | 115 |
|
50 | | -void EventRoundReset(Event hEvent, const char[] name, bool dontBroadcast) |
| 116 | +public void OnMapEnd() |
51 | 117 | { |
52 | | - decalThisTick = 0; |
53 | | - iLastTick = 0; |
| 118 | + ClearAllData(); |
54 | 119 | } |
55 | 120 |
|
56 | | -void BulletImpactEvent(Event hEvent, const char[] name, bool dontBroadcast) |
| 121 | +public void OnGameFrame() |
57 | 122 | { |
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 | + **/ |
61 | 128 |
|
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 | +} |
67 | 132 |
|
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; |
71 | 137 | } |
72 | 138 |
|
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; |
78 | 141 | } |
79 | 142 |
|
80 | | -Action TimerDelayShowDecal(Handle hTimer, ArrayStack hStack) |
| 143 | +void SendSendQueueDecals() |
81 | 144 | { |
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; |
88 | 162 | } |
| 163 | + |
| 164 | + iMaxPerTick = iCvarValue / 2; |
89 | 165 | } |
90 | 166 |
|
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(); |
92 | 201 | } |
93 | 202 |
|
94 | | -void SendDecal(int client, float pos[3]) |
| 203 | +void Event_BulletImpact(Event hEvent, const char[] sEventName, bool bDontBroadcast) |
95 | 204 | { |
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); |
99 | 232 | 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(); |
101 | 286 | } |
0 commit comments