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.

Every byte that travels between a TF2 server and its clients flows through a single abstraction: the net channel (CNetChan). The channel multiplexes reliable and unreliable message queues over UDP, fragments large payloads into sub-channels, and exposes flow statistics to the engine. On top of it sits a typed message layer (INetMessage) that both server-to-client (SVC_*) and client-to-server (CLC_*) messages implement. String tables keep precache lists and other shared string data synchronized without consuming per-tick bandwidth.

Protocol version

The protocol version is a single integer constant that must match between client and server. Any mismatch terminates the connection immediately:
// common/proto_version.h
#define PROTOCOL_VERSION    24

// Backward-compat constants for demo playback:
#define PROTOCOL_VERSION_23  23   // NET_MAX_PAYLOAD_BITS removed
#define PROTOCOL_VERSION_22  22   // sound index bits = 13
#define PROTOCOL_VERSION_21  21   // special DSP shipped
#define PROTOCOL_VERSION_20  20   // old-style dynamic model loading
#define PROTOCOL_VERSION_19  19   // post-Halloween sound flag bit
#define PROTOCOL_VERSION_18  18   // pre-Halloween sound flag bit
#define PROTOCOL_VERSION_17  17   // MD5 in map version
#define PROTOCOL_VERSION_14  14   // string table compression flag
#define PROTOCOL_VERSION_REPLAY 16  // Replay system launched
When you record or play back a demo file, the demo reader checks these constants to know which parsing path to use for each historical format.

The net channel (CNetChan)

CNetChan in engine/net_chan.h inherits from INetChannel. It owns the low-level UDP socket interaction for one peer-to-peer connection.

Sub-channels and fragmentation

Large messages (files, baselines) are split into fragments and tracked across up to eight parallel sub-channels:
// engine/net_chan.h (lines 30–79)
#define NET_FRAMES_BACKUP   64    // must be power of 2
#define NET_FRAMES_MASK     (NET_FRAMES_BACKUP - 1)
#define MAX_SUBCHANNELS     8     // 8 alternative send & wait bits

#define SUBCHANNEL_FREE     0   // free to use
#define SUBCHANNEL_TOSEND   1   // data pending, not sent yet
#define SUBCHANNEL_WAITING  2   // sent, waiting for ACK
#define SUBCHANNEL_DIRTY    3   // marked dirty during changelevel

struct subChannel_s
{
    int startFraggment[MAX_STREAMS];
    int numFragments[MAX_STREAMS];
    int sendSeqNr;
    int state;   // one of SUBCHANNEL_* above
    int index;
};

Flow statistics per frame

Each netframe_t records latency, dropped packets, and per-message-group byte counts that the net graph and net_stats commands display:
// engine/net_chan.h (lines 82–95)
typedef struct netframe_s
{
    float  time;            // net_time received/sent
    int    size;            // total size in bytes
    float  latency;         // raw ping for this packet
    float  avg_latency;     // smoothed ping
    bool   valid;           // false if dropped, lost, flushed
    int    choked;          // count of previously choked packets
    int    dropped;
    float  m_flInterpolationAmount;
    unsigned short msggroups[INetChannelInfo::TOTAL]; // bytes by group
} netframe_t;

Reliable vs. unreliable messages

CNetMessage carries a single m_bReliable flag. When set, the channel retransmits the message until it is acknowledged; when cleared, the message is sent once and silently dropped if the packet is lost:
// common/netmessages.h (lines 72–91)
class CNetMessage : public INetMessage
{
public:
    CNetMessage()
    {
        m_bReliable  = true;
        m_NetChannel = NULL;
    }

    virtual void SetReliable( bool state ) { m_bReliable = state; }
    virtual bool IsReliable() const        { return m_bReliable; }

protected:
    bool        m_bReliable;   // true → retransmit until ACKed
    INetChannel *m_NetChannel;
};

Reliable messages

Used for state that must arrive: signon, class info, string table creation, ConVar syncs, file transfers. If the underlying UDP packet is lost, the engine retransmits automatically.

