Uncomment net_pickable
This commit is contained in:
@@ -71,6 +71,189 @@ func _go_offline() -> void:
|
||||
unregister_world()
|
||||
|
||||
|
||||
|
||||
# --- Item grab-authority transfer -----------------------------------------
|
||||
|
||||
## Called by NetPickable when this peer grabs an item by hand. Godot rejects
|
||||
## rpc_id() targeting your own peer id ("RPC on yourself is not allowed"), so
|
||||
## when we ARE the server this runs the logic directly instead of round-
|
||||
## tripping an RPC to ourselves — otherwise every host-side grab/drop was
|
||||
## silently failing to run its server-side half (no denial checks, and
|
||||
## crucially no auto-snap-into-station on release).
|
||||
func request_item_authority_from(item_path: NodePath) -> void:
|
||||
if is_server():
|
||||
_grant_or_reject_item_authority(item_path, multiplayer.get_unique_id())
|
||||
else:
|
||||
_request_item_authority_rpc.rpc_id(1, item_path)
|
||||
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func _request_item_authority_rpc(item_path: NodePath) -> void:
|
||||
if not is_server():
|
||||
return
|
||||
_grant_or_reject_item_authority(item_path, multiplayer.get_remote_sender_id())
|
||||
|
||||
|
||||
## Runs on the server (called directly if the requester IS the server, or via
|
||||
## the RPC above otherwise). If the item was snapped into a station, the
|
||||
## station releases it so the grabber cleanly takes ownership.
|
||||
func _grant_or_reject_item_authority(item_path: NodePath, sender: int) -> void:
|
||||
var item := get_node_or_null(item_path)
|
||||
if item:
|
||||
var np := item.get_node_or_null("NetPickable")
|
||||
if np and np.net_held_by != 0 and np.net_held_by != sender:
|
||||
# Already legitimately held by a different live peer: reject the
|
||||
# requester's optimistic client-side grab instead of stealing it.
|
||||
print("NetworkManager request_item_authority: DENIED %s to peer %d (already held by %d)" % [item.name, sender, np.net_held_by])
|
||||
_force_release_item_to(sender, item_path)
|
||||
return
|
||||
print("NetworkManager request_item_authority: granting %s to peer %d" % [str(item.name) if item else str(item_path), sender])
|
||||
# Assign authority + held state first (disables the item on the server so its
|
||||
# snap zone won't re-grab it), then release it from any station.
|
||||
_set_item_authority.rpc(item_path, sender)
|
||||
if item:
|
||||
_release_from_snap_zones(item)
|
||||
|
||||
|
||||
## Called by NetPickable when this peer releases an item, forwarding its throw
|
||||
## velocity so the server can resume simulating it. Same self-RPC issue as
|
||||
## above: runs directly if we're the server.
|
||||
func release_item_authority_from(item_path: NodePath, lin: Vector3, ang: Vector3, xform: Transform3D) -> void:
|
||||
if is_server():
|
||||
_do_release_item_authority(item_path, lin, ang, xform, multiplayer.get_unique_id())
|
||||
else:
|
||||
_release_item_authority_rpc.rpc_id(1, item_path, lin, ang, xform)
|
||||
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func _release_item_authority_rpc(item_path: NodePath, lin: Vector3, ang: Vector3, xform: Transform3D) -> void:
|
||||
if not is_server():
|
||||
return
|
||||
_do_release_item_authority(item_path, lin, ang, xform, multiplayer.get_remote_sender_id())
|
||||
|
||||
|
||||
## Runs on the server. If released next to a station, the server snaps it in
|
||||
## (server-authoritative placement).
|
||||
func _do_release_item_authority(item_path: NodePath, lin: Vector3, ang: Vector3, xform: Transform3D, sender: int) -> void:
|
||||
print("NetworkManager release_item_authority: %s released by peer %d" % [str(item_path), sender])
|
||||
_set_item_authority.rpc(item_path, 1)
|
||||
var item := get_node_or_null(item_path)
|
||||
if item is RigidBody3D:
|
||||
# Adopt the releasing peer's own final transform rather than trusting our
|
||||
# copy's. That peer was the item's authority right up to this moment, and
|
||||
# its position updates travel on the synchronizer's separate, unordered
|
||||
# channel — this reliable RPC routinely overtakes them, leaving our copy
|
||||
# still sitting where the item was BEFORE the peer carried it away. The
|
||||
# snap decision below then reads that stale position and teleports the
|
||||
# item straight back into the station it was just picked up from.
|
||||
item.global_transform = xform
|
||||
item.freeze = false
|
||||
item.linear_velocity = lin
|
||||
item.angular_velocity = ang
|
||||
_try_snap_into_station.call_deferred(item)
|
||||
|
||||
|
||||
# All station snap zones in the world (every XRToolsSnapZone child of a node in
|
||||
# the "station" group — some stations, e.g. Table, have more than one).
|
||||
func _station_snap_zones() -> Array:
|
||||
var zones := []
|
||||
for station in get_tree().get_nodes_in_group("station"):
|
||||
for child in station.get_children():
|
||||
if child is XRToolsSnapZone:
|
||||
zones.append(child)
|
||||
return zones
|
||||
|
||||
|
||||
# If the item is snapped into any station, drop it from that station.
|
||||
func _release_from_snap_zones(item: Node) -> void:
|
||||
for zone in _station_snap_zones():
|
||||
if zone.picked_up_object == item:
|
||||
print("NetworkManager releasing %s from %s's snap zone (authority just granted elsewhere)" % [item.name, zone.get_parent().name])
|
||||
zone.drop_object()
|
||||
# Make the zone forget the item as well. These zones are snap_mode=RANGE,
|
||||
# so every frame they re-grab anything still listed in their grab area
|
||||
# that can be picked up — and Jolt does not emit body_exited when let_go()
|
||||
# switches the item's collision layer back out of the zone's mask, so the
|
||||
# entry goes stale and never clears. The station then snatches the item
|
||||
# straight back off the player who just took it, teleporting it home.
|
||||
# Bringing it near again re-adds it properly (a held item is on the layer
|
||||
# the zone watches), and releasing next to a station is handled
|
||||
# explicitly by _try_snap_into_station.
|
||||
if zone._object_in_grab_area.has(item):
|
||||
zone._object_in_grab_area.erase(item)
|
||||
|
||||
|
||||
# Snap the item into the nearest empty station snap zone within grab range.
|
||||
#
|
||||
# Called deferred from _do_release_item_authority: XRToolsFunctionPickup's own
|
||||
# "grab an item out of a snap zone" path calls zone.drop_object() BEFORE it
|
||||
# calls pick_up() on the hand's behalf. drop_object()'s let_go() synchronously
|
||||
# fires the pickable's `dropped` signal, which (via NetPickable) lands here —
|
||||
# if this ran synchronously it would immediately re-snap the item into the
|
||||
# very same zone it's still physically inside, stealing it away before the
|
||||
# hand's own pick_up() call (later in the same call stack) ever runs. That
|
||||
# leaves XRToolsFunctionPickup.picked_up_object pointing at an item whose
|
||||
# _grab_driver actually belongs to the zone — a stale reference that crashes
|
||||
# (null _grab_driver) the next time a controller button is pressed. Deferring
|
||||
# lets the hand's pick_up() go first; the is_picked_up() check below is a
|
||||
# second guard in case the item gets grabbed for real before this runs.
|
||||
func _try_snap_into_station(item: Node) -> void:
|
||||
if not (item is Node3D):
|
||||
return
|
||||
if item.has_method("is_picked_up") and item.is_picked_up():
|
||||
var by: Node = null
|
||||
if item.has_method("get_picked_up_by"):
|
||||
by = item.get_picked_up_by()
|
||||
print("NetworkManager skipped snapping %s: already held by %s (grab-race guard)" % [item.name, by.get_path() if by else "?"])
|
||||
return
|
||||
for zone in _station_snap_zones():
|
||||
if is_instance_valid(zone.picked_up_object):
|
||||
continue
|
||||
if zone.global_position.distance_to(item.global_position) <= zone.grab_distance:
|
||||
print("NetworkManager snapped %s into %s" % [item.name, zone.get_parent().name])
|
||||
zone.pick_up_object(item)
|
||||
return
|
||||
print("NetworkManager no station in range to snap %s into (or none empty)" % item.name)
|
||||
|
||||
|
||||
# Server broadcasts an authority assignment so every peer agrees on who owns the
|
||||
# item (set_multiplayer_authority is a local call and must run everywhere).
|
||||
@rpc("authority", "call_local", "reliable")
|
||||
func _set_item_authority(item_path: NodePath, peer: int) -> void:
|
||||
var item := get_node_or_null(item_path)
|
||||
if not item:
|
||||
return
|
||||
print("NetworkManager _set_item_authority: %s -> peer %d" % [item.name, peer])
|
||||
item.set_multiplayer_authority(peer) # recursive: item + synchronizer + NetPickable
|
||||
var np := item.get_node_or_null("NetPickable")
|
||||
if np:
|
||||
np.net_held_by = 0 if peer == 1 else peer
|
||||
np.apply_held_state()
|
||||
|
||||
|
||||
## Rejects peer's optimistic grab (the item was already legitimately held by
|
||||
## someone else). Same self-RPC concern: if the rejected peer is the server
|
||||
## itself, apply it directly rather than rpc_id-ing ourselves.
|
||||
func _force_release_item_to(peer: int, item_path: NodePath) -> void:
|
||||
if peer == 1:
|
||||
_do_force_release(item_path)
|
||||
else:
|
||||
force_release_item.rpc_id(peer, item_path)
|
||||
|
||||
|
||||
@rpc("authority", "reliable")
|
||||
func force_release_item(item_path: NodePath) -> void:
|
||||
_do_force_release(item_path)
|
||||
|
||||
|
||||
func _do_force_release(item_path: NodePath) -> void:
|
||||
print("NetworkManager force_release_item: dropping %s (server rejected our grab)" % str(item_path))
|
||||
var item := get_node_or_null(item_path)
|
||||
if item and item.has_method("drop"):
|
||||
item.drop()
|
||||
|
||||
|
||||
|
||||
func is_server() -> bool:
|
||||
return is_online() and multiplayer.is_server()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user