@playerthree/sidecar

The PlayerThree host for game servers that are not written in JavaScript: Unity, Unreal, Godot, C++, C#, Go. Your dedicated server keeps the simulation and the clock. The sidecar is a separate local process that runs a normal PlayerThreeHost and serves MCP, OAuth, guards, scopes, mandates, presence, policies and sessions. The two talk newline-delimited JSON over stdio or a local socket, never over a network interface (SPEC §20.8 (a)).

Engine SDKs that speak this protocol live in sdks/.

Run it

From a checkout (pre-release, not on npm yet; Node ≥ 24):

pnpm install
# Your server spawns the sidecar and talks on the child's stdin/stdout:
node packages/sidecar/bin/playerthree-sidecar.ts --stdio --port 8787
# …or the sidecar listens on a Unix socket (created 0600) and your server connects to it:
node packages/sidecar/bin/playerthree-sidecar.ts --socket /run/mygame/p3.sock

Options: --port (MCP port, default 8787, 0 = any; the URL is sent in ready), --listen (default 127.0.0.1), --call-timeout (ms per adapter call, default 5000), --hello-timeout (default 10000), --data-dir (persist commanders and the commander log), --devtools (operator console at /playerthree/devtools). Logs go to stderr. The process exits when the engine disconnects: 0 after an orderly close, 1 after a protocol error, timeout or failed handshake.

The protocol in 30 seconds

One JSON object per line, at most 1048576 UTF-8 bytes before the "\n". Unknown members are ignored.

engine → {"op":"hello","protocol":1,"manifest":{…plain JSON manifest…},"hooks":[]}
host   → {"op":"ready","surfaceHash":"…","mcp":"http://127.0.0.1:8787/mcp"}
engine → {"op":"host","id":1,"method":"createMatch","args":[{"seed":1,"controllers":{"north":{"puller":"agent"}}}]}
host   → {"op":"call","id":1,"session":"s1","method":"create","args":[{"seed":1,"teams":["north","south"]}]}
engine → {"op":"result","id":1,"ok":true,"data":{"tick":0}}
host   → {"op":"result","id":1,"ok":true,"data":{"session":"s1","tick":0}}
engine → {"op":"host","id":2,"method":"mintSeatToken","args":["s1","north","puller"]}   → data {"token":"…"}
engine → {"op":"advance","id":3,"session":"s1","n":1}
host   → {"op":"call","id":7,"session":"s1","method":"step","args":[]}               (and think, apply, observe…)
engine → {"op":"result","id":7,"ok":true,"data":{"tick":1}}
host   → {"op":"result","id":3,"ok":true,"data":{"tick":1}}
host   → {"op":"push","session":"s1","kind":"ended","data":{"outcomes":{…},"ranks":{…}}}
  • Host → engine calls are the adapter (§10) and the game's built-in bots, one at a time and synchronous: the host blocks until the engine answers. The engine must answer every call, including while its own request is outstanding. Methods: create, step, outcome, view, observe, apply, report, events, setSeatInfo; the hooks the engine declared in hello.hooks (matchData, debugView, observerView, end, forfeit, rank, clone, signalLimits); bots createBot, init, think; and dispose when the host lets a session go. Seats are {"team","role"}. Results are {"ok":true,"data"} or {"ok":false,"code"} (unknown_method, unknown_session, invalid, engine_error); every output is schema-guarded by the host as in-process.
  • Engine → host requests, one at a time, each answered by a result: advance (run up to n host ticks, data {tick}) and host methods createMatch, mintSeatToken, seats, cards, readiness, setController, setMandate, end, forfeit, dispose.
  • Pushes: seats (after any seat status change) and ended.
  • Fatal {"op":"error","code"} then close: too_large, bad_frame, timeout (a call missed its deadline), invalid_manifest, protocol. When the connection closes the host disposes every session (seat tokens revoked, MCP seats dropped); the engine should move agent and policy seats to its own bots.

The full message table is in the spec.

Conformance

conformance/ holds the Tug of War sample (the README game) as an engine-neutral fixture:

  • tug.manifest.json: the manifest every engine sample sends verbatim;
  • tug.transcript.jsonl: the recorded, normalized exchange of a passing run;
  • mock-engine.mjs: the engine side in plain Node (no PlayerThree imports);
  • run.ts: the runner. It is the real sidecar plus checks: it holds each engine request so the order of engine ticks and agent calls is fixed, seats an MCP agent on north that observes and pulls on the first three ticks, plays on until the south bot wins, sends error-case calls (unknown method and session) and a call of exactly 1048576 bytes, finally sends a 1048577-byte line (the engine must close), and compares the transcript.
# The engine spawns the runner in place of the sidecar (stdio):
node packages/sidecar/conformance/mock-engine.mjs -- node packages/sidecar/conformance/run.ts --stdio
# The runner spawns the engine, which connects to $PLAYERTHREE_SIDECAR_SOCKET:
node packages/sidecar/conformance/run.ts --socket -- node packages/sidecar/conformance/mock-engine.mjs

The sample flow an engine runs: hello with tug.manifest.json and no hooks; createMatch {"seed":1,"controllers":{"north":{"puller":"agent"},"south":{"puller":"bot"}},"botLevel":{"south":"easy"}}; mintSeatToken for north/puller (hand the token to its owner in an owner-only file, never in a log); advance one tick every 100 ms until the ended push; dispose; close. The Tug adapter and its bot (always pull 1) are the README's.

--record <file> writes the normalized transcript instead of comparing; --timeout <s> (default 60).

Supported, and not yet

Supported: stdio and Unix-domain sockets (Linux, macOS), plain-JSON manifests validated like in-process ones, every core adapter call plus the optional hooks above, built-in bots, engine-owned clock, the seat and lifecycle host operations above, pushes, size limit, per-call timeout, disconnect cleanup, MCP and DevTools.

Not yet: Windows named pipes are accepted (--socket \\.\pipe\<name>), but this build cannot restrict a pipe to the server's OS user and is untested; use stdio on Windows. The lobby operations reserve, release and capabilities (§20.4, §20.6) and game metrics recorders are not part of protocol 1. Practice, policies and replays run on host-created engine sessions, so the engine must support several sessions at once to offer them.

License

Apache-2.0, see LICENSE.