Multiplayer

Usage guide of the net.* kits: choosing the sync kit (state, claim, authoritative, rollback, async), turning it on, the settings and their defaults, server-side validation hooks, modding limits, events, the gfnet-server program and its flags, what each engine and libgfkits provide, errors.

Source: module-kits:kits/net/usage/en.mdView Markdown source

Also read offline — CLI: grip-forge docs netMCP tool: gripforge_docs { "topic": "net" }

Multiplayer for your game (net.* kits, gfnet-server) #

Pick how your game stays in sync across players, switch it on from the kits, tune it with a handful of settings, and run the server. Readable offline: gripforge docs net, or the MCP tool gripforge_docs (topic net).

What it is #

Every net.* kit runs the same Rust network core (gfnet-core): WebAssembly in the browser and in the Node server, a native library in Godot, Unity, Unreal and in the native server. One implementation everywhere, so a browser, a Godot build and a Unity build can play the same match. A game has one transport kit and one sync kit:

KitRole
net.transportthe session layer all sync kits run on (protocol gfnet/1): WebSocket, WebRTC (web), direct IP (native), handshake, heartbeats, resume after a drop, reconnection, per-peer rate limits
net.stateserver-validated actions on a shared, versioned state; no simulation tick
net.claimeach client simulates the objects it has claimed; the server only arbitrates and relays
net.authoritativethe server simulates; clients predict and reconcile; lag-compensated hits
net.rollbackpeers exchange only inputs and run the same deterministic simulation; rollback or lockstep
net.asyncnobody online at the same time: record a run, replay it as a ghost, boards, daily challenge, replay verification
net.physicsone rigid-body world that is bit-identical on every peer and on the server (used when a sync kit's physics is shared)
net.lobbya private waiting room: nickname, roster, ready flags, host migration, server-validated start (web)
net.roomsan always-on rooms service: presence, challenges, picks and ready flags, one match server per match

Choose the sync kit #

Your gameUseWhy
Cards, board, word, trivia, strategy, party games; anything with hidden information; inventories and lobbiesnet.stateevery move is checked by rules on the server; each seat only receives what it may see
Co-op, sandbox, physics games where responsiveness beats cheat-proofingnet.claimyour own avatar, the crate you push, the car you drive react with no latency; not for competitive play
Competitive shooters, anything with an economy or a rankingnet.authoritativethe client can only send inputs; costs server CPU and needs a dedicated server
Fighting games, arena brawlers, small-roster actionnet.rollbacktiny bandwidth, no input lag; needs a deterministic simulation; not for large player counts or late joiners
Time trials, ghosts, daily challenges, boardsnet.asyncplays against recorded runs; combine with a live kit for real-time matches

Install one sync kit per project (the other net.* kits come in as needed).

The model of a web game: what the room reads #

For a game that runs in the browser on the kits, the network model is not a Godot choice: it is the sync kit you install. The web room service reads it from the project itself and starts gfnet-server with that model, so nothing is copied and nothing can drift:

Installed sync kitRoom config.net.modelconfig.net.syncconfig.net.physics
net.claimclaimthe kit's settings that differ from the defaults (publishHz, leaseMs…)net.physics settings when that kit is installed
net.authoritativeauthoritativetickHz, snapshotHz, interpolationMs…same
net.rollbackrollbackmode, players, inputDelay…same
net.statestatepreset, actions, seats…none

A project with no sync kit has no room (not_networked). A room created by hand for a game that is not a project (the net field of POST /api/v1/network/rooms, { model, sync, physics }) takes the same shape, validated by the core. For a car or kart game that mostly steers its own vehicle, claim is the cheap, responsive choice; authoritative when hits or an economy must be cheat-proof; rollback for a small deterministic duel. The MCP tool gripforge_net_kit only covers the Godot listen-server snippets (GfNet); for a web game use gripforge_gamekit_install of the sync kit and read gripforge docs web-multiplayer.

Turn it on #

gripforge_gamekit_install   { project_id: "gkp_…", id: "net.claim" }       # brings net.transport with it
gripforge_gamekit_configure { project_id: "gkp_…", id: "net.transport",
                              config: { "app": "my-game@1", "url": "wss://play.example.com/gfnet" } }
gripforge_gamekit_configure { project_id: "gkp_…", id: "net.claim", config: { "publishHz": 30 } }

You write only the keys that differ from the defaults. gripforge kit api net.transport (or net.claim…) lists every setting, with its type, bounds and default, and says which ones can change on a running session.

Settings that matter #

net.transport (all keys: gripforge kit api net.transport):

KeyDefaultMeaning
transportautoauto, websocket, webrtc, tcp_udp, enet. auto is WebSocket (WebRTC when a room is set) on the web, TCP+UDP then the WebSocket bridge on native (enet is treated as tcp_udp)
appgripforge-game@1application id and build: a client of another build is refused. Bump it whenever two builds can no longer play together
url""WebSocket address of the server; empty = same host as the page, path /gfnet
host, port, udpPort127.0.0.1, 27015, 0native clients: server address; udpPort 0 = announced by the server
roleclientweb only: host runs the server in the page (listen server, WebRTC)
signalUrl, room, iceServers"", "", a public STUN serverWebRTC; a production game needs its own TURN server
maxPeers16server capacity (a lower value refuses new peers, never kicks)
heartbeatMs, timeoutMs, resumeMs1000, 5000, 10000liveness; a dropped peer is suspended for resumeMs and resumes with the same id
rate.messagesPerSecond, rate.burst120, 240per-peer limits; over budget counts as a violation, and 20 violations kick the peer
reconnect.*on, 500 to 8000 ms, 10 triesclient backoff (0 tries = unlimited)

Per sync kit, the settings you reach for first:

KitKeyDefaultMeaning
net.claimpublishHz, snapshotHz20, 20rate of the owner's states and of the server's snapshots
leaseMs, stickyMs750, 250an owner silent for leaseMs loses the object; a fresh owner cannot be stolen from for stickyMs
stealPolicyprioritynever, priority (strictly higher priority wins), always
onOwnerLeaverevertobjects created by a leaving peer: revert keeps them, owned by the server; destroy removes them
antiCheat, maxSpeed, worldLimiton, 300, 2000plausibility checks on the states an owner publishes
physicsnonenone, native (the engine's physics), shared (net.physics)
net.authoritativetickHz, snapshotHz30, 20server tick and snapshot rates
cmdRedundancy3each packet repeats the last commands
interpolationMs100remote entities are shown this far in the past
maxUnlagMs1000how far back the server rewinds others to judge a shot
relevanceRadius0metres of interest per player; 0 = everyone
predictautonone (everything interpolated), movement (the local player, reconciled on each snapshot), bodies (also the bodies around it; needs physics: native)
net.rollbackmoderollbackrollback (predict and re-simulate) or lockstep (wait for every input)
players, frameHz2, 60seats and simulation rate
inputDelay, maxRollback2, 8frames of delay, and how far back a re-simulation may go
hashEvery60frames between state hashes, to detect a desync
gamearenathe deterministic simulation; physics none or shared only
net.statepresetnoneboard-game, card-game, party-lobby, rooms, simultaneous-duel
actions, initial, visibilityyour rules, starting state and what each seat may see
turns.timeoutMs, seats.max0, 8turn timer and number of seats
maxStateBytes, actionsPerSecond262144, 10limits
net.asyncrecordbothinputs, samples or both
frameHz, sampleHz, maxFrames60, 10, 36000recording rates and length
board, order"", descthe board and how it sorts
rules.*verification: maxSpeed, maxScore, requireReplay, bounds…
net.physicsgravity, hz, iterations[0,-9.81,0], 0, 4the shared world

net.state rules are data: argument schemas, conditions on the state, the seat, the turn and the phase, and effects (set, inc, push, remove, move, draw, deal, shuffle, random). The set is complete enough that most card and board games need no server code.

Server-side code: validation hooks #

Two places let your own code decide on the server:

  • net.state actions declared with "hook": true are checked against their schema, turn, phase and conditions, then handed to the server host, which accepts them (with an optional state patch) or rejects them with a reason. No answer within hookTimeoutMs (5000) rejects the action. Engines expose it as GfNetState.validate (Godot), Validate / Validator (Unity) and OnValidate (Unreal).
  • net.rooms hooks: the rooms actions your host validates before the rules run (challenge, accept, decline, cancel, pick, ready, leave_match, watch, unwatch, join_server), refused with your reason. Used with your own login (auth: true). See gripforge docs rooms-server.

Modding #

All net.* kits are services of game.engine: they declare no engine hook and no setting is open to mods. Transports, addresses, ports, capacities, limits and rates belong to the project and must be identical on every machine of a match. A mod that sets one is refused (config_not_moddable). With mods applied, the application id peers send is app followed by ~mods.<fingerprint>, so a client whose mods differ from the server's is refused with net.refused { code: "mods" }. Godot, Unity and Unreal have no mods: their handshake is unchanged.

Events and variables #

net.transport emits net.connected { id, resumed, transport }, net.disconnected { reason, final }, net.peer { id, on }, net.refused { code, msg } and net.overflow; variables net.status (idle, connecting, connected, reconnecting, closed), net.rtt and net.peers. The sync kits add their own (net.owner and net.denied for claim, lobby.changed for the lobby, rooms.changed, rooms.challenged, rooms.match_ready, rooms.match_ended for rooms…).

The server: gfnet-server #

gfnet-server is the dedicated server of the net.* kits, a native program. One process is one match or one waiting room. It runs the same core as the clients and only opens sockets, pumps the core and prints the lines a supervisor reads. Build it from the repository (cargo build --release -p gfnet-server in native/gfnet).

gfnet-server --model claim --app my-game@1 --port 27015
gfnet-server --model state --app my-game@1 --config match.json --exit-on-end
FlagMeaning
--modelrequired: claim, authoritative, rollback, state, async or relay
--app <id>application id and build; clients of another build are refused
--config <file.json>{ transport, sync, physics, server: { pumpMs }, lobby, match }; validated by the core: a bad key or value exits with code 2 and names its path
--check-configvalidate and exit (0 = valid), printing GF_NET_CONFIG_OK {...}
--port, --udp-port, --hostlistening ports and address (env GF_GAME_PORT)
--max-peers, --pump-ms, --stats <seconds>capacity (env GF_MAX_PLAYERS), pump interval (5 ms), periodic stats
--lobby [json]the net.lobby rules for the state model: ready, start and return are validated in the server
--exit-on-endwhen the match ends, print GF_NET_MATCH_END {reason, seat, result} and stop (exit 0)
--stdiothe host owns the sockets (a front that terminates wss and checks tickets): one JSON object per line on stdin and stdout; no port is opened

It prints GF_NET_READY {...} once it listens, closes cleanly on SIGINT / SIGTERM, and exits 70 if the core panics. One port serves WebSocket and native TCP+UDP clients.

Not served by the native server: WebRTC, TLS (terminate it at a proxy), and the rooms launcher. The Node server gf-net-server (package @gripforgeai/net, same flags) covers WebRTC signalling and the rooms service (startRoomsService); the rooms server starter (gripforge create rooms-server) is built on it. Godot, Unity and Unreal projects can also run headless as dedicated servers.

Engines #

EngineClient transportServer
WebWebSocket, or WebRTC with a room and signalUrl; the page can host a listen server (role: host)gfnet-server, gf-net-server, or the page
GodotWebSocket, TCP+UDP; wasm core in web exportsthe same project headless, or gfnet-server
UnityTCP+UDP, WebSocket on desktopthe same project in batch mode, or gfnet-server
UnrealTCP+UDP, WebSocketthe same project as a server, or gfnet-server
Native player and libgfkitsthe core's own sockets: TCP+UDP, then the WebSocket bridge when fallback is onrole: host listens on the TCP / WebSocket port of the transport config

libgfkits provides the net.core service (and net.http for kits that talk to a web service) to the games it runs, with sockets of the machine. What a page alone can open is refused by name, never silently replaced: WebRTC ("webrtc needs a host link") and wss:// addresses. For those, use the WebSocket bridge of a server or run the client in a page. The local player of a room you host joins its own server through the listening port, as a TCP+UDP client of 127.0.0.1.

From a web page without the kits #

@gripforgeai/net is the package for a plain page or a three.js / React Three Fiber scene: typed sessions with the same options and event names as the engine wrappers.

import { connect } from '@gripforgeai/net';
const net = await connect({ model: 'claim', url: 'wss://play.example.com/gfnet', app: 'my-game@1' });
net.on('peer', ({ id, on }) => console.log(id, on ? 'joined' : 'left'));

Options are validated before the core is created: an unknown key, a wrong type or a value out of bounds throws a NetConfigError whose path names it. Commands never throw when the core refuses one: create returns 0, claim, input and the others return false, and command() returns { ok: false, error }.

When it does not work #

SymptomCause and fix
net.refused { code: "app" }the client and the server have a different app; bump and deploy both together
net.refused { code: "mods" }the server runs other mods; match the mods, or set the dedicated server's transport.app to a client's app()
net.refused { code: "full" }, rate, timeoutretried with backoff; raise maxPeers or the limits if legitimate
version, app, model, started, invalid, kicked, replacedfatal: no reconnection
gfnet-server exits 2the config is invalid; the message names the path (config.transport.heartbeatMs); check with --check-config
WebRTC refused on a native buildnot served natively; use WebSocket or TCP+UDP
Rollback desync reportedthe simulation is not deterministic (unordered iteration, floats, unseeded random); use physics: shared for physics