Skip to content

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

  1. Client calls CL_Connect(host, port) which resolves the hostname via NET_StringToAdr and sends an out-of-band "connect <version>\n<userinfo>" datagram to the server (the version comes from BZ_PROTOCOL_VERSION).
  2. The server's SV_ReadPackets reads the datagram, checks that the payload starts with "connect", validates the protocol version, and calls SV_DirectConnect(from, userinfo) to allocate a new client slot with NA_IP type. Missing or mismatched versions are rejected before allocation.
  3. 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.
  4. 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_Process drops 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 (-1 marker) are untouched.
  • Sessions. Every client_connect starts a new netchan session on both ends: the server rebuilds or resets the slot (SV_Map -> SV_RestoreLobbyClients when a lobby starts its game, SV_DirectConnect for a repeated connect from the same address, which also forgets the old session's delta bases via SV_ResetDeltaBase), and the client resets in CL_ClearState, which CL_ConnectionlessPacket runs 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.message is unreliable: snapshots, sounds, layouts are sent once. netchan.reliable is 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 for nodelta. 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, and Netchan_Transmit sends it in a second packet straight after. A message that overflowed (truncated) is dropped with a log, as Quake 2 does, and netchan.unreliable_sequence then 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_SignonLimit subtracts the pending reliable bytes from SV_DatagramLimit, and SV_QueueConfigString bounds a batch by SV_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". The svc_frame header names the base. The server keeps UPDATE_BACKUP snapshots 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 than UPDATE_BACKUP - 3 snapshots old or its entities have left the ring. The client keeps its last UPDATE_BACKUP applied 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_BuildClientFrame also sends entities with only a looping sound or an event, which the client lists in cl.modelless_entities beside the model ones in cl.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 nodelta string command on its reliable stream. The netchan still acknowledges that packet, so the server's SV_NoDelta_f -> SV_ResetDeltaBase sends 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 in Cvar_Init; bytes per second, default 0 = unlimited) models a client's link: after SV_SendClientDatagram sends, the client is busy for bytes / sv_rate, counting every packet Netchan_Transmit sent (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

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.fdf includes StandardTemplates.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_FindIndex deduplicates raw keys; merging resolved paths would change their independently addressable configstring identity.
  • CS_WORLD and CS_ASSET_SCOPE legitimately 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.