diff --git a/addons/sourcemod/gamedata/charger_collision_patch.txt b/addons/sourcemod/gamedata/charger_collision_patch.txt new file mode 100644 index 000000000..845b245fb --- /dev/null +++ b/addons/sourcemod/gamedata/charger_collision_patch.txt @@ -0,0 +1,104 @@ +"Games" +{ + "left4dead2" + { + "MemPatches" + { + /** + * This patch prevents marking survivors. + * -- + * Example: + * Reversed code of default behavior: pCharge->CCharge.m_hitSurvivors[pPlayer->CTerrorPlayer.m_survivorCharacter] = 1; + * New behavior when patch is enabled: pCharge->CCharge.m_hitSurvivors[pPlayer->CTerrorPlayer.m_survivorCharacter] = 0; + **/ + "CCharge::HandleCustomCollision()::MarkSurvivor" + { + "signature" "CCharge::HandleCustomCollision" + "linux" + { + "offset" "333" + "verify" "\x01" + "patch" "\x00" + + } + "windows" + { + "offset" "305" + "verify" "\x01" + "patch" "\x00" + } + } + } + "Functions" + { + "Lux::ThrowImpactedSurvivor" + { + "signature" "ThrowImpactedSurvivor" + "callconv" "cdecl" + "return" "int" + "this" "ignore" + "arguments" + { + "attacker" + { + "type" "cbaseentity" + } + "victim" + { + "type" "cbaseentity" + } + "charge_progress" + { + "type" "float" + } + "should_damage" + { + "type" "bool" + } + } + } + } + "Signatures" + { + /* + * credit from here for sigs/remarks for finding or outdate sigs + * https://github.com/SilvDev/Left4DHooks/blob/main/sourcemod/gamedata/left4dhooks.l4d2.txt + * https://github.com/Psykotikism/L4D1-2_Signatures/blob/main/l4d2/gamedata/l4d2_signatures.txt + */ + + /* + * CTerrorPlayer::Fling(Vector const&,PlayerAnimEvent_t,CBaseCombatCharacter *,float) + * + * Vector: how target is supposed to fly, Animevent is "76" for chargerbounce, CBasePlayer is attacker, float is Incap Animation time + * + * Find via CCharge::HandleCustomCollision (strings) -> ThrowImpactedSurvivor (strings, structure) -> Fling (feel it out) + * + */ + "CCharge::HandleCustomCollision" + { + "library" "server" + "linux" "@_ZN7CCharge21HandleCustomCollisionEP11CBaseEntityRK6VectorS4_P10CGameTraceP9CMoveData" + "windows" "\x2A\x2A\x2A\x2A\x2A\x2A\x2A\x2A\x2A\x2A\x2A\x2A\x2A\x2A\x33\x2A\x89\x2A\x2A\x8B\x2A\x2A\x53\x8B\x2A\x89\x2A\x2A\x8B\x2A\x2A\x2A\x2A\x2A\x56\x8B" + /* ? ? ? ? ? ? ? ? ? ? ? ? ? ? 33 ? 89 ? ? 8B ? ? 53 8B ? 89 ? ? 8B ? ? ? ? ? 56 8B */ + } + /* + * ThrowImpactedSurvivor(CTerrorPlayer *, CTerrorPlayer *, float, bool) + * Search: "charger_impact" + */ + "ThrowImpactedSurvivor" + { + "library" "server" + "linux" "@_Z21ThrowImpactedSurvivorP13CTerrorPlayerS0_fb" + "windows" "\x2A\x2A\x2A\x2A\x2A\x2A\x53\x56\x57\x8B\x2A\x2A\x8B\x2A\x2A\x2A\x2A\x2A\xC1\x2A\x2A\x2A\x2A\x74\x2A\x2A\x2A\x2A\x2A\x2A\x2A\x2A\x8B\x2A\x2A\x2A\x2A\x2A\x8B" + /* ? ? ? ? ? ? 53 56 57 8B ? ? 8B ? ? ? ? ? C1 ? ? ? ? 74 ? ? ? ? ? ? ? ? 8B ? ? ? ? ? 8B */ + } + "CBaseEntity::SetAbsVelocity" + { + "library" "server" + "linux" "@_ZN11CBaseEntity14SetAbsVelocityERK6Vector" + "windows" "\x2A\x2A\x2A\x2A\x2A\x2A\x56\x8B\x75\x08\xF3\x0F\x10\x06\x57\x8B\xF9" + /* ? ? ? ? ? ? 56 8B 75 08 F3 0F 10 06 57 8B F9 */ + } + } + } +} \ No newline at end of file diff --git a/addons/sourcemod/plugins/fixes/Charger_Collision_patch.smx b/addons/sourcemod/plugins/fixes/Charger_Collision_patch.smx new file mode 100644 index 000000000..ed9ff9b09 Binary files /dev/null and b/addons/sourcemod/plugins/fixes/Charger_Collision_patch.smx differ diff --git a/addons/sourcemod/scripting/Charger_Collision_patch.sp b/addons/sourcemod/scripting/Charger_Collision_patch.sp new file mode 100644 index 000000000..29b71fc1f --- /dev/null +++ b/addons/sourcemod/scripting/Charger_Collision_patch.sp @@ -0,0 +1,257 @@ +/* +* Fixes for gamebreaking bugs and stupid gameplay aspects +* Copyright (C) 2022 LuxLuma +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +*/ + +#pragma semicolon 1 + +#include +#include +#include +#include +#include + +#pragma newdecls required + +#define GAMEDATA "charger_collision_patch" +#define CHARGE_MARKSURVIVOR_KEY "CCharge::HandleCustomCollision()::MarkSurvivor" +#define CHARGE_ISINCAPPED_KEY "CCharge::HandleCustomCollision()::IsIncap" +#define PLUGIN_VERSION "2.0" + + +#define IMPACT_SND_INTERVAL 0.1 + +enum struct ChargerCharge +{ + int m_Index; + float m_NextImpactSND; + bool m_MarkHit[MAXPLAYERS+1]; + + void Reset() + { + this.m_NextImpactSND = 0.0; + for(int i; i <= MAXPLAYERS; ++i) + { + this.m_MarkHit[i] = false; + } + } +} + +ChargerCharge g_ChargerCharge[MAXPLAYERS+1]; + +MemoryPatch Charge_MarkSurvivor; +Handle g_hSetAbsVelocity; + + +public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) +{ + if(GetEngineVersion() != Engine_Left4Dead2) + { + strcopy(error, err_max, "Plugin only supports Left 4 Dead 2"); + return APLRes_SilentFailure; + } + return APLRes_Success; +} + +public Plugin myinfo = +{ + name = "[L4D2]Charger_Collision_Patch", + author = "Lux", + description = "Fixes charger only allow to his 1 survivor index & allows colliding same target more than once", + version = PLUGIN_VERSION, + url = "forums.alliedmods.net/showthread.php?p=2647017" +}; + + +public void OnPluginStart() +{ + CreateConVar("charger_collision_patch_version", PLUGIN_VERSION, "", FCVAR_NOTIFY|FCVAR_DONTRECORD); + + for(int i; i <= MAXPLAYERS; ++i) + { + g_ChargerCharge[i].m_Index = i; + } + + Handle hGamedata = LoadGameConfigFile(GAMEDATA); + if(hGamedata == null) + { + SetFailState("Failed to load \"%s.txt\" gamedata.", GAMEDATA); + } + + Charge_MarkSurvivor = MemoryPatch.CreateFromConf(hGamedata, CHARGE_MARKSURVIVOR_KEY); + if(!Charge_MarkSurvivor.Validate()) + { + SetFailState("Failed to validate patch \"%s\"", CHARGE_MARKSURVIVOR_KEY); + } + + Handle hDetour; + hDetour = DHookCreateFromConf(hGamedata, "Lux::ThrowImpactedSurvivor"); + if(!hDetour) + { + SetFailState("Failed to find 'Lux::ThrowImpactedSurvivor' signature"); + } + + StartPrepSDKCall(SDKCall_Entity); + if(!PrepSDKCall_SetFromConf(hGamedata, SDKConf_Signature, "CBaseEntity::SetAbsVelocity")) + { + SetFailState("Error finding the 'CBaseEntity::SetAbsVelocity' signature."); + } + + PrepSDKCall_AddParameter(SDKType_Vector, SDKPass_Pointer); + g_hSetAbsVelocity = EndPrepSDKCall(); + if(g_hSetAbsVelocity == null) + { + SetFailState("Unable to prep SDKCall 'CBaseEntity::SetAbsVelocity'"); + } + + if(!DHookEnableDetour(hDetour, false, ThrowImpactedSurvivor)) + { + SetFailState("Failed to detour 'Lux::ThrowImpactedSurvivor'"); + } + + if(Charge_MarkSurvivor.Enable()) + { + PrintToServer("[%s] Enabled \"%s\" patch", GAMEDATA, CHARGE_MARKSURVIVOR_KEY); + } + + delete hGamedata; + + HookEvent("round_start", RoundStart); + HookEvent("charger_charge_start", ClearMarkedSurvivors, EventHookMode_Pre); + HookEvent("charger_charge_end", ClearMarkedSurvivors, EventHookMode_Pre); + AddNormalSoundHook(ImpactSNDHook); + + for(int i = 1; i <= MaxClients; ++i) + { + if(IsClientInGame(i)) + OnClientPutInServer(i); + } +} + +public MRESReturn ThrowImpactedSurvivor(Handle hReturn, Handle hParams) +{ + int iCharger = DHookGetParam(hParams, 1); + int iVictim = DHookGetParam(hParams, 2); + bool ShouldDamage = DHookGetParam(hParams, 4); + + //sanity check everything avoids conflicts + if(!ShouldDamage) + return MRES_Ignored; + + if(iCharger < 1 || iCharger > MaxClients || + GetClientTeam(iCharger) != 3 || !IsPlayerAlive(iCharger)) + { + return MRES_Ignored; + } + + int iAbility = GetEntPropEnt(iCharger, Prop_Send, "m_customAbility"); + if(iAbility == -1 || !HasEntProp(iAbility, Prop_Send, "m_isCharging")) + { + return MRES_Ignored; + } + + if(!GetEntProp(iAbility, Prop_Send, "m_isCharging", 1)) + return MRES_Ignored; + + int iCarryVictim = GetEntPropEnt(iCharger, Prop_Send, "m_carryVictim"); + if(iCarryVictim == -1) + { + DHookSetReturn(hReturn, 1); + return MRES_Supercede; + } + + if(iCarryVictim == iVictim) + { + //PrintToChatAll("illegal damage on %N from %N", iVictim, iCharger); + g_ChargerCharge[iCharger].m_NextImpactSND = GetGameTime() + IMPACT_SND_INTERVAL; + DHookSetReturn(hReturn, 1); + return MRES_Supercede; + } + + //Set velocity to 0 so impulse velocity does not account for current velocity + static float vecNoVel[3] = {0.0, 0.0, 0.0}; + SDKCall(g_hSetAbsVelocity, iVictim, vecNoVel); + + g_ChargerCharge[iCharger].m_NextImpactSND = GetGameTime() + IMPACT_SND_INTERVAL; + if(g_ChargerCharge[iCharger].m_MarkHit[iVictim]) + { + DHookSetParam(hParams, 4, false); + DHookSetReturn(hReturn, 1); + return MRES_ChangedHandled; + } + g_ChargerCharge[iCharger].m_MarkHit[iVictim] = true; + return MRES_Ignored; +} + +public Action ImpactSNDHook(int clients[MAXPLAYERS], int &numClients, char sample[PLATFORM_MAX_PATH], int &iCharger, int &channel, float &volume, int &level, int &pitch, int &flags, char soundEntry[PLATFORM_MAX_PATH], int &seed) +{ + if(iCharger < 1 || iCharger > MaxClients || + GetClientTeam(iCharger) != 3 || !IsPlayerAlive(iCharger)) + { + return Plugin_Continue; + } + + int iAbility = GetEntPropEnt(iCharger, Prop_Send, "m_customAbility"); + if(iAbility == -1 || !HasEntProp(iAbility, Prop_Send, "m_isCharging")) + { + return Plugin_Continue; + } + + if(!GetEntProp(iAbility, Prop_Send, "m_isCharging", 1)) + return Plugin_Continue; + + if(g_ChargerCharge[iCharger].m_NextImpactSND > GetGameTime()) + { + if(StrContains(sample, "player/charger/hit/charger_smash_0", false) != -1) + return Plugin_Handled; + } + + return Plugin_Continue; +} + +public void OnClientPutInServer(int client) +{ + SDKHook(client, SDKHook_SpawnPost, OnSpawnPost); +} + +public void OnSpawnPost(int client) +{ + g_ChargerCharge[client].Reset(); + for(int i; i <= MAXPLAYERS; ++i) + { + g_ChargerCharge[i].m_MarkHit[client] = false; + } +} + +public void RoundStart(Event event, const char[] eventName, bool dontBroadcast) +{ + for(int i; i <= MAXPLAYERS; ++i) + { + g_ChargerCharge[i].Reset(); + } +} + +public void ClearMarkedSurvivors(Event event, const char[] eventName, bool dontBroadcast) +{ + int iCharger = GetClientOfUserId(event.GetInt("userid")); + if(iCharger < 1) + return; + + for(int i; i <= MAXPLAYERS; ++i) + { + g_ChargerCharge[iCharger].m_MarkHit[i] = false; + } +} \ No newline at end of file diff --git a/cfg/cfgogl/advanced8v8/confogl_plugins.cfg b/cfg/cfgogl/advanced8v8/confogl_plugins.cfg index de421a7fc..9740d1b8f 100644 --- a/cfg/cfgogl/advanced8v8/confogl_plugins.cfg +++ b/cfg/cfgogl/advanced8v8/confogl_plugins.cfg @@ -31,6 +31,9 @@ sm plugins load optional/l4d_superversus.smx // 药丸发光 sm plugins load optional/l4d2_random_glow_item.smx +// 修复牛牛创不动同样生还 +sm plugins load fixes/Charger_Collision_patch.smx + // 分算法 sm plugins load optional/l4d2_solidhealthmedkits.smx sm plugins load optional/l4d3_scoremod_DRDK_remake2.smx diff --git a/cfg/cfgogl/zonemod6v6/confogl_plugins.cfg b/cfg/cfgogl/zonemod6v6/confogl_plugins.cfg index b10e0efff..c0ef91752 100644 --- a/cfg/cfgogl/zonemod6v6/confogl_plugins.cfg +++ b/cfg/cfgogl/zonemod6v6/confogl_plugins.cfg @@ -27,5 +27,8 @@ sm plugins load optional/l4d_CreateSurvivorBot.smx // 多人多克插件 sm plugins load optional/l4d_superversus.smx +// 修复牛牛创不动同样生还 +sm plugins load fixes/Charger_Collision_patch.smx + // 药丸发光 sm plugins load optional/l4d2_random_glow_item.smx \ No newline at end of file diff --git a/cfg/cfgogl/zonemod8v8/confogl_plugins.cfg b/cfg/cfgogl/zonemod8v8/confogl_plugins.cfg index fb7ef25d0..177b82429 100644 --- a/cfg/cfgogl/zonemod8v8/confogl_plugins.cfg +++ b/cfg/cfgogl/zonemod8v8/confogl_plugins.cfg @@ -29,4 +29,7 @@ sm plugins load optional/l4d_CreateSurvivorBot.smx sm plugins load optional/l4d_superversus.smx // 药丸发光 -sm plugins load optional/l4d2_random_glow_item.smx \ No newline at end of file +sm plugins load optional/l4d2_random_glow_item.smx + +// 修复牛牛创不动同样生还 +sm plugins load fixes/Charger_Collision_patch.smx \ No newline at end of file