extends Node ## The whole interaction layer: two RPCs, for every object in the game. ## ## Registered as the "NetGrab" autoload so its RPCs resolve to the same node path ## on every peer. ## ## A hand picks something up locally the instant the player grabs it — no round ## trip, no waiting — and tells the server. The server answers by moving the ## object's NetXform to that peer, which is what actually makes the prediction ## work: a MultiplayerSynchronizer never applies inbound state on the peer that ## owns it, so from that moment the holder's own hand drives the object and the ## server's copy follows instead of fighting it. Releasing hands NetXform back, ## and the server decides where the object really ends up. ## ## Note what is NOT here. There is no per-object networking component, no ## replicated "who is holding this" field to keep in step with the authority, and ## no grant/reject/force-release negotiation. Authority is the single source of ## truth for who simulates an object, and NetWorld.apply_physics_role reads it. ## Starts watching a pickable. Called by NetWorld for every replicated object, so ## no scene has to include a networking node to take part. func watch(item: XRToolsPickable) -> void: if item.picked_up.is_connected(_on_picked_up): return item.picked_up.connect(_on_picked_up) item.dropped.connect(_on_dropped) func _on_picked_up(item: Node3D) -> void: if not NetworkManager.is_online(): return # A station snap zone grabbing something is the server placing it, not a # player taking it, so it must not move authority anywhere. if not (item.get_picked_up_by() is XRToolsFunctionPickup): return NetworkManager.log_line("%s grabbed by hand, claiming it" % item.name) if NetworkManager.is_server(): _claim(item.get_path(), 1) else: # rpc_id() to ourselves is rejected by Godot, which is why the server # branch calls straight through instead. _request_grab.rpc_id(1, item.get_path()) func _on_dropped(item: Node3D) -> void: if not NetworkManager.is_online(): return # Only the peer that was actually driving the object reports a release. A drop # caused by losing authority (see NetWorld.apply_physics_role) must not be # echoed back as if the player had let go. var xform := item.get_node_or_null(NetReplication.XFORM_NAME) if not xform or not xform.is_multiplayer_authority(): return # Our own final transform travels with the release. We were driving the object # right up to this moment and our position updates ride the synchronizer's # separate, unordered channel, which this reliable message routinely # overtakes — so the server's copy can still be back where the object was # before we carried it away. Deciding the snap from that stale position # teleports the object straight back into the station it was just taken from. if NetworkManager.is_server(): _settle(item.get_path(), item.global_transform, item.linear_velocity, item.angular_velocity) else: _request_release.rpc_id( 1, item.get_path(), item.global_transform, item.linear_velocity, item.angular_velocity ) # --- server side ----------------------------------------------------------- @rpc("any_peer", "reliable") func _request_grab(item_path: NodePath) -> void: if not NetworkManager.is_server(): return _claim(item_path, multiplayer.get_remote_sender_id()) @rpc("any_peer", "reliable") func _request_release(item_path: NodePath, xform: Transform3D, lin: Vector3, ang: Vector3) -> void: if not NetworkManager.is_server(): return _settle(item_path, xform, lin, ang) func _claim(item_path: NodePath, peer: int) -> void: var item := get_node_or_null(item_path) if not item: return NetworkManager.log_line("grant %s to peer %d" % [item.name, peer]) # Order matters: hand the object over first, so the station's zone has already # stopped owning it by the time it is told to let go. _set_xform_authority.rpc(item_path, peer) NetStations.release_from_zones(self, item) func _settle(item_path: NodePath, xform: Transform3D, lin: Vector3, ang: Vector3) -> void: var item := get_node_or_null(item_path) if not item: return NetworkManager.log_line("release %s" % item.name) if item is Node3D: item.global_transform = xform _set_xform_authority.rpc(item_path, 1) if item is RigidBody3D: item.linear_velocity = lin item.angular_velocity = ang NetStations.try_snap.call_deferred(self, item) ## set_multiplayer_authority is a local call, so every peer has to run it for ## them to agree on who is driving the object. @rpc("authority", "call_local", "reliable") func _set_xform_authority(item_path: NodePath, peer: int) -> void: var item := get_node_or_null(item_path) if not item: return var xform := item.get_node_or_null(NetReplication.XFORM_NAME) if not xform: return xform.set_multiplayer_authority(peer) NetworkManager.apply_physics_role(item) ## Hands everything a departing peer was holding back to the server. Without ## this, anything still in their hand when they dropped out would be frozen ## forever on every remaining peer, waiting on an authority that has gone. func reclaim_from(peer: int) -> void: if not NetworkManager.is_server(): return for item in NetworkManager.replicated_objects(): var xform := item.get_node_or_null(NetReplication.XFORM_NAME) if xform and xform.get_multiplayer_authority() == peer: NetworkManager.log_line("reclaiming %s from departed peer %d" % [item.name, peer]) _settle(item.get_path(), item.global_transform, Vector3.ZERO, Vector3.ZERO)