Note! This is different than our original post here : https://www.scg.wtf/jb/d/6705-important-update-massive-bug-fixes The original post shows how we were able to track it down, this is just a better explanation as to what went wrong.
The plugin in question causing the bug was The TF2 Donator Recognition plugin
In this thread, information is scarce but there are two posts in particular that mention problems similar to ones we've had.
The exact cause of the problem is the following.
stock CreateSprite(iClient, String:sprite[], Float:offset)
{
new String:szTemp[64];
Format(szTemp, sizeof(szTemp), "client%i", iClient);
DispatchKeyValue(iClient, "targetname", szTemp);
new Float:vOrigin[3];
GetClientAbsOrigin(iClient, vOrigin);
vOrigin[2] += offset;
new ent = CreateEntityByName("env_sprite_oriented");
if (ent)
{
DispatchKeyValue(ent, "model", sprite);
DispatchKeyValue(ent, "classname", "env_sprite_oriented");
DispatchKeyValue(ent, "spawnflags", "1");
DispatchKeyValue(ent, "scale", "0.1");
DispatchKeyValue(ent, "rendermode", "1");
DispatchKeyValue(ent, "rendercolor", "255 255 255");
DispatchKeyValue(ent, "targetname", "donator_spr");
DispatchKeyValue(ent, "parentname", szTemp);
DispatchSpawn(ent);
TeleportEntity(ent, vOrigin, NULL_VECTOR, NULL_VECTOR);
g_EntList[iClient] = ent;
}
}
This is the code that creates and puts a sprite above a player's head at the end of a round. It builds a new sprite entity and assigns the entity index to an array of players who have sprites over their heads.
g_EntList[iClient] = ent;
However, the problem is that on a new round, entity indexes can shift! So when we have this...
stock KillSprite(iClient)
{
if (g_EntList[iClient] > 0 && IsValidEntity(g_EntList[iClient]))
{
AcceptEntityInput(g_EntList[iClient], "kill");
g_EntList[iClient] = 0;
}
}
This is the code that will clean up the sprite entities when we are done with them. It is called when a player is killed during the end of the round and at the start of the next round. When a player is killed, it works as intended, because our entity index array is still correct. However when the next round begins and our entity indexes shift, this code will now kill whichever entity is in its place.
Debug - T name : zz_red_koth_timer, C name : team_round_timer
Server cvar 'mp_timelimit' changed to 25
[SCG] A fatal error has occured with the KOTH timers, the round was reset to prevent a crash!
Debug - T name : minecart_path_146, C name : path_track
Debug - T name : minecart_path_153, C name : path_track
Debug - T name : , C name : logic_auto
Debug - T name : , C name : tf_wearable
This is example text from when I added a catch to see what exactly was getting deleted at the start of a round. The first case is a zz_red_koth_timer, which would have led to the koth crash bug. As you can see, the plugin we wrote to catch and reset the round did catch this and reset things correctly.
The second is an example of the payload cart bug in action, we've managed to delete our minecart track somewhere, so our cart would have gotten stuck.
The third example is a tame one, nothing critical was deleted, we've maybe broken a door and someone's hat got removed. Something no one would have bat an eye over.
Because we were deleting entities at sheer random, and deleting as many as we had donators in game, the results were always wildly different and contributed to how hard it was to track down ultimately. While looking at our debug text, we noticed many cases where things that the game didn't even need at all get deleted.
This is how we fixed it.
In our fixed sprite generation code, instead of assigned our entity index to an array, we instead assign its entity reference to an array.
g_EntList[iClient] = EntIndexToEntRef(ent);
An entity reference is effectively a unique identifier to every created entity. Our killSprite code is now changed accordingly.
new ent = EntRefToEntIndex(g_EntList[iClient]);
Rather than delete the entity at will, we then make sure we're deleting the new entity index taken from our reference. As a double fail-safe, we add a check in to make sure we're only deleting a sprite entity with the target name assigned above, donator_spr.
Curiously, in our code that manages putting a sprite above someone's head...
public OnGameFrame()
{
if (!g_bRoundEnded) return;
new ent, Float:vOrigin[3], Float:vVelocity[3];
for(new i = 1; i <= MaxClients; i++)
{
if (!IsClientInGame(i)) continue;
if ((ent = g_EntList[i]) > 0)
{
if (!IsValidEntity(ent))
g_EntList[i] = 0;
else
if ((ent = EntRefToEntIndex(ent)) > 0)
{
GetClientEyePosition(i, vOrigin);
vOrigin[2] += 25.0;
GetEntDataVector(i, gVelocityOffset, vVelocity);
TeleportEntity(ent, vOrigin, NULL_VECTOR, vVelocity);
}
}
}
}
We can see if ((ent = EntRefToEntIndex(ent)) > 0)
be used, it is looking for an entity by reference however prior to our fixes, at no point did it ever save our index as a reference. That if/else statement is simply asking is our entity index is greater than zero, which it always would be.
This is most likely an oversight by the original programmer. Given this plugin wasn't as popular and thus didn't appear on many servers, it does explain why this issue was never found. But thankfully, all is now well. <3