The Systems Map's founding diagram draws one arrow labeled request · network pack and one labeled result · broadcast pack. This is what's actually inside those arrows: a length-prefixed byte stream, a double-dispatch visitor that turns a deserialized struct into a real function call, and — on the client — a sandwich around the moment state actually changes.
Every message on every connection is length-prefixed — a 4-byte size, then the payload. What differs is what's inside.
Three distinct connections can exist during a game's life: client ↔ match server (gameplay — the one this atlas's other five docs already assume), client ↔ lobby server (global multiplayer matchmaking and chat), and match server ↔ lobby server (a control channel, opened when a multiplayer room is created). Gameplay traffic is VCMI's own binary serializer — the same framework behind savegames, one shared h & object pattern for both, gated by a single version constant with no backward compatibility. Lobby traffic is plain UTF-8 JSON, and unlike the rest of the codebase's comment-tolerant JSON, every lobby message is validated as strict JSON with a mandatory "type" field.
NetworkConnection (real TCP, via a length-prefixed NetworkBuffer) isn't the only way two sides of this protocol talk, though — and this is the part the docs don't quite say. When a singleplayer game starts, ServerThreadRunner::connect() doesn't open a socket at all: it calls createInternalConnection(), which wires client and server together through InternalConnection, an in-process class that implements the identical INetworkConnection interface but hands framed messages directly between two objects in the same address space. The wire protocol — framing, packs, the visitor dispatch below — runs unconditionally either way; only the transport is conditional. Real TCP comes back for lobbyMode (true remote play) and, behind a separate build flag, for running the server as a detached OS process instead of a thread.
A deserialized CPack doesn't know what it is until something visits it — twice on the way in, and on the client, wrapped around the moment state actually changes.
Every incoming pack is a CPack subclass, and CPack::visit() dispatches in two stages: visitBasic(), implemented once per base category (CPackForServer, CPackForClient, CPackForLobby), then visitTyped(), overridden by every concrete leaf type, which calls the one matching method — visitMakeAction(), visitBattleAttack(), one of roughly 170 such methods declared on ICPackVisitor — on whichever visitor implementation is doing the asking. That's the whole trick: a `switch` the compiler writes for you, once per pack type, instead of one arm of a hand-maintained dispatch table.
gameState().apply(pack), because it does: ApplyFirstClientNetPackVisitor::visitBattleAttack() starts the strike animation and applies battleStacksAttacked while the target's HP is still the pre-hit value, specifically so the animation and the damage don't arrive in the same instant. This is what Battle System meant by “BattleStacksController plays it” without explaining how the timing actually works.A client request and a pending server question are correlated by two unrelated IDs, because they're answering two different questions.
stamped by the client on every CPackForServer
CGameHandler::handleReceivedPack() echoes it straight back twice: a PackageReceived the instant the pack arrives, then a PackageApplied (success flag included) once it's actually been processed. Two acks, not one — a client can tell “the server has my request” apart from “the server acted on it.”
stamped by the server on a Query, a CPackForClient subtype
isBlockedByQueries() refuses further action from a player while one of their queries is outstanding — the same CBattleQuery mechanism Battle System found locking both sides out for the length of a fight. QueryResolved broadcasts the ID once it's answered.
Getting two players behind different NATs onto one connection takes a mid-flight identity swap on both ends at once.
client asks the lobby to join an open room
notifies over the existing control connection
match server opens a new connection to the lobby, hands it to VCMIServer
client mirrors the move, hands its side to CServerHandler
MakeAction/BattleStart packs, just with the lobby quietly forwarding every byte in between.Four files, split by who sends and what context — and the traffic is lopsided in the direction you'd expect.
PacksForClient.h79 types — server → client, general gameplay: SetResources, SetMana, GiveBonus, FoWChange, PlayerStartsTurn… detailed state deltas, by far the largest file herePacksForServer.h39 types — client → server, requests: MoveHero, EndTurn, BuildStructure, BulkMoveArmy… terse intents, roughly half as manyPacksForClientBattle.h23 types — the battle-specific slice: BattleStart, BattleAttack, BattleNextRound, BattleResultAccepted — the exact names Battle System already tracedPacksForLobby.h32 types — the JSON side: clientLogin, sendChatMessage, activateGameRoom, the *ProxyLogin pair from aboveRoughly two broadcasts for every one request, which is the shape you'd expect from a server that's the sole writer of state: it has a lot more to report than any single client ever has to ask for.