Rebuild the multiplayer layer on stock MultiplayerSpawner/Synchronizer so
adding an object to the game needs no networking code.
MultiplayerSpawner only replicates node creation and deletion, so "sync
everything regardless of what it is" has to come from a SceneReplicationConfig
built by convention in code. NetReplication does that, giving every node two
generated synchronizers: NetSync for script state (always server-owned) and
NetXform for position (handed to whoever is holding the object).
test/spike/ establishes the four engine behaviours the design rests on. Two
are worth flagging: per-peer visibility CANNOT be used to stop the server
fighting a client's held object, because MultiplayerSpawner despawns and
respawns the node on every visibility flip; and set_visibility_for is only an
override on top of public_visibility, so calling it alone does nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds test/multiPlayerTest.tscn plus a driver that runs the kitchen flow
across two game instances: grab/drop, dirt station, sink washing, hob
cooking, counter combining and plating. Runs headless (run_mp_test.ps1),
in two visible windows (run_mp_test_windowed.ps1), or by hand with
keyboard controls (play_mp_test.ps1). 108 checks, exits non-zero on
failure.
After every step both peers snapshot every item's position and rendered
state and the server diffs them. Targeted assertions only look at the
thing a step touched, which misses desyncs elsewhere - that audit is
what caught the last bug below.
Bugs found and fixed:
- net_pickable: apply_held_state() only wrote `enabled` in its
non-authority branch, so once a client grabbed an item every other
peer set enabled=false and regaining authority never restored it. The
server could then never pick that item up again, and a station would
"snap" it (emitting has_picked_up, so a plate still got marked dirty)
while pick_up() bailed out on the disabled item - leaving the zone
holding an item with no grab driver.
- network_manager: station gating only happened in the spawn path, so
stations baked into a scene file kept running their snap zones on
clients and grabbed items straight out of the local hand. Added
gate_existing_stations().
- network_manager: despawn_item() only freed the server's copy. Items
baked into a scene aren't tracked by the MultiplayerSpawner, so
consuming one left a ghost on every client, which then blocked the
station it sat in and got grabbed instead of its replacement.
- network_manager: the snap-into-station decision read the server's own
copy of the item position, but the reliable release RPC routinely
overtakes the synchronizer's unordered position updates - so it acted
on a stale position and teleported items back into the station they
had just been carried away from. The releasing peer now sends its
final transform and the server adopts it first.
- container: contained_ids.append()/erase() mutate the array in place,
which never fires the setter that rebuilds the plate's visuals. The
peer that put food on a plate was the only peer that never redrew it;
remote peers looked right because the synchronizer assigns there.
Also null-guards XRServer.get_tracker() in the vendored xr-tools hand
grab point, which threw on every successful grab without an XR runtime,
and adds multiplayer_world.populate_from_layout so debug scenes can bake
their own content instead of spawning the whole kitchen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>