W3P Protocol
The complete contract between the runtime and external programs: eight shared memory blocks, reading world state, reading events, issuing commands, receipts, lane roles, the canvas, and UI & input. Read this page if you're integrating from a language other than Python.
The runtime and external programs exchange data only through shared memory. What follows is the whole of it.
- The reference implementation is the Python
sdk/python/w3world.py(read) andsdk/python/w3fast.py(write); every structure’s size and offsets are defined there and pinned by tests; - The protocol describes semantics only and is independent of the game version. When the game version changes, the runtime adapts itself and the protocol stays the same; new fields are only ever appended to the end of a block, so old clients keep working.
Most people don’t need this page — just use the Python SDK. You only need it if you want to integrate directly from C++ / C# / Rust / Go or another language, or you want to know what happens underneath the SDK.
1. Eight shared memory blocks
<pid> is the game’s process ID.
| Name | Direction | Contents | Synchronization |
|---|---|---|---|
Local\War3World_<pid> | runtime → you | World state: header + 16 players + up to 1024 units + 256 unit details + 256 ground items + extension area + production table | seqlock |
Local\War3Trees_<pid> | runtime → you | Up to 4096 destructables (trees, etc.), refreshed every 2 seconds | seqlock |
Local\War3Events_<pid> | runtime → you | Event ring, 8192 entries | each entry carries its own sequence number |
Local\War3Map_<pid> | runtime → you | Map: terrain cells (128 per cell, up to 256×256) + playable area bounds + start locations; computed in batches over the first few seconds of the game | seqlock (never changes once computed) |
Local\War3Fast_<pid> | both ways | Command lanes: 16 lanes × 16 slots; each slot holds one command + receipt; each lane has a role | single writer, single reader per slot |
Local\War3Canvas_<pid> | you → runtime | Canvas: 64-byte header + 256 elements × 112 bytes + 64 KB text / point pool; created only after you send canvas_enable once | seqlock (you write, the runtime reads every frame) |
Local\War3Msgs_<pid> | runtime → you | Screen message ring: full text of game hints, chat and system messages, 128 entries × 256 bytes | each entry carries its own sequence number |
Local\War3Input_<pid> | both ways | UI & input: the runtime writes back the mouse position, the ground point under the cursor and the hovered item; you write the hotkey table and mouse switches; the runtime takes over input only after you send input_enable once | seqlock for the hotkey table |
Several clients using the canvas and input at once: each of these two blocks exists only once, so clients writing independently would overwrite each other. The conventions below apply; follow them in your own client too:
- Canvas: hold the named mutex
Local\War3CanvasMutex_<pid>for a read-modify-write, replace only your own elements and leave everyone else’s as they are (repacking the pool offsets); clear elements whose owner process has exited and elements with no owner. An element’sreserved[1]= owner process ID,reserved[2]= in-process sequence number; element IDs are allocated from the counter at header offset 60 (starting at0x10000). - Input: each client registers its own hotkeys and mouse switches in
Local\War3InputClients_<pid>(16-byte header + 16 clients × 528 bytes). HoldingLocal\War3InputMutex_<pid>, it updates its own entry, then merges the live clients and writes the result into the input block: hotkeys are deduplicated by “key code + modifiers”, and mouse switches are combined as a union. Events go to all clients, and each one picks out its own hotkeys by “key code + modifiers”. Don’t sendinput_enable 0while other live clients are still in the registry. - Runtime: clickable elements whose owner process has exited stop intercepting clicks; every 2 seconds it checks the registry, and once all registered clients have exited, it zeroes the input block’s hotkey table and mouse switches.
2. Reading world state (seqlock)
loop:
s1 = block.seq (offset 8, int32)
if s1 is odd: retry (the runtime is writing)
copy header + players + units[unitCount] + details[detailCount] + items[itemCount]
if block.seq != s1: retry
- Header: publish counter (not increasing = publishing has stopped), engine game clock, an epoch that increments by 1 each game, local player number, whether a game is in progress, game speed, publish period, microseconds spent collecting this copy on the game thread, event sequence number, per-stage timings. Clients can write
requestedPeriodMsto request a publish period (16 ~ 1000 ms). - Unit (112 bytes): handle pair (identify units by handle pair — addresses get reused), type four-character code, owner, flags, coordinates, HP / mana (with maximums), current order + order target, task target (what it is actually attacking), hero level / XP / skill points, detail index,
visibleTo(bit p = player p can see it right now). - Details (288 bytes, heroes > player units > creeps, up to 256): 12 abilities (code / level / flags / cooldown seconds remaining), 8 buff codes, 6 inventory slots.
- End-of-block extensions (append-only, earlier offsets never move, so old clients keep working): extension area
EXT1(in-game time of day, day/night speed, production table entry count) and production tableprods[128](buildings currently training / researching / constructing / upgrading, queue, total duration, elapsed time, whether stuck). Only use them if the magic matches.
3. Reading events
head = ring.writeSeq (offset 8)
for seq in (cursor, head]:
e = ring.events[(seq - 1) % 8192]
if e.seq > seq: one was lost (overwritten because you read too slowly)
elif e.seq != seq: not fully written yet, read it next time
else: handle e
The event struct is 64 bytes: seq kind clockMs addr handleLo handleHi typeId owner a b x y value extra.
- Derived by diffing two consecutive publishes (precision = publish period):
unit.appeared / unit.died / unit.removed / unit.damaged / order.changed / hero.levelup / owner.changed / item.appeared / item.removed / game.started; - Engine-level (recorded by the runtime on the game thread as they happen, one for every single hit):
damage(source, damage type, attack type, position, actual HP lost, pre-armor damage),killed(killer); - Derived by tracking the production table:
production.done(four-character code of what finished, category, game seconds taken; also emitted for opponents); - Checked by the runtime on every publish:
spell.cast(the spell’s cooldown started:aspell four-character code,blevel,valuecooldown seconds,x/ycast point),player.left(aplayer number,bnew slot state),selection.changed(the local player’s selection; the full list is in the world block’s extension area),game.ended(left the game); - Screen messages:
message(a= message sequence number; look up the full text in the shared memoryLocal\War3Msgs_<pid>: 128 entries × 256 bytes, covering game hints, chat and system messages;b= message frame number); - UI & input (after
input_enable):ui.click(acanvas item id,b1 left button / 2 right button),ui.hover,hotkey(ahotkey id,bvirtual-key code),mouse.world(x/yground coordinates,value= 1 means it was swallowed); modifier keys are all inextra.
4. Issuing commands
- One client object occupies one lane: hold
Local\War3FastMutex_<pid>, find a free lane (or one whose owning process has died), and write the role, player number and your own pid. If the same process needs two roles, open two lanes; - Fill slots: semantic command flag, opcode,
args[11], deadlinedeadlineMs; - After all slots are written, mark them submitted and increment the lane’s
submitSeqby 1; - Wait for the
Local\War3FastDone_<pid>_<lane>event (or poll), read the receipts, and return the slots.
The runtime executes commands in batches inside the game thread’s event dispatch: each drain has a time budget of 4 ms (real high-resolution timer); anything over budget is left for the next dispatch. Slots past their deadline are never executed — so you never get “old commands running again after unpausing”.
args indices: 0..2 unit (address, handle lo, handle hi), 3 order ID or four-character code, 4..6 target, 7/8 x / y (float bits), 9 extra (player number / slot number / toggle / queue flag), 10 mode (0 no target / 1 point / 2 target).
Opcodes
| Opcode | Name | Description |
|---|---|---|
| 1 | point | Point order for a unit (move / attack-move / patrol / attack ground / point-targeted spell). extra bit0 = queue (insert after the current order) |
| 2 | target | Target order for a unit (right-click attack / harvest / repair / unit-targeted spell / pick up item); the target must be visible |
| 3 | immediate | No-target command (stop / hold position / train / research / upgrade / no-target spell) |
| 4 | build | Worker constructs a building (coordinates aligned to 32) |
| 5 | learn | Hero learns an ability |
| 6 | use_item | Use inventory slot extra |
| 7 | revive | Revive a hero at the altar |
| 8 | rally | Rally point (point / target) |
| 9 | buy | A shop sells an item to a nearby hero |
| 10 | item_drop | Let go of an item: give it to a teammate, sell it to a shop (code = the receiving unit), or drop it on the ground |
| 20 ~ 25 | Queries | q_tech tech count, q_feasible feasibility, q_visible visibility, q_mine_gold gold left in a mine, q_captain computer captain, q_dead_heroes dead hero list |
| 30 | pause | Pause / resume |
| 40 ~ 50 | Camera | Read camera state, set field, look at a point, follow, reset, rotate, bounds, smoothing, UI visibility, clean view, fog |
| 60 ~ 63 | HUD | Quest button text, quest panel title and description, refresh, read whether the panel is open |
| 70 | jass | Call a JASS native by name (1291 of them): the name and string arguments go in the slot’s extra area, the other arguments go into args according to the signature; the return value is in value[0]. Only for local-tool lanes; anything that takes a function argument or would suspend the script thread is rejected. See JASS channel |
| 71 / 72 | jass_handle_of / jass_unit_of | Convert between snapshot units and JASS handles (the handle pairs in the snapshot are not JASS handles) |
| 73 | canvas_enable | Create the canvas shared memory and install the draw hook; any lane can send it (the canvas only draws on the local screen). The first call has to install the hook, so give it a timeout of 2 seconds or more |
| 74 | input_enable | extra = 1 takes over the game window’s input (canvas item clicks / hover, hotkeys, ground clicks), 0 = hands it back. Input block Local\War3Input_<pid>: 128-byte header + 32 hotkeys × 16 bytes; you write the hotkey table and mouse switches, and the runtime writes back the mouse position, the ground point under the cursor and the hovered item. Any lane can send it (it only affects local input). See UI & input |
5. Receipts
A receipt is 52 bytes (+8 bytes of timing): status, engineReturn, verdict (rejection reason code), orderBefore / orderAfter (the unit’s order read back in the same frame), value[8] (query results), execUs (microseconds this command took to execute on the game thread), engineUs (the portion spent in the engine’s own order function).
For all status codes and reason codes, see Receipts and reason codes.
6. Lane roles
| Role | What it can do |
|---|---|
dev | Local tools: semantic commands (commanding the local player’s units) + the JASS channel |
player | Semantic commands only, and only for units owned by the lane’s player (anyone else’s = not_owner) |
observer | Queries, camera, reading HUD panel state, and enabling the canvas and local input only; everything else is forbidden |
Two AIs playing each other = two player lanes in the same game (player 0 / player 1).
In local mode, the role is declared by the client itself (a convention, not a security boundary). On the Arena, the referee process creates the lanes and hands only the player lane to each contestant.
7. Semantics verified in live games
- Right-click (smart) on an enemy = attack this one (both the order target and the task target are that unit); a raw attack order sent as a target command only switches to the attack order without recording the target, so the unit goes off and attacks something else nearby;
- The engine won’t accept target commands on units you can’t see: after nightfall, distant camps fall into the fog of war and every right-click is rejected (1001);
- A build being “accepted” only means the worker took the order: a spot inside a forest is also accepted on the spot, and the worker only fails once it gets there; obviously occupied spots are rejected immediately;
- A hero can only be revived about 3 game seconds after dying; it’s also rejected if you don’t have enough food (heroes cost food);
- Items in an inventory don’t count as ground items; picking one up emits
item.removed; - While paused, the engine clock stops, but commands can still be issued;
- A game started minimized has its simulation stopped (the clock doesn’t move).