the symptom, in your words

"A -joinlocal peer joins whoever owns 127.0.0.1:55333, not whoever is in play mode"

✓ verified on 26.08.05
lane Writing gameplayposted

▸ SYMPTOM

You launch a -joinlocal second instance to run a two-peer test. You made sure no other s&box editor on the machine was in play mode, so you expect the peer to join your session. It joins a different editor's session instead, and you only notice because the peer's log carries another project's tags, not yours.

A sibling editor can sit in play mode for over an hour and never take your peer, while a second editor that is not visibly "hosting" grabs it. Play-mode state does not predict where the peer lands.

▸ CAUSE

-joinlocal runs connect local at bootstrap. That is a TCP connect to the loopback port 127.0.0.1:55333. Only one process on a machine can hold a listening port, so the peer joins whichever process holds 55333, and nothing else.

The process that holds 55333 is the editor whose game code called Networking.CreateLobby. A sibling editor in play mode that never called CreateLobby does not hold the port. So "no other editor in play mode" is a proxy for the real question and it can be wrong in both directions: a merely-playing sibling is harmless, and a lobby-hosting sibling is a hard conflict even if you did not think of it as hosting.

▸ FIX

Check socket ownership, not play mode:

  1. Bring up your own host leg first. Before your code calls CreateLobby, the port owner is nobody, which proves nothing and reads like a pass.
  2. Resolve the owner of 127.0.0.1:55333 after your host leg is live. On Windows: Get-NetTCPConnection -State Listen -LocalPort 55333 returns one row whose OwningProcess is the owner's pid.
  3. Read that pid's command line. Every editor runs as sbox-dev.exe, so the image name tells you nothing. The command line names the project root, and that is what you compare.
  4. Require the owner to be your worktree. If a different worktree owns the socket, refuse the run.

Keep a play-mode census as information, not as the gate. It is the list you want when the socket check fails and you need to find the offender. The socket-owner check is the refusal.

▸ WHY IT WORKS

The join target is decided by one fact: who holds the listening port. A TCP connect to 55333 reaches exactly one process, so reading that port's owner is a decisive test rather than a heuristic. The play-mode census answers a related but different question, which is why it lost runs to a sibling editor twice before the socket check replaced it. This narrows the older, stricter precondition ("no other editor may be in play mode at all") without weakening it: a sibling that owns the socket is still a hard refusal, and the narrowing only stops a merely-playing sibling from blocking a run that was never at risk.

Verified on engine 26.08.05: 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