s&box/field-guide
method — how to build it, in what order

Networking methods — spec replication + host-authority patterns

lanes: Writing gameplayposted

Two proven architectures on top of the engine networking primitives: replicate the generator spec (not the geometry) for deterministic worlds, and retrofit local-only gameplay systems to host-authority without breaking single-player.

Two proven architectures on top of the engine primitives. Official docs cover [Sync], SyncFlags.FromHost, [Rpc.Host|Owner|Broadcast], NetworkSpawn, and Networking.* well — this guide does not re-explain them. What they don't cover is:

  • Part A — deterministic multiplayer: replicate the spec, not the geometry.
  • Part B — host-authority retrofit: making a dozen local-only gameplay systems host-authoritative without breaking single-player.

Baseline owner-simulated pattern is in owner-simulated-networking.


Part A — replicate the spec, not the geometry

The idea

If world generation is fully deterministic (same spec → byte-identical world), the host never streams terrain. It replicates the tiny generator input — a few dozen numbers — and every client regenerates the identical world locally. Bandwidth for the world is constant and tiny regardless of world size.

Prerequisites (they ARE the networking contract, not hygiene):

  • No System.Random in the gen path; hash noise only; pure functions of (seed, x, y).
  • One unit conversion at emission; no platform/float-order-sensitive topology decisions.
  • Spec fields are APPEND-ONLY — old and new peers must deserialize each other's specs (unknown fields skip, missing fields default).
  • A process-independent content hash over the generated data — this turns "deterministic" from a hope into a checkable gate.

The decisions

D1 — replicate the spec as its canonical JSON STRING, not a networked record: [Sync(SyncFlags.FromHost)] public string NetSpecJson on a scene-owned session component. A string is the most boring thing [Sync] handles; the JSON round-trip is already battle-tested by save/load; append-only field law makes it version-tolerant. Serialize with the engine Json.Serialize (invariant culture).

D2 — client regen = observed property change; host regen = direct call. Clients watch NetSpecJson and on change: validate → regenerate → report hash. The host NEVER round-trips through its own sync property (self-echo loop; single-player would depend on net plumbing) — it regenerates directly and publishes only after a successful local regen.

D3 — the hash handshake (desync becomes a loud, named failure). After a client's regen completes it sends ONE [Rpc.Host] ReportWorldHash(...). Host compares to its own: match → client marked world-ready; mismatch → log DESYNC with the first differing sub-hash and kick the client to menu — never let a desynced client walk a wrong world. No retry logic: a desync is a bug to fix, not a condition to paper over.

D4/D5 — an explicit mode state machine + per-mode UI lockdown. Authoring → Hosting / Joining → Joined as ONE enum; every UI element declares which modes it exists in. In a frozen-world MVP the HOST loses editing too — a host edit would silently desync every client's world. Enforce at the UI and in the mutation entry points.

D6/D7 — characters ride the standard owner-simulated pattern: host OnActive(Connection) → clone prefab at a validated spawn → NetworkSpawn(connection); each player simulates their own character; cameras strictly local. No server-authoritative movement for a friends-co-op MVP — that's a deliberate trust-model decision.

Scope cuts that make it small

  • Frozen-world MVP: the world locks at launch — no live edit replication.
  • Late join is cheap by construction: spec + regen + handshake (and, post-MVP, replay of the edit log — the delta-log save format is deliberately the same format the wire protocol needs).

Invite codes / lobby discovery

Verified on recent engine builds:

  • Networking.CreateLobby with Hidden=true, Privacy=Public = "anyone with the code, nobody without" (Private fights the code path; FriendsOnly blocks friends-of-friends).
  • Networking.QueryLobbies(Dictionary filters)LobbyInformation rows; stamp a protocol-version key in lobby data and refuse mismatches client-side BEFORE connecting.
  • Caveat: a host's self-query for its own hidden lobby often returns nothing — Steam commonly excludes lobbies the caller is already in, so self-query CANNOT verify the code path; only a real second peer can.

