How the Source Engine Works
Follow a player's command through Source's modules, prediction, server simulation, and snapshots, then connect those boundaries to settings and game files.
Cheatsheet for Lesson 9.10
A one-page summary of “How the Source Engine Works” you can keep beside your notes. Plain Markdown.
Lesson 9.10 — How the Source Engine Works: cheatsheet
- Remember: The client presents and predicts. The server decides the game state. The engine connects their game libraries.
- Also useful: Picture a shared menu of operations. The version name selects the menu both modules agree to use.
Key terms
- Source: Source is Valve's game-engine family behind Half-Life 2, Counter-Strike: Source, Team Fortress 2, Left 4 Dead, and the original Counter-Strike: Global Offensive.
- DLL: A DLL is a library loaded into a process while it runs, as introduced in Lesson 6.2.
- interface: That set is an interface: a contract describing which methods an object provides and how to call them.
- factory: A factory is a function that supplies an object through an agreed interface.
VClient017: In this revision,VClient017names the client game interface andVEngineServer023the engine services offered to server code.- RecvTable: A corresponding RecvTable describes how client game code receives those properties.
- tick: A tick is one step of the simulation clock.
- authoritative: The server still runs its own simulation, and its results are authoritative in the sense of Lesson 9.1: they are the state the client must reconcile with.
- snapshot: A snapshot describes server state at a point in simulation time.
- lumps: A Source BSP contains sections called lumps, holding map data such as geometry, visibility, and starting entity definitions.
Review
- Q: The server acknowledges command 3 at position 2. Command 4, a move of minus one, is still pending. How should the client reconcile? A: Start at position 2 and replay only command 4
- Q: Why doesn't a Source 1 interface name establish a CS2 layout? A: Counter-Strike 2 uses a different engine family
You press forward. Your player moves immediately, yet the server has not received the input. Later, a reply may adjust that movement. To understand why, follow the input across the pieces of the game that own each decision. Lesson 9.1 introduced client and server messages. Source gives us a concrete engine in which those messages connect to objects and time.
Why study Source?
Section titled “Why study Source?”Source is Valve’s game-engine family behind Half-Life 2, Counter-Strike: Source, Team Fortress 2, Left 4 Dead, and the original Counter-Strike: Global Offensive. They share architectural ideas, but their engine branches differ. Counter-Strike 2 uses Source 2, a later engine family. An interface name or memory layout from Source 1 is not a promise about Counter-Strike 2.
Valve’s public Source SDK 2013 supplies game code and interfaces for Half-Life 2, Half-Life 2: Deathmatch, and Team Fortress 2. An SDK, or software development kit, gives developers code and tools for building compatible software. This repository lets us inspect the game side of the boundary. It does not supply every implementation inside a shipping engine.
This lesson follows the SDK revision beginning b8cfb12. Treat its named
interfaces and defaults as facts about that revision. Keep experiments in
your own offline SDK mod. A mod supplies changed game content or behavior
while using the engine’s supported loading contracts.
The engine connects client and server game code
Section titled “The engine connects client and server game code”A launcher starts the engine and selects the game to load. The engine provides common services such as files, rendering, sound, and networking. Game-specific code defines weapons and entities, calculates movement and damage, and decides how those objects respond to input and contact.
On Windows, this game code commonly comes in client.dll and server.dll.
A DLL is a library loaded into a process while it runs, as introduced in
Lesson 8.1. Other operating systems use their own shared-library
formats for the same architectural division.
The client library handles the player’s input, local presentation, and prediction. The server library calculates movement, collisions, damage, and other changes to the match state; it sends the accepted results to clients. Some movement code is shared so both sides can compute comparable results. Rendering a player and deciding whether that player can pass a wall are separate responsibilities.
A listen server hosts the simulation inside a game that also has a local player. A dedicated server runs the hosting side without that local player’s view. Even a local game can retain the client/server division. The boundary describes who owns a decision, whether its messages cross the Internet or stay on one machine.
Modules ask for an interface by name
Section titled “Modules ask for an interface by name”The engine cannot call an arbitrary function inside a loaded library and hope its arguments fit. The two sides need an agreed set of operations. That set is an interface: a contract describing which methods an object provides and how to call them.
Source modules expose a factory named CreateInterface. A factory is a
function that supplies an object through an agreed interface. The caller
passes the requested name. The module searches its registered interfaces
and returns a pointer to the matching object, or a null pointer if it cannot
provide one. A separate, optional status output receives IFACE_OK or
IFACE_FAILED.
Each name carries a version suffix. In this revision, VClient017 names the
client game interface and VEngineServer023 the engine services offered to
server code. The suffix is part of the requested contract: the returned
object’s virtual methods follow that version’s C++ calling contract, which a
neighbouring version can change. CreateInterface itself is an ordinary
export, found by name the way
Lesson 8.1’s injector finds gha_start.
This explains a startup failure that otherwise looks mysterious: a library loads successfully, but it cannot provide the interface the engine asks for. Loading bytes and agreeing on their callable contract are different checks.
Entity state becomes selected network properties
Section titled “Entity state becomes selected network properties”Once the libraries connect, the server can create an entity, an object the game tracks: a player, door, projectile, or another piece of the world. Its full object contains more information than a client needs. Some fields are server bookkeeping. Others describe what the client must display.
A SendTable declares properties that server game code makes available for network encoding. A corresponding RecvTable describes how client game code receives those properties. These are descriptions of fields and their encoding, rather than a copy of the whole C++ object. Nested tables let one class reuse another class’s networked fields.
Think of a door whose server object stores its position, opening state, and internal scheduling data. The client needs enough state to place and draw the door. It does not need every scheduling field. The property declarations make that selection explicit. The engine can also limit which entities and updates are relevant to a particular client.
Alone Door holds position, open state, scheduling
Step 1: Server door (alone)
The server’s door object has more state than any client needs.
Data Position and opening state
Step 2: Server door to SendTable
The SendTable names which fields become network properties. These two are on the list.
Alone Scheduling stays here
Step 3: Server door (alone)
Fields that are not on the list never leave the server.
Data Network properties
Step 4: SendTable to RecvTable
The chosen values are sent to the client in a compact form.
Data Update the client’s door
Step 5: RecvTable to Client door
The RecvTable describes how those values land in the client’s copy of the door.
Alone Draw the door
Step 6: Client door (alone)
The client draws what it was told, with no knowledge of the server’s scheduling.
The server door holds position, opening state and scheduling. Position and opening state are sent; scheduling stays on the server.
Floating-point properties can trade precision for bandwidth through
quantization: representing a range with a limited set of codes. As an
illustration, four bits give 2⁴ = 16 codes. Spread those codes across a
chosen range from 0 to 15, including both ends. The spacing is
(15 − 0) ÷ (16 − 1) = 1. Values between the available steps need an
encoding rule that chooses a representable value.
The SDK’s SendPropFloat accepts a bit count, range, and flags. Some settings
use an unscaled float instead. The four-bit example explains the trade;
it does not describe every Source float on the wire. A client-side value can
therefore differ slightly from the server’s value even when both sides work
correctly.
Input becomes a command record
Section titled “Input becomes a command record”Now the player presses a movement key. The client game interface’s
CreateMove produces a user command, one record of player input for
simulation. Source names that record CUserCmd.
Its command_number identifies a command in the sequence. Its tick_count
records the tick when the client created it. A tick is one step of the
simulation clock. Command numbers and tick numbers have different jobs,
even when a small example gives them matching values.
The record includes view angles and the intended movement values
forwardmove, sidemove, and upmove. It also includes weapon selection
and mouse movement. These inputs describe what the player requests. They
do not establish the final position, damage, or inventory result.
The buttons field packs several requests into one integer. In this SDK,
attack uses bit 0 and jump uses bit 1. Their values are 2⁰ = 1 and
2¹ = 2. Requesting both sets the combined mask 1 | 2 = 3, whose two
lowest bits are on. The server can test each bit separately.
This memory object is not the packet format. Command encoding records changes relative to another command, and decoding rebuilds the record. Fields used for local prediction bookkeeping do not all become raw packet bytes. Keeping object layout and wire encoding separate matters just as it did for the message parsers earlier in this chapter.
A tick advances the world; prediction advances the local estimate
Section titled “A tick advances the world; prediction advances the local estimate”The SDK’s default tick interval is 0.015 seconds. Multiply by 1000 to get
15 milliseconds per tick. Divide one second by the interval to get
1 ÷ 0.015 ≈ 66.67 simulation steps per second. A game can supply its own
interval, so this default is not every Source game’s rate.
The server game interface exposes GameFrame for simulation work and
GetTickInterval for its interval. Drawing frames happens on another
schedule. A faster renderer can draw additional views between simulation
ticks; it does not give the player additional accepted movement.
Waiting for a network reply before showing every step would make movement feel delayed. Prediction lets the client run its local player’s movement using input commands immediately. The server still runs its own simulation, and its results are authoritative in the sense of Lesson 9.1: they are the state the client must reconcile with.
Use a deliberately small movement model to see the reconciliation. Start
at x = 0. Each accepted forward command adds one position unit. Commands
1 and 2 request +1, giving 0 + 1 = 1, then 1 + 1 = 2. These are toy
displacements, not Source’s velocity and collision formulas. The scene’s
15-millisecond clock comes from the SDK default.
A barrier now closes on the server and prevents movement past x = 2.
The client has not received that update. Command 3 requests another +1.
The client predicts 2 + 1 = 3; server collision keeps the position at 2.
Before the reply arrives, command 4 requests −1. The client’s current
prediction becomes 3 − 1 = 2.
Acknowledge commands, then replay only the pending ones. This step-by-step trace needs JavaScript. Here is the code and where the run ends.
client predicts all four commands
server executes the acknowledged prefix
receive server position and barrier
drop commands through the acknowledgement
replay the remaining commands from server state
report the corrected predicted positionResult: server snapshot 2, 1 pending command(s), corrected prediction 1. Replaying an already acknowledged command would apply its effect twice.
Final values: ack = 3; barrier = yes; old_prediction = 2; server_position = 2; pending = 1; prediction = 1.
Read every step of the lesson’s example
Step 1, line 1. Start with the selected inputs and follow how the next operation changes the state.
ack = 3; barrier = yes.
Step 2, line 2. The old client prediction ends at two.
ack = 3; barrier = yes; old_prediction = 2.
Step 3, line 3. The snapshot acknowledges a prefix, not every command the client has sent.
ack = 3; barrier = yes; old_prediction = 2; server_position = 2.
Step 4, line 4. Discard only the acknowledged prefix.
ack = 3; barrier = yes; old_prediction = 2; server_position = 2; pending = 1.
Step 5, line 5. Replay pending commands using the newly known barrier.
ack = 3; barrier = yes; old_prediction = 2; server_position = 2; pending = 1; prediction = 1.
Step 6, line 6. Result: server snapshot 2, 1 pending command(s), corrected prediction 1. Replaying an already acknowledged command would apply its effect twice.
ack = 3; barrier = yes; old_prediction = 2; server_position = 2; pending = 1; prediction = 1.
A toy client starts at position zero and predicts three positive moves to positions one, two, and three. A newly closed server barrier blocks the third move at two. A fourth negative move is still pending. A snapshot acknowledges command three at position two, removes the first three pending commands, updates the barrier, and replays the fourth move to position one.
Read the steps
- This is a toy one-axis track starting at x = 0. Each positive command requests one position unit; it is not a literal Source movement field. The SDK default tick is 0.015 seconds, or 15 milliseconds.
- Command 1 requests +1. The client predicts 0 + 1 = 1 immediately. The numbered command travels to the server, which accepts position 1. Keep the command until it is acknowledged.
- Command 2 requests +1. Both copies reach 1 + 1 = 2. The pending queue now contains commands 1 and 2 because the client has not received their acknowledgment yet.
- The server barrier closes, limiting this toy position to 2. The client still has the old open copy and predicts 2 + 1 = 3 for command 3. The server blocks that move and stays at 2.
- Before the snapshot arrives, command 4 requests −1. The client predicts 3 − 1 = 2 and keeps all four commands. The server snapshot reports x = 2 and acknowledges commands through 3.
- Apply the authoritative state at command 3: x = 2, with the closed barrier. Remove acknowledged commands 1, 2, and 3 from the pending queue. Command 4 still needs to be replayed.
- Replay the remaining command 4 on that restored state. Its −1 move gives 2 − 1 = 1. The corrected client ends at 1; the outline marks the old prediction at 2. Keep command 4 pending until its own acknowledgment.
The positions and steps are a toy fixture. Motion is slowed. Source movement also uses velocity and collision; its 15 ms default tick does not imply one-unit displacement.
The server reply acknowledges command 3 and reports position 2. An
acknowledgment identifies the input already covered by the returned
state. The client starts from that authoritative position and drops commands
1 through 3 from its pending history. Command 4 is still pending, so it
replays that −1: 2 − 1 = 1.
That replay preserves the player’s newer input while correcting the earlier prediction. Simply replacing the displayed position would lose pending movement. Replaying commands already acknowledged would repeat work the server’s state already contains.
Snapshots smooth other players; lag compensation checks past positions
Section titled “Snapshots smooth other players; lag compensation checks past positions”A snapshot describes server state at a point in simulation time. It may be encoded as changes from an earlier known snapshot. Snapshot delivery, tick rate, and rendering rate are separate schedules.
For another player’s movement, the client usually has received states rather than that player’s newest input. Interpolation draws intermediate positions between received states. Showing a slightly older timeline gives the renderer states on both sides of the time it wants to draw. That makes motion smooth instead of jumping whenever a message arrives.
Prediction estimates the local player’s response to input. Interpolation fills the visible gaps between other entities’ received states. Neither makes a client observation newer than the information actually received.
This delay also matters when a player fires. The target they saw may occupy an earlier position than the server’s current position. Lag compensation temporarily uses recorded earlier player states for the relevant server-side hit check, then restores the current simulation. The server still performs the hit check, and the world does not permanently travel backward because a packet arrived.
In this SDK, the initial target tick is the command’s tick_count minus
the interpolation delay expressed in ticks. The server also checks that
time against its latency estimate. A discrepancy greater than 0.2 seconds
uses a latency-based target instead. The inspected sv_maxunlag default is
1 second and bounds the allowed correction. This is a bound on the decision;
it is not a promise that every past state remains available for exactly one
second.
Console variables have permissions as well as values
Section titled “Console variables have permissions as well as values”Games need settings without recompiling each change. A console variable, or ConVar, is a named setting with a value and flags describing how it may be used. The developer console exposes these settings and named commands.
FCVAR_CHEAT restricts certain development controls to permitted cheat
contexts, including the server’s sv_cheats setting. FCVAR_REPLICATED
marks a server-controlled setting enforced on connected clients. Flags
describe permissions and ownership as well as storage behavior.
A server can ask for a client’s variable through StartQueryCvarValue.
The request returns a cookie, an identifier for matching a later reply
to that request. It does not return the variable’s value immediately.
OnQueryCvarValueFinished supplies the later status and value. The
FCVAR_SERVER_CANNOT_QUERY flag forbids that query for the marked variable.
For a defender, a client-reported setting is evidence about what the client reported, not a replacement for the server-side movement, inventory, and damage checks of Lesson 9.2. A timeout or failed query is not automatically proof of cheating either.
The same engine loads packages and compiled maps
Section titled “The same engine loads packages and compiled maps”Those entities need geometry, models, textures, and sounds. VPK, short for Valve PacK, bundles resource files behind a directory tree. An entry identifies where its bytes live, possibly in a companion archive. Some bytes can be stored with the directory as preload data. This lets the file system locate a resource without treating every small asset as a separate loose file.
A BSP is a compiled map. The name comes from binary space partitioning, which divides space to organize geometry queries. A Source BSP contains sections called lumps, holding map data such as geometry, visibility, and starting entity definitions. It can also contain embedded resources.
The inspected header starts with VBSP, then a version and a directory of
64 lump descriptors. Each descriptor provides an offset, length, and
version-related metadata. Different game branches can interpret sections
differently. Reading a file therefore starts with identifying its format
and validating its bounded sections, not treating its bytes as a live
entity object. Chapter 9 develops that file-validation work.
Use each boundary for the question it can answer
Section titled “Use each boundary for the question it can answer”An offline observer built with supported interfaces can record command numbers, simulation ticks, received properties, and prediction corrections. Those records answer different questions. A rendered location describes a view; a server result describes an accepted simulation step; a package entry locates stored content.
For your own mod, keep module versions explicit and record which side owns each value. Start work after DLL loading has finished, stop workers before unloading, and release their handles when stopping tools. For defenders, validate player requests against server-owned state and keep work per client bounded. Interpolation, quantization, and correction explain some ordinary discrepancies, so one mismatch needs context. Chapter 15 builds on this separation between requests, observations, and decisions.
Keep newer input during correction
The server acknowledges command 3 at position 2. Command 4, a move of minus one, is still pending. How should the client reconcile?
The acknowledged state already includes commands 1 through 3. Only command 4 remains unacknowledged. Replaying its minus-one displacement gives 2 minus 1, or position 1, without repeating accepted input.
Finished Lesson 9.10?
Mark it done and the lesson list shows a beside it. A chapter whose lessons are all done is marked done too. Progress is saved in this browser only.
Plain Markdown, saved in this browser.
This book grew out of the original Game Hacking Academy by attilathedud and has been rewritten as a beginner-first course. Like the original repository, its source is published under the Apache License 2.0.