The single decision the whole engine is built around: never send the game across the network. Send the inputs, and let every machine compute the identical game for itself.
Imagine two people with identical calculators agreeing to press the same buttons in the same order. They don't need to compare results — they're guaranteed to land on the same number. Now make the "calculator" a StarCraft match and the "buttons" your mouse clicks. That's lockstep.
Instead of one machine simulating the battle and broadcasting where every unit is — hundreds of positions, many times per second, far more than a modem could carry — each machine runs the full simulation locally. The only thing that crosses the network is the tiny stream of player commands: "select these units," "move here," "build a barracks there." A few bytes each.
Because nobody sends positions, the simulation on each machine must produce identical results from identical commands. The next chapter — determinism — is entirely about making that guarantee bulletproof. This chapter is about the timing machinery that keeps everyone in step.
Hard rule: no machine advances a frame until everyone's commands for it have arrived. Running ahead would mean computing a different game.
Naively that's a full round-trip per frame — unplayable on a 1998 modem. The fix is latency buffering: commands you issue now take effect a fixed number of frames in the future. While the network carries them, the game keeps simulating frames already locked in. OpenBW's default: 2 frames (≈83 ms at 24 fps).
// sync.h:25 — the default lookahead
int latency = 2;
// sync.h:279 — your action is stamped to land `latency` frames ahead
client->scheduled_actions.push_back(
{(uint8_t)(client->frame + sync_st.latency), pos, buffer_end});
The command you issue on frame N is scheduled for frame N+2. That 2-frame window is the slack the network gets to deliver it.
Two players. Every frame, each issues a command that's scheduled latency frames ahead. The server only advances the shared frame once all players' commands for it have arrived. Drop a packet and watch everyone stall — exactly as the real engine does.
sync.h · all_clients_in_sync() gates every frame
The server keeps a sync_frame counter and, for each client, the last frame it has heard from. It will only step forward when every client is within latency frames of the shared clock. One laggard freezes the whole match — the classic "waiting for players…" stall.
// sync.h:949 — the gate. Note the signed-byte math for wraparound.
bool all_clients_in_sync() {
for (auto* c : ptr(sync_st.clients)) {
if ((int8_t)(sync_st.sync_frame - c->frame) >= (int8_t)sync_st.latency)
return false; // someone hasn't caught up — stall
}
return true;
}
The frame counter is a uint8_t that wraps at 256. Casting the difference to int8_t makes "is client behind?" work correctly across the wrap.
uint8_t frame counter? deeperThe sync layer only ever cares about relative frame distance within a small window (a few frames of latency), never the absolute frame number. A single byte is enough to express "how far apart are we," costs almost nothing on the wire, and wraps harmlessly — the signed-difference trick above treats 255→0 as "+1," not "−255." The full 32-bit game frame lives elsewhere, in the simulation state.
A client that falls and stays behind isn't allowed to freeze the game indefinitely. The server times stragglers out:
// sync.h:616 — drop a client that's both behind and silent for 60s
if (now - c->last_synced >= std::chrono::seconds(60)) {
if ((int8_t)(sync_st.sync_frame - c->frame) >= (int8_t)sync_st.latency)
kill_client(c);
}
Each client is also given a generous per-client circular buffer for its in-flight commands — 1024 * 4 * latency bytes (sync.h:231), about 8 KB at the default latency.
The simulation runs at a fixed 24 logical frames per second. Game speed settings ("Fastest," "Fast," etc.) don't change the logic — they change how much real time each frame is allowed to take. At "Fastest," frames are pushed out as quickly as ~42 ms apart. Because the logic per frame is identical regardless of speed, a replay recorded on one speed plays back identically on another.
The sync layer's "sync frame" is a coarser network heartbeat than the 24 fps game frame; commands are batched and stamped to game frames as they're handed to the simulation (see the action stream).
Lockstep's strength is its fragility. One bit of disagreement and the games drift apart forever. Common causes:
Player A sees a Marine alive; B sees it dead. This is a desync — catastrophic, because there's no authoritative state to resync to. Every machine thinks it's right. Defense: hash the whole game state, compare constantly, drop whoever diverges — the next chapter.