f6ac103233
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>
101 lines
4.4 KiB
GDScript
101 lines
4.4 KiB
GDScript
extends MultiplayerSynchronizer
|
|
|
|
## Networked sync component for a pickable item. Added as a child literally
|
|
## named "NetPickable" (network_manager.gd's authority RPCs already expect
|
|
## this) of every net-synced pickable, replicating its transform and held
|
|
## state. Only the current multiplayer authority (the server while loose, or
|
|
## whichever peer is holding it) actually simulates physics for the item;
|
|
## every other peer freezes their local copy and just follows the synced
|
|
## transform.
|
|
|
|
## 0 = loose/server-simulated; otherwise the peer id currently holding it.
|
|
## Replicated at spawn and on change so a late joiner sees the current holder.
|
|
var net_held_by: int = 0: set = _set_net_held_by
|
|
|
|
var _pickable: XRToolsPickable
|
|
|
|
# This item's own baked freeze_mode (e.g. plate.tscn bakes KINEMATIC, not the
|
|
# RigidBody3D default of STATIC) — captured once so it can be restored when
|
|
# this peer regains ownership, instead of getting stuck on whatever
|
|
# apply_held_state() last forced it to while non-authority.
|
|
var _original_freeze_mode: int
|
|
|
|
|
|
func _ready() -> void:
|
|
_pickable = get_parent() as XRToolsPickable
|
|
if not _pickable:
|
|
push_error("NetPickable must be a child of an XRToolsPickable")
|
|
return
|
|
_original_freeze_mode = _pickable.freeze_mode
|
|
_pickable.picked_up.connect(_on_picked_up)
|
|
_pickable.dropped.connect(_on_dropped)
|
|
# Deferred: the pickable root captures its own original_collision_mask/
|
|
# original_collision_layer via @onready, which runs AFTER this child's
|
|
# _ready() but BEFORE the root's _ready() body. Calling apply_held_state
|
|
# synchronously here would freeze/mask the item before that capture runs,
|
|
# permanently corrupting the "restore on drop" values.
|
|
apply_held_state.call_deferred()
|
|
|
|
|
|
func _set_net_held_by(value: int) -> void:
|
|
net_held_by = value
|
|
apply_held_state()
|
|
|
|
|
|
## Puts the item in the right physics state for whether this peer currently
|
|
## owns it. Called locally after net_held_by changes, and directly by
|
|
## NetworkManager._set_item_authority right after an authority handoff.
|
|
func apply_held_state() -> void:
|
|
if not _pickable:
|
|
return
|
|
if not NetworkManager.is_online() or is_multiplayer_authority():
|
|
# We own this item's simulation (offline, loose+server, or currently
|
|
# holding it). If it's not actively in our own hand right now, make
|
|
# sure it isn't still left frozen/collision-less from a previous
|
|
# non-authority period (e.g. right after regaining authority when a
|
|
# client released it) — while actually held, XRToolsPickable's own
|
|
# pick_up()/let_go() already manage these fields, so leave those be.
|
|
if not _pickable.is_picked_up():
|
|
_pickable.freeze_mode = _original_freeze_mode
|
|
_pickable.collision_mask = _pickable.original_collision_mask
|
|
return
|
|
# A net_held_by/position sync update can race ahead of the
|
|
# authority-handoff RPC that's about to confirm a grab we just made
|
|
# optimistically (they travel on different channels with no ordering
|
|
# guarantee). Don't let a stale sync value yank an item out of our own
|
|
# hand mid-grab — only an explicit force_release_item rejection, or
|
|
# actually losing authority for real, should end a grab we initiated.
|
|
if _pickable.is_picked_up() and _pickable.get_picked_up_by() is XRToolsFunctionPickup:
|
|
return
|
|
# Someone else owns it: stop simulating locally, just follow the sync.
|
|
if _pickable.is_picked_up():
|
|
_pickable.drop()
|
|
_pickable.freeze = true
|
|
_pickable.freeze_mode = RigidBody3D.FREEZE_MODE_KINEMATIC
|
|
_pickable.collision_mask = 0
|
|
_pickable.enabled = (net_held_by == 0)
|
|
|
|
|
|
## Local hand grab (not a station snap zone, which is server-only): request
|
|
## authority immediately so the throw/drop can be reconciled, but let the grab
|
|
## happen instantly here rather than waiting on the round trip.
|
|
func _on_picked_up(_p) -> void:
|
|
var by := _pickable.get_picked_up_by()
|
|
if not (by is XRToolsFunctionPickup):
|
|
return
|
|
if NetworkManager.is_online():
|
|
NetworkManager.log_line("Grabbed %s by hand, requesting authority" % _pickable.name)
|
|
NetworkManager.request_item_authority.rpc_id(1, _pickable.get_path())
|
|
|
|
|
|
func _on_dropped(_p) -> void:
|
|
# Only forward if we're actually still the authority — a drop caused by
|
|
# apply_held_state() losing authority (see above) must not re-report.
|
|
if not is_multiplayer_authority():
|
|
return
|
|
if NetworkManager.is_online():
|
|
NetworkManager.log_line("Dropped %s, reporting release to server" % _pickable.name)
|
|
NetworkManager.release_item_authority.rpc_id(
|
|
1, _pickable.get_path(), _pickable.linear_velocity, _pickable.angular_velocity
|
|
)
|