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>
The world was populated before NetworkManager.world_ready() actually
called host()/join(), so owns_world() read true for every peer
(including a joining client) and each one built its own local,
unreplicated copy instead of the client receiving the server's spawn
through the MultiplayerSpawner. Population now happens after the
session is actually established.
Also adds net-log coverage for spawn/despawn, station gating, item
authority handoff, station snapping, avatar spawn/despawn, and scene
transitions, so multiplayer behavior is visible in
%TEMP%\vryhungry_net_<pid>.log instead of failing silently.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>