Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/sr2echa/TF2-Source-Code/llms.txt

Use this file to discover all available pages before exploring further.

TF2 separates its game logic into two distinct shared libraries that are loaded by the engine at startup: server.dll (or server.so on Linux) runs the authoritative simulation, and client.dll (or client.so) handles rendering, sound, and the local player’s input. They never call each other directly. Instead, the engine mediates every exchange — ticking the server at a fixed rate, serializing entity state changes as delta-compressed DataTable packets, and delivering them to the client on the next network frame.

The DLL split

server.dll / server.so

Contains all authoritative game logic: CTFPlayer, CTFWeaponBase, CObjectSentrygun, round timers, damage calculation, and AI bots. It runs at a fixed tick rate (default 66 Hz for TF2 competitive, 20–33 Hz for casual matchmaking servers) and communicates outward only through IVEngineServer and the DataTable network layer.

client.dll / client.so

Contains the client-side counterparts: C_TFPlayer, C_TFWeaponBase, HUD elements, particle effects, and clientside animations. It drives the render loop and processes user input. It communicates inward only through IVEngineClient and the RecvTable layer.
Both DLLs share code from game/shared/. Files in that directory are compiled twice — once for each DLL — with #ifdef CLIENT_DLL / #ifdef GAME_DLL guards controlling which blocks are active.

The game tick

The engine drives a fixed-interval simulation loop. Each iteration is called a tick.
Server tick loop (simplified):
  1. Process incoming CLC_Move packets (user commands from clients)
  2. Run CBaseEntity::PhysicsRunThink() on all entities
  3. Detect and send changed network properties (SendTable delta)
  4. Advance m_nTickCount; broadcast NET_Tick to all clients
The SVC_ServerInfo message sent at signon carries the tick interval the client must use for interpolation:
// common/netmessages.h (lines 380–411)
class SVC_ServerInfo : public CNetMessage
{
public:
    int    m_nProtocol;       // protocol version
    bool   m_bIsDedicated;    // dedicated server?
    bool   m_bIsHLTV;         // HLTV server?
    float  m_fTickInterval;   // server tick interval
    int    m_nMaxClients;
    int    m_nPlayerSlot;     // our client slot number
    const char *m_szGameDir;  // e.g. "tf2"
    const char *m_szMapName;
    // ...
};

Client interpolation

Because packets arrive less frequently than the client renders frames, the client maintains a short time-delayed view of the world (typically cl_interp seconds behind the server). Interpolated variables — positions, angles, floats — are tracked in VarMapping_t inside C_BaseEntity:
// game/client/c_baseentity.h (lines 91–100)
struct VarMapping_t
{
    VarMapping_t()
    {
        m_nInterpolatedEntries = 0;
    }

    CUtlVector< VarMapEntry_t > m_Entries;
    int                         m_nInterpolatedEntries;
    float                       m_lastInterpolationTime;
};
Each VarMapEntry_t wraps an IInterpolatedVar watcher. Every networked float, vector, or angle that should lerp between snapshots registers itself here.

Entity hierarchy

Server: CBaseEntity (game/server/baseentity.h)

CBaseEntity is the root of every server-side object. It holds the authoritative position, angles, model, team, health, and physics state. It is identified in the engine by an edict_t slot and an integer entity index.
// game/server/baseentity.h (lines 39–47)
typedef CHandle<CBaseEntity> EHANDLE;

#define MANUALMODE_GETSET_PROP(type, accessorName, varName)   \
    private:                                                   \
        type varName;                                          \
    public:                                                    \
        inline const type& Get##accessorName##() const        \
            { return varName; }                                \
        inline void Set##accessorName##( const type &val )    \
            { varName = val; m_NetStateMgr.StateChanged(); }
The StateChanged() call at the end of every setter marks the property dirty so the DataTable system knows to include it in the next delta update. This pattern — MANUALMODE_GETSET_PROP — appears throughout CBaseEntity and its subclasses.

Client: C_BaseEntity (game/client/c_baseentity.h)

C_BaseEntity is the client-side mirror. It receives networked state through RecvTable proxies and provides a rendering and prediction interface. It is identified by the same entity index as the server counterpart but lives only in the client DLL’s memory.
// game/client/c_baseentity.h (lines 1–7)
// Purpose: A base class for the client-side representation of entities.
//
//   This class encompasses both entities that are created on the server
//   and networked to the client AND entities that are created on the
//   client.
Key differences from CBaseEntity:
AspectCBaseEntity (server)C_BaseEntity (client)
PhysicsFull IPhysicsObject simulationClientside only (cosmetic)
NetworkSends state via SendTableReceives state via RecvTable
PredictionRuns on serverRuns locally via CPredictionCopy
ThinkPhysicsRunThink() each tickClientThink() each frame

The DataTable (DT) replication system

The DataTable system is the backbone of entity state replication. The server declares what to send with SendTable; the client declares how to receive it with RecvTable. The engine matches them up by name during connection.

