/* * Anti Rush * Copyright (C) 2023 Silvers * * 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 . */ #define PLUGIN_VERSION "1.19" #define DEBUG_BENCHMARK 0 // 0=Off. 1=Benchmark logic function. /*====================================================================================== Plugin Info: * Name : [L4D & L4D2] Anti Rush * Author : SilverShot * Descrp : Slowdown or teleport rushers and slackers back to the group. Uses flow distance for accuracy. * Link : https://forums.alliedmods.net/showthread.php?t=322392 * Plugins : https://sourcemod.net/plugins.php?exact=exact&sortby=title&search=1&author=Silvers ======================================================================================== Change Log: 1.19 (10-Mar-2023) - Added cvar "l4d_anti_rush_health" to hurt players who are rushing. Requested by "Voevoda". - Changed cvar "l4d_anti_rush_type" to allow disabling teleport or slowdown, to only enable health drain. - Translation phrases updated to support the health drain only type. Thanks to "Voevoda" and "HarryPotter" for updating Russian and Chinese translations. 1.18 (01-Jun-2022) - L4D1: Fixed throwing errors. - L4D2: Added map "c5m5_bridge" to the data config. 1.17 (04-Dec-2021) - Changes to fix warnings when compiling on SourceMod 1.11. 1.16 (19-Oct-2021) - Plugin now ignores players who are being healed or revived, or players healing or reviving someone. Requested to "Psyk0tik". 1.15 (09-Sep-2021) - Fixed the last update breaking the plugin in L4D1. 1.14 (25-Aug-2021) - Plugin now ignores players being carried by the Charger. Thanks to "Darkwob" for reporting. 1.13 (30-Jun-2021) - Plugin now ignores players inside elevators. 1.12 (16-Jun-2021) - L4D1: Fixed throwing errors about missing property "m_type". Thanks to "Dragokas" for reporting. 1.11 (20-Apr-2021) - Added cvars "l4d_anti_rush_flags" and "l4d_anti_rush_ignore" to make players with certain flags immune to the plugins actions. Requested by "Shadowart". 1.10a (12-Apr-2021) - Updated data config "data/l4d_anti_rush.cfg" with new triggers. Thanks to "jeremyvillanueva" for providing. 1.10 (23-Mar-2021) - Cleaning source code from last update. - Fixed potentially using the wrong "add" range from the config. 1.9 (22-Mar-2021) - Added optional config "data/l4d_anti_rush.cfg" to extend the trigger detection range during certain crescendo events. Requested by "SilentBr". 1.8 (09-Oct-2020) - Changed cvar "l4d_anti_rush_finale" to allow working in Gauntlet type finales only. Thanks to "Xanaguy" for requesting. - Renamed cvar "l4d_anti_rush_inacpped" to "l4d_anti_rush_incapped" fixing spelling mistake. 1.7 (09-Oct-2020) - Added cvar "l4d_anti_rush_tanks" to control if the plugins active when any tank is alive. - Fixed not resetting slowdown on team change or player death (optimization). 1.6 (15-Jul-2020) - Optionally added left4dhooks forwards "L4D_OnGetCrouchTopSpeed" and "L4D_OnGetWalkTopSpeed" to modify speed when walking or crouched. - Uncomment the section and recompile if you want to enable. Only required to slowdown players more than default. - Thanks to "SilentBr" for reporting. 1.5 (10-May-2020) - Added Traditional Chinese and Simplified Chinese translations. Thanks to "fbef0102". - Extra checks to prevent "IsAllowedGameMode" throwing errors. - Various changes to tidy up code. 1.4 (10-Apr-2020) - Added Hungarian translations. Thanks to "KasperH" for providing. - Added Russian translations. Thanks to "Dragokas" for updating with new phrases. - Added cvar "l4d_anti_rush_incapped" to ignored incapped players from being used to calculate rushers or slackers distance. - Added cvars "l4d_anti_rush_warn_last" and "l4d_anti_rush_warn_lead" to warn players about being teleported or slowed down. - Added cvar "l4d_anti_rush_warn_time" to control how often someone is warned. - Removed minimum value being set for "l4d_anti_rush_range_lead" cvar which prevented turning off lead feature. - The cvars "l4d_anti_rush_range_last" and "l4d_anti_rush_range_lead" minimum values are now set internally (1500.0). - Translation files and plugin updated. 1.3 (09-Apr-2020) - Added reset slowdown in case players are out-of-bound or have invalid flow distances to calculate the range. - Increased minimum value of "l4d_anti_rush_range_lead" cvar from 500.0 to 1000.0. - Removed minimum value being set for "l4d_anti_rush_range_last" cvar. Thanks to "Alex101192" for reporting. 1.2 (08-Apr-2020) - Added cvar "l4d_anti_rush_finale" to allow or disallow the plugin in finales. 1.1 (07-Apr-2020) - Changed how the plugin functions. Now calculates rushers/slackers by their flow distance to the nearest half of Survivors. - Fixed not accounting for multiple rushers with "type 2" setting. - Fixed "IsAllowedGameMode" from throwing errors when the "_tog" cvar was changed before MapStart. 1.0 (26-Mar-2020) - Added Russian translations to the .zip. Thanks to "KRUTIK" for providing. - No other changes. 1.0 (26-Mar-2020) - Initial release. ======================================================================================*/ #pragma semicolon 1 #pragma newdecls required #include #include #include #include #if DEBUG_BENCHMARK #include Handle g_Prof; float g_fBenchMin; float g_fBenchMax; float g_fBenchAvg; float g_iBenchTicks; #endif #define CVAR_FLAGS FCVAR_NONE #define MINIMUM_RANGE 1500.0 // Minimum range for last and lead cvars. #define EVENTS_CONFIG "data/l4d_anti_rush.cfg" ConVar g_hCvarAllow, g_hCvarMPGameMode, g_hCvarModes, g_hCvarModesOff, g_hCvarModesTog, g_hCvarFinale, g_hCvarFlags, g_hCvarIgnore, g_hCvarHealth, g_hCvarIncap, g_hCvarPlayers, g_hCvarRangeLast, g_hCvarRangeLead, g_hCvarSlow, g_hCvarTank, g_hCvarText, g_hCvarTime, g_hCvarType, g_hCvarWarnLast, g_hCvarWarnLead, g_hCvarWarnTime; float g_fCvarHealth, g_fCvarRangeLast, g_fCvarRangeLead, g_fCvarSlow, g_fCvarTime, g_fCvarWarnLast, g_fCvarWarnLead, g_fCvarWarnTime; int g_iCvarFinale, g_iCvarFlags, g_iCvarIgnore, g_iCvarIncap, g_iCvarPlayers, g_iCvarTank, g_iCvarText, g_iCvarType; bool g_bCvarAllow, g_bMapStarted, g_bLeft4Dead2; bool g_bInhibit[MAXPLAYERS+1]; float g_fHintLast[MAXPLAYERS+1]; float g_fHintWarn[MAXPLAYERS+1]; float g_fLastFlow[MAXPLAYERS+1]; float g_fHighestFlow[MAXPLAYERS+1]; Handle g_hTimer; char g_sMap[PLATFORM_MAX_PATH]; bool g_bFoundMap; bool g_bEventStarted; float g_fEventExtended; ArrayList g_hElevators; GlobalForward g_OnRushForward; // ==================================================================================================== // PLUGIN INFO / START / END // ==================================================================================================== public Plugin myinfo = { name = "[L4D & L4D2] Anti Rush", author = "SilverShot", description = "Slowdown or teleport rushers and slackers back to the group. Uses flow distance for accuracy.", version = PLUGIN_VERSION, url = "https://forums.alliedmods.net/showthread.php?t=322392" } public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) { EngineVersion test = GetEngineVersion(); if( test == Engine_Left4Dead ) g_bLeft4Dead2 = false; else if( test == Engine_Left4Dead2 ) g_bLeft4Dead2 = true; else { strcopy(error, err_max, "Plugin only supports Left 4 Dead 1 & 2."); return APLRes_SilentFailure; } return APLRes_Success; } public void OnPluginStart() { LoadTranslations("anti_rush.phrases"); g_hCvarAllow = CreateConVar( "l4d_anti_rush_allow", "1", "0=Plugin off, 1=Plugin on.", CVAR_FLAGS ); g_hCvarModes = CreateConVar( "l4d_anti_rush_modes", "", "Turn on the plugin in these game modes, separate by commas (no spaces). (Empty = all).", CVAR_FLAGS ); g_hCvarModesOff = CreateConVar( "l4d_anti_rush_modes_off", "", "Turn off the plugin in these game modes, separate by commas (no spaces). (Empty = none).", CVAR_FLAGS ); g_hCvarModesTog = CreateConVar( "l4d_anti_rush_modes_tog", "0", "Turn on the plugin in these game modes. 0=All, 1=Coop, 2=Survival, 4=Versus, 8=Scavenge. Add numbers together.", CVAR_FLAGS ); g_hCvarFinale = CreateConVar( "l4d_anti_rush_finale", g_bLeft4Dead2 ? "2" : "0", "Should the plugin activate in finales. 0=Off. 1=All finales. 2=Gauntlet type finales (L4D2 only).", CVAR_FLAGS ); g_hCvarFlags = CreateConVar( "l4d_anti_rush_flags", "", "Players with these flags will be immune from teleporting forward when behind or slowing down when ahead.", CVAR_FLAGS ); g_hCvarHealth = CreateConVar( "l4d_anti_rush_health", "0", "0=Off. Amount of health to remove every second when someone is rushing.", CVAR_FLAGS); g_hCvarIgnore = CreateConVar( "l4d_anti_rush_ignore", "0", "Should players with the immune flags be counted toward total flow distance. 0=Ignore them. 1=Count them.", CVAR_FLAGS ); g_hCvarIncap = CreateConVar( "l4d_anti_rush_incapped", "0", "0=Off. How many survivors must be incapped before ignoring them in calculating rushers and slackers.", CVAR_FLAGS ); g_hCvarPlayers = CreateConVar( "l4d_anti_rush_players", "3", "Minimum number of alive survivors before the function kicks in. Must be 3 or greater otherwise the lead/last and average cannot be detected.", CVAR_FLAGS, true, 3.0 ); g_hCvarRangeLast = CreateConVar( "l4d_anti_rush_range_last", "3000.0", "0.0=Off. How far behind someone can travel from the average Survivor distance before being teleported forward.", CVAR_FLAGS ); g_hCvarRangeLead = CreateConVar( "l4d_anti_rush_range_lead", "3000.0", "How far forward someone can travel from the average Survivor distance before being teleported, slowed down or health drained.", CVAR_FLAGS, true, MINIMUM_RANGE ); g_hCvarSlow = CreateConVar( "l4d_anti_rush_slow", "75.0", "Maximum speed someone can travel when being slowed down.", CVAR_FLAGS, true, 20.0 ); g_hCvarTank = CreateConVar( "l4d_anti_rush_tanks", "1", "0=Off. 1=On. Should Anti-Rush be enabled when there are active Tanks.", CVAR_FLAGS ); g_hCvarText = CreateConVar( "l4d_anti_rush_text", "1", "0=Off. 1=Print To Chat. 2=Hint Text. Display a message to someone rushing, or falling behind.", CVAR_FLAGS ); g_hCvarTime = CreateConVar( "l4d_anti_rush_time", "10", "How often to print the message to someone if slowdown is enabled and affecting them.", CVAR_FLAGS ); g_hCvarType = CreateConVar( "l4d_anti_rush_type", "1", "What to do with rushers. 0=Ignore (used for health drain / forward only). 1=Slowdown player speed when moving forward. 2=Teleport back to group.", CVAR_FLAGS ); g_hCvarWarnLast = CreateConVar( "l4d_anti_rush_warn_last", "2500.0", "How far behind someone can travel from the average Survivor distance before being warned about being teleported.", CVAR_FLAGS, true, MINIMUM_RANGE ); g_hCvarWarnLead = CreateConVar( "l4d_anti_rush_warn_lead", "2500.0", "How far forward someone can travel from the average Survivor distance before being warned about being teleported or slowed down.", CVAR_FLAGS, true, MINIMUM_RANGE ); g_hCvarWarnTime = CreateConVar( "l4d_anti_rush_warn_time", "15.0", "0.0=Off. How often to print a message to someone warning them they are ahead or behind and will be teleported or slowed down.", CVAR_FLAGS ); CreateConVar( "l4d_anti_rush_version", PLUGIN_VERSION, "Anti Rush plugin version.", FCVAR_NOTIFY|FCVAR_DONTRECORD); AutoExecConfig(true, "l4d_anti_rush"); g_hCvarMPGameMode = FindConVar("mp_gamemode"); g_hCvarMPGameMode.AddChangeHook(ConVarChanged_Allow); g_hCvarModes.AddChangeHook(ConVarChanged_Allow); g_hCvarModesOff.AddChangeHook(ConVarChanged_Allow); g_hCvarModesTog.AddChangeHook(ConVarChanged_Allow); g_hCvarAllow.AddChangeHook(ConVarChanged_Allow); g_hCvarFinale.AddChangeHook(ConVarChanged_Cvars); g_hCvarFlags.AddChangeHook(ConVarChanged_Cvars); g_hCvarHealth.AddChangeHook(ConVarChanged_Cvars); g_hCvarIgnore.AddChangeHook(ConVarChanged_Cvars); g_hCvarIncap.AddChangeHook(ConVarChanged_Cvars); g_hCvarPlayers.AddChangeHook(ConVarChanged_Cvars); g_hCvarRangeLast.AddChangeHook(ConVarChanged_Cvars); g_hCvarRangeLead.AddChangeHook(ConVarChanged_Cvars); g_hCvarTank.AddChangeHook(ConVarChanged_Cvars); g_hCvarText.AddChangeHook(ConVarChanged_Cvars); g_hCvarSlow.AddChangeHook(ConVarChanged_Cvars); g_hCvarTime.AddChangeHook(ConVarChanged_Cvars); g_hCvarType.AddChangeHook(ConVarChanged_Cvars); g_hCvarWarnLast.AddChangeHook(ConVarChanged_Cvars); g_hCvarWarnLead.AddChangeHook(ConVarChanged_Cvars); g_hCvarWarnTime.AddChangeHook(ConVarChanged_Cvars); g_hElevators = new ArrayList(); #if DEBUG_BENCHMARK g_Prof = CreateProfiler(); #endif g_OnRushForward = new GlobalForward("OnAntiRush", ET_Event, Param_Cell, Param_CellByRef, Param_Float); } // ==================================================================================================== // CVARS // ==================================================================================================== public void OnConfigsExecuted() { IsAllowed(); } void ConVarChanged_Allow(Handle convar, const char[] oldValue, const char[] newValue) { IsAllowed(); } void ConVarChanged_Cvars(Handle convar, const char[] oldValue, const char[] newValue) { GetCvars(); } void GetCvars() { char sTemp[32]; g_hCvarFlags.GetString(sTemp, sizeof(sTemp)); g_iCvarFlags = ReadFlagString(sTemp); g_iCvarIgnore = g_hCvarIgnore.IntValue; g_iCvarFinale = g_hCvarFinale.IntValue; g_fCvarHealth = g_hCvarHealth.FloatValue; g_iCvarIncap = g_hCvarIncap.IntValue; g_iCvarPlayers = g_hCvarPlayers.IntValue; g_fCvarTime = g_hCvarTime.FloatValue; g_iCvarTank = g_hCvarTank.IntValue; g_iCvarText = g_hCvarText.IntValue; g_fCvarSlow = g_hCvarSlow.FloatValue; g_iCvarType = g_hCvarType.IntValue; g_fCvarRangeLast = g_hCvarRangeLast.FloatValue; g_fCvarRangeLead = g_hCvarRangeLead.FloatValue; g_fCvarWarnLast = g_hCvarWarnLast.FloatValue; g_fCvarWarnLead = g_hCvarWarnLead.FloatValue; g_fCvarWarnTime = g_hCvarWarnTime.FloatValue; if( g_fCvarRangeLast && g_fCvarRangeLast < MINIMUM_RANGE ) g_fCvarRangeLast = MINIMUM_RANGE; if( g_fCvarRangeLead && g_fCvarRangeLead < MINIMUM_RANGE ) g_fCvarRangeLead = MINIMUM_RANGE; if( g_fCvarWarnLast && g_fCvarWarnLast < MINIMUM_RANGE ) g_fCvarWarnLast = MINIMUM_RANGE; if( g_fCvarWarnLead && g_fCvarWarnLead < MINIMUM_RANGE ) g_fCvarWarnLead = MINIMUM_RANGE; } void IsAllowed() { bool bCvarAllow = g_hCvarAllow.BoolValue; bool bAllowMode = IsAllowedGameMode(); GetCvars(); if( g_bCvarAllow == false && bCvarAllow == true && bAllowMode == true ) { g_bCvarAllow = true; HookEvent("round_start", Event_RoundStart); HookEvent("round_end", Event_RoundEnd); HookEvent("player_death", Event_PlayerDeath); HookEvent("player_team", Event_PlayerTeam); Event_RoundStart(null, "", false); } else if( g_bCvarAllow == true && (bCvarAllow == false || bAllowMode == false) ) { g_bCvarAllow = false; UnhookEvent("round_start", Event_RoundStart); UnhookEvent("round_end", Event_RoundEnd); UnhookEvent("player_death", Event_PlayerDeath); UnhookEvent("player_team", Event_PlayerTeam); ResetSlowdown(); ResetPlugin(); } } int g_iCurrentMode; bool IsAllowedGameMode() { if( g_hCvarMPGameMode == null ) return false; int iCvarModesTog = g_hCvarModesTog.IntValue; if( iCvarModesTog != 0 ) { if( g_bMapStarted == false ) return false; g_iCurrentMode = 0; int entity = CreateEntityByName("info_gamemode"); if( IsValidEntity(entity) ) { DispatchSpawn(entity); HookSingleEntityOutput(entity, "OnCoop", OnGamemode, true); HookSingleEntityOutput(entity, "OnSurvival", OnGamemode, true); HookSingleEntityOutput(entity, "OnVersus", OnGamemode, true); HookSingleEntityOutput(entity, "OnScavenge", OnGamemode, true); ActivateEntity(entity); AcceptEntityInput(entity, "PostSpawnActivate"); if( IsValidEntity(entity) ) // Because sometimes "PostSpawnActivate" seems to kill the ent. RemoveEdict(entity); // Because multiple plugins creating at once, avoid too many duplicate ents in the same frame } if( g_iCurrentMode == 0 ) return false; if( !(iCvarModesTog & g_iCurrentMode) ) return false; } char sGameModes[64], sGameMode[64]; g_hCvarMPGameMode.GetString(sGameMode, sizeof(sGameMode)); Format(sGameMode, sizeof(sGameMode), ",%s,", sGameMode); g_hCvarModes.GetString(sGameModes, sizeof(sGameModes)); if( sGameModes[0] ) { Format(sGameModes, sizeof(sGameModes), ",%s,", sGameModes); if( StrContains(sGameModes, sGameMode, false) == -1 ) return false; } g_hCvarModesOff.GetString(sGameModes, sizeof(sGameModes)); if( sGameModes[0] ) { Format(sGameModes, sizeof(sGameModes), ",%s,", sGameModes); if( StrContains(sGameModes, sGameMode, false) != -1 ) return false; } return true; } void OnGamemode(const char[] output, int caller, int activator, float delay) { if( strcmp(output, "OnCoop") == 0 ) g_iCurrentMode = 1; else if( strcmp(output, "OnSurvival") == 0 ) g_iCurrentMode = 2; else if( strcmp(output, "OnVersus") == 0 ) g_iCurrentMode = 4; else if( strcmp(output, "OnScavenge") == 0 ) g_iCurrentMode = 8; } // ==================================================================================================== // HOOK CUSTOM ARLARM EVENTS // ==================================================================================================== int g_iSectionLevel; void LoadEventConfig() { g_bFoundMap = false; char sPath[PLATFORM_MAX_PATH]; BuildPath(Path_SM, sPath, sizeof(sPath), EVENTS_CONFIG); if( FileExists(sPath) ) { ParseConfigFile(sPath); } } bool ParseConfigFile(const char[] file) { SMCParser parser = new SMCParser(); SMC_SetReaders(parser, ColorConfig_NewSection, ColorConfig_KeyValue, ColorConfig_EndSection); parser.OnEnd = ColorConfig_End; char error[128]; int line = 0, col = 0; SMCError result = parser.ParseFile(file, line, col); if( result != SMCError_Okay ) { parser.GetErrorString(result, error, sizeof(error)); SetFailState("%s on line %d, col %d of %s [%d]", error, line, col, file, result); } delete parser; return (result == SMCError_Okay); } SMCResult ColorConfig_NewSection(Handle parser, const char[] section, bool quotes) { g_iSectionLevel++; // Map if( g_iSectionLevel == 2 && strcmp(section, g_sMap) == 0 ) { g_bFoundMap = true; } else { g_bFoundMap = false; } return SMCParse_Continue; } SMCResult ColorConfig_KeyValue(Handle parser, const char[] key, const char[] value, bool key_quotes, bool value_quotes) { // On / Off if( g_iSectionLevel == 2 && g_bFoundMap ) { if( strcmp(key, "add") == 0 ) { g_fEventExtended = StringToFloat(value); } else { static char sSplit[3][64]; int len = ExplodeString(value, ":", sSplit, sizeof(sSplit), sizeof(sSplit[])); if( len != 3 ) { LogError("Malformed string in l4d_anti_rush.cfg. Section [%s] key [%s] value [%s].", g_sMap, key, value); } else { int entity = FindByClassTargetName(sSplit[0], sSplit[1]); if( entity != INVALID_ENT_REFERENCE ) { if( strcmp(key, "1") == 0 ) { HookSingleEntityOutput(entity, sSplit[2], OutputStart); } else if( strcmp(key, "0") == 0 ) { HookSingleEntityOutput(entity, sSplit[2], OutputStop); } } } } } return SMCParse_Continue; } void OutputStart(const char[] output, int caller, int activator, float delay) { g_bEventStarted = true; } void OutputStop(const char[] output, int caller, int activator, float delay) { g_bEventStarted = false; } SMCResult ColorConfig_EndSection(Handle parser) { g_iSectionLevel--; return SMCParse_Continue; } void ColorConfig_End(Handle parser, bool halted, bool failed) { if( failed ) SetFailState("Error: Cannot load the config file: \"%s\"", EVENTS_CONFIG); } int FindByClassTargetName(const char[] sClass, const char[] sTarget) { char sName[64]; int entity = INVALID_ENT_REFERENCE; // Is targetname numeric? bool numeric = true; for( int i = 0; i < strlen(sTarget); i++ ) { if( IsCharNumeric(sTarget[i]) == false ) { numeric = false; break; } } // Search by hammer ID or targetname while( (entity = FindEntityByClassname(entity, sClass)) != INVALID_ENT_REFERENCE ) { if( numeric ) { if( GetEntProp(entity, Prop_Data, "m_iHammerID") == StringToInt(sTarget) ) return entity; } else { GetEntPropString(entity, Prop_Data, "m_iName", sName, sizeof(sName)); if( strcmp(sTarget, sName) == 0 ) return entity; } } return INVALID_ENT_REFERENCE; } // ==================================================================================================== // EVENTS // ==================================================================================================== void Event_RoundStart(Event event, const char[] name, bool dontBroadcast) { delete g_hTimer; // Finales allowed, or not finale if( g_iCvarFinale || (g_iCvarFinale == 0 && L4D_IsMissionFinalMap() == false) ) { // Gauntlet finale only if( g_iCvarFinale == 2 && g_bLeft4Dead2 ) { int entity = FindEntityByClassname(-1, "trigger_finale"); if( entity != -1 ) { if( GetEntProp(entity, Prop_Data, "m_type") != 1 ) return; } } g_hTimer = CreateTimer(1.0, TimerTest, _, TIMER_REPEAT); LoadEventConfig(); } // Get elevators g_hElevators.Clear(); int entity = -1; while( (entity = FindEntityByClassname(entity, "func_elevator")) != INVALID_ENT_REFERENCE ) { g_hElevators.Push(EntIndexToEntRef(entity)); } } void Event_RoundEnd(Event event, const char[] name, bool dontBroadcast) { ResetSlowdown(); ResetPlugin(); } void Event_PlayerDeath(Event event, const char[] name, bool dontBroadcast) { int client = GetClientOfUserId(event.GetInt("userid")); if( client ) { ResetClient(client); } } void Event_PlayerTeam(Event event, const char[] name, bool dontBroadcast) { int client = GetClientOfUserId(event.GetInt("userid")); if( client ) { ResetClient(client); } } public void OnMapStart() { GetCurrentMap(g_sMap, sizeof(g_sMap)); g_bMapStarted = true; } public void OnMapEnd() { g_bMapStarted = false; ResetPlugin(); } void ResetPlugin() { for( int i = 1; i <= MAXPLAYERS; i++ ) { ResetClient(i); } delete g_hTimer; g_hElevators.Clear(); g_fEventExtended = 0.0; g_bEventStarted = false; } void ResetClient(int i) { g_bInhibit[i] = false; g_fHintLast[i] = 0.0; g_fHintWarn[i] = 0.0; g_fLastFlow[i] = 0.0; g_fHighestFlow[i] = 0.0; SDKUnhook(i, SDKHook_PreThinkPost, PreThinkPost); } void ResetSlowdown() { for( int i = 1; i <= MaxClients; i++ ) { if( g_bInhibit[i] && IsClientInGame(i) ) { SDKUnhook(i, SDKHook_PreThinkPost, PreThinkPost); } g_bInhibit[i] = false; } } // ==================================================================================================== // LOGIC // ==================================================================================================== Action TimerTest(Handle timer) { if( !g_bMapStarted ) return Plugin_Continue; #if DEBUG_BENCHMARK StartProfiling(g_Prof); #endif static bool bTanks; if( g_iCvarTank == 0 ) { if( L4D2_GetTankCount() > 0 ) { if( !bTanks ) { bTanks = true; ResetSlowdown(); } return Plugin_Continue; } else { bTanks = false; } } float currentFlow; int count, countflow, index; // Get survivors flow distance ArrayList aList = new ArrayList(2); // Account for incapped int clients[MAXPLAYERS+1]; int incapped, client; // Check valid survivors, count incapped for( int i = 1; i <= MaxClients; i++ ) { if( IsClientInGame(i) && GetClientTeam(i) == 2 && IsPlayerAlive(i) ) { // Immune players - ignore from flow calculations if( g_iCvarFlags != 0 && g_iCvarIgnore == 0 && CheckCommandAccess(client, "", g_iCvarFlags, true) ) { continue; } // Count clients[count++] = i; if( g_iCvarIncap ) { if( GetEntProp(i, Prop_Send, "m_isIncapacitated", 1) ) incapped++; } } } for( int i = 0; i < count; i++ ) { client = clients[i]; // Ignore incapped if( g_iCvarIncap && incapped >= g_iCvarIncap && GetEntProp(client, Prop_Send, "m_isIncapacitated", 1) ) continue; // Ignore healing / using stuff if( g_bLeft4Dead2 && GetEntPropEnt(client, Prop_Send, "m_useActionTarget") > 0 ) continue; // Ignore reviving if( GetEntPropEnt(client, Prop_Send, "m_reviveOwner") > 0 || GetEntPropEnt(client, Prop_Send, "m_reviveTarget") > 0 ) continue; // Ignore pinned by Charger if( g_bLeft4Dead2 && GetEntPropEnt(client, Prop_Send, "m_carryAttacker") != -1 ) continue; // Ignore in Elevator int lift = GetEntPropEnt(client, Prop_Send, "m_hGroundEntity"); if( lift > MaxClients ) { lift = EntIndexToEntRef(lift); if( g_hElevators.FindValue(lift) != -1 ) continue; } // Get flow currentFlow = L4D2Direct_GetFlowDistance(client); if( currentFlow && currentFlow != -9999.0 ) // Invalid flows { // Only get the highest flow if(currentFlow > g_fHighestFlow[client]) { g_fHighestFlow[client] = currentFlow; } countflow++; index = aList.Push(currentFlow); aList.Set(index, client, 1); } // Reset slowdown if players flow is invalid else if( g_bInhibit[client] == true ) { g_bInhibit[client] = false; SDKUnhook(client, SDKHook_PreThinkPost, PreThinkPost); } } // In case not enough players or some have invalid flow distance, we still need an average. if( countflow >= g_iCvarPlayers ) { aList.Sort(Sort_Descending, Sort_Float); int clientAvg; float lastFlow; float distance; // Detect rushers if( g_fCvarRangeLead ) { // Loop through survivors from highest flow for( int i = 0; i < countflow; i++ ) { client = aList.Get(i, 1); // Immune players if( g_iCvarFlags != 0 && g_iCvarIgnore == 1 && CheckCommandAccess(client, "", g_iCvarFlags, true) ) { continue; } bool flowBack = true; // Only check nearest half of survivor pack. if( i < countflow / 2 ) { currentFlow = aList.Get(i, 0); // Loop through from next survivor to mid-way through the pack. for( int x = i + 1; x <= countflow / 2; x++ ) { // We instead use the highest flow for the other survivors, to prevent survivors going back from triggering antirush on a player // The player we check uses their latest flow to prevent issues if they themselves glitched ahead in somecase then returned. lastFlow = g_fHighestFlow[x]; if(lastFlow <= 0.0) continue; distance = currentFlow - lastFlow; if( g_bEventStarted ) distance -= g_fEventExtended; // Warn ahead hint if( g_iCvarText && g_fCvarWarnTime && g_fCvarWarnLead && distance > g_fCvarWarnLead && distance < g_fCvarRangeLead && g_fHintWarn[client] < GetGameTime() ) { g_fHintWarn[client] = GetGameTime() + g_fCvarWarnTime; switch( g_iCvarType ) { case 0: ClientHintMessage(client, "Warn_Health"); case 1: ClientHintMessage(client, "Warn_Slowdown"); case 2: ClientHintMessage(client, "Warn_Ahead"); } } // Compare higher flow with next survivor, they're rushing if( distance > g_fCvarRangeLead ) { int punishType = g_iCvarType, result; Call_StartForward(g_OnRushForward); Call_PushCell(client); Call_PushCellRef(punishType); Call_PushFloat(distance); if(Call_Finish(result) == SP_ERROR_NONE && result > 0) break; // PrintToServer("RUSH: %N %f", client, distance); flowBack = false; // Slowdown enabled? if( g_fCvarHealth || punishType == 1 ) { // Inhibit moving forward // Only check > or < because when == the same flow distance, they're either already being slowed or running back, so we don't want to change/affect them within the same flow NavMesh. if( currentFlow > g_fLastFlow[client] ) { g_fLastFlow[client] = currentFlow; if( g_iCvarType == 1 && g_bInhibit[client] == false ) { g_bInhibit[client] = true; SDKHook(client, SDKHook_PreThinkPost, PreThinkPost); } // Hint if( g_iCvarText && g_fHintLast[client] < GetGameTime() ) { g_fHintLast[client] = GetGameTime() + g_fCvarTime; switch( g_iCvarType ) { case 0: ClientHintMessage(client, "Rush_Health"); case 1: ClientHintMessage(client, "Rush_Slowdown"); } } // Hurt for rushing? if( g_fCvarHealth ) { SDKHooks_TakeDamage(client, 0, 0, g_fCvarHealth); } } else if( currentFlow < g_fLastFlow[client] ) { flowBack = true; g_fLastFlow[client] = currentFlow; } } // Teleport enabled? if( punishType == 2 && IsClientPinned(client) == false ) { clientAvg = aList.Get(x, 1); float vPos[3]; GetClientAbsOrigin(clientAvg, vPos); // Hint if( g_iCvarText) { ClientHintMessage(client, "Rush_Ahead"); } TeleportEntity(client, vPos, NULL_VECTOR, NULL_VECTOR); } break; } } } // Running back, allow full speed if( flowBack && g_bInhibit[client] == true ) { g_bInhibit[client] = false; SDKUnhook(client, SDKHook_PreThinkPost, PreThinkPost); } } } // Teleport slacker if( g_fCvarRangeLast ) { // Loop through survivors from lowest flow to mid-way through the pack. for( int i = countflow - 1; i > countflow / 2; i-- ) { client = aList.Get(i, 1); // Immune players if( g_iCvarFlags != 0 && g_iCvarIgnore == 1 && CheckCommandAccess(client, "", g_iCvarFlags, true) ) { continue; } currentFlow = aList.Get(i, 0); // Loop through from next survivor to mid-way through the pack. for( int x = i - 1; x < countflow; x++ ) { lastFlow = g_fHighestFlow[x]; distance = lastFlow - currentFlow; if( g_bEventStarted ) distance -= g_fEventExtended; // Warn behind hint if( g_iCvarText && g_fCvarWarnTime && g_fCvarWarnLast && distance > g_fCvarWarnLast && distance < g_fCvarRangeLead && g_fHintWarn[client] < GetGameTime() ) { g_fHintWarn[client] = GetGameTime() + g_fCvarWarnTime; ClientHintMessage(client, "Warn_Behind"); } // Compare lower flow with next survivor, they're behind if( distance > g_fCvarRangeLast && IsClientPinned(client) == false ) { // PrintToServer("SLOW: %N %f", client, distance); clientAvg = aList.Get(x, 1); float vPos[3]; GetClientAbsOrigin(clientAvg, vPos); // Hint if( g_iCvarText ) { ClientHintMessage(client, "Rush_Behind"); } TeleportEntity(client, vPos, NULL_VECTOR, NULL_VECTOR); break; } } } } } else { ResetSlowdown(); } delete aList; #if DEBUG_BENCHMARK StopProfiling(g_Prof); float speed = GetProfilerTime(g_Prof); if( speed < g_fBenchMin ) g_fBenchMin = speed; if( speed > g_fBenchMax ) g_fBenchMax = speed; g_fBenchAvg += speed; g_iBenchTicks++; PrintToServer("Anti Rush benchmark: %f (Min %f. Avg %f. Max %f)", speed, g_fBenchMin, g_fBenchAvg / g_iBenchTicks, g_fBenchMax); #endif return Plugin_Continue; } /* Remove this line to enable, if you want to limit speed (slower) than default when walking/crouched. public Action L4D_OnGetCrouchTopSpeed(int target, float &retVal) { if( g_bInhibit[target] ) { retVal = g_fCvarSlow; return Plugin_Handled; } return Plugin_Continue; } public Action L4D_OnGetWalkTopSpeed(int target, float &retVal) { if( g_bInhibit[target] ) { retVal = g_fCvarSlow; return Plugin_Handled; } return Plugin_Continue; } // */ void PreThinkPost(int client) { SetEntPropFloat(client, Prop_Send, "m_flMaxspeed", g_fCvarSlow); } void ClientHintMessage(int client, const char[] translation) { static char sMessage[256]; Format(sMessage, sizeof(sMessage), "%T", translation, client); if( g_iCvarText == 1 ) { ReplaceColors(sMessage, sizeof(sMessage), false); PrintToChat(client, sMessage); } else { ReplaceColors(sMessage, sizeof(sMessage), true); PrintHintText(client, sMessage); } } void ReplaceColors(char[] translation, int size, bool hint) { ReplaceString(translation, size, "{white}", hint ? "" : "\x01"); ReplaceString(translation, size, "{cyan}", hint ? "" : "\x03"); ReplaceString(translation, size, "{orange}", hint ? "" : "\x04"); ReplaceString(translation, size, "{green}", hint ? "" : "\x05"); } bool IsClientPinned(int client) { if( GetEntProp(client, Prop_Send, "m_isIncapacitated", 1) || GetEntProp(client, Prop_Send, "m_isHangingFromLedge", 1) || GetEntPropEnt(client, Prop_Send, "m_tongueOwner") > 0 || GetEntPropEnt(client, Prop_Send, "m_pounceAttacker") > 0 ) return true; if( g_bLeft4Dead2 && ( GetEntPropEnt(client, Prop_Send, "m_jockeyAttacker") > 0 || GetEntPropEnt(client, Prop_Send, "m_carryAttacker") > 0 || GetEntPropEnt(client, Prop_Send, "m_pummelAttacker") > 0 )) return true; return false; }