the symptom, in your words

"A code-built world can run TWICE on a network join and stack a second world on the first"

✓ verified on 26.07.15a
lane Writing gameplayposted

▸ SYMPTOM

Your world is built in code (procedural placement of props, climb fields, ropes, etc.). On a network join you notice the joining client has roughly twice as much of some things as the host. Concretely, in one verified session: host reported rows=74 climbs=8 ropes=4 throwables=10; the joining client reported rows=96 climbs=16 ropes=8 throwables=20 — exactly 2× the climb fields, ropes, and throwables, while boxes=14 and kit=38 stayed single.

A world-hash guard that compares peers then flags a mismatch, and depending on your collision setup the doubled geometry can be visible or subtly wrong.

▸ CAUSE

A network-joining client can run the scene's code-built world twice: once when the local StartupScene loads, and again on the networked-session scene handoff. If the world author is not idempotent — it clears its static tracking lists at the top of each build but never destroys the GameObjects a prior build created — the second build stacks a whole second world on top of the first.

The tell for which mechanism doubled a given count:

  • Counts sourced from a live scene scan (Scene.GetAllComponents<T>()) double, because there really are two sets of objects in the scene.
  • Counts sourced from a per-build static list (cleared at the top of each build) stay single, because the list only reflects the most recent build.

That split is the proof it was a second build, not network replication: a non-networked component (one with no [Sync]/NetworkSpawn, which physically cannot replicate) still doubled. Replication cannot explain a doubled non-replicated component; a second OnStart-driven build can.

▸ FIX

Two independent fixes, do both:

  1. Make the code-built world author idempotent. Before rebuilding, destroy the prior world root (or skip the redundant build entirely on a joining client). A second OnStart on join is real — design for it:

    snippet
    protected override void OnStart()
    {
        WorldRoot?.Destroy();          // tear down any prior build
        WorldRoot = new GameObject("World");
        BuildWorld(WorldRoot);
    }
  2. Compute any determinism / MP-sync world hash from the build RECIPE, not a live scan. Hash the static placement lists (the inputs to the build), not Scene.GetAllComponents. A recipe hash cannot be perturbed by a second build or a replicated proxy, so a join-time world-hash guard compares each peer's own deterministic build apples-to-apples.

▸ WHY IT WORKS

The doubling comes from state that accumulates in the scene surviving across two builds. Destroying the prior root removes that accumulation, so a second build produces one world, not two. And hashing the recipe rather than the live scene decouples the integrity check from scene population entirely — the number of GameObjects currently alive (one build, two builds, or a set of replicated proxies) no longer changes the hash, so the guard measures the thing you actually care about: whether two peers agree on what to build.

Verified on engine 26.07.15a: seen in a real project.
s&box moves fast; an undated fix is a liability. Spot a stale detail?
changelog
  • Published

Want to know when new guides or fixes drop? Join the community to help build this out. Report gotchas, flag outdated fixes, or just lurk.

Join the Discord