Unreliable messages

Used for high-frequency, time-sensitive data: tick heartbeats, voice data, movement commands. Losing an occasional packet is acceptable; retransmitting stale data would be worse.

Message taxonomy

The message system uses three prefixes to indicate direction and purpose:
Messages that flow in either direction. Declared with DECLARE_NET_MESSAGE.
// common/netmessages.h

// Synchronize ConVar values
class NET_SetConVar : public CNetMessage
{
    DECLARE_NET_MESSAGE( SetConVar );
    // ...
    CUtlVector<cvar_t> m_ConVars;
};

// Execute a console command string on the remote peer
class NET_StringCmd : public CNetMessage
{
    DECLARE_NET_MESSAGE( StringCmd );
    const char *m_szCommand;
};

// Tick heartbeat — always unreliable
class NET_Tick : public CNetMessage
{
    DECLARE_NET_MESSAGE( Tick );
    // m_bReliable = false in constructor
    int   m_nTick;
    float m_flHostFrameTime;
    float m_flHostFrameTimeStdDeviation;
};

// Signon state negotiation
class NET_SignonState : public CNetMessage
{
    DECLARE_NET_MESSAGE( SignonState );
    int m_nSignonState;   // SIGNONSTATE_* constant
    int m_nSpawnCount;    // server session number
};

Message macros

