Skip to content

Commit 9ce4911

Browse files
committed
Add functionality to l4d2_smoker_drag_damage_interval
Add a ConVar that allows us to carry over the remaining time until doing damage when transitioning between dragging and choking a survivor as smoker. In Vanilla the timer would get fully reset. Also deals with a situation where the smoker sometimes gets an instant tick of damage (basically a 2x) Example: Survivor gets pulled (drag), they take 2 ticks of damage and are about to take another tick of damage but they are "fully reeled in" or "stuck" (choke), instead of applying the damage it resets the damage timer entirely, missing out on damage.
1 parent 1a9fab2 commit 9ce4911

3 files changed

Lines changed: 229 additions & 12 deletions

File tree

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
"Games"
2+
{
3+
"left4dead2"
4+
{
5+
"Offsets"
6+
{
7+
/*
8+
* CountdownTimer read by CTerrorPlayer::UpdateHangingFromTongue
9+
* while the victim is being DRAGGED (m_isHangingFromTongue == 0).
10+
*/
11+
"CTerrorPlayer->m_tongueDragDamageTimer"
12+
{
13+
"linux" "13312"
14+
"windows" "13332"
15+
}
16+
17+
/* CountdownTimer read by CTerrorPlayer::UpdateHangingFromTongue
18+
* while the victim is HANGING/CHOKING (m_isHangingFromTongue == 1).
19+
*/
20+
"CTerrorPlayer->m_tongueChokeDamageTimer"
21+
{
22+
"linux" "13288"
23+
"windows" "13308"
24+
}
25+
}
26+
27+
"Functions"
28+
{
29+
"CTerrorPlayer::OnStartHangingFromTongue"
30+
{
31+
"signature" "CTerrorPlayer::OnStartHangingFromTongue"
32+
"callconv" "thiscall"
33+
"return" "void"
34+
"this" "entity"
35+
"arguments"
36+
{
37+
"method"
38+
{
39+
"type" "int"
40+
}
41+
}
42+
}
43+
}
44+
45+
"Signatures"
46+
{
47+
"CTerrorPlayer::OnStartHangingFromTongue"
48+
{
49+
"library" "server"
50+
"linux" "@_ZN13CTerrorPlayer24OnStartHangingFromTongueEi"
51+
"windows" "\x55\x8B\xEC\x51\x8B\x45\x08\x53\x57\x8B\xF9"
52+
/* 55 8B EC 51 8B 45 08 53 57 8B F9 */
53+
}
54+
}
55+
}
56+
}
Binary file not shown.

addons/sourcemod/scripting/l4d2_smoker_drag_damage_interval.sp

Lines changed: 173 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,15 @@
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:
@@ -22,14 +33,14 @@
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

@@ -54,19 +65,31 @@ enum
5465
int
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

5978
ConVar
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

6487
public 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

91117
void 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

110151
void LateLoad()
@@ -121,6 +162,16 @@ void LateLoad()
121162
public 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

126177
void 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 %.2fs (vanilla would have reset to %.2fs)", \
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 %.2fs (vanilla would have fired immediately)", \
343+
iVictim, fRemaining);
344+
#endif
345+
}
346+
186347
float GetFirstDamageInterval()
187348
{
188349
float fTongueFirstDamageInterval = g_hTongueDragFirstDamageInterval.FloatValue;

0 commit comments

Comments
 (0)