Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file not shown.
Binary file not shown.
215 changes: 161 additions & 54 deletions addons/sourcemod/scripting/l4d2_smoker_drag_damage_interval.sp
Original file line number Diff line number Diff line change
@@ -1,44 +1,91 @@
/**
* Version 2.3 by A1m`
*
* Changes:
* 1. Removed duplicate plugins:
* - l4d2_smoker_drag_damage_interval_zone
* - l4d2_smoker_drag_damage_interval.
*
* 2. Removed untrusted timer-based code:
* - Replaced with safer, hook-based implementation using OnTakeDamage.
* - Ensures more stable and reliable drag damage behavior without unnecessary timers.
*
* Notes:
* The timing of damage is not perfect, as 'CTerrorPlayer::PostThink' is not called every frame,
* but after a certain period of time depending on the number of players and the tick rate (see function 'CTerrorPlayer::ShouldPostThink').
* For this reason, it is difficult to calculate using game netprops whether this was the first damage dealt or not,
* the code becomes too complicated, so we introduce our own variables to determine this.
**/

#pragma semicolon 1
#pragma newdecls required

#include <sourcemod>
#include <sdkhooks>

#define DEBUG 0
#define GAMEDATA "l4d2_si_ability"

// DMG_CHOKE = 1048576 = 0x100000 = (1 << 20)
#define DMG_CHOKE (1 << 20)

#define GAMEDATA "l4d2_si_ability"
#define IT_TIMESTAMP_INDEX 0
#define CT_DURATION_OFFSET 4
#define CT_TIMESTAMP_OFFSET 8

#define DURATION_OFFSET 4
#define TIMESTAMP_OFFSET 8
#define TEAM_SURVIVOR 2
#define TEAM_INFECTED 3

#define TEAM_SURVIVOR 2
#define EPSILON 0.001

int
m_tongueDragDamageTimerDuration,
m_tongueDragDamageTimerTimeStamp;
#if DEBUG
float
g_fDebugDamageInterval = 0.0;
#endif

ConVar
tongue_drag_damage_interval;
enum
{
eUserId = 0,
eHitCount,

eDamageInfo_Size
};

int
g_iTongueHitCount[MAXPLAYERS + 1][eDamageInfo_Size],
g_iTongueDragDamageTimerDurationOffset = -1,
g_iTongueDragDamageTimerTimeStampOffset = -1;

ConVar
g_hTongueDragDamageInterval = null,
g_hTongueDragFirstDamageInterval = null,
g_hTongueDragFirstDamage = null;

public Plugin myinfo =
{
name = "L4D2 Smoker Drag Damage Interval",
author = "Visor, A1m`",
name = "L4D2 smoker drag damage interval",
author = "Visor, Sir, A1m`",
description = "Implements a native-like cvar that should've been there out of the box",
version = "0.7",
version = "2.3",
url = "https://github.com/SirPlease/L4D2-Competitive-Rework"
};

public void OnPluginStart()
{
InitGameData();
HookEvent("tongue_grab", OnTongueGrab);

char value[32];
ConVar tongue_choke_damage_interval = FindConVar("tongue_choke_damage_interval");
tongue_choke_damage_interval.GetString(value, sizeof(value));

tongue_drag_damage_interval = CreateConVar("tongue_drag_damage_interval", value, "How often the drag does damage.");

ConVar tongue_choke_damage_amount = FindConVar("tongue_choke_damage_amount");
tongue_choke_damage_amount.AddChangeHook(tongue_choke_damage_amount_ValueChanged);

HookEvent("tongue_grab", Event_OnTongueGrab);

// Get the default value of cvar 'tongue_choke_damage_interval'
char sCvarVal[32];
ConVar hTongueChokeDamageInterval = FindConVar("tongue_choke_damage_interval");
hTongueChokeDamageInterval.GetDefault(sCvarVal, sizeof(sCvarVal));

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);
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);
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);

LateLoad();
}

void InitGameData()
Expand All @@ -48,59 +95,119 @@ void InitGameData()
if (!hGamedata) {
SetFailState("Gamedata '%s.txt' missing or corrupt.", GAMEDATA);
}
int m_tongueDragDamageTimer = GameConfGetOffset(hGamedata, "CTerrorPlayer->m_tongueDragDamageTimer");
if (m_tongueDragDamageTimer == -1) {

int iTongueDragDamageTimer = GameConfGetOffset(hGamedata, "CTerrorPlayer->m_tongueDragDamageTimer");
if (iTongueDragDamageTimer == -1) {
SetFailState("Failed to get offset 'CTerrorPlayer->m_tongueDragDamageTimer'.");
}
m_tongueDragDamageTimerDuration = m_tongueDragDamageTimer + DURATION_OFFSET;
m_tongueDragDamageTimerTimeStamp = m_tongueDragDamageTimer + TIMESTAMP_OFFSET;

g_iTongueDragDamageTimerDurationOffset = iTongueDragDamageTimer + CT_DURATION_OFFSET;
g_iTongueDragDamageTimerTimeStampOffset = iTongueDragDamageTimer + CT_TIMESTAMP_OFFSET;

delete hGamedata;
}

void tongue_choke_damage_amount_ValueChanged(ConVar convar, const char[] oldValue, const char[] newValue)
void LateLoad()
{
convar.SetInt(1); // hack-hack: game tries to change this cvar for some reason, can't be arsed so HARDCODETHATSHIT
for (int i = 1; i <= MaxClients; i++) {
if (!IsClientInGame(i)) {
continue;
}

OnClientPutInServer(i);
}
}