The three declaration macros inject the same boilerplate methods into every message class:
// common/netmessages.h (lines 44–66)
#define DECLARE_BASE_MESSAGE( msgtype )                     \
    public:                                                  \
        bool        ReadFromBuffer( bf_read  &buffer );     \
        bool        WriteToBuffer( bf_write &buffer );      \
        const char *ToString() const;                        \
        int         GetType()  const { return msgtype; }    \
        const char *GetName()  const { return #msgtype; }

#define DECLARE_SVC_MESSAGE( name )                         \
    DECLARE_BASE_MESSAGE( svc_##name );                     \
    IServerMessageHandler *m_pMessageHandler;               \
    bool Process() { return m_pMessageHandler->Process##name( this ); }

#define DECLARE_CLC_MESSAGE( name )                         \
    DECLARE_BASE_MESSAGE( clc_##name );                     \
    IClientMessageHandler *m_pMessageHandler;               \
    bool Process() { return m_pMessageHandler->Process##name( this ); }
To add a new server-to-client message, you declare the class with DECLARE_SVC_MESSAGE(MyMsg), implement ReadFromBuffer / WriteToBuffer, and add a ProcessMyMsg virtual to IServerMessageHandler.

Delta compression of entity state

Entity state is transmitted as delta packets: only properties that changed since the last acknowledged tick are included. The flow is:
1

Baseline snapshot

On initial connection the server sends a full entity snapshot (the “baseline”) via SVC_PacketEntities with m_bIsDelta = false. The client stores this as baseline 0 or 1 (alternating, identified by m_nBaseline).
2

Delta generation

Each tick, the server compares current property values against the last acknowledged snapshot per client. Only changed bits are packed into the delta using the SendProp bit widths declared in each SendTable.
3

Client acknowledgment

The client sends CLC_BaselineAck to confirm it received and applied the baseline, so the server knows which snapshot to delta from next time.
4

Apply and interpolate

The client’s RecvVarProxyFn functions write incoming values into C_BaseEntity fields. The interpolation system then smooths between the last two received snapshots to produce per-frame positions.
If the client’s CLC_ClientInfo.m_nSendTableCRC does not match the server’s send table CRC, the connection is rejected before any entity data is sent. Keep send and recv table definitions in sync.

Network string tables (CNetworkStringTable)

String tables provide a lightweight mechanism to synchronize named string lists — model names, sound precaches, downloadable file lists, map name — without sending the strings in every packet.
// engine/networkstringtable.h (lines 41–98)
class CNetworkStringTable : public INetworkStringTable
{
public:
    CNetworkStringTable( TABLEID id, const char *tableName,
                         int maxentries, int userdatafixedsize,
                         int userdatanetworkbits, bool bIsFilenames );

    const char *GetTableName( void ) const;
    TABLEID     GetTableId( void ) const;
    int         GetNumStrings( void ) const;
    int         GetMaxStrings( void ) const;

    // Networking
    void        SetTick( int tick );
    bool        ChangedSinceTick( int tick ) const;

    int         AddString( bool bIsServer, const char *value,
                           int length = -1, const void *userdata = NULL );
    const char *GetString( int stringNumber );

    // Per-client delta write/parse (server side)
    int         WriteUpdate( CBaseClient *client, bf_write &buf, int tick_ack );
    void        ParseUpdate( bf_read &buf, int entries );

    // HLTV rollback support
    void        EnableRollback();
    void        RestoreTick( int tick );

    bool        WriteBaselines( SVC_CreateStringTable &msg,
                                char *msg_buffer, int msg_buffer_size );
};

Well-known table names

Table nameContents
modelprecacheModel paths precached with PrecacheModel()
soundprecacheSound paths precached by the server
downloadablesFiles the client must download before joining
genericprecacheGeneric files (VTF overlays, etc.)
lightstylesLight style strings for flickering lights
Each table entry has an optional userdata blob. For modelprecache, this blob carries the model’s MDLCacheFlattenedBone_t to pre-warm the model cache on the client.

Change tracking

ChangedSinceTick(tick) lets the server answer “has this table changed since the client’s last acknowledged tick?” efficiently. The engine calls this before including a string table update in a packet to avoid sending unchanged tables:
// engine/networkstringtable.h (lines 58–59)
void SetTick( int tick );
bool ChangedSinceTick( int tick ) const;

HLTV (SourceTV) and demo recording

SourceTV is integrated at the network channel level. An HLTV proxy appears to the server as a normal client (its player_info_t.ishltv flag is true) but fans out the stream to thousands of spectators.
// public/cdll_int.h — player_info_t
typedef struct player_info_s
{
    char          name[MAX_PLAYER_NAME_LENGTH];
    int           userID;
    char          guid[SIGNED_GUID_LEN + 1];
    uint32        friendsID;
    bool          fakeplayer;   // bot
    bool          ishltv;       // HLTV proxy
    CRC32_t       customFiles[MAX_CUSTOM_FILES];
    unsigned char filesDownloaded;
} player_info_t;
The CNetworkStringTable::EnableRollback() / RestoreTick(tick) API exists specifically for HLTV: when a spectator seeks backward in a live demo, the string table state can be rolled back to any previously recorded tick without a full reconnect.
Demo files embed the raw net channel stream, including all SVC_* messages and string table baselines. The backward-compatibility constants in proto_version.h (PROTOCOL_VERSION_17 through PROTOCOL_VERSION_23) tell the demo reader exactly which field layout to expect for each recorded protocol version.

Server browser and master server

The server advertises itself to Valve’s master server over UDP using the A2S_* query protocol. Clients use SVC_ServerInfo.m_szHostName, m_nMaxClients, and m_szMapName to populate the in-game server browser. None of this logic lives in the game DLL — it is entirely engine-owned and requires no source changes to work.

Networking data flow

Client                          Engine (net channel)             Server
  │                                    │                            │
  │── CLC_Move (unreliable) ──────────►│── deliver user cmds ──────►│
  │                                    │                            │ tick N
  │                                    │◄─ SVC_PacketEntities ──────│
  │◄── delta entity bits ─────────────│   (delta from tick N-1)    │
  │◄── SVC_Sounds (unreliable) ───────│                            │
  │◄── NET_Tick ──────────────────────│                            │
  │                                    │                            │
  │── CLC_BaselineAck ────────────────►│                            │
Every arrow over the UDP wire is managed by CNetChan. Reliable messages ride in the same packet stream but are retransmitted in subsequent packets until acknowledged by the receiver’s sequence number.

Build docs developers (and LLMs) love