Pairs with the debug-log-heavy investigation workflow: Godot writes its
own engine log to logs/godot.log alongside the existing NetworkManager
temp-file log, and the directory is excluded from version control since
it's runtime output, not source.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The snap-zone crash fixed just before this was invisible in the logs
until traced through addon source by hand — the log showed grabs,
drops, and authority handoffs, but nothing about which peer/hand/zone
actually held an item at each step, or when a guard clause silently
changed behavior.
Adds a _holder_desc() helper (reports "loose", "hand(<path>)",
"zone(<station>)", or "other(...)") and logs every net_held_by
transition, every reclaim that has to restore freeze_mode/collision_mask
from a stuck state, every time the grab-race guard protects an actively-
held item from a stale sync, every force-drop triggered by a losing
authority race, and every pickup/drop event including the ones that
don't get forwarded (picked up by something other than a hand, or
dropped while not authority — exactly the silent case behind the
snap-zone crash). NetworkManager's snap/release-from-zone calls get the
same treatment: which station an item is pulled from, and whether a
snap attempt succeeded, was skipped by the race guard, or found nothing
in range.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
request_item_authority and release_item_authority were always sent as
an RPC to peer 1, even when the caller already WAS peer 1 (the host's
own player). Godot rejects rpc_id() targeting your own peer id ("RPC on
yourself is not allowed by selected mode"), so every host-side grab or
release was silently failing to run its server-side half: no denial
check on grab, and — the more damaging part — no
_try_snap_into_station()/_release_from_snap_zones() call on release,
leaving a station's snap zone holding a stale reference once the host
picked something back up. That's what made the host "no longer able to
interact" with an item after a client had handled it.
Split each entry point into a public function that runs the logic
directly when we're already the server, and a private @rpc handler used
only when an actual remote client calls it. force_release_item gets the
same treatment for the symmetric case (server rejecting its own grab
attempt).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Four bugs from real cross-network play:
- Player/net_player.gd: the per-peer spawn-spread offset (added to avoid
stacking joiners on top of each other) could push a joining client up
to 12 units from center, but the floor is only 15x15 (~7.5 unit
half-extent) — landing a client off the edge. Bounded to ~2.7 units.
- Player/net_player.gd: the glove scenes used for remote-hand visuals
carry hand.gd (XRToolsHand), whose root node sets top_level = true and
repositions itself to its parent's transform every physics frame,
expecting to be parented under a live XRController3D. Parented under
the plain avatar node instead, it fought both the local transform copy
and the replicated sync every tick — whichever wrote last that frame
won, which read as hands stuck near origin except momentarily. Disabled
physics processing on those nodes; nothing else in the script matters
without a real controller ancestor.
- Net/net_pickable.gd: grabbing an item sends an RPC to the server to
confirm authority, but the item's regular state sync travels on a
different channel with no ordering guarantee against that RPC. A
stale "still loose" sync packet could arrive after an optimistic local
grab but before the confirmation, and was being treated as a real
authority loss, force-dropping the item — explaining "first attempt
fails, second succeeds". Now a sync update can't yank an item out of
our own hand mid-grab; only an explicit rejection or an actual loss of
authority can.
- Net/net_pickable.gd: the non-authority path zeroes collision_mask and
forces freeze_mode to KINEMATIC, but nothing ever restored either when
a peer regained authority (e.g. the server after a client's release) —
so a released item stayed collision-less forever, looking physics-less.
Now both are restored from the item's own baked values whenever a peer
owns a loose (not actively held) item again.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The per-peer spread offset used the raw ENet peer id directly
((peer_id - 1) * 1.5), assuming ids are small sequential numbers. Real
peer ids are large effectively-random 32-bit values, so a joining
client's XR rig was being shifted by hundreds of millions of units off
the origin — the world had actually replicated correctly, the player
was just teleported far away from all of it, with float precision bad
enough at that range to jitter the view and destabilize physics.
Bound the offset to a small deterministic slot instead; the host still
keeps its original baked spot.
Co-Authored-By: Claude Sonnet 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>