I can confirm this is resolved on 1.26.51.1.
I don’t have a way to test on 1.26.50.20. Do you find that it is also not reproducible on 1.26.40.8?
Attached are two minimal reproduction packs and a content log.
Pack A — @minecraft/server 2.9.0-beta + @minecraft/server-gametest 1.0.0-beta. Spawns a SimulatedPlayer on /scriptevent repro:spawn.
Pack B — @minecraft/server 2.3.0 (stable). Subscribes to playerSpawn and iterates getAllPlayers() each tick, accessing the same properties/methods that real published addons use.
Reproduce: add both packs to a world with Beta APIs enabled, load in (real-player spawn works fine — all accesses succeed), then run /scriptevent repro:spawn.
Result: Pack B receives event.player === undefined for the SimulatedPlayer and every property/method access throws TypeError — .isValid, .dimension, .location, .getVelocity(), .typeId, .getBlockFromViewDirection(), .id, .getProperty(), .hasTag(), .setDynamicProperty(), .runCommand(), .getDynamicProperty(), .getComponent(), .name, .nameTag, .getGameMode(). The per-tick loop repeats these continuously, matching the error flood seen in real multi-addon crashes. Pack A (beta API) receives the identical SimulatedPlayer as a fully valid object.
This property/method set is taken directly from a real crash log involving published addons (RealismCraft, Eternal End, More Simple Structures, etc.). When multiple installed addons hit this every tick simultaneously, the exception volume escalates to a full world crash as soon as a SimulatedPlayer is spawned.
Root cause: SimulatedPlayer (defined in @minecraft/server-gametest) is not marshaled to addons depending only on stable @minecraft/server; they receive undefined instead of the entity downcast to its base Player type.
I've got a working setup now and figured out what's actually going on. There are two things here, and the second one corrects something I had backwards earlier.
Neither peer advertises a routable candidate.
Setup: BDS 1.26.51.1 on Ubuntu, single interface at 192.168.50.221, IPv4 only, behind home NAT.
transport=nethernet.Signaling is plaintext HTTP, so you can read the SDP with tcpdump. An external client's offer contains exactly one candidate, its own private LAN address:
The server's answer does the same thing with its private address. No server reflexive candidate on either side, so the server sends STUN checks to an address it can't reach and the client does the same. Nothing pairs and you get Door / InitialConnection-83.
I put a small HTTP proxy in front of the signaling port that rewrites only the address on the
c=anda=candidate:lines. The client's private address becomes its public address (taken from the TCP source), and the server's private address becomes the server's public IP. ufrag, ice-pwd and fingerprint are left alone, and ICE integrity still validates because each side computes over the SDP it actually received. Connections work normally after that.There is no single player cap. BDS allocates one UDP socket per session, and each one has to be reachable.
This is the part I had wrong before. With one player connected I saw one socket and assumed
server-udp-portswas being ignored. It isn't. The range pins which ports the per session sockets come from, and BDS hands out the next one for each additional player:Player 1's answer advertises 19150, player 2's advertises 19151, player 3's advertises 19152, and
ssshows a bound socket per connected player inside that range. If only one of those ports is reachable from outside then only one player can connect, which is exactly what the cap looked like.One thing worth flagging: my router silently ignored a
19150-19158range forward. Individual per port rules worked fine. If you're testing this, verify the forward actually applies instead of trusting the range.With the candidate rewriting plus one forwarded port per player slot, I currently have a local PS5, an external PC, and an external PS5 all connected at the same time.
So the underlying bug looks like candidate gathering. NetherNet's HTTP signaling uses full ICE with STUN and TURN discouraged, nothing ever gathers a reflexive candidate, and there's no relay fallback. Two peers both behind NAT have no working path to offer each other. That fits why LAN works, why servers with a public IP directly on the interface work, and why self hosted behind NAT doesn't.