Network Architecture
OpenWarcraft3 uses the same runtime-dispatch networking model as Quake 2. A
single send/receive API (NET_SendPacket / NET_GetPacket) handles all
communication; only the lowest layer changes path depending on whether the
destination is in the same process (loopback), on LAN (UDP), or in an EOS Internet room (P2P).
Address types (netadr_t)
Every packet is addressed with a netadr_t:
typedef enum {
NA_LOOPBACK, // in-process ring buffer (zero copy, zero latency)
NA_IP, // unicast UDP
NA_EOS, // EOS P2P, authenticated lobby Product User ID
NA_BROADCAST, // broadcast UDP
} netadrtype_t;
typedef struct {
netadrtype_t type;
unsigned char ip[4]; // network byte order
unsigned short port; // network byte order
char peer[33]; // EOS Product User ID, used only for NA_EOS
} netadr_t;
The type field is the only routing key. ip and port are only
inspected when type is NA_IP or NA_BROADCAST.
Out-of-band packet buffer lifetime
Netchan_OutOfBand builds a packet with a four-byte marker followed by the
payload. Its temporary packet storage is sized to the exact payload and
allocated on the heap, rather than reserving MAX_MSGLEN (256 KiB) on the
caller's stack. The payload must fit within MAX_MSGLEN - 4; packets outside
that limit or whose buffer cannot be allocated are not transmitted. This
matters especially for the WebAssembly engine thread's limited stack during
listen-server startup; the packet layout and routing remain unchanged.
Loopback (listen-server / single-process)
When the executable starts with +map it runs both the server and the local
client in the same process. The local client slot is assigned address type
NA_LOOPBACK; no socket is involved.
Two 256 KiB ring buffers carry traffic in each direction:
bufs[NS_CLIENT] ← client writes (clc_move, clc_connect, …)
bufs[NS_SERVER] ← server writes (svc_packetentities, svc_serverdata, …)
Sending to NA_LOOPBACK appends a 4-byte little-endian length prefix followed
by the payload into the appropriate ring buffer. Receiving polls the other
side's ring buffer.
// common/net.c
void NET_SendPacket(NETSOURCE netsrc, int length, const void *data, netadr_t to) {
switch (to.type) {
case NA_LOOPBACK:
NET_SendLoopPacket(netsrc, length, data);
break;
case NA_IP:
case NA_BROADCAST:
NET_SendUDPPacket(length, data, to);
break;
}
}
int NET_GetPacket(NETSOURCE netsrc, netadr_t *from, LPSIZEBUF msg) {
int r = NET_GetLoopPacket(netsrc, from, msg); // loopback first
if (r) return r;
return NET_GetUDPPacket(netsrc, from, msg); // then real socket
}
UDP (remote client)
When the client searches LAN servers or starts a remote connection, it calls
NET_Config(true). This opens a client socket on an ephemeral port. When a
LAN lobby is hosted, the server socket opens on game_port, defaulting to
PORT_SERVER (27910).
UDP datagrams are sent raw, matching Quake 2. An out-of-band packet begins
with the normal -1 message marker in the payload; there is no extra network
length prefix.
Initialisation
NET_Init() is called once from main.c and only clears loopback state. UDP
sockets are opened and closed through NET_Config(multiplayer), matching
Quake 2:
| Mode | Call | Effect |
|---|---|---|
Local map (+map) |
NET_Config(false) or no multiplayer config |
Loopback only; no UDP sockets |
| LAN lobby host | NET_Config(true) |
Server socket on game_port, client socket on an ephemeral port |
| LAN search / remote connect | NET_Config(true) |
Client socket on an ephemeral port, server socket available if hosting |
NET_Shutdown() calls NET_Config(false) to close any open sockets.
Connection handshake
Remote client → server
- Client calls
CL_Connect(host, port)which resolves the hostname viaNET_StringToAdrand sends an out-of-band"connect <version>\n<userinfo>"datagram to the server (the version comes fromBZ_PROTOCOL_VERSION). - The server's
SV_ReadPacketsreads the datagram, checks that the payload starts with"connect", validates the protocol version, and callsSV_DirectConnect(from, userinfo)to allocate a new client slot withNA_IPtype. Missing or mismatched versions are rejected before allocation. - The server replies
"client_connect <version>"; the client validates that version before clearing its state and sending the"new"command. This also rejects an old server's unversioned reply. - From this point the normal
clc_*/svc_*message exchange proceeds over UDP, identical to the loopback exchange.
Local client (loopback)
SV_Map calls SV_ClientConnect() which allocates slot 0 with type
NA_LOOPBACK and sends the same versioned "client_connect" reply through the ring buffer.
Map restarts and lobby-to-game transitions also retain this versioned reply.
Command-line interface
# Listen server + local client (loopback; no real socket traffic)
openwarcraft3 -data=/path/to/Warcraft3 +map Maps\Campaign\Human02.w3m
# Remote client, default port (PORT_SERVER = 27910)
openwarcraft3 -data=/path/to/Warcraft3 -connect=192.168.1.10
# Remote client, explicit port
openwarcraft3 -data=/path/to/Warcraft3 -connect=192.168.1.10:27910
| Argument | Description |
|---|---|
-data=<folder> |
Warcraft III data folder containing MPQs and optional loose maps |
+map <path> |
Internal MPQ path of the map to load; starts a listen server |
-map=<path> |
Compatibility form for +map <path> |
-connect=<host[:port]> |
Hostname or IP of the server to join; starts a remote-client session |
+map and -connect are mutually exclusive. Omitting both (with a valid
-data) starts the client menu.
Entity snapshot flags
entityState_t.flags is a uint32_t serialized as NFT_LONG by common/msg.c. Engine-level bits describe generic client
presentation/interaction capabilities; game modules decide when to set them in server-authored snapshots. EF_HOVER_HEALTH means the
client may expose the entity's compressed health/mana through a server-declared world-hover layout. WC3 and WoW author hover
capabilities per recipient; SC2 currently sends an empty hover layer and does not set them. EF_HOSTILE and EF_NEUTRAL are recipient-relative
presentation relationships; when neither is set, game-specific renderers may treat the entity as friendly. Any new field, flag, or
packed value in entityState_t requires a MSG_WriteDeltaEntity/MSG_ReadDeltaEntity round-trip test; WC3 snapshot flag tests live in
games/warcraft-3/tests/test_server_net.c.
Protocol version 8 adds entityState_t.hover_value, an optional recipient-filtered DWORD for a live numeric detail attached to a
server-authored world-hover name frame. Zero means absent; present values are transmitted as the displayed value plus one so a
depleted resource can still display zero. Game modules own when it is populated and the frame owns its label. WC3 uses the generic
field for Gold Mine reserves, including the authoritative hidden parent reservoir behind Haunted/Entangled mine overlays. Clients
and servers must use the same protocol version because this field is inserted into the entity delta schema.
Protocol version 19 expands each fixed-size CS_GENERAL hover-name record from 15 to 31 display characters. To keep each
configstring payload at MAX_PATHLEN, each configstring now packs eight records instead of sixteen; the total name pool remains
256 entries. The record layout is shared by all game modules and clients, so mixed protocol versions are rejected.
Sequenced netchan, acknowledged deltas and rate (protocol 20)
UDP (NA_IP) is lossy, so its packets (lobby, startup and game) follow Quake 2's netchan. Loopback and EOS (which is
reliable-ordered) are lossless: they carry no header, sequence or acknowledgement, and frames delta against the last one
sent. They are not byte-identical to protocol 19, though: the reliable stream (live configstrings) rides in front of
the message, and when the two do not fit one packet the reliable part goes alone first, then the message
(netchan.lossless_streams_too_large_for_one_packet_go_as_two; it used to send neither and let the message grow until
it overflowed). The rest of this section applies to NA_IP.
- Header. Every in-game datagram starts with two 32-bit words: its own sequence number, whose top bit says the packet
carries the reliable message, and the highest sequence it has received from the peer, whose top bit is the parity of the
reliable messages received.
Netchan_Processdrops stale, duplicated or overtaken packets, and acknowledgements for packets never sent, so the game never parses the same unreliable data twice or out of order. Out-of-band packets (-1marker) are untouched. - Sessions. Every
client_connectstarts a new netchan session on both ends: the server rebuilds or resets the slot (SV_Map->SV_RestoreLobbyClientswhen a lobby starts its game,SV_DirectConnectfor a repeated connect from the same address, which also forgets the old session's delta bases viaSV_ResetDeltaBase), and the client resets inCL_ClearState, whichCL_ConnectionlessPacketruns on that reply. Before this the client kept its lobby sequence numbers, so after a UDP lobby started the game each side dropped the other's packets as stale and startup deadlocked (client_session.connection_reply_restarts_sequenced_netchan,server_net.udp_lobby_to_game_restarts_both_netchans,server_net.udp_same_port_reconnect_restarts_server_netchan). - Two streams.
netchan.messageis unreliable: snapshots, sounds, layouts are sent once.netchan.reliableis resent whenever a later packet is acknowledged without it. Live configstrings (SV_QueuePendingConfigStrings) use it, so a lost datagram can no longer lose a unit name or image; the client uses it fornodelta. One reliable batch is in flight at a time; a configstring that does not fit is retried next frame. The message is never dropped to make room: a reliable message that does not fit beside it waits, andNetchan_Transmitsends it in a second packet straight after. A message that overflowed (truncated) is dropped with a log, as Quake 2 does, andnetchan.unreliable_sequencethen does not name that packet. - Datagram budget. A startup reply plus whatever reliable message the same packet may carry (a new batch, or a resend of
the one in flight) plus the 8-byte header stays within
BZ_SIGNON_SIZE:SV_SignonLimitsubtracts the pending reliable bytes fromSV_DatagramLimit, andSV_QueueConfigStringbounds a batch bySV_DatagramLimit(server_net.udp_signon_reply_and_reliable_stream_fit_one_datagram). In-game snapshots are not bounded by it. - Acknowledgements. A peer that received data answers with at least a bare header-only packet. A bare acknowledge is never acknowledged back.
- Deltas against what the client holds. After sending, the server records which snapshot the packet carried, only if
the netchan really put the message in it (
unreliable_sequence); when the client acknowledges the packet, that snapshot becomes the delta base (client->lastframe), instead of "the last one sent". Thesvc_frameheader names the base. The server keepsUPDATE_BACKUPsnapshots per client, indexed by how many were built for it (a rate-limited client skips game frames), and sends a full snapshot (-1) when the base is more thanUPDATE_BACKUP - 3snapshots old or its entities have left the ring. The client keeps its lastUPDATE_BACKUPapplied snapshots (entity table and player state) and, when the named base is not the one it just applied, rebuilds that base before applying the delta (CL_RestoreDeltaBase); a full snapshot clears the entity table first. "Entity table" means every entity the snapshot carried:SV_BuildClientFramealso sends entities with only a looping sound or an event, which the client lists incl.modelless_entitiesbeside the model ones incl.active_entities, and a delta for an entity already in the snapshot decodes from its current state, not its baseline, whether or not it has a model (client_frames.entities_without_a_model_survive_delta_base_rebuild). - Missing base. If the named base is not held, the client reads past the frame (header, player state, entities) without
applying or saving it, stays on its last good frame, and queues one
nodeltastring command on its reliable stream. The netchan still acknowledges that packet, so the server'sSV_NoDelta_f->SV_ResetDeltaBasesends the next snapshot in full and forgets every earlier packet's snapshot as a base. It used to apply the delta onto whatever was current and keep the corrupt result as a base (client_frames.delta_against_missing_frame_is_discarded_and_requests_full_frame,server_net.nodelta_request_sends_full_frame_and_forgets_earlier_frames,server_net.overflowed_frame_is_not_sent_or_used_as_delta_base). Lossless transports keep no history and never discard. - Rate.
sv_rate(registered inCvar_Init; bytes per second, default0= unlimited) models a client's link: afterSV_SendClientDatagramsends, the client is busy forbytes / sv_rate, counting every packetNetchan_Transmitsent (headers and the reliable stream included), and snapshots due in that window are skipped rather than queued. The next one deltas against what the client acknowledged, so a slow client sees a lower update rate, not a growing backlog (server_net.rate_charges_reliable_bytes).
Tests: netchan.* and server_net.udp_deltas_use_acknowledged_frames_and_configstrings_survive_loss (lossy link, default run),
the client_frames.* suite, and the stress_net.* suite (make test-stress):
a 33.6 kbps link with latency, jitter that reorders packets and 8% loss, asserting every delta base is a frame the client
holds and every unit name arrives.
Cinematic filter player state (protocol 23)
Protocol 22 adds cinefilter_blendmode to the generic player-state delta
schema; protocol 23 appends cinefilter_uv (NFT_BOX2) and
cinefilter_texmapflags. It is appended after the existing filter fields so prior player-state
delta bit assignments remain stable. The texture, RGBA tint/opacity, and blend
mode are carried in player-state deltas. Clients and servers must use protocol
23 together; the connection handshake rejects older peers before they can
decode snapshots with these fields.
The WC3 cinematic portrait model index reuses player stats slot 29, and
cinematic_portrait is now a presence flag. This changes WC3's use and saved
meaning of those existing values without changing the generic stats wire
structure. WC3 save format 78 rejects older saves.
Entity heading encoding
Protocol version 9 keeps entityState_t.angle two bytes wide but encodes its radian value as an unsigned
16-bit turn: 65536 steps span 2*pi, rounding to the nearest step and wrapping negative/multi-turn input.
Decode the short as unsigned before converting back to radians. Quarter turns are exact; the maximum
rounding error is half a step, about 0.00275 degrees. Camera Euler vectors retain their existing degree encoding.
Versions through 8 scaled this radian field as if it held degrees (angle / 360 * 65535). That coarse,
non-periodic grid introduced a 0.2991-degree bias when SC2 shifted placed-object heading by a quarter turn
before serialization and restored its native model basis afterward. The wire meaning changed, so mixed
version 8/9 peers are rejected by the versioned connection request/reply. Earlier builds declared
BZ_PROTOCOL_VERSION but did not negotiate it. Struct layouts and save data
are unchanged. net.entity_delta_preserves_radian_headings covers signed, wrapped, cardinal and arbitrary
headings; sc2_control.snapshot_preserves_authored_placement exercises the codec and renderer together.
See coordinate contracts.
playerState_t.viewoffset and playerState_t.eyeoffset are generic, game-evaluated transient offsets
of the look-at target and the derived orbit eye (camera shake). They follow Quake 2's viewoffset /
kick_angles split: the game module samples whatever waveform it owns once per server frame, the
values ride ordinary player-state deltas (no bytes while zero), and the client only interpolates the
previous and current sample before re-aiming the view. They are deliberately separate from
vieworigin, so game logic, input focus movement and camera getters keep reading a steady camera.
Protocol 15 briefly sent WC3 noise parameters (magnitude, velocity, vertOnly) and evaluated the
waveform in client/cl_view.c; protocol 17 replaced that with these offsets, and older peers are
rejected by the versioned handshake.
Key files
| File | Purpose |
|---|---|
common/net.c |
NET_SendPacket, NET_GetPacket, loopback buffers, UDP socket, Netchan_* |
common/net.h |
netadr_t, netchan_t, NETSOURCE, public API |
common/main.c |
CLI parsing, NET_Init, mode selection |
server/sv_init.c |
SV_ClientConnect (loopback slot), SV_DirectConnect (UDP slot) |
server/sv_main.c |
SV_ReadPackets — OOB routing and per-slot dispatch |
client/cl_main.c |
CL_Connect, CL_ReadPackets |
See Also
- Epic Online Services integration — optional Internet transport, Battle.net entry point, credentials and verification requirements
- Server-Authored UI Payloads —
svc_layoutframe payload contract and unsigned size-byte handling
LAN lobby and startup
Waiting connections receive pending messages or svc_nop at a one-second interval, including the listen
server's loopback client. ss_lobby runs transport without advancing the simulation. Keepalives continue
for cs_connected peers while other players are already running the map; cancelling still uses the normal
disconnect/shutdown path. The client's ordinary timeout remains enabled for a vanished host.
Startup follows Quake II's client-requested configstring and baseline pages. Remote startup messages are
bounded by BZ_SIGNON_SIZE (1400 bytes, leaving UDP/IP header room within a 1500-byte LAN MTU); loopback
retains the engine message budget. svc_mirror asks for the next configstrings <index> or baselines <index>
page. The final baseline reply is precache, which opens the client registration gate; registration then queues begin. Opening that
gate at the first baseline request would let registration and gameplay overtake the remaining entity pages.
The early loading presentation still puts its media before the eight binary loading-layout configstrings.
SV_SetConfigString does not queue live resynchronization while ss_loading: every connecting client fetches
those values through signon. Runtime changes in ss_game still mark the slot for broadcast. Broadcasting the
initial table as well as serving signon pages defeats the packet budget by filling a connected client's pending
message before its next request.
The September 10 reproduction used two macOS processes and Maps/(2)PlunderIsle.w3m. The idle host timed out
after 10,034 ms without server traffic. The remote configstring reply was 25,395 bytes and failed sendto
with EMSGSIZE (40); paging exposed 10,772 bytes of queued initial-table updates already included in that reply. A subsequent remote-only
world-registration crash resolved to G_WorldReadFile calling an uninitialized game import. A listen server
masks that boundary error because it initializes the game module before client registration.
An instrumented two-process audit of the same ROC map measured the complete configstring reply's table at 14,612 bytes, including each entry's opcode, index and string terminator (excluding the early binary loading slots):
| Pool | Entries | Wire bytes |
|---|---|---|
| Models | 54 | 2,066 |
| Sounds | 111 | 4,950 |
| Images | 129 | 7,301 |
| Font/size pairs | 7 | 173 |
| World/scope and other metadata | 7 | 122 |
| Total | 308 | 14,612 |
The original packet decomposes exactly as 10,772 + 14,612 + 11 = 25,395: queued duplicate updates,
the table, and svc_mirror plus "baselines" and its terminator. These are indexed resource names and metadata,
not asset-file contents. After suppressing loading-time live updates, the audit observed zero pending bytes
at the first configstring request. The table volume is reasonable for the map's units, creeps, projectiles,
world models, sound sets and UI, but is not a minimal dependency list:
- Loading preparation registers 2 models, 59 images and 4 font styles (3,780 wire bytes). Its media is sent
early and again in the complete table. Native
Loading.fdfincludesStandardTemplates.fdf; eagerly indexing these declarations accounts for 3,580 bytes of images, including unused button/scrollbar templates. A future reduction belongs in registration of consumed FDF resources, not filtering arbitrary network entries. - Sixteen image entries (914 bytes) resolve to paths also used by other entries. They are distinct raw skin keys
or a literal path versus a skin key, not repeated registrations of the same key.
SV_FindIndexdeduplicates raw keys; merging resolved paths would change their independently addressable configstring identity. CS_WORLDandCS_ASSET_SCOPElegitimately repeat the map path for different consumers; font sizes likewise require distinct entries. The figures above describe this map/edition, not a fixed protocol budget.
CS_IMAGES has 2,047 usable nonzero slots (MAX_IMAGES == 2048). The earlier 255-slot limit was too small for
Warcraft III's long-lived HUD: command cards, build menus, inventories, status panels, and FDF chrome can discover new art throughout
a map session, and SV_FindIndex() cannot safely recycle an occupied network slot while retained layouts still reference its number.
The larger table changes only index capacity; signon still serializes nonempty entries rather than the entire reserved range, and the
renderer-local texture registry remains independently unbounded.
Runtime resource publication is ordered with dependent UI payloads. SV_FindIndex() marks a new slot unsynced;
SV_QueuePendingConfigStrings() appends those svc_configstring records to every current client before PF_Unicast() appends and
transmits the layout/window payload that may contain the new index. The regular frame-send path calls the same queue helper. Do not
restore deferred-only publication: an immediate unicast can otherwise reach the client one packet before the configstring it uses.
To repeat the audit, temporarily log nonempty entries and SV_ConfigStringWireSize at the first
SV_Configstrings_f request, then run the bounded paired reproduction below. The loading layout now travels
separately through svc_loading_screen; see the loading transport contract.
Inspect raw keys as well as ge->GetThemeValue output to distinguish aliases from duplicate registration;
remove the instrumentation and rebuild afterward.
Reference implementations: Quake II server sending
(SV_SendClientMessages) and startup commands
(SV_Configstrings_f, SV_Baselines_f). These lifecycle rules use this engine's existing messages. Startup pages
travel in the unreliable message, so the sequenced netchan above does not resend a lost page.
Regression coverage lives in games/warcraft-3/tests/test_server_net.c (idle loopback/UDP keepalives,
loading-time versus runtime updates, complete paged UDP configstrings/baselines) and tests/test_net.c
(the precache gate). make test-server-net requires permission to bind local UDP sockets.
WC3 builds common/world_w3.c into the executable with BZ_CLIENT_WORLD. The engine's CM_LoadMapFormat
therefore uses engine filesystem/allocation functions. The same WPM reader feeds either the client's terrain-cell
storage (placement previews) or the game's routing buffers; routing jobs and edict-dependent obstacle handling
remain in common/routing.c, compiled only into the game module. CM_SetupPathMap and
CM_GetPathingFlagsAt have client implementations for this reason. Do not fix remote registration by initializing
a local server, skipping WPM data, or guarding NULL game imports. The crash originally traversed
CL_PrepRefresh -> CM_LoadMap -> G_WorldReadFile with gi.ReadFile == NULL; compiling only the map-format reader
without its path-data consumer still reaches G_WorldMemAlloc through CM_ReadPathMap and crashes.
In-engine input tests must set client collision bounds explicitly: game test-world bounds are not the client world.
The client_world regression checks terrain-cell lookup, replacement, and teardown. nm build/bin/openwarcraft3
should show defined text symbols for CM_LoadMapFormat, CM_SetupPathMap, and CM_GetPathingFlagsAt.
Keep the implementations included by games/warcraft-3/game/g_world.c under hidden symbol visibility.
The executable and game deliberately own separate world records, map readers, and path buffers. ELF symbol
interposition otherwise redirects game calls to identically named executable functions. Linux CI diagnostics
confirmed CM_SetupTestPathmap(64, 64, ...) entered the client initializer and left game routing at 0x0 with
NULL storage, causing movement/footprint failures and a crash; macOS's normal symbol binding masked the error.
Hide the whole game world implementation, including its state, rather than only the two pathmap functions.
wc3_pathfinding.terrain_flags_and_routing_share_game_storage checks blocked/open replacement through both
flag and routing queries; the existing client-world tests independently exercise the engine copy. On Linux,
readelf -Ws build/lib/libgame-wc3-test.so should show game CM_* implementations and world as local symbols.
For a bounded two-terminal reproduction, write a host script and start the second terminal during its wait:
python3 - <<'PYLAN'
from pathlib import Path
Path('/tmp/lan-check.cfg').write_text(
'wait\n' * 10 +
'lobby_start "Maps/(2)PlunderIsle.w3m"\n'
'lobby_config 2 2 PlunderIsle\n'
'lobby_slot 0 1 0 1 1 0 0 Host\n'
'lobby_slot 1 1 1 0 1 1 1 Open\n' +
'wait\n' * 600 + 'map "Maps/(2)PlunderIsle.w3m"\n')
PYLAN
build/bin/openwarcraft3 -data 'data/Warcraft III' -roc +set vid_hidden 1 +com_maxfps 30 +com_frame_limit 1500 +exec /tmp/lan-check.cfg
# Second terminal, after the host prints "Lobby initialized":
build/bin/openwarcraft3 -data 'data/Warcraft III' -roc -connect 127.0.0.1:27910 +set vid_hidden 1 +com_maxfps 30 +com_frame_limit 1500
Use an installed melee map path on both processes. 127.0.0.1 exercises UDP; localhost selects the in-process
loopback transport. Omit the final map command to verify an idle lobby. +set vid_hidden 1 is required for a
hidden window; +vid_hidden 1 alone can be parsed too late to hide it. Mac display access and local socket
permissions are required even for these hidden runs. Both peers must run the updated binary for svc_nop and
the final precache handshake.
Verification on September 10: a bounded idle lobby survived 800 frames at 30 fps (about 26 seconds); the
PlunderIsle UDP guest and host both sent begin, the server called G_ClientBegin for players 0 and 1 at distinct
start locations, and both processes exited normally. make test passed 2,927 tests / 61,017 assertions.
For hidden-window gameplay screenshots, set cl_camera_edge_scroll to 0; otherwise the inactive window's
pointer can pan the camera into unexplored fog. For a command-line guest, also use
+set cl_start_menu menu_ingame +screenshot 5: CL_Connect defers the startup command tail until the first
active frame, so the default queued menu_main would otherwise reopen after loading. This CLI-only diagnostic
setting is unnecessary when joining through the LAN menu. Final engine screenshots confirmed the blue guest
and red host at their respective bases with terrain, units, fog, and HUD visible.
Player-stat deltas (protocol 14)
Player-state fields retain a 32-bit field mask. After those fields, a separate
32-bit stat mask identifies changed slots in player_t.stats; each set bit is
followed by its unsigned 16-bit value in ascending slot order, including zero
when clearing a stat. This follows Quake 2's player-stat delta pattern. The
previous selected-pair field list omitted slots 25 and above; the cursor model,
mode and image regression exposed that omission. Tests cover all 32 slots,
unchanged values and clears. Protocol 14 rejects older peers at connection time.
Version 13 introduced this stat mask; version 14 separates pointer interaction
values from resolved game-renderer modes and adds pointer input-policy flags.
Terrain-deformation events (protocol 16)
Protocol 16 adds Warcraft III's transient TE_TERRAIN_DEFORM,
TE_TERRAIN_DEFORM_STOP, and TE_TERRAIN_DEFORM_STOP_ALL event payloads. Older
clients cannot parse those event types, so protocol 15 and older peers are rejected during
the connection handshake before gameplay messages are exchanged. The event
payloads remain transient and are not part of entity snapshots or saves.
The generic cursor presentation stats carry a registered model, an opaque game-owned interaction, a registered cursor image and generic input-policy flags. WC3 resolves race skins and inventory/color art on the server; the shared client only looks up registered handles. See cursor integration.
Compressed loading-screen layout scratch space
CL_ParseLoadingScreen assembles the server's compressed loading-screen layout
into cl.loading and inflates it into an independently allocated MAX_MSGLEN
scratch buffer. This buffer must not be an automatic array: the browser's
WebAssembly engine pthread has a bounded stack, and a loading screen may
require the entire 256 KiB output capacity. The scratch allocation is released
after parsing and loading preparation, including rejected layouts. The network
message format and maximum decoded size are unchanged.