CSGO: Fake angles demystified
Fake angles explained, the analysis
Motivation
I have been reading around the forum for a bit on this subject and came to the conclusion that there is no sophisticated and refined explanation as-to why ‘fake angles’ work.
Because this feature/issue has a high degree of complexity, I felt like it needs an in-depth explanation, not only on how it works (which has been taught and discussed many times over the years), but also why it works, because I doubt that I am the only one interested in the deep-seated cause of this issue inside the engine.
Introduction
I will be going over the different parts of the engines networking and animation code.
The first two paragraphs will be dedicated to; respectively the client-sided and server-sided networking code, they will be explained using 2007 Source codebase leak¹, the references to the code pieces can be found in the source section.
The third paragraph, will be explained using the latest Mac Binaries (.dylibs) that feature debug symbols, dating from July 7 2017². The source for those can also be found in the final source section.
Additionally I would like to thank Kiro for reading this before I published it.
Part 1: Analysis of client-sided netcode
Traditionally users on the forum that brought up the question how they would get fake angles to work were told to create a sequence where you would set bSendPacket to false every even tick (respectively choking the packet), and to true every uneven tick, then the ‘fake angle’ would become the tick where bSendPacket was true, and the ‘real angle’ would become the one where bSendPacket was false.
In this first part we are going to take a look how changing bSendPacket influences the internal client networking.
First of all let us take a look at CL_Move³, this the function that invokes CHLClient::CreateMove, which takes care of converting user-input to the user-command structure, i.e. mouse movement to view-angles.
This so said CL_Move function holds the local variable bSendPacket, which is commonly used by cheat developers to produce the ‘fake angles’-effect.
int nextcommandnr = cl.lastoutgoingcommand + cl.chokedcommands + 1;
// Have client .dll create and store usercmd structure
g_ClientDLL->CreateMove(
nextcommandnr,
host_state.interval_per_tick - accumulated_extra_samples,
!cl.IsPaused() );
// Store new usercmd to dem file
if ( demorecorder->IsRecording() )
{
// Back up one because we've incremented outgoing_sequence each frame by 1 unit
demorecorder->RecordUserInput( nextcommandnr );
}
if ( bSendPacket )
{
CL_SendMove();
}
else
{
// netchanll will increase internal outgoing sequnce number too
cl.m_NetChannel->SetChoked();
// Mark command as held back so we'll send it next time
cl.chokedcommands++;
}
If you take the code sample provided above in mind, you can see that this conditional will evaluated after the execution of CHLClient::CreateMove therefore, the aforementioned becomes the ideal place to modify the bSendPacket local variable, that’s why modifying this variable in any of the CreateMove implementations is so extremely viable.
If this conditional of checking the value of bSendPacket gets evaluated negatively (bSendPacket was set to false), the client increases its choke counter, increments the netchannel choke counter and outgoing sequence counter (used to construct command numbers), and then returns out of CL_Move.
Since networking in the Source Engine (and other Quake 3 based multiplayer games) is in real-time, these commands will held back on the client, and will be sent out in a batch when bSendPacket will be evaluated to true again, we will now take a look at how that happens on the client.
This is where CL_SendMove⁴ comes into play.
What we can learn from looking at this functions body, is that it constructs a CLC_Move object, which is the raw network packet structure that is sent off to the server, and after that it does a number of interesting things.
// How many real new commands have queued up
moveMsg.m_nNewCommands = 1 + cl.chokedcommands;
moveMsg.m_nNewCommands = clamp( moveMsg.m_nNewCommands, 0, MAX_NEW_COMMANDS );
One of them being the above provided example of clamping the max amount of choked commands that we can send in a batch, this is useful later on if we want to know how many consecutive times we can force bSendPacket to false, to not send a packet.
Let me go over what happens here with the value MAX_NEW_COMMANDS⁵:
– convert the decimal ‘1’ to binary -> (1)^10 = (00000001)^2
– shift bits 4 places to the left -> 00010000
– convert binary back to decimal -> (00010000)^2 = (16)^10
– subtract one -> 16 – 1 = 15
This explains the maximum amount of consecutive choked packets (15), commonly referenced to as the ‘choke limit’ or ‘maximum fake-lag’.
The remaining code present in the function body of CL_SendMove is not of much interest to us for this subject, it iterates all the commands that we need to send, writes them to the accumulated buffer inside the CLC_Move object, and if all of that worked, it’ll get sent off to the server.
This wraps up our peek at the client code, let us evaluate what we have learned:
– the client has the ability not to send certain user-commands.
– the client will send all held back user-commands in a batch when bSendPacket gets evaluated to true.
– the maximum amount of user-commands to hold back is 15.
Part 2.1: Analysis of server-sided netcode
For the second part, which is about the server processing of our CLC_Move object/packet, we will be taking a look how this packet containing our user-commands will be decoded back to valid user-commands and will be processed.
Lets jump right into where the CLC_Move packet gets picked up on the server again, that would be CGameClient::ProcessMove⁶.
serverGameClients->ProcessUsercmds
(
edict, // Player edict
&msg->m_DataIn,
msg->m_nNewCommands,
totalcmds, // Commands in packet
netdrop, // Number of dropped commands
ignore, // Don't actually run anything
paused // Run, but don't actually do any movement
);
Basically the only thing we need to know about CGameClient::ProcessMove is that it calls CServerGameClients::ProcessUsercmds⁷, which then on its turn decodes all the user-commands from the buffer and forwards them to CBasePlayer::ProcessUsercmds⁸.
int i;
for ( i = totalcmds - 1; i >= 0; i-- )
{
ctx->cmds.AddToTail( cmds[ totalcmds - 1 - i ] );
}
CBasePlayer::ProcessUsercmds on its turn appends the new user-commands to its newly allocated command context (as seen above).
After all the data has been indexed and all the user-commands have been stored into the newly allocated command context, it is time for simulation to be ran, to update the players position and whereabouts on the server.
Part 2.2: Analysis of server-sided simulation code
During simulation the server will call CBasePlayer::PhysicsSimulate⁹ on ever valid player,
CBasePlayer::PhysicsSimulate will then on its turn construct a CUtlVector instance with all the commands from this server-frame that we have to simulate the player with. (example below)
// Now run any new command(s). Go backward because the most recent command is at index 0.
for ( int i = ctx->numcmds - 1; i >= 0; i-- )
{
vecAvailCommands.AddToTail( ctx->cmds[i] );
}
Because of this method, the oldest user-command will be at the head of the container, and the newest user-command will be at the tail of the container, this makes sense because when simulating them all you want to do it in the same order they were created in. When iterating the container, the most recent command (the one that would’ve had bSendPacket true on it), is processed last.
NOTE: keep this in mind, it’s crucial later on.
for ( int i = 0; i < commandsToRun; ++i )
{
PlayerRunCommand( &vecAvailCommands[ i ], MoveHelperServer() );
...
}
Next up as seen in the code sample above, for every single of these commands, the simulation will be handled by CBasePlayer::PlayerRunCommand¹⁰
if ( pl.fixangle == FIXANGLE_NONE)
{
VectorCopy ( ucmd->viewangles, pl.v_angle );
}
Above we see a snippet of code out of the function, you can see that CPlayerState::v_angle will get set to the user-command view-angles.
Out of this we can conclude that after simulating all the packets, CPlayerState::v_angle will always contain the view-angles of the most recent user-command.
CBasePlayer::PlayerRunCommand will then on its turn call CPlayerMove::RunCommand which will do the actual simulation that will at some point down the line result in an origin change etc. based on the data from the user-command, after all that has been done it will call CPlayerMove::RunPostThink which will then on its turn call CCSPlayer::PostThink¹¹.
Here we will see that the m_angEyeAngles netvar will be set to CPlayerState::v_angle which still holds the last processed user-command view-angles. The netvar will afterwards get networked to our client.
This wraps up all there is to say about the 2007 codebase server code, let us evaluate what we have learned so far:
- The server simulates all the packets it receives in one frame (from old to new).
- Only the eye angles of the last (non-choked) packet will make it to our client.
Part 3: Analysis of server-sided animation code
The previous two parts have been based around the 2007 SDK leaked codebase, the real issue why 'fake angles' work is not present inside that codebase, so therefore we must continue our journey inside of the CS:GO code. I have chosen to utilize the .dylib binaries for this because they are recent (June 2017) and contain debug symbols; function names/globals/etc, which can be useful for reversing purposes. Additionally I would like to note that any code samples provided here originate from those binaries.
First of all let us take a step back, in the Source Engine, both the client and the server use the netvar m_flPoseParameter to construct a bone matrix via SetupBones, do not let its name fool you, while m_flPoseParameter is a netvar, they are networked, the client and server compute them independently from each other, if this was not the case, and the server would just network its pose parameters to the clients instead, this angle desynchronization issue, commonly referenced to as 'fake angles' would not be a problem.
Although, not networking the pose parameters between server and client is not the core source of this issue.
Let us go back to the function in which we ended up in the previous part:
CCSPlayer::PostThink, let it be the case that this function also calls CCSGOPlayerAnimState::Update which is responsible for computing the poseparameters and other taking care of other miscellaneous animation related stuff.
if ( *(_BYTE *)(player + 0x2408) )
{
CCSGOPlayerAnimState::Update(
*(CCSGOPlayerAnimState **)(player + 0x2570),
*(float *)(player + 0x25A4),
*(float *)(player + 0x25A0),
0);
}
The interesting thing about this function CCSGOPlayerAnimState::Update
is that it contains the core issue, why this form of angle desynchronization is even possible at all.
Since we are still on the callstack of the prediction inside CCSPlayer::PostThink, that means that the most recent command is the tail of the CUtlVector, since the prediction will simulate the user-commands from old to new.
a quick flashback to what I told you to keep in mind:
...the oldest user-command will be at the head of the container, and the newest user-command will be at the tail of the container...
gpGlobals->curtime = player->m_nTickBase * TICK_INTERVAL;
...
run simulation
...
RunPostThink( player );
// Let time pass
player->m_nTickBase++;
During prediction CGlobalVarsBase::curtime is set to: player tick * ipt (as seen above).
And after all the prediction code and post think has ran, the player current tick on the server gets incremented, to let the time pass.
Because CCSGOPlayerAnimState::Update gets ran inside the prediction code, every time a player needs to be re-simulated because of new data arriving form the clients, the player will also be animated, and his poseparameters will be computed.
In order to prevent animating players when the player did not have to be re-simulated, for instance when there would be no new data from the client, valve added in this check inside of CCSGOPlayerAnimState::Update:
if ( reset_on_spawn )
{
current_time = gpGlobals->curtime;
last_time = *(float *)(this + 0x70);
}
else
{
last_time = *(float *)(this + 0x70);
current_time = gpGlobals->curtime;
if ( last_time == current_time || *(_DWORD *)(this + 0x74) == gpGlobals->framecount )
return;
}
... update all stuff
*(float *)(this + 0x70) = gpGlobals->curtime;
*(_DWORD *)(this + 0x74) = gpGlobals->framecount;
As you can see at the bottom of CCSGOPlayerAnimState::Update, it saves these variables, you could call them m_flLastAnimTime and m_nLastAnimFrame whatever floats your boat.
So as you at this point from looking at the code might have realized, is that, while the CGlobalVarsBase::curtime does advance between executing all the delayed user-commands in a batch, we are still in the same server frame, so CGlobalVarsBase::frametime is still the same as the previous user-command we tried to animate.
So the server will only process the oldest user-commands view-angles into its poseparameters.
This is because the oldest user-command was simulated first, and the newest user-command was simulated last.
Conclusion
Finally to conclude and to summarize all of the above, what happens is that only the user-commands that have bSendPacket true on them will have their angles broadcasted to the other clients,
while only the first choked user-commands angles will be used in the server 'hitreg'.
I hope this will help people understand why you can 'fake angles' in CS:GO.
Sources
1. 2007 leaked codebase
2. 2017 dylibs with symbols
3. CL_Move
4. CL_SendMove
5. MAX_NEW_COMMANDS
6. CGameClient::ProcessMove
7. CServerGameClients::ProcessUsercmds
8. CBasePlayer::ProcessUsercmds
9. CBasePlayer::PhysicsSimulate
10. CBasePlayer::PlayerRunCommand
11. CCSPlayer::PostThink
|