Fixing how Collision Groups are set in CS:S

Counter-Strike: Source (CS:S) was released November 1, 2004, over 15 years ago. Since its release, it did not take long before people were already pushing the game to its limits with modifications using SourceMod. In pushing the limit, though, mistakes were made and things got a bit messy. One of the biggest issues (at least that I ran into) was the Collision Groups going to hell once you changed the group for an entity (usually a client) known as the “Physics Mayhem bug”. The problem was acknowledged as early as 2009, as seen in this blog post. Since then, there had been no fix released and it was chalked up as a “Valve” problem. This was the “normal” way of changing a client’s group so they no longer collided with other entities:

SetEntProp(client, Prop_Data, "m_CollisionGroup", COLLISION_GROUP_DEBRIS_TRIGGER, 4);

What this would do, at least from the best I can tell, was directly change the member variable associated with the CBaseEntity for that client. While this seems somewhat alright (disregarding that we’re changing a member variable in memory), this would often lead to all Collision Groups for entities being messed up. Guns would fall through maps, some map props would disappear, etc. It was not fun, and a server that often toggled Collision Groups had this problem on average 2.6 times a day over the span of 71 days. The only way to fix this was to restart the server, which would often result in only half the player base coming back.

Some of the people that read this might wonder why I, or anyone, would even care about this problem. The game is over 15 years old, and it has an active playerbase of < 10,000 unique players a month. It is a dying, old game, but I still enjoy playing it (or did, until I finally got burnt out recently).

Recently, a friend found this set of methods from here:

void CBaseEntity::SetCollisionGroup( int collisionGroup )
{
	if ( (int)m_CollisionGroup != collisionGroup )
	{
		m_CollisionGroup = collisionGroup;
		CollisionRulesChanged();
	}
}

void CBaseEntity::CollisionRulesChanged()
{
	// ivp maintains state based on recent return values from the collision filter, so anything
	// that can change the state that a collision filter will return (like m_Solid) needs to call RecheckCollisionFilter.
	if ( VPhysicsGetObject() )
	{
		extern bool PhysIsInCallback();
		if ( PhysIsInCallback() )
		{
			Warning("Changing collision rules within a callback is likely to cause crashes!\n");
			Assert(0);
		}
		IPhysicsObject *pList[VPHYSICS_MAX_OBJECT_LIST_COUNT];
		int count = VPhysicsGetObjectList( pList, ARRAYSIZE(pList) );
		for ( int i = 0; i < count; i++ )
		{
			if ( pList[i] != NULL ) //this really shouldn't happen, but it does >_<
				pList[i]->RecheckCollisionFilter();
		}
	}
}

This looks pretty promising; there is a method specifically used to set the collision groups, as well as another method that seems to clean everything up afterwards. Maybe this would fix the problem?

The problem that we immediately ran into was that the SetCollisionGroup method was not easy to find at all. There are 20,440 functions and methods to look for, and there was nothing immediately noticeable about the source code that would help lead us to finding this method. Fortunately, though, a method that SetCollisionGroup calls has a string that is easily searchable.

Behold, the CollisionRulesChanged method (screenshot from IDA):

CollisionRulesChanged method

For Windows, we get the function signature of:

\x55\x8b\xec\xb8\x00\x10\x00\x00\xe8****\x56\x8b\xf1\x83\xbe\xe4\x01\x00\x00\x00\x74\x4d

Following this, we were able to call CollisionRulesChanged from within SourcePawn, a scripting language used for SourceMod. We originally ran with only having this and called it every time after we manually changed the Collision Group, and it worked. This, however, was not enough. We wanted to call the SetCollisionGroup method since it already did exactly what we wanted it to do. The problem was, we still could not pinpoint the method SetCollisionGroup.

I ended up finding a function that calls SetCollisionGroup with the same technique as earlier: looking at strings unique to the method. This ended up allowing me to find VPhysicsUpdate. I now had VPhysicsUpdate that called SetCollisionGroup that called CollisionRulesChanged, and all I had to do was find the missing middle piece, which was now trivial.

The SetCollisionGroup method (screenshot from IDA):

SetCollisionGroup Method

For Windows, we get the function signature of:

\x55\x8b\xec\x53\x8b\x5d\x08\x56\x57\x8b\xf9\x39\x9f\xe0\x01\x00\x00\x74\x4f\x8b

I wrote a quick plugin (meant to be used as more of a library) and threw it on my GitHub. If you install this plugin on your CS:S server, all you need to do to properly change physics so players do not collide (also known as noblock) is:

#include <SetCollisionGroup>


SetEntityCollisionGroup(client, COLLISION_GROUP_DEBRIS_TRIGGER);

If you want to re-enable block so players collide once again:

#include <SetCollisionGroup>


SetEntityCollisionGroup(client, COLLISION_GROUP_PLAYER);

Simple, easy, and doesn’t cause the bug to happen.

Since having transitioned to using this method of changing Collision Groups about a week ago, the “Physics Mayhem” bug has not happened once which means the server did not have to be restarted around 18 times. Turns out, the problem wasn’t really due to Valve’s code (arguably); it was that instead of trying to properly change the Collision Groups, someone suggested a poor, easy way of doing it that results in over a decade of a single bug ruining gameplay for people who wanted to change Collision Groups somewhat regularly. The original solution was marketed as the only solution, but here I am 10+ years later showing the right solution which took a couple of dedicated hours to find.

If this bug lives in CS:GO, TF2, or any other games supported by SourceMod, please feel free to submit a PR on the repository and I will accept it pending verification.

EDIT: Thanks to Scags for adding support for TF2.

Time to figure out how to fix the spray exploit where you can completely crash client games, the voice exploit which causes a buffer overflow, and countless other exploits that are popping up every now and then.

Credits