Docs Tools

Gateway

A WebSocket / JSON gateway: the public APIs the Python SDK can call are callable from JS, C#, Go, Rust, browser pages and programs on another machine. Three roles, with a bundled JS client and browser demo page; latency is the fast lane plus about 1 ms.

The gateway wraps the fast lane and the pushed state in WebSocket / JSON. The public APIs the Python SDK can call in the API catalog are callable from JS, C#, Go, Rust, browser pages, programs on another machine and LLMs, with the same method names and parameters. Latency is the fast lane plus about 1 ms.

Easiest: Farsight’s home page, Control Center → Gateway → Start (stop, restart, view logs and open the demo page from the same card). From the command line:

python gateway/server.py                 # ws://127.0.0.1:8870/ws (the port is ports.gateway in openwar3.json)
python gateway/server.py --open          # same as above, and opens the demo page http://127.0.0.1:8870/demo once the port is listening
python gateway/server.py --host 0.0.0.0  # for LAN use: a token is required automatically (bin/gateway/token.txt)
python gateway/server.py --allow-origin http://localhost:5173   # lets your own web page connect too

Connections and roles

Connection URL: ws://127.0.0.1:8870/ws?inst=9&role=dev (you can use pid= instead of inst=; add &token= when a token is required).

RoleCan callBest for
devEverything: observe, command, game control, sandbox (changing the world with JASS), drawing UILocal tools, gameplay mods, companions
player (add &player=N)Observe, command player N’s units, drawing UI; fair mode by default, so it sees only what’s within player N’s vision (&fair=0 turns it off)A bot or LLM playing for one player
observerRead-only (the runtime rejects its commands outright)Spectating, commentary, data collection

player doesn’t get: game control such as ending the game, changing the speed or pausing; players and enemy_ai_plan, which reveal other players’ hands; canvas.image, which makes the game process open a local file; or JASS. Queries that take a player number, such as resources, tech and stats, can only query its own player.

One connection is one session and takes one fast lane (the runtime has 16 in total). The gateway allows at most 12 sessions at once, leaving a few for bots, mods and Farsight. On disconnect, only what this session drew and its own hotkeys are removed; what other programs drew is left alone.

Messages

After connecting, you first receive hello: the protocol version, the role, the game’s process ID, and the list of methods this role can call. After that, every request carries an id, and the response carries the same id:

→ {"id": 1, "op": "call", "method": "units", "args": ["me"]}
← {"id": 1, "ok": true, "result": [{"addr": 123456, "type": "hpea", "owner": 0, "x": -4480, "y": 2120, "hp": 220, "hp_max": 220}]}

→ {"id": 2, "op": "call", "method": "move", "args": [[{"unit": 123456}], 100, 200]}
→ {"id": 3, "op": "call", "method": "ui.button", "args": ["shop", "Buy potion"], "kwargs": {"screen": [40, 300]}}
→ {"id": 4, "op": "subscribe", "state": {"hz": 4, "units": "mine"}, "events": true}
← {"type": "state", ...}    {"type": "events", ...}        pushed continuously from then on
→ {"id": 5, "op": "overview"}                             one-page overview: resources, unit counts, heroes, visible enemies, production
→ {"id": 6, "op": "jass", "code": "call PingMinimapEx(0, 0, 3, 255, 0, 0, false)"}    dev only
→ {"id": 7, "op": "api"}                                  method catalog (there are also ping / unsubscribe)
  • Unit arguments are written as {"unit": addr}, where the address is the addr in the unit’s JSON; you can add "handle": [lo, hi] to check that the address hasn’t been reused by another unit.
  • Method names are the public methods of Game, plus ui.* (button / choice / toast / hotkey / mouse / cursor…), canvas.* (text / panel / bar / image / circle / path / remove…) and jass.<function name> (dev only).
  • A remote client can’t pass callback functions: clicks and hotkeys arrive through the event push, and the ui.click event carries key. See UI & input.
  • A failed call gets an error response for that call only (ok: false plus error); the connection stays open. The same goes for a message that isn’t JSON.
  • Event JSON fields match the W3P protocol, plus convenience fields (spell, key, text, chat, button, player, mods).

HTTP works too, which suits one-off calls and curl: GET /api?role=player lists the method catalog, and POST /call with inst, role, method, args and kwargs makes a single call. /call reuses sessions: when the game is restarted in a new process it switches to a new session automatically, and sessions idle for 10 minutes are closed.

Clients

JS (browser or Node 22+, zero dependencies): gateway/clients/js/openwar3.mjs

import { OpenWar3, unit } from "./openwar3.mjs";

const ow = new OpenWar3("ws://127.0.0.1:8870/ws", { inst: 9, role: "dev" });
await ow.connect();
const mine = await ow.api.units("me");
await ow.api.move(mine.slice(0, 3).map(unit), 100, 200);
await ow.api.ui.button("hi", "Click me", { screen: [40, 300] });  // a trailing plain object = keyword arguments
ow.on("event:ui.click", (e) => console.log("clicked", e.key));
await ow.subscribe({ state: { hz: 2, units: "mine" }, events: true });

Node 20 / 21 needs --experimental-websocket. The full example is in gateway/clients/js/example.mjs.

Browser demo page http://127.0.0.1:8870/demo: the game overview, a table of our units, placing a button in the game, and the event stream, all on one page.

Other languages: any WebSocket library + the JSON above is all you need; no need to touch shared memory.

LLMs: use the MCP server directly; it turns the common tasks into ready-made tools.

Measured

2026-09-25, checked item by item against a real game, 16/16 (9 for the gateway + 7 for MCP): handshake (121 methods for the dev role), units('me'), the one-page overview, a screen toast, placing a button; after subscribing, clicking that button in the game → ui.click pushed to the client; JASS; passing a bad unit reports an error for that call only; HTTP /call (observer role).

The JS client (Node) and the browser demo page were run too: a button placed from the web page was clicked in the game, and the page’s event log received ui.click.

Security

  • By default it listens only on 127.0.0.1 and needs no token (same as Farsight). When --host isn’t a local address, a token is required automatically; --auth requires one locally too.
  • Other websites in a browser can’t connect: every connection a browser opens carries its origin (Origin), and the gateway only accepts its own demo page and the URLs given with --allow-origin. Programs such as Python, Node and curl send no origin and connect as usual. When listening only locally, it also checks Host, blocking attacks that resolve an outside domain name to the local machine.
  • The role is declared by the client when it connects: in local mode it’s a convention, not a security boundary. For the Arena, the referee process has to decide who gets which role; see Arena.