Skip to content

Instantly share code, notes, and snippets.

@hjhee
Created June 14, 2017 03:44
Show Gist options
  • Save hjhee/75665918ff5426c487b0b04151ba45bb to your computer and use it in GitHub Desktop.
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
/* _
* ___ _ __ ___ _ __ ___ ___ __| |
* / __| '__/ __| '_ ` _ \ / _ \ / _` |
* \__ \ | \__ \ | | | | | (_) | (_| |
* |___/_| |___/_| |_| |_|\___/ \__,_|
*
* _| _ _ _ _ _ . _ |` _ __|_ _ _|
* (_|(/__\|_)(_|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