extends Node class_name NetStations ## Server-side placement rules for stations. Generic over the "station" group — ## there is nothing per-station here, and nothing any individual station scene ## has to opt into. ## ## Placement is a server decision for the same reason cooking is: every peer runs ## its own copy of a snap zone, so letting each decide independently means the ## same object gets grabbed in two places at once. ## Every snap zone belonging to a station. Some stations (Table) have more than ## one, so this is not a one-per-station lookup. static func zones(context: Node) -> Array: var found := [] for station in context.get_tree().get_nodes_in_group("station"): for child in station.get_children(): if child is XRToolsSnapZone: found.append(child) return found ## Frees an object from whatever station is currently holding it, because ## someone has just taken it by hand. static func release_from_zones(context: Node, item: Node) -> void: for zone in zones(context): if zone.picked_up_object == item: zone.drop_object() # Make the zone forget it as well. These zones are snap_mode=RANGE, so # every frame they re-grab anything still listed in their grab area — and # Jolt does NOT emit body_exited when let_go() switches the object's # collision layer back out of the zone's mask, so the entry never clears # on its own. The station then snatches the object straight back off the # player who just took it and teleports it home. Bringing it near again # re-adds it properly, and releasing next to a station is handled # explicitly by try_snap below. if zone._object_in_grab_area.has(item): zone._object_in_grab_area.erase(item) ## Snaps a just-released object into the nearest empty station zone in range. ## ## Call this DEFERRED. XRToolsFunctionPickup's own "grab an object out of a snap ## zone" path calls zone.drop_object() before it calls pick_up() on the hand's ## behalf, and drop_object() synchronously fires the `dropped` signal that lands ## here. Running inline would immediately re-snap the object into the very zone ## it is still physically inside, stealing it before the hand's own pick_up() ## later in the same call stack ever runs — leaving the hand pointing at an ## object whose grab driver belongs to the zone, which crashes on the next ## controller press. The is_picked_up() check below is the second guard, for the ## case where something grabs it for real before this runs. static func try_snap(context: Node, item: Node) -> void: if not (item is Node3D) or not is_instance_valid(item): return if item.has_method("is_picked_up") and item.is_picked_up(): return for zone in zones(context): if is_instance_valid(zone.picked_up_object): continue if zone.global_position.distance_to(item.global_position) <= zone.grab_distance: NetworkManager.log_line("snapped %s into %s" % [item.name, zone.get_parent().name]) zone.pick_up_object(item) return