diff --git a/addons/sourcemod/gamedata/l4d2_smoker_drag_damage_interval.txt b/addons/sourcemod/gamedata/l4d2_smoker_drag_damage_interval.txt new file mode 100644 index 000000000..f5c2e9b1d --- /dev/null +++ b/addons/sourcemod/gamedata/l4d2_smoker_drag_damage_interval.txt @@ -0,0 +1,56 @@ +"Games" +{ + "left4dead2" + { + "Offsets" + { + /* + * CountdownTimer read by CTerrorPlayer::UpdateHangingFromTongue + * while the victim is being DRAGGED (m_isHangingFromTongue == 0). + */ + "CTerrorPlayer->m_tongueDragDamageTimer" + { + "linux" "13312" + "windows" "13332" + } + + /* CountdownTimer read by CTerrorPlayer::UpdateHangingFromTongue + * while the victim is HANGING/CHOKING (m_isHangingFromTongue == 1). + */ + "CTerrorPlayer->m_tongueChokeDamageTimer" + { + "linux" "13288" + "windows" "13308" + } + } + + "Functions" + { + "CTerrorPlayer::OnStartHangingFromTongue" + { + "signature" "CTerrorPlayer::OnStartHangingFromTongue" + "callconv" "thiscall" + "return" "void" + "this" "entity" + "arguments" + { + "method" + { + "type" "int" + } + } + } + } + + "Signatures" + { + "CTerrorPlayer::OnStartHangingFromTongue" + { + "library" "server" + "linux" "@_ZN13CTerrorPlayer24OnStartHangingFromTongueEi" + "windows" "\x55\x8B\xEC\x51\x8B\x45\x08\x53\x57\x8B\xF9" + /* 55 8B EC 51 8B 45 08 53 57 8B F9 */ + } + } + } +} diff --git a/addons/sourcemod/plugins/optional/l4d2_smoker_drag_damage_interval.smx b/addons/sourcemod/plugins/optional/l4d2_smoker_drag_damage_interval.smx index 5c8818068..d497ce18f 100644 Binary files a/addons/sourcemod/plugins/optional/l4d2_smoker_drag_damage_interval.smx and b/addons/sourcemod/plugins/optional/l4d2_smoker_drag_damage_interval.smx differ diff --git a/addons/sourcemod/scripting/l4d2_smoker_drag_damage_interval.sp b/addons/sourcemod/scripting/l4d2_smoker_drag_damage_interval.sp index 3d29db1f6..faf730d02 100644 --- a/addons/sourcemod/scripting/l4d2_smoker_drag_damage_interval.sp +++ b/addons/sourcemod/scripting/l4d2_smoker_drag_damage_interval.sp @@ -1,4 +1,15 @@ /** + * Version 2.4 + * + * Additions: + * - Added ConVar `tongue_damage_continuity` (Default 0: OFF) + * + * When enabled the internal damage timer carries over between dragging a survivor & choking a survivor. + * In Vanilla the timer gets reset to its full duration when transitioning between states. + * + * Gamedata: + * - We now use our own gamedata as we need some additional info. + * * Version 2.3 by A1m` * * Changes: @@ -22,14 +33,14 @@ #include #include +#include #define DEBUG 0 -#define GAMEDATA "l4d2_si_ability" +#define GAMEDATA "l4d2_smoker_drag_damage_interval" // DMG_CHOKE = 1048576 = 0x100000 = (1 << 20) #define DMG_CHOKE (1 << 20) -#define IT_TIMESTAMP_INDEX 0 #define CT_DURATION_OFFSET 4 #define CT_TIMESTAMP_OFFSET 8 @@ -54,19 +65,31 @@ enum int g_iTongueHitCount[MAXPLAYERS + 1][eDamageInfo_Size], g_iTongueDragDamageTimerDurationOffset = -1, - g_iTongueDragDamageTimerTimeStampOffset = -1; + g_iTongueDragDamageTimerTimeStampOffset = -1, + g_iTongueChokeDamageTimerTimeStampOffset = -1, + g_iTongueReleaseTick[MAXPLAYERS + 1]; + +float + g_fDragTimestampSnapshot[MAXPLAYERS + 1]; + +bool + g_bSnapshotValid[MAXPLAYERS + 1]; ConVar g_hTongueDragDamageInterval = null, g_hTongueDragFirstDamageInterval = null, - g_hTongueDragFirstDamage = null; + g_hTongueDragFirstDamage = null, + g_hTongueDamageContinuity = null; + +DynamicDetour + g_hOnStartHangingFromTongue = null; public Plugin myinfo = { 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 = "2.3", + description = "Implements a native-like cvar and functionality that should've been there out of the box", + version = "2.4", url = "https://github.com/SirPlease/L4D2-Competitive-Rework" }; @@ -75,6 +98,8 @@ public void OnPluginStart() InitGameData(); HookEvent("tongue_grab", Event_OnTongueGrab); + HookEvent("tongue_release", Event_OnTongueRelease); + HookEvent("choke_end", Event_OnChokeEnd); // Get the default value of cvar 'tongue_choke_damage_interval' char sCvarVal[32]; @@ -84,27 +109,43 @@ public void OnPluginStart() 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); + 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); LateLoad(); } void InitGameData() { - Handle hGamedata = LoadGameConfigFile(GAMEDATA); - - if (!hGamedata) { + GameData gd = new GameData(GAMEDATA); + if (!gd) { SetFailState("Gamedata '%s.txt' missing or corrupt.", GAMEDATA); } - int iTongueDragDamageTimer = GameConfGetOffset(hGamedata, "CTerrorPlayer->m_tongueDragDamageTimer"); + int iTongueDragDamageTimer = gd.GetOffset("CTerrorPlayer->m_tongueDragDamageTimer"); if (iTongueDragDamageTimer == -1) { SetFailState("Failed to get offset 'CTerrorPlayer->m_tongueDragDamageTimer'."); } - g_iTongueDragDamageTimerDurationOffset = iTongueDragDamageTimer + CT_DURATION_OFFSET; g_iTongueDragDamageTimerTimeStampOffset = iTongueDragDamageTimer + CT_TIMESTAMP_OFFSET; - delete hGamedata; + int iTongueChokeDamageTimer = gd.GetOffset("CTerrorPlayer->m_tongueChokeDamageTimer"); + if (iTongueChokeDamageTimer == -1) { + SetFailState("Failed to get offset 'CTerrorPlayer->m_tongueChokeDamageTimer'."); + } + g_iTongueChokeDamageTimerTimeStampOffset = iTongueChokeDamageTimer + CT_TIMESTAMP_OFFSET; + + g_hOnStartHangingFromTongue = DynamicDetour.FromConf(gd, "CTerrorPlayer::OnStartHangingFromTongue"); + if (!g_hOnStartHangingFromTongue) { + SetFailState("Failed to set up detour for 'CTerrorPlayer::OnStartHangingFromTongue'."); + } + if (!g_hOnStartHangingFromTongue.Enable(Hook_Pre, Detour_OnStartHangingFromTongue_Pre)) { + SetFailState("Failed to enable Pre detour on 'CTerrorPlayer::OnStartHangingFromTongue'."); + } + if (!g_hOnStartHangingFromTongue.Enable(Hook_Post, Detour_OnStartHangingFromTongue_Post)) { + SetFailState("Failed to enable Post detour on 'CTerrorPlayer::OnStartHangingFromTongue'."); + } + + delete gd; } void LateLoad() @@ -121,6 +162,16 @@ void LateLoad() public void OnClientPutInServer(int iClient) { SDKHook(iClient, SDKHook_OnTakeDamage, Hook_OnTakeDamage); + g_fDragTimestampSnapshot[iClient] = -1.0; + g_bSnapshotValid[iClient] = false; + g_iTongueReleaseTick[iClient] = -1; +} + +public void OnClientDisconnect(int iClient) +{ + g_fDragTimestampSnapshot[iClient] = -1.0; + g_bSnapshotValid[iClient] = false; + g_iTongueReleaseTick[iClient] = -1; } 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 return (bFirstDamage) ? Plugin_Changed : Plugin_Continue; } +MRESReturn Detour_OnStartHangingFromTongue_Pre(int client, DHookParam hParams) +{ + if (client < 1 || client > MaxClients) { + return MRES_Ignored; + } + + // Set it to false here as it's called non-stop during the pull, easy reset. + g_bSnapshotValid[client] = false; + + if (GetEntProp(client, Prop_Send, "m_isHangingFromTongue", 1) > 0) { + return MRES_Ignored; + } + + // We only land here when the client is about to officially StartHangingFromTongue. + g_fDragTimestampSnapshot[client] = GetEntDataFloat(client, g_iTongueDragDamageTimerTimeStampOffset); + g_bSnapshotValid[client] = true; + return MRES_Ignored; +} + +MRESReturn Detour_OnStartHangingFromTongue_Post(int client, DHookParam hParams) +{ + if (client < 1 || client > MaxClients) { + return MRES_Ignored; + } + + // Store current & reset + bool bValid = g_bSnapshotValid[client]; + float fSnapshot = g_fDragTimestampSnapshot[client]; + g_bSnapshotValid[client] = false; + g_fDragTimestampSnapshot[client] = -1.0; + + // Player is already hanging + if (!bValid) { + return MRES_Ignored; + } + + if (!g_hTongueDamageContinuity.BoolValue) { + return MRES_Ignored; + } + + // Shouldn't happen, but just in case. (Lets Vanilla behavior continue) + if (fSnapshot < 0.0) { + return MRES_Ignored; + } + + float fNow = GetGameTime(); + float fRemaining = fSnapshot - fNow; + if (fRemaining < 0.0) { + fRemaining = 0.0; + } + + /* + Game just set its choke_timer.m_timestamp to now + tongue_choke_damage_interval + Replace with now + remaining_drag_time so the damage timer continues instead of restarting. + We leave m_duration alone because UpdateHangingFromTongue restamps it to the cvar on the first choke tick. + */ +#if DEBUG + float fEngineChokeTs = GetEntDataFloat(client, g_iTongueChokeDamageTimerTimeStampOffset); + float fEngineWouldFireIn = fEngineChokeTs - fNow; + PrintToChatAll("[tongue_continuity] %N drag->choke: damage clock CONTINUED - next damage in %.2fs (vanilla would have reset to %.2fs)", \ + client, fRemaining, fEngineWouldFireIn); +#endif + + SetEntDataFloat(client, g_iTongueChokeDamageTimerTimeStampOffset, fNow + fRemaining, false); + + return MRES_Ignored; +} + +void Event_OnTongueRelease(Event hEvent, const char[] eName, bool bDontBroadcast) +{ + // Store the exact tick on which a player got released from a tongue. + int iVictim = GetClientOfUserId(hEvent.GetInt("victim")); + if (iVictim < 1 || iVictim > MaxClients) { + return; + } + + g_iTongueReleaseTick[iVictim] = GetGameTickCount(); +} + +void Event_OnChokeEnd(Event hEvent, const char[] eName, bool bDontBroadcast) +{ + if (!g_hTongueDamageContinuity.BoolValue) { + return; + } + + int iVictim = GetClientOfUserId(hEvent.GetInt("victim")); + if (iVictim < 1 || iVictim > MaxClients) { + return; + } + + // If tongue_release fired on the exact same tick, then it's guaranteed to be a survivor clear rather than transition. + if (g_iTongueReleaseTick[iVictim] == GetGameTickCount()) { + return; + } + + float fChokeTs = GetEntDataFloat(iVictim, g_iTongueChokeDamageTimerTimeStampOffset); + float fNow = GetGameTime(); + float fRemaining = fChokeTs - fNow; + if (fRemaining < 0.0) { + fRemaining = 0.0; + } + + SetEntDataFloat(iVictim, g_iTongueDragDamageTimerTimeStampOffset, fNow + fRemaining, false); + +#if DEBUG + PrintToChatAll("[tongue_continuity] %N choke->drag: damage clock CONTINUED - next damage in %.2fs (vanilla would have fired immediately)", \ + iVictim, fRemaining); +#endif +} + float GetFirstDamageInterval() { float fTongueFirstDamageInterval = g_hTongueDragFirstDamageInterval.FloatValue; diff --git a/cfg/stripper/zonemod/maps/dkr_m2_carnival.cfg b/cfg/stripper/zonemod/maps/dkr_m2_carnival.cfg index a2cf27b20..5e10c50d6 100644 --- a/cfg/stripper/zonemod/maps/dkr_m2_carnival.cfg +++ b/cfg/stripper/zonemod/maps/dkr_m2_carnival.cfg @@ -33,20 +33,15 @@ add: ; "targetname" "eb_fix01" } - ; --- Fix broken infected ladder on telephone pole after the warehouse -filter: -{ - "hammerid" "1184297" -} -add: +modify: { - "classname" "func_simpleladder" - "origin" "-1.00049 7.33105 -90.5992" - "angles" "0 0 3" - "model" "*205" - "normal.x" "0" - "normal.y" "-0.99863" - "normal.z" "-0.052336" - "team" "2" + match: + { + "hammerid" "1184297" + } + insert: + { + "origin" "-5 -5 10" + } } \ No newline at end of file