Left 4 Dead 2 Weapon Accuracy

Well, I’ve decieded to get back into messing around in L4D2 with a few friends of mine, and we’ve decided to do as much stuff as we can on it just for laughs. One of the first things I started looking into was getting the weapon accuracy as good as possible. This meant dealing with both recoil and spread. I’m assuming you have already got a hook in place for one of the createmove functions and have previous experience with source engine games.

Recoil

Recoil is the kick up you get when firing your weapon and it is done in the standard source engine way. There are two networked variabes stored inside the player entity that immediately stand out to anyone who has previously worked with the source engine before:

m_vecPunchAngle    @ entity + 0x1224
m_vecPunchAngleVel @ entity + 0x1230

Well cool, so we have these now what are they and what do we use them for? m_vecPunchAngle is basically the angle caused by the weapons recoil that when used to offset your view angles will give you the proper bullet direction. m_vecPunchAngleVel is used in the calculating the next m_vecPunchAngle value for the next tick. So, we just take our UserCmd and remove m_vecPunchAngle from the view angles to counter act the servers calculation and poof, we’re done … right? Not quite. As a lot of people are aware now, the value of m_vecPunchAngle isn’t quite perfect from inside a hooked CreateMove function. Just removing it will work but it won’t be perfectly accurate.

CreateMove is called really early on in the frame, basically these functions jobs are sampling user input and filling out a CUserCmd structure with the actions the player has taken this tick. The problem lies that on the server end, the bullets traces will not be performed until AFTER the player has been updated with the new tick information, which includes the newest value for the m_vecPunchAngle variable. How we fix this is by making use of the Prediction interface of the source engine, which takes care of keeping the client up to date without the need for an immediate update from the server. The nice thing is that it uses the exact same movement and player update code as the server does, so we know it will be correct if our UserCmd’s are. A bit of work has to be done reversing and rebuilding the inner workings of the CPrediction::ProcessMovement function and its internals as calling it will result in it also retrigger the sounds it plays, causing double ups and issues with some effects. Most of this has been publicly documented but I would urge you to reverse the function yourself as most of the paste ready implementations tend to be only half complete.

The other thing that should be considered is trying to grab uncompressed netvar values from the game. Read this article by Valve for more information about netvars and how compression and how high-precision can be obtained.

After running prediction on your entity with uncompressed netvar values and your usercmd, you should have the most accurate recoil angle to use and now you can simple minus it from your usercmd viewangles.

Spread

Lucky for us, the spread calculation in this game has been greatly simplified from Counter Strike. Looking at the following snippet from CTerrorGun::FireBullets in IDA we can see how it’s done… IDA Spread

Alright, simple enough. Looking inside SharedRandomFloat you can easily find the global used to store the current seed for the random number generation. This is important because we will need to set the same seed as the game uses. All the game does is set it to the random seed member of the CUserCmd. We can see that there is a maxium spread angle stored inside the weapon and we just use the negative of that as the minimum in the random number generation. And then this is just tacked onto our view angles along with our recoil. Well shit, this is nothing in comparison to other valve games.

So we can just do something like this:

set_prediction_random_seed(cmd);
float max_spread = weap->max_spread();

vector3 spread;
spread.x = shared_random_float("CTerrorGun::FireBullet HorizSpread", -max_spread, max_spread, 0);
spread.y = shared_random_float("CTerrorGun::FireBullet VertSpread", -max_spread, max_spread, 0);
spread.z = 0.f;

set_prediction_random_seed(nullptr);

cmd->view_angles -= spread;

If you give this a shot, you’ll notice it works pretty well, but it isn’t perfect. If you jump around a little and shoot at a wall, you’ll notice that it is off by a little. Looking at our code theres only one thing it can be, the max spread value in the weapon.

This isn’t predicted when we run our prediction code like the recoil is, so we need to figure out how to update it. Searching the string “spread”, you will spot a debug format string which looks something like:

(%.1f) spread %.1f ( avg %.1f ) ( max %.1f )

After a little reversing of what’s going on, its clear we are in the right place: IDA Spread Update

Instead of pulling appart this whole function, I decided just to call it. If you just add a call to this function before you apply your spread compensation, you’ll notice it appears your accuracy has just got even worse. This is simply due to the fact that you have just updated the spread, and then the game will go and do it again before rendering the decals, meaning the game will have added even more to the max_spread member of the weapon. Don’t worry though, its just a matter of backing up the original max spread value and restoring it again after you have performed your calculations.

Conclusion

After a bit of work, you should have something like the below video. Happy reversing and have fun :^)

Written on May 10, 2017