Created
June 14, 2017 03:44
-
-
Save hjhee/75665918ff5426c487b0b04151ba45bb to your computer and use it in GitHub Desktop.
prevent server crash by using /use z_spawn_old instead of z_spawn
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* _ | |
* ___ _ __ ___ _ __ ___ ___ __| | | |
* / __| '__/ __| '_ ` _ \ / _ \ / _` | | |
* \__ \ | \__ \ | | | | | (_) | (_| | | |
* |___/_| |___/_| |_| |_|\___/ \__,_| | |
* | |
* _| _ _ _ _ _ . _ |` _ __|_ _ _| | |
* (_|(/__\|_)(_|VV| | || |~|~(/_(_ | (/_(_| | |
* | _ _ _ _| | _ | |
* | | |(_)(_||_||(/_ | |
* | |
****************************************** | |
****************************************** | |
****************************************** | |
* . _ _ _ | _ _ _ _ _ _|_ _ _|_. _ _ | |
* || | ||_)|(/_| | |(/_| | | (_| | |(_)| | | |
* | | |
* Using Disassembly and the Linux binary files, we investigated | |
* how L4D2 keeps track of Survivor Advancement in a Map. It's called Flow | |
* and stands for any Entities linear Progress on the "Path" from Starting | |
* to Ending Saferoom, in ingame float units. We call the native game | |
* functions to retrieve both Players and Infected Flow value. | |
* | |
* From there it's a matter of comparing them, and, since the Infected | |
* sometimes spawn very far away during events, adding some extra checks | |
* which require the Survivors to advance before removing Zombies occurs. | |
* | |
* Sometimes the flow is negative (e.g. infected-only area), so we check | |
* for that also. Furthermore in order to prevent aggressive despawning | |
* (e.g. during crescendos) we do a minimum lifetime check on the zombies | |
* to make sure they've been spawned for at least a few seconds (which | |
* gives them a chance to run to the survivors if they just spawned). | |
* | |
* To make sure the Plugin does not remove Zombies in plain Sight it runs | |
* Ray Traces from all Survivors to a leftbehind Zombie and checks for | |
* them being disrupted by something. Also, the Respawner stops working | |
* as the Survivors approach a Saferoom. | |
*/ | |
#define MODULE_NAME "DespawnInfected" | |
static const String:GAMECONFIG_FILE[] = "srsmod"; | |
static const String:GAMECONFIG_INFECTED_FLOW[] = "Infected_GetFlowDistance"; | |
static const String:GAMECONFIG_PLAYER_FLOW[] = "CTerrorPlayer_GetFlowDistance"; | |
static const String:CLASSNAME_INFECTED[] = "infected"; | |
static const String:CLASSNAME_WITCH[] = "witch"; | |
static const String:CLASSNAME_PHYSPROPS[] = "prop_physics"; | |
//why not static const? cause pawn is cool and wont let you use consts to initialize other consts | |
#define L4D2_MAX_ENTITIES 4096 | |
static const FLOWTYPE_DEFAULT = 0; | |
static const Float: TRACE_TOLERANCE = 75.0; | |
static const Float: ZOMBIE_CHECK_INTERVAL = 1.5; | |
static const Float: ZOMBIE_RESPAWN_INTERVAL = 0.5; | |
static Handle:fGetInfFlowDist = INVALID_HANDLE; | |
static Handle:fPlayerGetFlowDistance = INVALID_HANDLE; | |
static Handle:cvarDespawnInfected = INVALID_HANDLE; | |
static Handle:cvarDespawnDistance = INVALID_HANDLE; | |
static Handle:cvarDespawnNeededAdvance = INVALID_HANDLE; | |
static Handle:cvarDespawnNeededLifetime = INVALID_HANDLE; | |
static Handle:cvarRespawnRemovedCI = INVALID_HANDLE; | |
static Handle:cvarSafeRoomNear = INVALID_HANDLE; | |
static infectedInSpawnQueue = 0; | |
static Float:lastLowestSurvivorFlow = 0.0; | |
static Float:zombieLifetimes[L4D2_MAX_ENTITIES+1] = 0.0; | |
static bool:despawnerEnabled = false; | |
static bool:eventsHooked = false; | |
public DespawnInfected_OnModuleLoaded() | |
{ | |
_DI_PrepareSDKCalls(); | |
cvarDespawnInfected = CreateConVar("srs_infected_despawn", "1", " Enable or Disable the Infected Despawner ", SRS_CVAR_DEFAULT_FLAGS); | |
cvarDespawnDistance = CreateConVar("srs_infected_despawn_distance", "700.0", " How far behind a Zombie has to be for removal, in Ingame Distance Units per Second ", SRS_CVAR_DEFAULT_FLAGS); | |
cvarDespawnNeededAdvance = CreateConVar("srs_infected_despawn_min_advance", "33.0", " How much distance in Ingame Distance Units per Second the Survivors must have advanced for Despawning to trigger ", SRS_CVAR_DEFAULT_FLAGS); | |
cvarDespawnNeededLifetime = CreateConVar("srs_infected_despawn_min_lifetime", "15.0", " How many seconds a Zombie should be alive for before it can be despawned", SRS_CVAR_DEFAULT_FLAGS); | |
cvarSafeRoomNear = CreateConVar("srs_infected_despawn_near_safety", "1000.0", " If the Survivors are this close to the Saferoom the Despawner stops working ", SRS_CVAR_DEFAULT_FLAGS); | |
cvarRespawnRemovedCI = CreateConVar("srs_infected_respawn", "1", " Enable or Disable respawning of de-spawned Common Infected ", SRS_CVAR_DEFAULT_FLAGS); | |
_DI_OnModuleEnabled(); // default ON | |
HookConVarChange(cvarDespawnInfected, _DI_FlipActivation); | |
} | |
public _DI_FlipActivation(Handle:convar, const String:oldValue[], const String:newValue[]) | |
{ | |
if (ACTIVATION_FLIP_OFF_TO_ON) | |
{ | |
_DI_OnModuleEnabled(); | |
} | |
else if (ACTIVATION_FLIP_ON_TO_OFF) | |
{ | |
_DI_OnModuleDisabled(); | |
} | |
} | |
static _DI_OnModuleEnabled() | |
{ | |
CreateTimer(ZOMBIE_CHECK_INTERVAL, _DI_Check_Timer, _, TIMER_REPEAT); // this is our Zombie Checking Call | |
despawnerEnabled = true; | |
Debug_Print("Infected Despawner: MaxEntities = %d", GetMaxEntities()); | |
if (GetMaxEntities() > L4D2_MAX_ENTITIES) | |
{ | |
ThrowError("Too many entities for this plugin to handle %d -- please recompile plugin with new L4D2_MAX_ENTITIES", GetMaxEntities()); | |
} | |
HookEvent("round_start", _DI_RoundStart_Event, EventHookMode_PostNoCopy); | |
eventsHooked = true; | |
} | |
static _DI_OnModuleDisabled() | |
{ | |
if (!IsPluginEnding() && eventsHooked) | |
{ | |
UnhookEvent("round_start", _DI_RoundStart_Event, EventHookMode_PostNoCopy); | |
eventsHooked = false; | |
} | |
despawnerEnabled = false; | |
} | |
public _DI_OnEntityCreated(entity, const String:classname[]) | |
{ | |
if (StrEqual(classname, CLASSNAME_INFECTED, .caseSensitive = false)) | |
{ | |
zombieLifetimes[entity] = GetGameTime(); | |
} | |
} | |
public _DI_OnEntityDestroyed(entity) | |
{ | |
if (entity > 0 && entity < L4D2_MAX_ENTITIES) | |
{ | |
zombieLifetimes[entity] = 0.0; | |
} | |
} | |
public Action:_DI_RoundStart_Event(Handle:event, const String:name[], bool:dontBroadcast) | |
{ | |
new maxEnts = GetMaxEntities(); | |
for (new i = CLIENT_VALID_LAST+1; i <= maxEnts; i++) | |
{ | |
zombieLifetimes[i] = 0.0; | |
} | |
} | |
static _DI_PrepareSDKCalls() | |
{ | |
new Handle:conf = LoadGameConfigFile(GAMECONFIG_FILE); | |
if (conf != INVALID_HANDLE) | |
{ | |
Debug_Print("%s.txt Gamedata file loaded", GAMECONFIG_FILE); | |
} | |
else | |
{ | |
ThrowError("[SRSMOD] Failed to load %s.txt in gamedata folder", GAMECONFIG_FILE); | |
} | |
StartPrepSDKCall(SDKCall_Entity); | |
Debug_Print("GetInfectedFlowDistance Call prepped"); | |
new bool:bGetInfFlowDistFuncLoaded = PrepSDKCall_SetFromConf(conf, SDKConf_Signature, GAMECONFIG_INFECTED_FLOW); | |
if (!bGetInfFlowDistFuncLoaded) | |
{ | |
ThrowError("[SRSMOD] Could not load the GetInfectedFlowDistance signature"); | |
} | |
PrepSDKCall_SetReturnInfo(SDKType_Float, SDKPass_Plain); | |
Debug_Print("GetInfectedFlowDistance Signature prepped"); | |
fGetInfFlowDist = EndPrepSDKCall(); | |
if (fGetInfFlowDist == INVALID_HANDLE) | |
{ | |
ThrowError("[SRSMOD] Could not prep the GetInfectedFlowDistance function"); | |
} | |
StartPrepSDKCall(SDKCall_Player); | |
Debug_Print("PlayerGetFlowDistance Call prepped"); | |
new bool:bPGetFlowDistFuncLoaded = PrepSDKCall_SetFromConf(conf, SDKConf_Signature, GAMECONFIG_PLAYER_FLOW); | |
if (!bPGetFlowDistFuncLoaded) | |
{ | |
ThrowError("[SRSMOD] Could not load the PlayerGetFlowDistance signature"); | |
} | |
PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); | |
PrepSDKCall_SetReturnInfo(SDKType_Float, SDKPass_Plain); | |
Debug_Print("PlayerGetFlowDistance Signature prepped"); | |
fPlayerGetFlowDistance = EndPrepSDKCall(); | |
if (fPlayerGetFlowDistance == INVALID_HANDLE) | |
{ | |
ThrowError("[SRSMOD] Could not prep the PlayerGetFlowDistance function"); | |
} | |
} | |
// Infected::GetFlowDistance(void)const | |
static Float:L4D2_GetInfectedFlowDistance(entity) | |
{ | |
return SDKCall(fGetInfFlowDist, entity); | |
} | |
// CTerrorPlayer::GetFlowDistance(TerrorNavArea::FlowType)const | |
static Float:L4D2_GetPlayerFlowDist(client) // integer flowtype does not actually have an effect, 0 or 1 or anything work fine | |
{ | |
return SDKCall(fPlayerGetFlowDistance, client, FLOWTYPE_DEFAULT); | |
} | |
public Action:_DI_Check_Timer(Handle:timer) | |
{ | |
if (!despawnerEnabled) return Plugin_Stop; // Kill Timer if Module was disabled | |
if (!IsAllowedGameMode()) return Plugin_Continue; | |
if (!survivorCount) return Plugin_Continue; // zero Survivor case | |
if (!CountInGameHumans()) return Plugin_Continue; // zero Human case against Logspam | |
new Float:lastSurvivorFlow = 0.0; | |
new Float:firstSurvivorFlow = 0.0; | |
new Float:checkAgainst = 0.0; | |
new bool:foundOne = false; | |
new firstSurvivor = 0; | |
FOR_EACH_ALIVE_SURVIVOR_INDEXED(i) | |
{ | |
checkAgainst = L4D2_GetPlayerFlowDist(i); | |
if (checkAgainst < lastSurvivorFlow || lastSurvivorFlow == 0.0) | |
{ | |
lastSurvivorFlow = checkAgainst; | |
foundOne = true; | |
} | |
if (checkAgainst > firstSurvivorFlow || firstSurvivorFlow == 0.0) | |
{ | |
firstSurvivorFlow = checkAgainst; | |
firstSurvivor = i; | |
} | |
} | |
if (!foundOne) return Plugin_Continue; // no valid Survivor Flow found? abort | |
TryDespawningCommonZombies(lastSurvivorFlow, firstSurvivor); | |
lastLowestSurvivorFlow = lastSurvivorFlow; | |
return Plugin_Continue; | |
} | |
static TryDespawningCommonZombies(Float:lastSurvivorFlow, firstSurvivor) | |
{ | |
new Float:despawnDistance = GetConVarFloat(cvarDespawnDistance) * ZOMBIE_CHECK_INTERVAL; | |
new Float:requiredAdvance = GetConVarFloat(cvarDespawnNeededAdvance) * ZOMBIE_CHECK_INTERVAL; | |
if (lastSurvivorFlow < despawnDistance) | |
{ | |
Debug_PrintSpam("Lowest Flow Survivor (%f) found within (%f) of Map Start Area, aborting", lastSurvivorFlow, despawnDistance); | |
return; | |
} | |
decl Float:distanceToSafeRoom; | |
if (IsNearSafeRoom(firstSurvivor, /* out */distanceToSafeRoom)) | |
{ | |
Debug_PrintSpam("Survivor (%N), Flow (%f) is too close (%f) to the Saferoom, aborting Despawner", firstSurvivor, L4D2_GetPlayerFlowDist(firstSurvivor), distanceToSafeRoom); | |
return; | |
} | |
new Float:flowDifference = (lastSurvivorFlow - lastLowestSurvivorFlow); | |
if (flowDifference < requiredAdvance) | |
{ | |
Debug_PrintSpam("Flow advance of (%f) too low, want (%f) to run Despawning", flowDifference, requiredAdvance); | |
return; | |
} | |
new maxEnts = GetMaxEntities(); | |
decl String:entClass[128]; | |
decl Float:zombieFlow; | |
for (new i = CLIENT_VALID_LAST+1; i <= maxEnts; i++) | |
{ | |
if (!IsValidEntity(i)) continue; // ent validity checkAgainst | |
GetEdictClassname(i, entClass, sizeof(entClass)); | |
if (!StrEqual(entClass, CLASSNAME_INFECTED, .caseSensitive = false)) continue; // and BAM it's a zombie | |
zombieFlow = L4D2_GetInfectedFlowDistance(i); | |
if (zombieFlow < 0) //zombies in infected-only areas don't have flow | |
{ | |
Debug_PrintSpam("Skipping Zombie %i, Flow (%f) is in an infected only area", i, zombieFlow); | |
continue; | |
} | |
new Float:changeInLifetime = GetGameTime() - zombieLifetimes[i]; | |
if (changeInLifetime < GetConVarFloat(cvarDespawnNeededLifetime)) | |
{ | |
Debug_PrintSpam("Lifetime for Zombie %i, Flow (%f) is not enough (%f secs)", i, zombieFlow, changeInLifetime); | |
continue; | |
} | |
if (lastSurvivorFlow - despawnDistance > zombieFlow) // if the Zombie is left far behind | |
{ | |
if (!IsVisibleToSurvivors(i)) | |
{ | |
AcceptEntityInput(i, "Kill"); | |
Debug_Print("Removed Zombie %i, Flow (%f) for being way behind (Last Survivor :%f) and invisible", i, zombieFlow, lastSurvivorFlow); | |
if (GetConVarBool(cvarRespawnRemovedCI)) | |
{ | |
if (infectedInSpawnQueue < 1) | |
{ | |
CreateTimer(ZOMBIE_RESPAWN_INTERVAL, _DI_RespawnInfected_Timer, _, TIMER_REPEAT); | |
} | |
infectedInSpawnQueue++; | |
} | |
} | |
else | |
{ | |
Debug_PrintSpam("Found Zombie %i way behind but visible, Flow (%f), Last Survivorflow (%f)", i, zombieFlow, lastSurvivorFlow); | |
} | |
} | |
} | |
} | |
static bool:IsNearSafeRoom(firstSurvivor, &Float:distance = 0.0) | |
{ | |
decl Float:safeRoomPosition[3]; | |
if (GetSafeRoomPosition(safeRoomPosition)) // since this returns false on Finale Maps were covered | |
{ | |
decl Float:firstSurvivorPosition[3]; | |
GetClientAbsOrigin(firstSurvivor, firstSurvivorPosition); | |
distance = GetVectorDistance(firstSurvivorPosition, safeRoomPosition); | |
if (distance < GetConVarFloat(cvarSafeRoomNear)) | |
{ | |
return true; | |
} | |
} | |
return false; | |
} | |
public Action:_DI_RespawnInfected_Timer(Handle:timer) // respawn Infected with pauses, z_spawn does not spawn 10 things at once | |
{ | |
if (infectedInSpawnQueue < 1) | |
{ | |
return Plugin_Stop; // only work if there is a respawn needed, kill timer if not | |
} | |
CheatCommand(_, "z_spawn_old", "infected auto"); | |
Debug_Print("One Zombie was respawned, %i remain in queue", infectedInSpawnQueue); | |
infectedInSpawnQueue--; | |
return Plugin_Continue; | |
} | |
static bool:IsVisibleToSurvivors(entity) // loops alive Survivors and checks entity for being visible | |
{ | |
FOR_EACH_ALIVE_SURVIVOR_INDEXED(i) | |
{ | |
if (IsVisibleTo(i, entity)) | |
{ | |
return true; | |
} | |
} | |
return false; | |
} | |
static bool:IsVisibleTo(client, entity) // check an entity for being visible to a client | |
{ | |
decl Float:vAngles[3], Float:vOrigin[3], Float:vEnt[3], Float:vLookAt[3]; | |
GetClientEyePosition(client,vOrigin); // get both player and zombie position | |
GetEntityAbsOrigin(entity, vEnt); | |
MakeVectorFromPoints(vOrigin, vEnt, vLookAt); // compute vector from player to zombie | |
GetVectorAngles(vLookAt, vAngles); // get angles from vector for trace | |
// execute Trace | |
new Handle:trace = TR_TraceRayFilterEx(vOrigin, vAngles, MASK_SHOT, RayType_Infinite, _DI_TraceFilter); | |
new bool:isVisible = false; | |
if (TR_DidHit(trace)) | |
{ | |
decl Float:vStart[3]; | |
TR_GetEndPosition(vStart, trace); // retrieve our trace endpoint | |
if ((GetVectorDistance(vOrigin, vStart, false) + TRACE_TOLERANCE) >= GetVectorDistance(vOrigin, vEnt)) | |
{ | |
isVisible = true; // if trace ray lenght plus tolerance equal or bigger absolute distance, you hit the targeted zombie | |
} | |
} | |
else | |
{ | |
Debug_Print("Zombie Despawner Bug: Player-Zombie Trace did not hit anything, WTF"); | |
isVisible = true; | |
} | |
CloseHandle(trace); | |
return isVisible; | |
} | |
public bool:_DI_TraceFilter(entity, contentsMask) | |
{ | |
if (entity <= CLIENT_VALID_LAST || !IsValidEntity(entity)) // dont let WORLD, players, or invalid entities be hit | |
{ | |
return false; | |
} | |
decl String:class[128]; | |
GetEdictClassname(entity, class, sizeof(class)); // also not zombies or witches, as unlikely that may be, or physobjects (= windows) | |
if (StrEqual(class, CLASSNAME_INFECTED, .caseSensitive = false) | |
|| StrEqual(class, CLASSNAME_WITCH, .caseSensitive = false) | |
|| StrEqual(class, CLASSNAME_PHYSPROPS, .caseSensitive = false)) | |
{ | |
return false; | |
} | |
return true; | |
} | |
#undef MODULE_NAME |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment