#networking
39 items (4 guides · 35 fixes)
When the world is deterministically generated from a spec, a save is {version, spec, edits[]}, never a geometry snapshot. Load = validate → regenerate → replay edits. Same format works for save, co-op edit sync, and late-join replay.
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.
The operational recipe for shipping player-hosted (P2P) multiplayer in s&box (lobby mechanics, invite codes, the join handshake liveness contract, replication traps, and the three-rung testing ladder), covering the layer the official docs don't document and where live multi-peer sessions actually break.
The complete method for wiring s&box's built-in Voice component into a networked game (push-to-talk, 3D positional playback, custom falloff curves, speaking indicators, and lip-sync), with no third-party voice SDK.
A component with [Sync] fields added in OnStart (after NetworkSpawn) never replicates: each peer creates its own local instance and the sync pair silently never matches. The law covers engine components too: a Rigidbody built post-spawn is exactly this bug (Velocity is [Sync]-backed).
A second editor instance launched with -joinlocal is not a real Steam lobby member, yet Networking.GetData still resolves the host's Networking.SetData values -- they arrive over the ServerDataMsg the host sends on connect, not via lobby membership. The message can lag scene load by a few frames, so read GetData in a short poll loop.
A -joinlocal second instance connects to whichever process holds the loopback port 127.0.0.1:55333, so the precondition to check is socket ownership, not which editor is in play mode.
A -joinlocal second instance never runs the in-game Join UI: it connects straight to the editor's loopback dev-host socket, so it presents an empty invite code. Any host that verifies the code on the wire will correctly reject it, breaking the local two-peer test even though every other hop is healthy. It's a harness artifact, not a product bug.
A joining client can build a code-generated world twice -- once on local StartupScene load, once on the networked scene handoff. A non-idempotent world author that clears its tracking lists but never destroys prior GameObjects stacks a whole second world. Make the author idempotent, and compute any sync hash from the build recipe, not a live scene scan.
Networking.CreateLobbyAsync branches on Application.IsDedicatedServer straight into the dedicated-server path and never opens a lobby, so lobby-creation code written for peer-hosted play does nothing useful on a dedicated server.
Running a second s&box process under the same Steam account (an editor or dev instance alongside the published client) correlates with P2P joins to a live remote host failing rendezvous inside the engine's fixed ~3.5s connect budget. Close every second same-account instance before drawing any conclusion from a P2P connectivity failure.
Deriving a visual's facing from horizontal velocity causes 180-degree snaps on any momentum reversal. Lock facing to an attach-time azimuth for pendulums and oscillators.
The stock citizen animgraph ships combat params that CitizenAnimationHelper does NOT wrap -- drive them with renderer.Set() directly, and replicate them yourself because the engine only networks locomotion.
Networking.HostStats and Connection.Stats read zero on a local loopback two-instance session, including Ping and ConnectionQuality, so a bandwidth instrument built on those fields latches zero even while traffic flows. Count bytes at the application layer instead.
Networking.CreateLobby is async: Networking.IsActive is still false on the same frame, so any branch on IsActive takes the wrong path. Gate on your own synchronous mode enum instead.
A headless sbox-server running an unpublished local .sbproj boots and creates a lobby, but every joiner fails with 'Package local.<ident> wasn't found!': publish the package first (Hidden visibility is sufficient).
Dev-host streaming sends assemblies and compiled assets but NOT loose data files read via FileSystem.Mounted: a joining client that loads a raw JSON manifest builds differently, silently.
A [Sync(SyncFlags.FromHost)] field on a runtime-created singleton never replicates: the object needs NetworkSpawn, not just NetworkMode.Snapshot.
When a lobby query drives a connect, validate the exact lobby candidate you will connect to (not results[0]), or a peer can connect to a lobby it never validated (build-skew, wrong-host connect that no gate catches).
Deferred Destroy() leaves the old code-built world overlapping the fresh one for a frame: use DestroyImmediate for teardown before rebuild, and pair it with a recipe hash for belt-and-suspenders join verification.
A static Instance claimed behind if (!IsProxy) in OnStart grabs the host's character on a joining client: the client camera follows the wrong player forever.
A joining client's static join state (invite code, mode, attempt ID) gets wiped by the networked scene handoff: the bootstrap's OnEnabled resets statics before the join handshake uses them.
sbox.exe -joinlocal +instanceid 1 gives you a real second peer against an editor host with no publish, no second Steam account, and no lobby discovery. It has no host selector: it joins whichever local editor is in PLAY MODE, so no other editor on the machine may be playing.
A batch of networking API facts, verified from the installed build's source and XML, that overturn common assumptions when you plan multiplayer code before it touches the compiler. LobbyInformation is a struct, so a null guard does not compile. Networking.Connections is deprecated in favour of Connection.All and emits CS0618. Connection.MaxChunkSize is internal, so game code hard-codes the 131072 value instead of referencing the symbol. NetworkAccessor exposes Owner, OwnerTransfer, and OrphanedMode as get-only, with AssignOwnership, SetOwnerTransfer, and SetOrphanedMode called after NetworkSpawn. Networking.TryConnectSteamId exists and is public. SB1000 whitelisting is assembly-level, so the real constraint on a networking call is public versus internal accessibility, not the whitelist. A green dotnet build does not mean an editor-green result for networked types.
A cached Component/GameObject reference guarded with == null still throws NullReferenceException after the object is destroyed: only IsValid() catches destroyed objects.
A singleton claimed in OnEnabled and nulled in OnDisabled traps any re-adopt poll that gates on Instance.IsValid(): the entity is never re-driven after a disable.
Any cross-peer consumer (UI, host validators, scorers, range checks) that reads an owner-only simulation field gets frozen state on network proxies. Anchor off the replicated transform or redirect the field's getter.
[Sync] owner→proxies, IsProxy early-out; FromHost for shared truth. Every object carrying a [Sync] field has to be NetworkSpawned, singletons included.
A published-build client join reloads the game assembly, wiping all statics. The reconstruct-not-reset fix from the scene-handoff case has nothing to reconstruct from unless join intent is persisted to disk.
The Sandbox.Voice component defaults PushToTalkInput to 'voice', but not every project ships a matching 'Voice' InputAction. PTT is a dead key with zero errors until you add it to Input.config.
Networking.QueryLobbies appends a hidden:0 filter unless you pass a truthy hidden key, so a Hidden lobby is structurally excluded from every ordinary query. The bool overload param is includeServers, not hidden-inclusion. And editor hosts force Private privacy, which no filter overrides.
Quit-to-menu tears the game scene down inside Networking.DisconnectScope, so networking is still active during game-side teardown. A local quit is distinguishable from a host disband, and teardown code can still send graceful goodbyes.
An [Rpc.Broadcast] called on a component whose GameObject is not networked executes locally and silently never crosses the wire -- no warning at any log level.
App 1892930 via SteamCMD runs a headless server with +game pointing at a local .sbproj: clients stream code/assets, no sbox.game publish required.
Sandbox.Json.Serialize always emits pretty-printed JSON and exposes no public option to disable it: every network payload pays a whitespace tax. Re-drive the same serializer through a compact writer for a 41–56% size cut.
On the host, spawning a joiner's character clobbers the host's own camera-target singleton because IsProxy is false at Components.Create time: re-resolve the claim at the first OnFixedUpdate.
A Steam lobby survives its host switching to a different s&box game with stale metadata: a joiner connects and loads the wrong game's content.
The joiner's engine-side connect budget after entering a Steam lobby is a fixed ~3 seconds that game code cannot extend: any host-side delay exhausts it fast, so join recovery must be a fresh retry, never a longer wait.
The C# layer of s&box never closes per-pair Steam P2P sessions, so a host crash or task-kill poisons the pairwise transport state between two SteamIDs until Steam-side expiry clears it, typically minutes.