Verification ladder

  1. Determinism suite on one machine: same spec regenerated N× byte-identical (see /guides/agent-test-harness).
  2. -joinlocal two-peer on one machine — no publish, no second Steam account. Exchange the world hash at join and refuse on mismatch — dev-host streaming does NOT send loose data files, so a code-built world can silently diverge.
  3. A real second peer/account (the only test that settles Steam-backend behaviors).

Part B — host-authority patterns: retrofitting local systems for co-op

The situation this solves: a game built single-player-first has a dozen mutable systems (gates, pickups, NPC AI, progress, day/night, scoreboard, win latch) that every client would simulate independently — double-consumes, contradictory NPC targets, multiple winners.

Load-bearing engine facts

  1. GameObject.NetworkMode defaults to Snapshot — objects created identically on every peer by a deterministic bootstrap are networked as part of the scene snapshot: [Sync] / [Sync(SyncFlags.FromHost)] fields on them converge with no NetworkSpawn. NetworkSpawn/NetworkMode.Object is only for per-owner objects (player bodies, transient projectiles).
  2. The engine does NOT stop a non-host writing a FromHost field locally — the write just gets steamrolled by the next snapshot. The manual guard is therefore load-bearing:
    snippet
    if ( Networking.IsActive && !Networking.IsHost ) return;   // host-authoritative singletons
    if ( IsProxy ) return;                                     // per-owner objects
  3. Instance [Rpc.Host] methods on snapshot-built objects route to the host's matching instance. Rpc.Caller identifies the requester; using (Rpc.FilterInclude(conn)) SomeBroadcast(...) narrows a broadcast to exactly one client — the confirm/deny-to-caller primitive.
  4. INetworkListener.OnConnected/OnDisconnected fire host-only (for remote peers); a joiner learns its own connection completed by polling Networking.IsActive && !Networking.IsConnecting.

The per-system recipe

For each mutable system, apply mechanically:

  1. Authoritative fields → [Sync(SyncFlags.FromHost)]; every mutation host-gated (fact 2).
  2. Client intent → [Rpc.Host] RequestX(...). The host validates against its own state — never against client-sent positions.
  3. First-request-wins via an idempotent latch (_opened, _consumed, HolderId != Empty): the second simultaneous request finds the latch set and no-ops. This one pattern kills double-consumption, simultaneous grab, and multiple winners.
  4. Confirm/deny to the caller via Rpc.FilterInclude(Rpc.Caller).
  5. Presentation = a pure function of the synced fields, run per-client with a primed edge-detect, so late-joiners reconstruct state without replaying one-shot side effects.
  6. Single-player short-circuit: Networking.IsActive == false takes the direct pre-networking code path — byte-identical behavior, no RPC hop.
  7. Disconnect cleanup: host-side liveness poll of recorded Connections each tick.

NPC AI — decompose, don't rewrite

The biggest anti-double-apply surface is locally-simulated NPC AI. The structural fix is small and staged: (a) gate every NPC's OnFixedUpdate to the host — this alone kills divergent decisions; (b) NetworkSpawn the NPCs + transform sync + a FromHost presentation-state enum; (c) reactions fire host-side; (d) any vision/targeting that reads a local singleton must scan ALL players. Do not rewrite the mega-classes — add the host gate and sync at their public seams.

Player locomotion stays owner-authoritative (deliberately)

Movement, climb, swing, charge/jump remain owner-predicted with [Sync]'d outputs — prediction is accepted for feel; the host is authoritative over the WORLD the player acts on, not the player's own body.

Predict-then-confirm rollback without a rollback API

A denied prediction needs NO callback plumbing if the sim already reconciles against the owning object's state every tick: the deny handler just clears the object's local flag; the player sim's existing "item gone" path unwinds the prediction next tick. Look for the existing per-tick reconcile before inventing a rollback seam.

Late-join checklist

Walk every host-authoritative bit and ask: does a mid-run joiner reconstruct it? Open gates, moved NPCs, consumed pickups, progress meters, clock value, run/win state. Most fall out of FromHost syncs + the pure-presentation rule; anything driven by a one-shot event needs an explicit snapshot. Test by joining a session where everything has already happened.

Riskiest interactions — name them in every test plan

Simultaneous grab · double consumption · multiple winners · double daze/stat-bump · per-client divergent AI targets · late-join divergence. Each maps to exactly one recipe step above.