void OnTongueGrab(Event hEvent, const char[] eName, bool dontBroadcast)
public void OnClientPutInServer(int iClient)
{
int userid = hEvent.GetInt("victim");
int client = GetClientOfUserId(userid);

SetDragDamageInterval(client);

float fTimerUpdate = tongue_drag_damage_interval.FloatValue + 0.1;
CreateTimer(fTimerUpdate, FixDragInterval, userid, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE);
SDKHook(iClient, SDKHook_OnTakeDamage, Hook_OnTakeDamage);
}

Action FixDragInterval(Handle hTimer, any userid)
void Event_OnTongueGrab(Event hEvent, const char[] eName, bool bDontBroadcast)
{
int client = GetClientOfUserId(userid);
if (client > 0 && GetClientTeam(client) == TEAM_SURVIVOR && IsSurvivorBeingDragged(client)) {
SetDragDamageInterval(client);
// Replacing variable value 'CTerrorPlayer::m_tongueDragDamageTimer',
// ​​after calling a function 'CTerrorPlayer::OnGrabbedByTongue'.
// Fix damage interval.

int iVictim = GetClientOfUserId(hEvent.GetInt("victim"));
bool bIsHangingFromTongue = (GetEntProp(iVictim, Prop_Send, "m_isHangingFromTongue", 1) > 0);

if (!bIsHangingFromTongue) { // Dragging?
SetDragDamageTimer(iVictim, GetFirstDamageInterval());
}

g_iTongueHitCount[iVictim][eUserId] = hEvent.GetInt("victim");
g_iTongueHitCount[iVictim][eHitCount] = 0;

#if DEBUG
g_fDebugDamageInterval = GetGameTime();
#endif
}

Action Hook_OnTakeDamage(int iVictim, int &iAttacker, int &iInflictor, float &fDamage, int &iDamageType)
{
// Replacing the function patch 'CTerrorPlayer::UpdateHangingFromTongue'.
// This dmg function is called after variable 'CTerrorPlayer::m_tongueDragDamageTimer' is set, we can't get it here.
if (!(iDamageType & DMG_CHOKE)) {
return Plugin_Continue;
}

int iTongueOwner = GetEntPropEnt(iVictim, Prop_Send, "m_tongueOwner");
if (iTongueOwner < 1 || iTongueOwner > MaxClients || iTongueOwner != iAttacker) {
return Plugin_Continue;
}

// Stop dragging.
if (GetEntProp(iVictim, Prop_Send, "m_isHangingFromTongue", 1) > 0) {
return Plugin_Continue;
}
return Plugin_Stop;

// Fix damage interval.
SetDragDamageTimer(iVictim, g_hTongueDragDamageInterval.FloatValue);

// First damage if cvar enabled.
g_iTongueHitCount[iVictim][eHitCount]++;
bool bFirstDamage = false;

if (g_hTongueDragFirstDamage.FloatValue > 0.0) {
if (g_iTongueHitCount[iVictim][eHitCount] == 1 && g_iTongueHitCount[iVictim][eUserId] == GetClientUserId(iVictim)) {
fDamage = g_hTongueDragFirstDamage.FloatValue;
bFirstDamage = true;
}
}

#if DEBUG
DebugPrint(iVictim, fDamage, bFirstDamage);
#endif

return (bFirstDamage) ? Plugin_Changed : Plugin_Continue;
}

void SetDragDamageInterval(int client)
float GetFirstDamageInterval()
{
float fCvarValue = tongue_drag_damage_interval.FloatValue;
float fTimeStamp = GetGameTime() + fCvarValue;

SetEntDataFloat(client, m_tongueDragDamageTimerDuration, fCvarValue); //duration
SetEntDataFloat(client, m_tongueDragDamageTimerTimeStamp, fTimeStamp); //timestamp
float fTongueFirstDamageInterval = g_hTongueDragFirstDamageInterval.FloatValue;
if (fTongueFirstDamageInterval > 0.0) {
return fTongueFirstDamageInterval;
}

return g_hTongueDragDamageInterval.FloatValue;
}

bool IsSurvivorBeingDragged(int client)
void SetDragDamageTimer(int iClient, float fDuration)
{
return ((GetEntPropEnt(client, Prop_Send, "m_tongueOwner") > 0) && !IsSurvivorBeingChoked(client));
// 'CTerrorPlayer::m_tongueDragDamageTimer', this is not netprop
float fTimeStamp = GetGameTime() + fDuration;

SetEntDataFloat(iClient, g_iTongueDragDamageTimerDurationOffset, fDuration, false); // 'CountdownTimer::duration'
SetEntDataFloat(iClient, g_iTongueDragDamageTimerTimeStampOffset, fTimeStamp, false); // 'CountdownTimer::timestamp'
}

bool IsSurvivorBeingChoked(int client)
#if DEBUG
void DebugPrint(int iVictim, float fDamage, bool bFirstDamage)
{
return (GetEntProp(client, Prop_Send, "m_isHangingFromTongue") > 0);
PrintToChatAll("[DEBUG] Victim: %N, %sdamage: %f, time: %f, game time: %f", \
iVictim, (bFirstDamage) ? "first " : "", fDamage, GetGameTime() - g_fDebugDamageInterval, GetGameTime());

g_fDebugDamageInterval = GetGameTime();
}
#endif
Loading