From cf55b1c0689d7ef94e9f4f95bde748897a0a4a35 Mon Sep 17 00:00:00 2001 From: A1mDev <33463136+A1mDev@users.noreply.github.com> Date: Sat, 4 Oct 2025 17:51:01 +0700 Subject: [PATCH 1/2] [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. --- .../sourcemod/gamedata/visualise_impacts.txt | 28 ++ .../sourcemod/scripting/visualise_impacts.sp | 293 ++++++++++++++---- 2 files changed, 267 insertions(+), 54 deletions(-) create mode 100644 addons/sourcemod/gamedata/visualise_impacts.txt diff --git a/addons/sourcemod/gamedata/visualise_impacts.txt b/addons/sourcemod/gamedata/visualise_impacts.txt new file mode 100644 index 000000000..fa8bc3bd0 --- /dev/null +++ b/addons/sourcemod/gamedata/visualise_impacts.txt @@ -0,0 +1,28 @@ +"Games" +{ + "left4dead2" + { + "Signatures" + { + /** @A1m: + * [Windows] + * To locate the `CBaseEntity::RemoveAllDecals` function, follow these steps: + * 1. Find the `CC_Ent_Create` function — it can be identified by the string "Cannot ent_create players!\n". + * 2. Near the end of `CC_Ent_Create`, there is a call to `UTIL_DropToFloor`. Check the xrefs to this function. + * 3. Among the cross-references, look for `CItem::Respawn` (usually the second-to-last one). + * 4. Inside `CItem::Respawn`, you will find a call to `CBaseEntity::RemoveAllDecals`. + * + * Signature: + * void CBaseEntity::RemoveAllDecals( void ) + */ + "CBaseEntity::RemoveAllDecals" + { + "library" "server" + + "linux" "@_ZN11CBaseEntity15RemoveAllDecalsEv" + /* 6A 00 51 E8 ? ? ? ? 6A 01 */ + "windows" "\x6A\x00\x51\xE8\x2A\x2A\x2A\x2A\x6A\x01" + } + } + } +} \ No newline at end of file diff --git a/addons/sourcemod/scripting/visualise_impacts.sp b/addons/sourcemod/scripting/visualise_impacts.sp index 7e0d2728d..57ab65d26 100644 --- a/addons/sourcemod/scripting/visualise_impacts.sp +++ b/addons/sourcemod/scripting/visualise_impacts.sp @@ -1,13 +1,23 @@ -/* @A1m`: - * We cannot send to the client temporary objects larger than specified in cvar 'sv_multiplayer_maxtempentities'. - * A large number of decals will not be displayed if you do not set a delay in sending, - * or we need to increase the cvar 'sv_multiplayer_maxtempentities' value, by default it is 32 (we can set 255). +/** @A1m`: + * The engine does not allow sending temporary entities larger than the value set in the cvar 'sv_multiplayer_maxtempentities'. + * If too many decals are sent in a single tick, some will not be displayed unless we add a delay, + * or increase the cvar value (default is 32, can be raised up to 255). * - * TE_SendToClient with the set delay does not fix this issue. - * Now the plugin shows all impacts correctly. - * The plugin also correctly resets this delay with some time, so we don't get high delay. - * Fix plugin not working after loading the map, it was necessary to constantly reload it. -*/ + * Note: Using TE_SendToClient with a delay alone does not fix this issue. + * + * This plugin solves the problem by properly queuing decals, so all bullet impacts are displayed. + * The delay is cleared automatically after a short period, so it won’t accumulate. + * Additionally, the plugin fixes an issue where it would stop working after map load (previously it required a manual reload) + * (Add PrecacheDecal in `OnMapStart`). + * + * The plugin now supports automatic decal removal after the configured time period. + * + * Original code & Notes (Author Jahze): https://github.com/Jahze/l4d2_plugins/tree/master/spread_patch + * + * Note: For some reason, calling function `CBaseEntity::RemoveAllDecals` for the client doesn't work to clear decals. + * Note: Use command `r_removedecals` for client to clean old decals. + * +**/ #pragma semicolon 1 #pragma newdecls required @@ -15,87 +25,262 @@ #include #include -#define DECAL_NAME "materials/decals/metal/metal01b.vtf" +#define GAMEDATA_FILE "visualise_impacts" +#define REMOVEALLDECALS_SIGN "CBaseEntity::RemoveAllDecals" +#define DECAL_NAME "materials/decals/metal/metal01b.vtf" int - decalThisTick = 0, - iLastTick = 0, g_iPrecacheDecal = 0; - -public Plugin myinfo = + +float + g_hRemoveDecalsTime = 0.0; + +ConVar + g_hCvarMultiplayerMaxTempEnts = null, + g_hCvarRemoveDecalsTime = null; + +Handle + g_hCallRemoveAllDecals = null; + +ArrayList + g_DecalQueue = null; + +public Plugin myinfo = { name = "Visualise impacts", - author = "Jahze?, A1m`", - version = "1.3", - description = "See name", + author = "A1m`", + version = "1.6", + description = "Shows bullet impacts (based on the original by Jahze, fully rewritten and improved)", url = "https://github.com/SirPlease/L4D2-Competitive-Rework" }; public void OnPluginStart() { + InitGameData(); + InitPlugin(); + + 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); + + RegConsoleCmd("sm_remove_decals", Cmd_RemoveDecals); +} + +void InitGameData() +{ + GameData hGameData = new GameData(GAMEDATA_FILE); + if (hGameData == null) { + SetFailState("Could not load gamedata/%s.txt", GAMEDATA_FILE); + } + + StartPrepSDKCall(SDKCall_Entity); + + if (!PrepSDKCall_SetFromConf(hGameData, SDKConf_Signature, REMOVEALLDECALS_SIGN)) { + SetFailState("Function '%s' not found", REMOVEALLDECALS_SIGN); + } + + g_hCallRemoveAllDecals = EndPrepSDKCall(); + if (g_hCallRemoveAllDecals == null) { + SetFailState("Function '%s' found, but something went wrong", REMOVEALLDECALS_SIGN); + } + + delete hGameData; +} + +void InitPlugin() +{ + g_hCvarMultiplayerMaxTempEnts = FindConVar("sv_multiplayer_maxtempentities"); + + g_DecalQueue = new ArrayList(); + g_iPrecacheDecal = PrecacheDecal(DECAL_NAME, true); - - HookEvent("bullet_impact", BulletImpactEvent, EventHookMode_Post); - HookEvent("round_start", EventRoundReset, EventHookMode_PostNoCopy); - HookEvent("round_end", EventRoundReset, EventHookMode_PostNoCopy); + + HookEvent("bullet_impact", Event_BulletImpact, EventHookMode_Post); + + HookEvent("round_start", Event_RoundChangeState, EventHookMode_PostNoCopy); + HookEvent("round_end", Event_RoundChangeState, EventHookMode_PostNoCopy); +} + +public void OnPluginEnd() +{ + ClearAllData(); } public void OnMapStart() { + ClearAllData(); + if (!IsDecalPrecached(DECAL_NAME)) { g_iPrecacheDecal = PrecacheDecal(DECAL_NAME, true); //true or false? } } -void EventRoundReset(Event hEvent, const char[] name, bool dontBroadcast) +public void OnMapEnd() { - decalThisTick = 0; - iLastTick = 0; + ClearAllData(); } -void BulletImpactEvent(Event hEvent, const char[] name, bool dontBroadcast) +public void OnGameFrame() { - float pos[3]; - int userid = hEvent.GetInt("userid"); - //int client = GetClientOfUserId(userid); + /** @A1m`: + * We only use half the possible value for reliability if any other decals were sent. + * We use a function `OnGameFrame` instead of creating a bunch of timers, + * and no longer ignore cvar `sv_multiplayer_maxtempentities`. + **/ - pos[0] = hEvent.GetFloat("x"); - pos[1] = hEvent.GetFloat("y"); - pos[2] = hEvent.GetFloat("z"); - - int iTick = GetGameTickCount(); + SendSendQueueDecals(); + ShouldRemoveAllDecals(); +} - if (iTick != iLastTick) { - decalThisTick = 0; - iLastTick = iTick; +void ShouldRemoveAllDecals() +{ + if (g_hRemoveDecalsTime <= 0.5 || GetGameTime() < g_hRemoveDecalsTime) { + return; } - ArrayStack hStack = new ArrayStack(sizeof(pos)); - hStack.PushArray(pos[0], sizeof(pos)); - hStack.Push(userid); - - CreateTimer(++decalThisTick * GetTickInterval(), TimerDelayShowDecal, hStack, TIMER_FLAG_NO_MAPCHANGE | TIMER_HNDL_CLOSE); + RemoveAllDecalsForAll(); + g_hRemoveDecalsTime = 0.0; } -Action TimerDelayShowDecal(Handle hTimer, ArrayStack hStack) +void SendSendQueueDecals() { - if (!hStack.Empty) { - int client = GetClientOfUserId(hStack.Pop()); - if (client > 0) { - float pos[3]; - hStack.PopArray(pos[0], sizeof(pos)); - SendDecal(client, pos); + if (g_DecalQueue.Length <= 0) { + return; + } + + int iMaxPerTick = 32 / 2; // 32 - default value + + if (g_hCvarMultiplayerMaxTempEnts != null) { + int iCvarValue = g_hCvarMultiplayerMaxTempEnts.IntValue; + + // Disabled? + // We protect against division by zero and guarantee that at least one decal will be send. + if (iCvarValue < 1) { + return; + } + + if (iCvarValue < 2) { + iCvarValue = 2; } + + iMaxPerTick = iCvarValue / 2; } - return Plugin_Stop; + int iProcessed = 0; + + while (g_DecalQueue.Length > 0 && iProcessed < iMaxPerTick) { + DataPack hDp = g_DecalQueue.Get(0); + + if (hDp != null) { + hDp.Reset(); + + int iClient = GetClientOfUserId(hDp.ReadCell()); + if (iClient > 0) { + float fPos[3]; + hDp.ReadFloatArray(fPos, sizeof(fPos)); + + SendDecal(iClient, fPos); + } + } + + CloseHandle(hDp); + g_DecalQueue.Erase(0); + iProcessed++; + } +} + +Action Cmd_RemoveDecals(int iClient, int iArgs) +{ + RemoveAllDecalsForAll(); + + ReplyToCommand(iClient, "All decals removed successfully!"); + return Plugin_Handled; +} + +void Event_RoundChangeState(Event hEvent, const char[] sEventName, bool bDontBroadcast) +{ + ClearAllData(); } -void SendDecal(int client, float pos[3]) +void Event_BulletImpact(Event hEvent, const char[] sEventName, bool bDontBroadcast) { - TE_Start("BSP Decal"); - TE_WriteVector("m_vecOrigin", pos); - TE_WriteNum("m_nEntity", 0); + int iUserId = hEvent.GetInt("userid"); + + float fPos[3]; + fPos[0] = hEvent.GetFloat("x"); + fPos[1] = hEvent.GetFloat("y"); + fPos[2] = hEvent.GetFloat("z"); + + DataPack hDp = new DataPack(); + hDp.WriteCell(iUserId); + hDp.WriteFloatArray(fPos, sizeof(fPos), false); + + g_DecalQueue.Push(hDp); + + g_hRemoveDecalsTime = GetGameTime() + g_hCvarRemoveDecalsTime.FloatValue; +} + +void SendDecal(int iClient, float fPos[3]) +{ + /** @A1m`: + * "World Decal" instead of "BSP Decal" allows you to use command `r_cleardecal` for clearing. + * Command `r_cleardecal` cannot be executed by the server only by the client. =( + * But it seems like it's impossible to clean "BSP Decal" at all. + **/ + + TE_Start("World Decal"); + + TE_WriteVector("m_vecOrigin", fPos); TE_WriteNum("m_nIndex", g_iPrecacheDecal); - TE_SendToClient(client, 0.0); + + TE_SendToClient(iClient, 0.0); + + g_hRemoveDecalsTime = GetGameTime() + g_hCvarRemoveDecalsTime.FloatValue; +} + +void RemoveAllDecalsForAll() +{ + for (int iIter = 1; iIter <= MaxClients; iIter++) { + if (!IsClientInGame(iIter) || IsFakeClient(iIter)) { + continue; + } + + RemoveAllDecals(iIter); + } +} + +/** @A1m`: + * I don't know how to implement this code in sourcemod, it seems impossible, + * it takes the index of the entity class instead of the usermessage index. + * +#define BASEENTITY_MSG_REMOVE_DECALS 1 + +void CBaseEntity::RemoveAllDecals( void ) +{ + EntityMessageBegin( this ); + MessageWriteByte( BASEENTITY_MSG_REMOVE_DECALS ); + MessageEnd(); +} +**/ + +void RemoveAllDecals(int iClient) +{ + PrintToChat(iClient, "[Note] Use command `r_removedecals` for client to clean old decals."); + + SDKCall(g_hCallRemoveAllDecals, iClient); +} + +void ClearAllData() +{ + g_hRemoveDecalsTime = 0.0; + + for (int iIter = 0; iIter < g_DecalQueue.Length; iIter++) { + DataPack hDp = g_DecalQueue.Get(0); + + if (hDp != null) { + CloseHandle(hDp); + } + + g_DecalQueue.Erase(0); + } + + g_DecalQueue.Clear(); } From 2e0efff944266956c4431f1d8fb4260091a9e4ed Mon Sep 17 00:00:00 2001 From: A1mDev <33463136+A1mDev@users.noreply.github.com> Date: Sat, 4 Oct 2025 17:55:53 +0700 Subject: [PATCH 2/2] Remove non-working code:( --- .../sourcemod/gamedata/visualise_impacts.txt | 28 ------- .../sourcemod/scripting/visualise_impacts.sp | 81 ++++--------------- 2 files changed, 14 insertions(+), 95 deletions(-) delete mode 100644 addons/sourcemod/gamedata/visualise_impacts.txt diff --git a/addons/sourcemod/gamedata/visualise_impacts.txt b/addons/sourcemod/gamedata/visualise_impacts.txt deleted file mode 100644 index fa8bc3bd0..000000000 --- a/addons/sourcemod/gamedata/visualise_impacts.txt +++ /dev/null @@ -1,28 +0,0 @@ -"Games" -{ - "left4dead2" - { - "Signatures" - { - /** @A1m: - * [Windows] - * To locate the `CBaseEntity::RemoveAllDecals` function, follow these steps: - * 1. Find the `CC_Ent_Create` function — it can be identified by the string "Cannot ent_create players!\n". - * 2. Near the end of `CC_Ent_Create`, there is a call to `UTIL_DropToFloor`. Check the xrefs to this function. - * 3. Among the cross-references, look for `CItem::Respawn` (usually the second-to-last one). - * 4. Inside `CItem::Respawn`, you will find a call to `CBaseEntity::RemoveAllDecals`. - * - * Signature: - * void CBaseEntity::RemoveAllDecals( void ) - */ - "CBaseEntity::RemoveAllDecals" - { - "library" "server" - - "linux" "@_ZN11CBaseEntity15RemoveAllDecalsEv" - /* 6A 00 51 E8 ? ? ? ? 6A 01 */ - "windows" "\x6A\x00\x51\xE8\x2A\x2A\x2A\x2A\x6A\x01" - } - } - } -} \ No newline at end of file diff --git a/addons/sourcemod/scripting/visualise_impacts.sp b/addons/sourcemod/scripting/visualise_impacts.sp index 57ab65d26..84058d5a3 100644 --- a/addons/sourcemod/scripting/visualise_impacts.sp +++ b/addons/sourcemod/scripting/visualise_impacts.sp @@ -25,9 +25,7 @@ #include #include -#define GAMEDATA_FILE "visualise_impacts" -#define REMOVEALLDECALS_SIGN "CBaseEntity::RemoveAllDecals" -#define DECAL_NAME "materials/decals/metal/metal01b.vtf" +#define DECAL_NAME "materials/decals/metal/metal01b.vtf" int g_iPrecacheDecal = 0; @@ -39,57 +37,30 @@ ConVar g_hCvarMultiplayerMaxTempEnts = null, g_hCvarRemoveDecalsTime = null; -Handle - g_hCallRemoveAllDecals = null; - ArrayList - g_DecalQueue = null; + g_hDecalQueue = null; public Plugin myinfo = { name = "Visualise impacts", author = "A1m`", - version = "1.6", + version = "1.7", description = "Shows bullet impacts (based on the original by Jahze, fully rewritten and improved)", url = "https://github.com/SirPlease/L4D2-Competitive-Rework" }; public void OnPluginStart() { - InitGameData(); - InitPlugin(); - 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); - RegConsoleCmd("sm_remove_decals", Cmd_RemoveDecals); -} - -void InitGameData() -{ - GameData hGameData = new GameData(GAMEDATA_FILE); - if (hGameData == null) { - SetFailState("Could not load gamedata/%s.txt", GAMEDATA_FILE); - } - - StartPrepSDKCall(SDKCall_Entity); - - if (!PrepSDKCall_SetFromConf(hGameData, SDKConf_Signature, REMOVEALLDECALS_SIGN)) { - SetFailState("Function '%s' not found", REMOVEALLDECALS_SIGN); - } - - g_hCallRemoveAllDecals = EndPrepSDKCall(); - if (g_hCallRemoveAllDecals == null) { - SetFailState("Function '%s' found, but something went wrong", REMOVEALLDECALS_SIGN); - } - - delete hGameData; + InitPlugin(); } void InitPlugin() { g_hCvarMultiplayerMaxTempEnts = FindConVar("sv_multiplayer_maxtempentities"); - g_DecalQueue = new ArrayList(); + g_hDecalQueue = new ArrayList(); g_iPrecacheDecal = PrecacheDecal(DECAL_NAME, true); @@ -142,7 +113,7 @@ void ShouldRemoveAllDecals() void SendSendQueueDecals() { - if (g_DecalQueue.Length <= 0) { + if (g_hDecalQueue.Length <= 0) { return; } @@ -166,8 +137,8 @@ void SendSendQueueDecals() int iProcessed = 0; - while (g_DecalQueue.Length > 0 && iProcessed < iMaxPerTick) { - DataPack hDp = g_DecalQueue.Get(0); + while (g_hDecalQueue.Length > 0 && iProcessed < iMaxPerTick) { + DataPack hDp = g_hDecalQueue.Get(0); if (hDp != null) { hDp.Reset(); @@ -182,19 +153,11 @@ void SendSendQueueDecals() } CloseHandle(hDp); - g_DecalQueue.Erase(0); + g_hDecalQueue.Erase(0); iProcessed++; } } -Action Cmd_RemoveDecals(int iClient, int iArgs) -{ - RemoveAllDecalsForAll(); - - ReplyToCommand(iClient, "All decals removed successfully!"); - return Plugin_Handled; -} - void Event_RoundChangeState(Event hEvent, const char[] sEventName, bool bDontBroadcast) { ClearAllData(); @@ -213,7 +176,7 @@ void Event_BulletImpact(Event hEvent, const char[] sEventName, bool bDontBroadca hDp.WriteCell(iUserId); hDp.WriteFloatArray(fPos, sizeof(fPos), false); - g_DecalQueue.Push(hDp); + g_hDecalQueue.Push(hDp); g_hRemoveDecalsTime = GetGameTime() + g_hCvarRemoveDecalsTime.FloatValue; } @@ -247,40 +210,24 @@ void RemoveAllDecalsForAll() } } -/** @A1m`: - * I don't know how to implement this code in sourcemod, it seems impossible, - * it takes the index of the entity class instead of the usermessage index. - * -#define BASEENTITY_MSG_REMOVE_DECALS 1 - -void CBaseEntity::RemoveAllDecals( void ) -{ - EntityMessageBegin( this ); - MessageWriteByte( BASEENTITY_MSG_REMOVE_DECALS ); - MessageEnd(); -} -**/ - void RemoveAllDecals(int iClient) { PrintToChat(iClient, "[Note] Use command `r_removedecals` for client to clean old decals."); - - SDKCall(g_hCallRemoveAllDecals, iClient); } void ClearAllData() { g_hRemoveDecalsTime = 0.0; - for (int iIter = 0; iIter < g_DecalQueue.Length; iIter++) { - DataPack hDp = g_DecalQueue.Get(0); + for (int iIter = 0; iIter < g_hDecalQueue.Length; iIter++) { + DataPack hDp = g_hDecalQueue.Get(0); if (hDp != null) { CloseHandle(hDp); } - g_DecalQueue.Erase(0); + g_hDecalQueue.Erase(0); } - g_DecalQueue.Clear(); + g_hDecalQueue.Clear(); }