11/* *
2+ * Version 2.4
3+ *
4+ * Additions:
5+ * - Added ConVar `tongue_damage_continuity` (Default 0: OFF)
6+ *
7+ * When enabled the internal damage timer carries over between dragging a survivor & choking a survivor.
8+ * In Vanilla the timer gets reset to its full duration when transitioning between states.
9+ *
10+ * Gamedata:
11+ * - We now use our own gamedata as we need some additional info.
12+ *
213 * Version 2.3 by A1m`
314 *
415 * Changes:
2233
2334#include <sourcemod>
2435#include <sdkhooks>
36+ #include <dhooks>
2537
2638#define DEBUG 0
27- #define GAMEDATA " l4d2_si_ability "
39+ #define GAMEDATA " l4d2_smoker_drag_damage_interval "
2840
2941// DMG_CHOKE = 1048576 = 0x100000 = (1 << 20)
3042#define DMG_CHOKE (1 << 20 )
3143
32- #define IT_TIMESTAMP_INDEX 0
3344#define CT_DURATION_OFFSET 4
3445#define CT_TIMESTAMP_OFFSET 8
3546
5465int
5566 g_iTongueHitCount [MAXPLAYERS + 1 ][eDamageInfo_Size ],
5667 g_iTongueDragDamageTimerDurationOffset = - 1 ,
57- g_iTongueDragDamageTimerTimeStampOffset = - 1 ;
68+ g_iTongueDragDamageTimerTimeStampOffset = - 1 ,
69+ g_iTongueChokeDamageTimerTimeStampOffset = - 1 ,
70+ g_iTongueReleaseTick [MAXPLAYERS + 1 ];
71+
72+ float
73+ g_fDragTimestampSnapshot [MAXPLAYERS + 1 ];
74+
75+ bool
76+ g_bSnapshotValid [MAXPLAYERS + 1 ];
5877
5978ConVar
6079 g_hTongueDragDamageInterval = null ,
6180 g_hTongueDragFirstDamageInterval = null ,
62- g_hTongueDragFirstDamage = null ;
81+ g_hTongueDragFirstDamage = null ,
82+ g_hTongueDamageContinuity = null ;
83+
84+ DynamicDetour
85+ g_hOnStartHangingFromTongue = null ;
6386
6487public Plugin myinfo =
6588{
6689 name = " L4D2 smoker drag damage interval" ,
6790 author = " Visor, Sir, A1m`" ,
68- description = " Implements a native-like cvar that should've been there out of the box" ,
69- version = " 2.3 " ,
91+ description = " Implements a native-like cvar and functionality that should've been there out of the box" ,
92+ version = " 2.4 " ,
7093 url = " https://github.com/SirPlease/L4D2-Competitive-Rework"
7194};
7295
@@ -75,6 +98,8 @@ public void OnPluginStart()
7598 InitGameData ();
7699
77100 HookEvent (" tongue_grab" , Event_OnTongueGrab );
101+ HookEvent (" tongue_release" , Event_OnTongueRelease );
102+ HookEvent (" choke_end" , Event_OnChokeEnd );
78103
79104 // Get the default value of cvar 'tongue_choke_damage_interval'
80105 char sCvarVal [32 ];
@@ -84,27 +109,43 @@ public void OnPluginStart()
84109 g_hTongueDragDamageInterval = CreateConVar (" tongue_drag_damage_interval" , sCvarVal , " How often the drag does damage. Allowed values: 0.01 - 15.0." , _ , true , 0.01 , true , 15.0 );
85110 g_hTongueDragFirstDamageInterval = CreateConVar (" tongue_drag_first_damage_interval" , " -1.0" , " After how many seconds do we apply our first tick of damage? 0.0 - disable, max value - 15.0." , _ , false , 0.0 , true , 15.0 );
86111 g_hTongueDragFirstDamage = CreateConVar (" tongue_drag_first_damage" , " -1.0" , " How much damage do we apply on the first tongue hit? 0.0 - disable" , _ , false , 0.0 , true , 100.0 );
112+ g_hTongueDamageContinuity = CreateConVar (" tongue_damage_continuity" , " 0" , " Preserve damage timer between drag <-> choke transitions. 0 = Vanilla behavior, 1 = carry over remaining time of the choke/drag to the next timer" , _ , true , 0.0 , true , 1.0 );
87113
88114 LateLoad ();
89115}
90116
91117void InitGameData ()
92118{
93- Handle hGamedata = LoadGameConfigFile (GAMEDATA );
94-
95- if (! hGamedata ) {
119+ GameData gd = new GameData (GAMEDATA );
120+ if (! gd ) {
96121 SetFailState (" Gamedata '%s .txt' missing or corrupt." , GAMEDATA );
97122 }
98123
99- int iTongueDragDamageTimer = GameConfGetOffset ( hGamedata , " CTerrorPlayer->m_tongueDragDamageTimer" );
124+ int iTongueDragDamageTimer = gd . GetOffset ( " CTerrorPlayer->m_tongueDragDamageTimer" );
100125 if (iTongueDragDamageTimer == - 1 ) {
101126 SetFailState (" Failed to get offset 'CTerrorPlayer->m_tongueDragDamageTimer'." );
102127 }
103-
104128 g_iTongueDragDamageTimerDurationOffset = iTongueDragDamageTimer + CT_DURATION_OFFSET ;
105129 g_iTongueDragDamageTimerTimeStampOffset = iTongueDragDamageTimer + CT_TIMESTAMP_OFFSET ;
106130
107- delete hGamedata ;
131+ int iTongueChokeDamageTimer = gd .GetOffset (" CTerrorPlayer->m_tongueChokeDamageTimer" );
132+ if (iTongueChokeDamageTimer == - 1 ) {
133+ SetFailState (" Failed to get offset 'CTerrorPlayer->m_tongueChokeDamageTimer'." );
134+ }
135+ g_iTongueChokeDamageTimerTimeStampOffset = iTongueChokeDamageTimer + CT_TIMESTAMP_OFFSET ;
136+
137+ g_hOnStartHangingFromTongue = DynamicDetour .FromConf (gd , " CTerrorPlayer::OnStartHangingFromTongue" );
138+ if (! g_hOnStartHangingFromTongue ) {
139+ SetFailState (" Failed to set up detour for 'CTerrorPlayer::OnStartHangingFromTongue'." );
140+ }
141+ if (! g_hOnStartHangingFromTongue .Enable (Hook_Pre , Detour_OnStartHangingFromTongue_Pre )) {
142+ SetFailState (" Failed to enable Pre detour on 'CTerrorPlayer::OnStartHangingFromTongue'." );
143+ }
144+ if (! g_hOnStartHangingFromTongue .Enable (Hook_Post , Detour_OnStartHangingFromTongue_Post )) {
145+ SetFailState (" Failed to enable Post detour on 'CTerrorPlayer::OnStartHangingFromTongue'." );
146+ }
147+
148+ delete gd ;
108149}
109150
110151void LateLoad ()
@@ -121,6 +162,16 @@ void LateLoad()
121162public void OnClientPutInServer (int iClient )
122163{
123164 SDKHook (iClient , SDKHook_OnTakeDamage , Hook_OnTakeDamage );
165+ g_fDragTimestampSnapshot [iClient ] = - 1.0 ;
166+ g_bSnapshotValid [iClient ] = false ;
167+ g_iTongueReleaseTick [iClient ] = - 1 ;
168+ }
169+
170+ public void OnClientDisconnect (int iClient )
171+ {
172+ g_fDragTimestampSnapshot [iClient ] = - 1.0 ;
173+ g_bSnapshotValid [iClient ] = false ;
174+ g_iTongueReleaseTick [iClient ] = - 1 ;
124175}
125176
126177void Event_OnTongueGrab (Event hEvent , const char [] eName , bool bDontBroadcast )
@@ -183,6 +234,116 @@ Action Hook_OnTakeDamage(int iVictim, int &iAttacker, int &iInflictor, float &fD
183234 return (bFirstDamage ) ? Plugin_Changed : Plugin_Continue ;
184235}
185236
237+ MRESReturn Detour_OnStartHangingFromTongue_Pre (int client , DHookParam hParams )
238+ {
239+ if (client < 1 || client > MaxClients ) {
240+ return MRES_Ignored ;
241+ }
242+
243+ // Set it to false here as it's called non-stop during the pull, easy reset.
244+ g_bSnapshotValid [client ] = false ;
245+
246+ if (GetEntProp (client , Prop_Send , " m_isHangingFromTongue" , 1 ) > 0 ) {
247+ return MRES_Ignored ;
248+ }
249+
250+ // We only land here when the client is about to officially StartHangingFromTongue.
251+ g_fDragTimestampSnapshot [client ] = GetEntDataFloat (client , g_iTongueDragDamageTimerTimeStampOffset );
252+ g_bSnapshotValid [client ] = true ;
253+ return MRES_Ignored ;
254+ }
255+
256+ MRESReturn Detour_OnStartHangingFromTongue_Post (int client , DHookParam hParams )
257+ {
258+ if (client < 1 || client > MaxClients ) {
259+ return MRES_Ignored ;
260+ }
261+
262+ // Store current & reset
263+ bool bValid = g_bSnapshotValid [client ];
264+ float fSnapshot = g_fDragTimestampSnapshot [client ];
265+ g_bSnapshotValid [client ] = false ;
266+ g_fDragTimestampSnapshot [client ] = - 1.0 ;
267+
268+ // Player is already hanging
269+ if (! bValid ) {
270+ return MRES_Ignored ;
271+ }
272+
273+ if (! g_hTongueDamageContinuity .BoolValue ) {
274+ return MRES_Ignored ;
275+ }
276+
277+ // Shouldn't happen, but just in case. (Lets Vanilla behavior continue)
278+ if (fSnapshot < 0.0 ) {
279+ return MRES_Ignored ;
280+ }
281+
282+ float fNow = GetGameTime ();
283+ float fRemaining = fSnapshot - fNow ;
284+ if (fRemaining < 0.0 ) {
285+ fRemaining = 0.0 ;
286+ }
287+
288+ /*
289+ Game just set its choke_timer.m_timestamp to now + tongue_choke_damage_interval
290+ Replace with now + remaining_drag_time so the damage timer continues instead of restarting.
291+ We leave m_duration alone because UpdateHangingFromTongue restamps it to the cvar on the first choke tick.
292+ */
293+ #if DEBUG
294+ float fEngineChokeTs = GetEntDataFloat (client , g_iTongueChokeDamageTimerTimeStampOffset );
295+ float fEngineWouldFireIn = fEngineChokeTs - fNow ;
296+ PrintToChatAll (" [tongue_continuity] %N drag->choke: damage clock CONTINUED - next damage in %.2f s (vanilla would have reset to %.2f s)" , \
297+ client , fRemaining , fEngineWouldFireIn );
298+ #endif
299+
300+ SetEntDataFloat (client , g_iTongueChokeDamageTimerTimeStampOffset , fNow + fRemaining , false );
301+
302+ return MRES_Ignored ;
303+ }
304+
305+ void Event_OnTongueRelease (Event hEvent , const char [] eName , bool bDontBroadcast )
306+ {
307+ // Store the exact tick on which a player got released from a tongue.
308+ int iVictim = GetClientOfUserId (hEvent .GetInt (" victim" ));
309+ if (iVictim < 1 || iVictim > MaxClients ) {
310+ return ;
311+ }
312+
313+ g_iTongueReleaseTick [iVictim ] = GetGameTickCount ();
314+ }
315+
316+ void Event_OnChokeEnd (Event hEvent , const char [] eName , bool bDontBroadcast )
317+ {
318+ if (! g_hTongueDamageContinuity .BoolValue ) {
319+ return ;
320+ }
321+
322+ int iVictim = GetClientOfUserId (hEvent .GetInt (" victim" ));
323+ if (iVictim < 1 || iVictim > MaxClients ) {
324+ return ;
325+ }
326+
327+ // If tongue_release fired on the exact same tick, then it's guaranteed to be a survivor clear rather than transition.
328+ if (g_iTongueReleaseTick [iVictim ] == GetGameTickCount ()) {
329+ return ;
330+ }
331+
332+ float fChokeTs = GetEntDataFloat (iVictim , g_iTongueChokeDamageTimerTimeStampOffset );
333+ float fNow = GetGameTime ();
334+ float fRemaining = fChokeTs - fNow ;
335+ if (fRemaining < 0.0 ) {
336+ fRemaining = 0.0 ;
337+ }
338+
339+ SetEntDataFloat (iVictim , g_iTongueDragDamageTimerTimeStampOffset , fNow + fRemaining , false );
340+
341+ #if DEBUG
342+ PrintToChatAll (" [tongue_continuity] %N choke->drag: damage clock CONTINUED - next damage in %.2f s (vanilla would have fired immediately)" , \
343+ iVictim , fRemaining );
344+ #endif
345+ }
346+
186347float GetFirstDamageInterval ()
187348{
188349 float fTongueFirstDamageInterval = g_hTongueDragFirstDamageInterval .FloatValue ;
0 commit comments