Send side (server, public/dt_send.h)

A SendVarProxyFn translates a C++ member variable into a network-transmissible DVariant. The engine calls your proxy every tick for properties that may have changed:
// public/dt_send.h (lines 41–52)
typedef void (*SendVarProxyFn)(
    const SendProp *pProp,
    const void *pStructBase,   // e.g. CBaseEntity*
    const void *pData,         // address of the variable
    DVariant   *pOut,          // write your value here
    int         iElement,      // array element index (or 0)
    int         objectID       // entity index for debugging
);

typedef void* (*SendTableProxyFn)(
    const SendProp          *pProp,
    const void              *pStructBase,
    const void              *pData,
    CSendProxyRecipients    *pRecipients,
    int                      objectID
);
Returning nullptr from a SendTableProxyFn is equivalent to calling pRecipients->ClearAllRecipients() — no delta is sent to any client for that sub-table this tick.

Receive side (client, public/dt_recv.h)

RecvVarProxyFn does the inverse: it writes the incoming DVariant value into the client entity’s memory:
// public/dt_recv.h (lines 28–47)
class CRecvProxyData
{
public:
    const RecvProp *m_pRecvProp;  // the property being received
    DVariant        m_Value;      // the incoming value
    int             m_iElement;   // array element index
    int             m_ObjectID;   // entity index
};

// pStruct = the base structure (like C_BaseEntity*)
// pOut    = the variable this proxy represents
typedef void (*RecvVarProxyFn)(
    const CRecvProxyData *pData,
    void *pStruct,
    void *pOut
);

Standard proxies

CStandardSendProxies and CStandardRecvProxies provide ready-made proxy functions for the most common type conversions so you do not need to write them yourself:
// public/dt_send.h (lines 74–100)
class CStandardSendProxiesV1
{
public:
    SendVarProxyFn m_Int8ToInt32;
    SendVarProxyFn m_Int16ToInt32;
    SendVarProxyFn m_Int32ToInt32;
    SendVarProxyFn m_UInt8ToInt32;
    SendVarProxyFn m_UInt16ToInt32;
    SendVarProxyFn m_UInt32ToInt32;
    SendVarProxyFn m_FloatToFloat;
    SendVarProxyFn m_VectorToVector;
};
// public/dt_recv.h (lines 67–80)
class CStandardRecvProxies
{
public:
    RecvVarProxyFn m_Int32ToInt8;
    RecvVarProxyFn m_Int32ToInt16;
    RecvVarProxyFn m_Int32ToInt32;
    RecvVarProxyFn m_FloatToFloat;
    RecvVarProxyFn m_VectorToVector;
};

Client-side prediction

Prediction eliminates the “laggy controls” feel by running player movement locally before the server confirms it. The client simulates the same CUserCmd (mouse + keyboard input) that it sent to the server, stores the predicted result, and corrects its position when the authoritative server update arrives. CTFPlayer (and its shared base CTFPlayerShared) declare both a send table and a recv table in the same header file using compile-time guards:
// game/shared/tf/tf_player_shared.h (lines 38–47)
// Client specific.
#ifdef CLIENT_DLL
    EXTERN_RECV_TABLE( DT_TFPlayerShared );
// Server specific.
#else
    EXTERN_SEND_TABLE( DT_TFPlayerShared );
#endif
When CLIENT_DLL is defined, the compiler sees EXTERN_RECV_TABLE; when GAME_DLL is defined it sees EXTERN_SEND_TABLE. The table name DT_TFPlayerShared matches on both sides, allowing the engine to bind them together.
If you add a networked variable to a shared class, you must add both a SendPropXxx entry to the server’s SendTable definition and a matching RecvPropXxx to the client’s RecvTable. A mismatch causes a CRC check failure at connect time and boots the client.

The game/shared/ directory

Code in game/shared/ compiles into both the server and client DLLs. This is where you find:
  • tf/tf_player_shared.h / tf_player_shared.cpp — condition flags, stun state, ammo tracking
  • tf/tf_weaponbase_shared.cpp — firing logic and recoil that must match on both sides
  • shareddefs.h — team numbers, max players, damage flags used everywhere
  • basegrenade_shared.cpp — grenade physics that must be deterministic for prediction
When deciding where to put new game logic, ask: “Does the client need to predict this?” If yes, put it in game/shared/ and guard server-only side effects with #ifndef CLIENT_DLL. If not, put it directly in game/server/.

Data flow summary

Server tick N:
  CBaseEntity::SomeNetVar changes


  SendTable proxy runs (dt_send.h)
        │  delta bits

  engine compresses & sends UDP packet


Client receives packet (FRAME_NET_UPDATE_START)


  RecvTable proxy runs (dt_recv.h)
        │  writes C_BaseEntity::SomeNetVar

  FRAME_NET_UPDATE_POSTDATAUPDATE_END


  Interpolation & prediction correction


  FRAME_RENDER_START → draw frame

Build docs developers (and LLMs) love