How to properly get the free edict count
Compared to other source games CSGO has a pretty different way of handling edicts, there's 2048 slots that can be allocated and then used.
Edict slots can be in 3 different states. They can be
allocated or
not allocated, they can be
free, they can be on a
timer.
Because of these states it can be pretty hard to get the number of used/free edicts on a server as the engine doesn't store such number anywhere.
What most plugins do to count the number of edicts is to loop over all entities and use
IsValidEdict(edict);. Now here's the part why that's wrong.
The first 66 edict slots will
ALWAYS be reserved for players and cannot be assigned to other entities, so all edict counts in plugins are off at least by 66 - clientCount.
Source
The second reason why these counters will be off is because everytime an edict slot is freed an internal cooldown will be set on that slot. While this cooldown is still present that edict slot cannot be used.
Source
Why is GetEntityCount() not a viable option?
GetEntityCount returns
sv.num_edicts which represents the number of
allocated edicts. Not the number of
unused edicts. Once an edict is allocated once it will never be unallocated until next restart, it will only be freed.
Source
Proof of concept
Now to the most interesting part, how to actually get the real number of free edicts.
PHP Code:
#define EDICT_FREETIME 1.0
float cacheTTL;
int cacheLastValue;
Address g_EdictFreeTime;
public void OnPluginStart()
{
GameData hGameConf;
hGameConf = LoadGameConfigFile("EdictLimiter");
if(!hGameConf)
{
SetFailState("Failed to find EdictLimiter");
}
g_EdictFreeTime = view_as<Address>(LoadFromAddress(hGameConf.GetAddress("g_edictFreeTime"), NumberType_Int32));
delete hGameConf;
}
int GetFreeEdicts()
{
// I recommend having a cache for this function if you're going to call it on every entity creation
if(GetEngineTime() < cacheTTL)
return cacheLastValue;
int edict_ents;
for(int i = MAXPLAYERS + 1; i < 2048; i++)
{
if(!IsValidEdict(i)) // Check if slot is not allocated or is free, sourcemod checks if the slot edict->IsFree()
{
float edictFreeTime = GetEdictFreeTime(i);
if(edictFreeTime < 2 || (CBaseServer_GetTime() - GetEdictFreeTime(i)) >= EDICT_FREETIME) // Optional, game will ignore this if the server is about to reach the max edict limit
edict_ents++;
}
}
cacheTTL = GetEngineTime() + 0.5;
cacheLastValue = edict_ents;
return edict_ents;
}
float CBaseServer_GetTime()
{
return GetGameTickCount() * GetTickInterval();
}
float GetEdictFreeTime(int edict)
{
return view_as<float>(LoadFromAddress(g_EdictFreeTime + view_as<Address>(edict * 4), NumberType_Int32));
}
PHP Code:
"Signatures"
{
"g_edictFreeTime"
{
"library" "engine"
"windows" "\xF3\x0F\x10\x0C\xBD\x2A\x2A\x2A\x2A\x3B\xFB"
"linux" "\xF3\x0F\x10\x04\x9D\x2A\x2A\x2A\x2A\x39\xD3"
}
}
"Offsets"
{
"g_edictFreeTime"
{
"signature" "g_edictFreeTime"
"offset" "5"
}
}
About EDICT_FREETIME
As a last resort the game will use edict slots even if they are in a cooldown, this is supposed to prevent some cheats in dota2. If this feature wasn't a thing many CSGO maps would crash the server on every new round.
If you want to use the edict count code to prevent edict limit crashes you should remove the freetime checks as those will not be changing any edict behaviour if the server is about to hit the edict limit