Source Engine Entity Replication: Choosing Between PVS and FL_EDICT_ALWAYS
In Source SDK 2013, entity replication is driven by Send/Recv tables and the engine's PVS check. Learn when to trust the default and when to force FL_EDICT_ALWAYS.
19 Aug 2025, 23:48 UTC

The practical answer
In Source SDK 2013, you do not choose which clients receive an entity. The engine replicates each entity's networked state only to clients whose potentially visible set (PVS) contains it. Your job is to decide what state belongs in the Send/Recv table and whether any entity must override that visibility rule with FL_EDICT_ALWAYS.
Declaring the replication contract
Server entities declare a SendTable using IMPLEMENT_SERVERCLASS_ST and END_SEND_TABLE with SendProp* entries. The client mirrors it with a RecvTable under the same DT_ name. Snapshots are delta-compressed each tick, so bandwidth scales with what each client can see, not with total entity count.
Consider a flag entity that needs to replicate its owning team and the player carrying it. On the server:
// server: flag.h
class CMyFlag : public CBaseAnimating
{
public:
DECLARE_CLASS( CMyFlag, CBaseAnimating );
DECLARE_SERVERCLASS();
CNetworkVar( int, m_iTeam );
CNetworkHandle( CBaseEntity, m_hCarrier );
};
LINK_ENTITY_TO_CLASS( my_flag, CMyFlag );
IMPLEMENT_SERVERCLASS_ST( CMyFlag, DT_MyFlag )
SendPropInt( SENDINFO( m_iTeam ), 3, SPROP_UNSIGNED ),
SendPropEHandle( SENDINFO( m_hCarrier ) ),
END_SEND_TABLE()
The client repeats identical names and order. A mismatch in name, order, or bit count causes the client to log a Send/Recv table mismatch error and the class fails to link.
// client: flag.cpp
class C_MyFlag : public C_BaseAnimating
{
public:
DECLARE_CLASS( C_MyFlag, C_BaseAnimating );
DECLARE_CLIENTCLASS();
int m_iTeam;
CBaseHandle m_hCarrier;
};
IMPLEMENT_CLIENTCLASS_DT( C_MyFlag, DT_MyFlag, CMyFlag )
RecvPropInt( RECVINFO( m_iTeam ), 0, NULL ),
RecvPropEHandle( RECVINFO( m_hCarrier ) ),
END_RECV_TABLE()
Members must be declared as CNetworkVar or CNetworkHandle on the server so the engine can track changes for delta compression. On the client, plain members are sufficient because the engine writes decoded values directly.
How the engine decides who receives what
By default, the engine performs a PVS check. If an entity is outside every client's PVS, it goes dormant and stops thinking. Clients interpolate other entities between updates and predict only their own player and weapons. A higher tickrate reduces but never removes interpolation delay, and per-client bandwidth still grows with visible entity count and prop size.
Overriding the default: UpdateTransmitState
When an entity must reach every client regardless of location—a round manager, a global timer, or a spectator HUD anchor—override UpdateTransmitState():
int CMyGlobalEntity::UpdateTransmitState()
{
// Call base first to preserve default PVS logic
int state = BaseClass::UpdateTransmitState();
// Override only when this entity must reach every client
if ( m_bAlwaysTransmit )
{
state = SetTransmitState( FL_EDICT_ALWAYS );
}
return state;
}
FL_EDICT_ALWAYS forces the entity into every client's snapshot. FL_EDICT_DONTSEND hides it entirely. The escape hatch is per-entity, not per-client: you cannot target a subset of players directly.
Risk: FL_EDICT_ALWAYS multiplies an entity's bandwidth cost by the number of connected clients. Measure with net_graph before relying on it.
Limits and common mistakes
If the client console reports a table mismatch, compare the order and bit count of every SendProp against its RecvProp counterpart before checking anything else. Other frequent errors include:
- Raw ints for entity references: Use
SendPropEHandle, notSendPropInt, for entity handles. Raw indices break when edicts are reused. - Replicating derivable state: If the client can compute a value from other replicated data, leave it out. Every prop costs bits in every snapshot that includes the entity.
- Expecting instant propagation: Changes ride the next snapshot. There is no per-entity push.
- Dormancy: Entities outside every PVS go dormant and stop thinking. Never place must-run logic in a
Thinkthat can be culled. Use an always-transmit manager entity or engine timers instead. - Coordinate precision: SendProp coordinates use reduced precision (
SPROP_COORD_MP); do not expect exact float round-trips for positions.
Verification in a 2013 mod
Build server.dll and client.dll in Visual Studio, launch a dedicated server plus a client, and confirm the client console (with developer 1) is free of Send/Recv table mismatch errors for the new class.
To measure impact, spawn many copies of the entity and watch net_graph 1 or net_status on the client to measure snapshot size, then toggle FL_EDICT_ALWAYS on one instance and compare.
Use report_entities on the server console for edict usage and ent_text (requires sv_cheats 1 in singleplayer) to inspect one entity and confirm its replicated props update as expected.
As a learning exercise, deliberately rename a RecvProp in a scratch build to learn the exact console error text, so real mismatches are recognizable during development.
Version assumptions
This describes Source SDK 2013 (Singleplayer/Multiplayer) and SDK Base 2013; exact defaults such as tickrate, snapshot rate, and edict limits vary by game and engine branch, so confirm against the headers of the build you compile against. It does not describe Source 2, CS2, or unrelated products with similar names.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.