diff --git a/Containers/container.gd b/Containers/container.gd index b2e8326..85dad62 100644 --- a/Containers/container.gd +++ b/Containers/container.gd @@ -11,11 +11,16 @@ extends Node3D @onready var _meal_container: Node3D = $MealContainer @onready var _side_container: Node3D = $SidesContainer - var contained_items: Array[FoodItem] # Expose nice list of others to read +# Item roots the server has promised a slot to but whose absorb RPC hasn't run +# yet. body_entered fires again for a body already inside the area whenever its +# collision_mask is rewritten — which apply_held_state() does the moment item +# authority moves back to the server, i.e. exactly when a client drops food on a +# plate. Both calls would otherwise see the same free slot and absorb twice. +# Keyed on the item root / pickable. +var _pending_absorb: Array[Node3D] = [] -# Called when the node enters the scene tree for the first time. func _ready() -> void: area_3d.body_entered.connect(_on_body_entered) if not area_3d: @@ -29,15 +34,14 @@ func _ready() -> void: func _on_body_entered(body: Node3D) -> void: if not NetworkManager.owns_world(): return - SweetLogger.debug("Enabled: {0}", [enabled]) if not enabled: SweetLogger.debug("Disabled in _on_body_entered body") return if not body.is_in_group(target_group): + SweetLogger.debug("{0} is not in group {1}", [body.name, target_group]) return SweetLogger.debug("Body: {0}", [body]) - # If one of us is in a station var picked_by = xr_pickable.get_picked_up_by() var body_pickable = body as XRToolsPickable @@ -45,179 +49,175 @@ func _on_body_entered(body: Node3D) -> void: if (picked_by and picked_by.is_in_group("station_zone")) or (body_picked_by and body_picked_by.is_in_group("station_zone")): # If enough space, add item - var food_item = body.get_node("FoodItem") + var food_item = Helper.find_food_item(body) as FoodItem SweetLogger.debug("In station found {0} of type {1}: {2}", [target_group, FoodItem.Type.keys()[food_item.type], body.name], "container.gd", "_on_body_entered") - var meal_count := contained_items.filter(func(f): return f.type == FoodItem.Type.MEAL).size() - var side_count := contained_items.filter(func(f): return f.type == FoodItem.Type.SIDE).size() - if food_item.type == FoodItem.Type.MEAL and meal_count < meal_positions.size(): - SweetLogger.debug("Adding meal") - _add_item(body) - if food_item.type == FoodItem.Type.SIDE and side_count < side_positions.size(): - SweetLogger.debug("Adding side") - _add_item(body) + SweetLogger.debug("{0} pending={1} contained={2}", [get_path(), _pending_absorb.size(), contained_items.size()]) + if body in _pending_absorb or _is_contained(body): + SweetLogger.debug("{0} is already in {1}", [body.name, get_parent().name]) + return + if food_item.type == FoodItem.Type.MEAL and _free_slots(FoodItem.Type.MEAL) > 0: + SweetLogger.info("{0}, Adding meal", [name]) + _add_item(body, food_item) + if food_item.type == FoodItem.Type.SIDE and _free_slots(FoodItem.Type.SIDE) > 0: + SweetLogger.info("{0}, Adding side", [name]) + _add_item(body, food_item) +# Already sitting in one of this container's slots. +func _is_contained(item: Node3D) -> bool: + return item.get_parent() == _meal_container or item.get_parent() == _side_container -# Contents are synced as data (ids on the plate's PlateController), not -# reparented nodes: reparenting a MultiplayerSpawner-tracked item out of -# WorldContent would despawn it on every client the instant it happened. -func _add_item(item: Node3D) -> void: - var pickable = item as XRToolsPickable - if pickable and pickable.is_picked_up(): - pickable.drop() - var food_node := item.get_node_or_null("FoodItem") as FoodItem - if not food_node: - return +# Room left for [param type], counting slots already promised to items whose +# absorb RPC hasn't landed yet. +func _free_slots(type: FoodItem.Type) -> int: + var is_meal := type == FoodItem.Type.MEAL + var used := (_meal_container if is_meal else _side_container).get_child_count() + for item in _pending_absorb: + var food_item := Helper.find_food_item(item) + if food_item and food_item.type == type: + used += 1 + return (meal_positions if is_meal else side_positions).size() - used - # Copy the data out before despawning the real item. - var data := FoodItem.new() - data.id = food_node.id - data.type = food_node.type - data.sell_value = food_node.sell_value - contained_items.append(data) - var plate_controller := xr_pickable.get_node_or_null("PlateController") as PlateController - if plate_controller: - # Reassign rather than append in place. contained_ids has a setter that - # rebuilds the plate's visuals, and mutating the array never triggers it — - # so the peer that actually added the food was the one peer that never - # redrew the plate. Remote peers looked right (the synchronizer assigns - # the value there, which does fire the setter), and the stale peer only - # caught up if someone else took the plate and sent the value back. - # duplicate() keeps the Array[String] typing that the property requires. - var updated := plate_controller.contained_ids.duplicate() - updated.append(food_node.id) - plate_controller.contained_ids = updated +func _add_item(item: Node3D, food_item: FoodItem) -> void: + SweetLogger.debug("->[]") + # Claim the slot now: the RPC below only runs once the current frame's + # signal handlers have all had their turn. + _pending_absorb.append(item) + everyone_absorb_item.rpc(item.get_path()) - NetworkManager.despawn_item(item) # In case the container is on a table that needs to register this addition, # ask all table in scene to absorb any new items. SweetLogger.debug("Group call absorb_items()") - get_tree().call_group("table", "absorb_items") + get_tree().call_group("table", "absorb_items") +## Runs on every peer: the item stops being an independent networked object and +## becomes a child of this container, carried by the container's transform. +@rpc("authority", "call_local", "reliable") +func everyone_absorb_item(item_path: NodePath) -> void: + var item := Helper.get_node_from_path(self, item_path) as Node3D + if not item: + SweetLogger.error("Container cant find item") + return + var food_item := Helper.find_first_child_of_type(item, FoodItem) as FoodItem + if not food_item: + SweetLogger.error("Container cant find food item") + return + # The reservation is now being honoured (or is about to be refused); either way it + # stops holding a slot from here on. + _pending_absorb.erase(item) + if _is_contained(item): + SweetLogger.warning("{0} is already in {1}", [item.name, get_parent().name]) + return + + # Pick the slot + var target: Node3D + var slot: Node3D + if food_item.type == FoodItem.Type.MEAL and _meal_container.get_child_count() < meal_positions.size(): + target = _meal_container + slot = meal_positions[_meal_container.get_child_count()] + elif food_item.type == FoodItem.Type.SIDE and _side_container.get_child_count() < side_positions.size(): + target = _side_container + slot = side_positions[_side_container.get_child_count()] + else: + SweetLogger.error("{0}: no free slot for {1}", [name, item.name]) + return + + _hand_over_to_container(item) + item.reparent(target, false) # keep_global_transform=false: the slot is expressed in container-local space + item.position = slot.position + item.rotation = slot.rotation + # After the reparent, not before: re-entering the tree puts the body back + # into the physics space, so it can only be taken out again once it's there. + _remove_from_physics(item) + + contained_items.append(food_item) + _publish_contained_ids() + SweetLogger.info("{0} absorbed {1}, contained_items: {2}", [get_parent().name, item.name, contained_items.size()]) + + +## Server decision, mirrored to every peer. func erase_item(item: FoodItem) -> void: - contained_items.erase(item) - var plate_controller := xr_pickable.get_node_or_null("PlateController") as PlateController - if plate_controller: - # Same reason as _add_item: assign so the visuals actually refresh. - var remaining := plate_controller.contained_ids.duplicate() - remaining.erase(item.id) - plate_controller.contained_ids = remaining + if not NetworkManager.owns_world(): + return + everyone_erase_item.rpc(item.get_parent().get_path()) +@rpc("authority", "call_local", "reliable") +func everyone_erase_item(item_path: NodePath) -> void: + var item := Helper.get_node_from_path(self, item_path) as Node3D + if not item: + return + contained_items.erase(Helper.find_first_child_of_type(item, FoodItem)) + item.queue_free() + _publish_contained_ids() + + +## Server decision, mirrored to every peer. func clear() -> void: + if not NetworkManager.owns_world(): + return + everyone_clear.rpc() + + +@rpc("authority", "call_local", "reliable") +func everyone_clear() -> void: + for child in _meal_container.get_children() + _side_container.get_children(): + child.queue_free() contained_items.clear() - var plate_controller := xr_pickable.get_node_or_null("PlateController") as PlateController - if plate_controller: - plate_controller.contained_ids = [] + _publish_contained_ids() -## Rebuilds the purely-cosmetic visual representation of the plate's contents -## from a synced id list. Runs on every peer (called from PlateController -## whenever contained_ids changes, whether set locally or by the network). -func refresh_visuals(ids: Array[String]) -> void: - for child in _meal_container.get_children(): - child.queue_free() - for child in _side_container.get_children(): - child.queue_free() - - var meal_idx := 0 - var side_idx := 0 - for id in ids: - var scene := RecipeManager.get_item_scene(id) - if not scene: - continue - var visual := scene.instantiate() - var food_node := visual.get_node_or_null("FoodItem") as FoodItem - - var container_root: Node3D - var positions: Array[Node3D] - var idx: int - if food_node and food_node.type == FoodItem.Type.MEAL and meal_idx < meal_positions.size(): - container_root = _meal_container - positions = meal_positions - idx = meal_idx - meal_idx += 1 - elif food_node and food_node.type == FoodItem.Type.SIDE and side_idx < side_positions.size(): - container_root = _side_container - positions = side_positions - idx = side_idx - side_idx += 1 - else: - visual.queue_free() - continue - - _make_cosmetic(visual) - container_root.add_child(visual) - # Must happen after add_child: entering the world is what puts the body - # into the physics space, so it can only be taken out again afterwards. - _remove_from_physics(visual) - visual.position = positions[idx].position - visual.rotation = positions[idx].rotation - - -# Strip interactivity/networking from a display-only copy: it's not spawned -# through NetworkManager, so it must never try to sync (its NetPickable child, -# if any, would have no corresponding replicated identity on other peers) or -# be grabbable/collidable. -func _make_cosmetic(visual: Node3D) -> void: - var net_pickable := visual.get_node_or_null("NetPickable") +# Hand the item over to the container: from here on the container's parent (e.g. +# a plate a client is carrying with local authority) is the only thing that +# decides where the item is, so it must stop behaving as a pickable and stop +# replicating a transform of its own. +func _hand_over_to_container(item: Node3D) -> void: + var pickable := item as XRToolsPickable + if pickable: + if pickable.is_picked_up(): + pickable.drop() + pickable.enabled = false + var net_pickable := item.get_node_or_null("NetPickable") as NetPickable if net_pickable: - # Detach and free it outright rather than queue_free(): this runs before - # `visual` is added to the tree, and a merely-queued node still enters the - # tree with its parent and runs _ready() (which starts syncing and logging) - # before the queued deletion lands at the end of the frame. - visual.remove_child(net_pickable) - net_pickable.free() - if visual is RigidBody3D: - visual.freeze = true - # STATIC, not KINEMATIC: a kinematic body is still driven by the physics - # engine (see _remove_from_physics), and "static decoration" is what this - # actually is. - visual.freeze_mode = RigidBody3D.FREEZE_MODE_STATIC - visual.collision_layer = 0 - visual.collision_mask = 0 - if visual is XRToolsPickable: - visual.enabled = false - visual.set_process(false) - visual.set_physics_process(false) + net_pickable.transform_owned = false + else: + SweetLogger.error("{0}: {1} has no NetPickable", [name, item.name]) + var despawning := Helper.find_first_child_of_type(item, DespawningItem) as DespawningItem + if despawning: + despawning.enabled = false -# Take a display-only copy out of the physics simulation completely. +# Take the item out of the physics simulation entirely so it simply follows its +# new parent. # # Freezing is not enough. Under Jolt (this project's physics engine) a frozen # KINEMATIC RigidBody3D is still simulated: it is driven toward its target # transform by velocity rather than being teleported. Parented to a plate that -# gets picked up and carried, it therefore lags behind, keeps its velocity, and -# overshoots — so the food visibly slid off the plate and ended up metres away, -# differently on each peer since each simulates its own copy. A body with no -# space is never touched by the engine, so it simply follows its parent. -func _remove_from_physics(visual: Node3D) -> void: - if visual is RigidBody3D: - PhysicsServer3D.body_set_space((visual as RigidBody3D).get_rid(), RID()) - var despawning_item = visual.get_node_or_null("DespawningItem") - if despawning_item: - NetworkManager.despawn_item(despawning_item) +# gets picked up and carried, it therefore lags behind, keeps its velocity and +# overshoots — the food visibly slid off the plate, differently on each peer. +# A body with no space is never touched by the engine. +func _remove_from_physics(item: Node3D) -> void: + var body := item as RigidBody3D + if not body: + return + body.freeze_mode = RigidBody3D.FREEZE_MODE_STATIC + body.freeze = true + body.collision_layer = 0 + body.collision_mask = 0 + PhysicsServer3D.body_set_space(body.get_rid(), RID()) -#plate (Pickalbe) - #XRGrapPoints - #container (script) (meal positions[1], side positions[4]) - #area - #meals - #meal - burger - #sides - #side - chips - #side - onion rings - -#tray (Pickalbe) - #XRGrapPoints - #container (script) (meal positions[4], side positions[0]) - #food items - #meals - #meal - cookie - #meal - cookie - #meal - cookie - #meal - cookie +# The contents are real child nodes now; contained_ids is just the synced +# summary that the plate's UI and the multiplayer tests read. +func _publish_contained_ids() -> void: + var plate_controller := xr_pickable.get_node_or_null("PlateController") as PlateController + if not plate_controller: + return + var ids: Array[String] = [] + for food_item in contained_items: + ids.append(food_item.id) + plate_controller.contained_ids = ids + SweetLogger.debug("{0} now contains: {1}", [get_parent().name, ids]) diff --git a/Containers/plate.tscn b/Containers/plate.tscn index 81a2ed9..b768e51 100644 --- a/Containers/plate.tscn +++ b/Containers/plate.tscn @@ -35,13 +35,13 @@ properties/1/path = NodePath(".:quaternion") properties/1/spawn = false properties/1/replication_mode = 1 properties/2/path = NodePath("NetPickable:net_held_by") -properties/2/spawn = true +properties/2/spawn = false properties/2/replication_mode = 1 properties/3/path = NodePath("PlateController:contained_ids") -properties/3/spawn = true -properties/3/replication_mode = 1 +properties/3/spawn = false +properties/3/replication_mode = 2 properties/4/path = NodePath("PlateController:is_dirty") -properties/4/spawn = true +properties/4/spawn = false properties/4/replication_mode = 1 [node name="Plate" type="RigidBody3D" unique_id=190487773] diff --git a/Containers/plate_controller.gd b/Containers/plate_controller.gd index b2ceeba..653669a 100644 --- a/Containers/plate_controller.gd +++ b/Containers/plate_controller.gd @@ -6,12 +6,10 @@ extends Node @export var is_dirty: bool = false -## Synced plate contents (item ids), replacing reparented child nodes so -## contents survive replication (a MultiplayerSpawner-tracked item would -## despawn on every client the instant it was reparented out of WorldContent). -## Server writes it via ItemContainer; every peer (server included) renders -## the cosmetic result via the setter below. -@export var contained_ids: Array[String] = []: set = _set_contained_ids +## Synced summary of the plate's contents (item ids). The contents themselves +## are the real item nodes ItemContainer reparents under the plate; this is only +## what other systems read to ask "what is on this plate" without walking them. +@export var contained_ids: Array[String] = [] func get_food_items() -> Array[FoodItem]: return container.contained_items @@ -29,22 +27,3 @@ func _process(_delta: float) -> void: else: container.enabled = true dirty_node.visible = false - - -func _set_contained_ids(value: Array[String]) -> void: - # Only rebuild when the contents actually changed. This property is - # replicated in ALWAYS mode, so the synchronizer assigns it every network - # tick on every peer that doesn't own the plate — and refresh_visuals() - # frees and re-instantiates a scene per item each time. That was thousands - # of throwaway nodes per run (and a log line from each one's NetPickable). - if contained_ids == value: - return - contained_ids = value - # Deferred: this can be written by the replicated spawn payload before - # this node's own @onready vars (container) have resolved. - _refresh_visuals.call_deferred() - - -func _refresh_visuals() -> void: - if container: - container.refresh_visuals(contained_ids) diff --git a/Items/hamburger.tscn b/Items/hamburger.tscn index e4e8ee3..345a8f9 100644 --- a/Items/hamburger.tscn +++ b/Items/hamburger.tscn @@ -1,82 +1,52 @@ [gd_scene format=3 uid="uid://b3m2ag8g5rj4r"] -[ext_resource type="PackedScene" uid="uid://c8l60rnugru40" path="res://addons/godot-xr-tools/objects/pickable.tscn" id="1_3gf3l"] -[ext_resource type="PackedScene" uid="uid://c25yxb0vt53vc" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_hand_left.tscn" id="2_0mkco"] -[ext_resource type="Animation" uid="uid://db62hs5s4n2b3" path="res://addons/godot-xr-tools/hands/animations/left/Grip 4.res" id="3_6tnvi"] -[ext_resource type="Script" uid="uid://dvobm6vcfnqe8" path="res://addons/godot-xr-tools/hands/poses/hand_pose_settings.gd" id="4_cljtj"] -[ext_resource type="PackedScene" uid="uid://ctw7nbntd5pcj" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_hand_right.tscn" id="5_dunns"] -[ext_resource type="Animation" uid="uid://d1xnpyc08njjx" path="res://addons/godot-xr-tools/hands/animations/right/Grip 4.res" id="6_fh0f6"] -[ext_resource type="PackedScene" uid="uid://bfj80jh13t6e5" path="res://prefabs/food_item.tscn" id="7_yy3y8"] -[ext_resource type="PackedScene" uid="uid://b5ukku8i0hilb" path="res://prefabs/despawning_item.tscn" id="9_ej8jm"] -[ext_resource type="Script" uid="uid://bwd0pe2udb5xo" path="res://Net/net_pickable.gd" id="20_netpk"] +[ext_resource type="PackedScene" uid="uid://d8jo0402pgl6" path="res://abstract/food_item.tscn" id="1_food_item"] [sub_resource type="CylinderShape3D" id="CylinderShape3D_fco8w"] height = 0.10392761 radius = 0.096191406 -[sub_resource type="Resource" id="Resource_lc22d"] -script = ExtResource("4_cljtj") -closed_pose = ExtResource("3_6tnvi") -metadata/_custom_type_script = "uid://dvobm6vcfnqe8" - -[sub_resource type="Resource" id="Resource_qyiot"] -script = ExtResource("4_cljtj") -closed_pose = ExtResource("6_fh0f6") -metadata/_custom_type_script = "uid://dvobm6vcfnqe8" - [sub_resource type="StandardMaterial3D" id="StandardMaterial3D_3gf3l"] albedo_color = Color(0.6784191, 0.54546416, 0.016760282, 1) [sub_resource type="StandardMaterial3D" id="StandardMaterial3D_6l01i"] albedo_color = Color(0.29, 0.101500005, 0, 1) -[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_np_hamburger"] -properties/0/path = NodePath(".:position") -properties/0/spawn = false -properties/0/replication_mode = 1 -properties/1/path = NodePath(".:quaternion") -properties/1/spawn = false -properties/1/replication_mode = 1 -properties/2/path = NodePath("NetPickable:net_held_by") -properties/2/spawn = true -properties/2/replication_mode = 1 - -[node name="Hamburger" unique_id=1675596942 groups=["platalbe_item"] instance=ExtResource("1_3gf3l")] +[node name="Hamburger" unique_id=779551583 groups=["platalbe_item"] instance=ExtResource("1_food_item")] gravity_scale = 0.04 -second_hand_grab = 1 [node name="CollisionShape3D" parent="." index="0"] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.02, 0.02, 0) shape = SubResource("CylinderShape3D_fco8w") -[node name="GrabPointHandLeft" parent="." index="1" unique_id=1571481674 instance=ExtResource("2_0mkco")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.067650706, 0.04601878, -0.08606844) -hand_pose = SubResource("Resource_lc22d") +[node name="FoodItem" parent="." index="5" unique_id=63948206] +id = "hamburger" +type = 0 +sell_value = 4 -[node name="GrabPointHandRight" parent="." index="2" unique_id=514404634 instance=ExtResource("5_dunns")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.06531982, 0.042291984, -0.08604182) -hand_pose = SubResource("Resource_qyiot") +[node name="CombinableItem" parent="." index="7" unique_id=996936271] +enabled = false -[node name="CSGCombiner3DBuns" type="CSGCombiner3D" parent="." index="3" unique_id=678881053] +[node name="CSGCombiner3DBuns" type="CSGCombiner3D" parent="Model" parent_id_path=PackedInt32Array(1015960306) index="0" unique_id=678881053] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.021904588, 0.025076538, -0.0044593215) autosmooth = true smoothing_angle = 138.9 -[node name="CSGCylinder3D2" type="CSGCylinder3D" parent="CSGCombiner3DBuns" index="0" unique_id=776920274] +[node name="CSGCylinder3D2" type="CSGCylinder3D" parent="Model/CSGCombiner3DBuns" index="0" unique_id=776920274] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.022625268, -0.03479519, 0.0027926378) radius = 0.085 height = 0.04 sides = 16 material = SubResource("StandardMaterial3D_3gf3l") -[node name="CSGCylinder3D3" type="CSGCylinder3D" parent="CSGCombiner3DBuns" index="1" unique_id=448728548] +[node name="CSGCylinder3D3" type="CSGCylinder3D" parent="Model/CSGCombiner3DBuns" index="1" unique_id=448728548] transform = Transform3D(0.82770383, -0.5611652, 0, 0.5611652, 0.82770383, 0, 0, 0, 1, -0.07384333, 0.015895892, 0) radius = 0.085 height = 0.04 sides = 16 material = SubResource("StandardMaterial3D_3gf3l") -[node name="CSGCylinder3D4" type="CSGCylinder3D" parent="CSGCombiner3DBuns/CSGCylinder3D3" index="0" unique_id=837229235] +[node name="CSGCylinder3D4" type="CSGCylinder3D" parent="Model/CSGCombiner3DBuns/CSGCylinder3D3" index="0" unique_id=837229235] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 5.9604645e-08, 0.040259957, 0) radius = 0.085 height = 0.04 @@ -84,20 +54,9 @@ sides = 16 cone = true material = SubResource("StandardMaterial3D_3gf3l") -[node name="CSGCylinder3D" type="CSGCylinder3D" parent="." index="4" unique_id=59319982] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.02356495, 0) +[node name="CSGCylinder3D" type="CSGCylinder3D" parent="Model/CSGCombiner3DBuns" index="2" unique_id=59319982] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.021904588, -0.0015115887, 0.0044593215) radius = 0.08300781 height = 0.03 sides = 16 material = SubResource("StandardMaterial3D_6l01i") - -[node name="FoodItem" parent="." index="5" unique_id=63948206 instance=ExtResource("7_yy3y8")] -id = "hamburger" -type = 0 -sell_value = 4 - -[node name="NetPickable" type="MultiplayerSynchronizer" parent="." index="6" unique_id=56164120] -replication_config = SubResource("SceneReplicationConfig_np_hamburger") -script = ExtResource("20_netpk") - -[node name="DespawningItem" parent="." index="7" unique_id=303090111 instance=ExtResource("9_ej8jm")] diff --git a/Net/net_pickable.gd b/Net/net_pickable.gd index b67ee0e..62a9b06 100644 --- a/Net/net_pickable.gd +++ b/Net/net_pickable.gd @@ -1,3 +1,4 @@ +class_name NetPickable extends MultiplayerSynchronizer ## Networked sync component for a pickable item. Added as a child literally @@ -8,33 +9,37 @@ extends MultiplayerSynchronizer ## every other peer freezes their local copy and just follows the synced ## transform. -## 0 = loose/server-simulated; otherwise the peer id currently holding it. +## Nobody is holding the item, so it is loose and simulated by the server. Not +## the same as "the server holds it" — that would be the server's peer id. +const NOT_HELD := 0 + +## NOT_HELD, or 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 net_held_by: int = NOT_HELD: set = _set_net_held_by + +## Transform properties, kept apart from the rest of the replicated set because +## set_transform_owned() drops them while something else drives the item. +const TRANSFORM_PROPERTIES: Array[String] = [".:position", ".:quaternion"] + +## False while something else drives this item's transform — e.g. it has been +## reparented into a container and is carried by its new parent. See set_transform_owned(). +var transform_owned: bool = true: set = set_transform_owned 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. +# RigidBody3D default of STATIC) — captured once so it can be restored. var _original_freeze_mode: RigidBody3D.FreezeMode # Same idea for the pickable's authored `enabled` flag, which the non-authority # branch of apply_held_state() clears while someone else is holding the item. var _original_enabled: bool -# Intent set by external game logic (e.g. a station enabling/disabling a tool), -# independent of hold state. apply_held_state() is the sole writer of the -# pickable's actual `enabled` property, so this is how other systems express -# "should be enabled" without fighting that reconciliation every tick. -var _tool_enabled: bool = true - -# Whether the "our own hand still holds this" guard has already been logged for -# the current grab. apply_held_state() runs every network tick, so without this -# the guard message repeats for as long as you hold the item. -var _grab_race_logged := false - +## Intent set by external game logic (e.g. a station enabling/disabling a tool), +## independent of hold state. apply_held_state() is the sole writer of the +## pickable's actual `enabled` property, so this is how other systems express +## "should be enabled" without fighting that reconciliation every tick. +var grabbable: bool = true: set = set_grabbable func _ready() -> void: _pickable = get_parent() as XRToolsPickable @@ -45,11 +50,8 @@ func _ready() -> void: # Configure Multiplayer Synchronizer # This overwrites any changes made in the inspector. replication_config = SceneReplicationConfig.new() - var properties: Array[NodePath] = [".:position", ".:quaternion", ".:enabled", ".:visible"] - for property_path in properties: - replication_config.add_property(property_path) - replication_config.property_set_replication_mode(property_path, SceneReplicationConfig.REPLICATION_MODE_ALWAYS) - replication_config.property_set_spawn(property_path, false) + for property_path in TRANSFORM_PROPERTIES + [".:enabled", ".:visible"]: + _add_always_property(property_path) # Unlike the properties above, this one is replicated at spawn too, so a # late joiner sees the current holder immediately. var held_by_path := NodePath("NetPickable:net_held_by") @@ -69,14 +71,38 @@ func _ready() -> void: apply_held_state.call_deferred() -## Called by external game logic (e.g. Counter._enable_tool/_disable_tool) to +func _add_always_property(property_path: String) -> void: + replication_config.add_property(property_path) + replication_config.property_set_replication_mode(property_path, SceneReplicationConfig.REPLICATION_MODE_ALWAYS) + replication_config.property_set_spawn(property_path, false) + + +## Set false by whatever takes over driving this item's transform (e.g. an +## ItemContainer reparenting it onto a plate). Disables apply_held_state() +## Without this net_held_by tick and would keep restoring `enabled`, `freeze` and the +## collision mask. +func set_transform_owned(value: bool) -> void: + if transform_owned == value: + return + transform_owned = value + for property_path in TRANSFORM_PROPERTIES: + if value: + _add_always_property(property_path) + else: + replication_config.remove_property(property_path) + # Ours again: re-derive the physics state we stopped maintaining. + if value: + apply_held_state() + + +## Set by external game logic (e.g. Counter._enable_tool/_disable_tool) to ## express whether this item should be usable right now, independent of hold ## state. Reapplies immediately so the change takes effect without waiting for ## the next net_held_by tick. -func set_tool_enabled(value: bool) -> void: - if _tool_enabled == value: +func set_grabbable(value: bool) -> void: + if grabbable == value: return - _tool_enabled = value + grabbable = value apply_held_state() @@ -91,19 +117,12 @@ func _set_net_held_by(value: int) -> void: ## 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. -## -## IMPORTANT: this runs on every network tick, not just on a real change. -## net_held_by is replicated in ALWAYS mode, so the synchronizer assigns it every -## tick on non-authority peers — unchanged value included — and that assignment -## lands in _set_net_held_by(), which calls this. So every branch here has to be -## idempotent and silent when there is nothing to do: otherwise each item logs a -## line and rewrites four physics properties every tick on every peer that -## doesn't own it. func apply_held_state() -> void: if not _pickable: return + if not transform_owned: # We don't own it. + return if not NetworkManager.is_online() or (is_inside_tree() and is_multiplayer_authority()) : - _grab_race_logged = false # 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 @@ -128,7 +147,7 @@ func apply_held_state() -> void: # e.g. a plate still got marked dirty) while pick_up() bailed out on # the disabled item, leaving the zone holding an item with no grab # driver that then fell out of the station. - var want_enabled_authority := _original_enabled and _tool_enabled + var want_enabled_authority := _original_enabled and grabbable if _pickable.enabled != want_enabled_authority: if NetworkManager.is_online(): SweetLogger.debug("{0}: reclaiming ownership, restoring enabled {1}->{2}", [_pickable.name, _pickable.enabled, want_enabled_authority]) @@ -141,12 +160,7 @@ func apply_held_state() -> void: # 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: - # Log once per grab, not once per tick. - if NetworkManager.is_online() and not _grab_race_logged: - _grab_race_logged = true - SweetLogger.debug("{0}: ignoring non-authority sync (net_held_by={1}) — still actively held by our own hand (grab-race guard)", [_pickable.name, net_held_by]) return - _grab_race_logged = false # Someone else owns it: stop simulating locally, just follow the sync. if _pickable.is_picked_up(): if NetworkManager.is_online(): @@ -159,14 +173,14 @@ func apply_held_state() -> void: # physics-property writes per item per tick. The comparison also means we # still re-apply if something else perturbs the state (e.g. let_go() # restoring the collision mask after a force-drop). - var want_enabled := (net_held_by == 0) and _tool_enabled + var want_enabled := (net_held_by == NOT_HELD) and grabbable if _pickable.freeze \ and _pickable.freeze_mode == RigidBody3D.FREEZE_MODE_KINEMATIC \ and _pickable.collision_mask == 0 \ and _pickable.enabled == want_enabled: return - # if NetworkManager.is_online(): # This was spamming that Knife was frozen every frame. - # print("%s: freezing (non-authority, owner=peer %d)" % [_pickable.name, net_held_by]) + if NetworkManager.is_online(): # This was spamming that Knife was frozen every frame. + SweetLogger.debug("{0}: freezing (non-authority, owner=peer {1})", [_pickable.name, net_held_by]) _pickable.freeze = true _pickable.freeze_mode = RigidBody3D.FREEZE_MODE_KINEMATIC _pickable.collision_mask = 0 @@ -177,8 +191,7 @@ func apply_held_state() -> void: ## 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): + if not (_pickable.get_picked_up_by() is XRToolsFunctionPickup): # e.g. a station snap zone grabbed it (server-side auto-snap, or the # addon's own "grab out of a snap zone" shortcut mid-cascade) — not a # player-initiated hand grab, so no authority request from here. diff --git a/Net/network_manager.gd b/Net/network_manager.gd index 42754be..18bb5f3 100644 --- a/Net/network_manager.gd +++ b/Net/network_manager.gd @@ -10,6 +10,8 @@ extends Node const DEFAULT_PORT := 24565 const MAX_CLIENTS := 7 +## Godot's fixed peer id for the server. +const SERVER_PEER_ID := 1 ## Emitted on every peer (including the server for its own local player) when a ## player peer joins. On the server this fires for each remote peer; the server @@ -194,7 +196,7 @@ 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: + if np and np.net_held_by != NetPickable.NOT_HELD 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. log_line("request_item_authority: DENIED %s to peer %d (already held by %d)" % [item.name, sender, np.net_held_by]) @@ -320,9 +322,18 @@ func _set_item_authority(item_path: NodePath, peer: int) -> void: return log_line("_set_item_authority: %s -> peer %d" % [item.name, peer]) item.set_multiplayer_authority(peer) # recursive: item + synchronizer + NetPickable + # ...except an ItemContainer under it. Deciding what a plate absorbs stays a + # server call, broadcast with an "authority" RPC — and Godot only lets a + # node's current authority send those. Letting the recursion above hand the + # container to whichever client is holding the plate made the client reject + # the server's own everyone_absorb_item, so the food was absorbed on the + # server only and stayed loose (and grabbable, at its old path) on the client. + var container := Helper.find_first_child_of_type(item, ItemContainer) + if container: + container.set_multiplayer_authority(SERVER_PEER_ID) var np := item.get_node_or_null("NetPickable") if np: - np.net_held_by = 0 if peer == 1 else peer + np.net_held_by = NetPickable.NOT_HELD if peer == SERVER_PEER_ID else peer np.apply_held_state() diff --git a/Prefabs/food_item.gd b/Prefabs/food_item.gd index 6590b8f..ad20356 100644 --- a/Prefabs/food_item.gd +++ b/Prefabs/food_item.gd @@ -4,6 +4,8 @@ extends Node # Define the Enum at the top of your script enum Type { MEAL, SIDE, INGREDIENT, NONE } +@export var enabled: bool = true + @export var id: String @export var type: Type = Type.NONE @export var sell_value = 1 diff --git a/Scenes/AlgoScene.tscn b/Scenes/AlgoScene.tscn index 152f203..46cdb9d 100644 --- a/Scenes/AlgoScene.tscn +++ b/Scenes/AlgoScene.tscn @@ -3,7 +3,7 @@ [ext_resource type="Script" uid="uid://d1cefhe3yyyds" path="res://Net/multiplayer_world.gd" id="1_nlnup"] [ext_resource type="PackedScene" uid="uid://j5s5wus7tuhb" path="res://prefabs/XROrigin.tscn" id="2_8pfaf"] [ext_resource type="Material" uid="uid://cmia50cfqxxo4" path="res://textures/dev_material_3d.tres" id="3_gatbn"] -[ext_resource type="PackedScene" uid="uid://j7caslh27nor" path="res://stations/Hob_old.tscn" id="4_3lufs"] +[ext_resource type="PackedScene" uid="uid://b052o1fgwq5bw" path="res://stations/Hob.tscn" id="4_3lufs"] [ext_resource type="Sky" uid="uid://c1abtpwhdm2d5" path="res://textures/sky/NightSkyHDRI009_16K.tres" id="5_kh26v"] [ext_resource type="PackedScene" uid="uid://c6rift56ql3f8" path="res://stations/BurgerBunsDispenser.tscn" id="7_tejmp"] [ext_resource type="PackedScene" uid="uid://dpot5qie20vf6" path="res://items/burger.tscn" id="9_rwx83"] diff --git a/Scenes/JonScene.tscn b/Scenes/JonScene.tscn index 8b19ca2..c14f4e4 100644 --- a/Scenes/JonScene.tscn +++ b/Scenes/JonScene.tscn @@ -11,7 +11,7 @@ [ext_resource type="PackedScene" uid="uid://dlw5geheupgpt" path="res://stations/hatch.tscn" id="12_dpf3b"] [ext_resource type="PackedScene" uid="uid://e4i6o5oriecx" path="res://items/PickupCube.tscn" id="13_1f3bw"] [ext_resource type="PackedScene" uid="uid://caf0xanmxbshy" path="res://stations/table.tscn" id="13_yjosk"] -[ext_resource type="PackedScene" uid="uid://j7caslh27nor" path="res://stations/Hob_old.tscn" id="14_2ndvi"] +[ext_resource type="PackedScene" uid="uid://b052o1fgwq5bw" path="res://stations/Hob.tscn" id="14_2ndvi"] [ext_resource type="PackedScene" uid="uid://clujaf3u776a3" path="res://addons/godot-xr-tools/objects/viewport_2d_in_3d.tscn" id="14_di04w"] [ext_resource type="PackedScene" uid="uid://efaec6ymgabo" path="res://stations/Counter.tscn" id="15_di04w"] [ext_resource type="PackedScene" uid="uid://xtcei2uvd5li" path="res://UI/game_over_panel.tscn" id="15_eoxxy"] diff --git a/Scenes/multiPlayer.tscn b/Scenes/multiPlayer.tscn index dda5761..c31b5e3 100644 --- a/Scenes/multiPlayer.tscn +++ b/Scenes/multiPlayer.tscn @@ -10,6 +10,7 @@ [ext_resource type="PackedScene" uid="uid://bnwb7imcotkod" path="res://prefabs/build_mode_controller.tscn" id="10_464r1"] [ext_resource type="PackedScene" uid="uid://3i1xb74cfsh5" path="res://test/vr_spectator_camera.tscn" id="11_5wx0o"] [ext_resource type="PackedScene" uid="uid://dxe05wp60jg3l" path="res://scenes/queue_controller.tscn" id="11_de1dy"] +[ext_resource type="PackedScene" uid="uid://b052o1fgwq5bw" path="res://stations/Hob.tscn" id="12_hob"] [ext_resource type="Script" uid="uid://biu4qr3nr1eny" path="res://test/mp_test_driver.gd" id="99_mptst"] [sub_resource type="BoxShape3D" id="BoxShape3D_vlqg6"] @@ -31,9 +32,6 @@ sdfgi_enabled = true [sub_resource type="BoxShape3D" id="BoxShape3D_arao0"] size = Vector3(15, 20, 0.1) -[sub_resource type="Resource" id="Resource_464r1"] -metadata/__load_path__ = "res://stations/Hob_old.tscn" - [node name="Main" type="Node3D" unique_id=1312265607 node_paths=PackedStringArray("items_spawner", "players_spawner")] script = ExtResource("1_kdan8") items_spawner = NodePath("ItemsSpawner") @@ -94,7 +92,7 @@ script = ExtResource("99_mptst") transform = Transform3D(1, 0, -1.7484555e-07, 0, 1, 0, 1.7484555e-07, 0, 1, 0, 0, -1.2) script = ExtResource("6_pw2j5") kitchen_scene = ExtResource("7_0sjqq") -hob_scene = SubResource("Resource_464r1") +hob_scene = ExtResource("12_hob") [node name="DayController" parent="." unique_id=1418639156 instance=ExtResource("9_5wx0o")] diff --git a/Scenes/test_small.tscn b/Scenes/test_small.tscn index 7d4f440..31dffca 100644 --- a/Scenes/test_small.tscn +++ b/Scenes/test_small.tscn @@ -4,7 +4,7 @@ [ext_resource type="Sky" uid="uid://c1abtpwhdm2d5" path="res://textures/sky/NightSkyHDRI009_16K.tres" id="2_1jq86"] [ext_resource type="PackedScene" uid="uid://j5s5wus7tuhb" path="res://prefabs/XROrigin.tscn" id="3_bryd6"] [ext_resource type="Material" uid="uid://cmia50cfqxxo4" path="res://textures/dev_material_3d.tres" id="4_kpgm6"] -[ext_resource type="PackedScene" uid="uid://j7caslh27nor" path="res://stations/Hob_old.tscn" id="5_gfdi7"] +[ext_resource type="PackedScene" uid="uid://b052o1fgwq5bw" path="res://stations/Hob.tscn" id="5_gfdi7"] [ext_resource type="PackedScene" uid="uid://efaec6ymgabo" path="res://stations/Counter.tscn" id="7_jnxsa"] [ext_resource type="PackedScene" uid="uid://bnwb7imcotkod" path="res://prefabs/build_mode_controller.tscn" id="9_hooyg"] [ext_resource type="PackedScene" uid="uid://caf0xanmxbshy" path="res://stations/table.tscn" id="11_gaw43"] diff --git a/Stations/counter.gd b/Stations/counter.gd index 9342d27..737532d 100644 --- a/Stations/counter.gd +++ b/Stations/counter.gd @@ -36,13 +36,13 @@ func _process(delta: float) -> void: func _enable_tool(_item: XRToolsPickable): SweetLogger.debug("Enable tool {0} on {1}", [_item.name, name]) _item.visible = true - _item.get_node("NetPickable").set_tool_enabled(true) + _item.get_node("NetPickable").grabbable = true func _disable_tool(_item: XRToolsPickable): SweetLogger.debug("Disable tool {0} on {1}", [_item.name, name]) _item.visible = false - _item.get_node("NetPickable").set_tool_enabled(false) + _item.get_node("NetPickable").grabbable = false func _disable_all_tools(): SweetLogger.debug("Disable all tools on {0}", [name]) diff --git a/abstract/food_item.tscn b/abstract/food_item.tscn index 9bf6e4d..bb11d57 100644 --- a/abstract/food_item.tscn +++ b/abstract/food_item.tscn @@ -35,9 +35,11 @@ properties/1/replication_mode = 1 properties/2/path = NodePath("NetPickable:net_held_by") properties/2/spawn = true properties/2/replication_mode = 1 -properties/3/path = NodePath(".:enabled") -properties/3/spawn = false -properties/3/replication_mode = 1 + +[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_rb7yx"] +properties/0/path = NodePath("FoodItem:enabled") +properties/0/spawn = false +properties/0/replication_mode = 1 [node name="FoodItem" unique_id=1675596942 instance=ExtResource("1_cvkx3")] second_hand_grab = 1 @@ -58,14 +60,17 @@ hand_pose = SubResource("Resource_qyiot") replication_config = SubResource("SceneReplicationConfig_np_burger") script = ExtResource("9_8m8gg") -[node name="FoodItem" parent="." index="4" unique_id=63948206 instance=ExtResource("8_tkk28")] +[node name="MultiplayerSynchronizerFoodItem" type="MultiplayerSynchronizer" parent="." index="4" unique_id=225229041] +replication_config = SubResource("SceneReplicationConfig_rb7yx") + +[node name="FoodItem" parent="." index="5" unique_id=63948206 instance=ExtResource("8_tkk28")] id = "item_id" sell_value = 0 -[node name="DespawningItem" parent="." index="5" unique_id=303090111 instance=ExtResource("10_vkoit")] +[node name="DespawningItem" parent="." index="6" unique_id=303090111 instance=ExtResource("10_vkoit")] -[node name="CombinableItem" parent="." index="6" unique_id=996936271 instance=ExtResource("11_combinable")] +[node name="CombinableItem" parent="." index="7" unique_id=996936271 instance=ExtResource("11_combinable")] -[node name="SoundManager" type="Node3D" parent="." index="7" unique_id=118155212] +[node name="SoundManager" type="Node3D" parent="." index="8" unique_id=118155212] -[node name="Model" type="Node3D" parent="." index="8" unique_id=1015960306] +[node name="Model" type="Node3D" parent="." index="9" unique_id=1015960306] diff --git a/addons/sweet-logger/sweet_logger.gd b/addons/sweet-logger/sweet_logger.gd index 6df3b32..9627df1 100644 --- a/addons/sweet-logger/sweet_logger.gd +++ b/addons/sweet-logger/sweet_logger.gd @@ -98,7 +98,7 @@ const TIMESTAMP_BG_COLOR = "#1e3a5f" #===================================================================================# ## Column widths for alignment (in characters) const PEER_ID_COLUMN_WIDTH = 6 -const LOG_TYPE_COLUMN_WIDTH = 10 +const LOG_TYPE_COLUMN_WIDTH = 12 ## Default mm:ss:ms; with SHOW_TIMESTAMP_HOURS, hh:mm:ss:ms const TIMESTAMP_COLUMN_WIDTH_MMSSMS = 9 const TIMESTAMP_COLUMN_WIDTH_HHMMSSMS = 12 diff --git a/addons/yaml/bin/~libgdyaml.windows.debug.x86_64.dll b/addons/yaml/bin/~libgdyaml.windows.debug.x86_64.dll~RF673246.TMP similarity index 100% rename from addons/yaml/bin/~libgdyaml.windows.debug.x86_64.dll rename to addons/yaml/bin/~libgdyaml.windows.debug.x86_64.dll~RF673246.TMP diff --git a/addons/yaml/bin/~libgdyaml.windows.debug.x86_64.dll~RF95cd0f.TMP b/addons/yaml/bin/~libgdyaml.windows.debug.x86_64.dll~RF95cd0f.TMP new file mode 100644 index 0000000..e0bf381 Binary files /dev/null and b/addons/yaml/bin/~libgdyaml.windows.debug.x86_64.dll~RF95cd0f.TMP differ diff --git a/content/static_resturants/resturant.tscn b/content/static_resturants/resturant.tscn index 4c7b714..da0c61a 100644 --- a/content/static_resturants/resturant.tscn +++ b/content/static_resturants/resturant.tscn @@ -1,7 +1,7 @@ [gd_scene format=3 uid="uid://bu6jt26pq2y0k"] [ext_resource type="PackedScene" uid="uid://q37nqadkibbp" path="res://environment/door.tscn" id="1_hgrxd"] -[ext_resource type="PackedScene" uid="uid://j7caslh27nor" path="res://stations/Hob_old.tscn" id="1_p8umn"] +[ext_resource type="PackedScene" uid="uid://b052o1fgwq5bw" path="res://stations/Hob.tscn" id="1_p8umn"] [ext_resource type="PackedScene" uid="uid://caf0xanmxbshy" path="res://stations/table.tscn" id="2_d63j6"] [ext_resource type="PackedScene" uid="uid://dlw5geheupgpt" path="res://stations/hatch.tscn" id="3_d63j6"] [ext_resource type="PackedScene" uid="uid://da6yrsnxvsrif" path="res://environment/wall.tscn" id="4_41vpv"] diff --git a/content/station_layouts/small_line.tscn b/content/station_layouts/small_line.tscn index 91a2f80..bbfc60f 100644 --- a/content/station_layouts/small_line.tscn +++ b/content/station_layouts/small_line.tscn @@ -9,12 +9,18 @@ [ext_resource type="PackedScene" uid="uid://caf0xanmxbshy" path="res://stations/table.tscn" id="7_0342k"] [ext_resource type="PackedScene" uid="uid://cnjwtnhwh0i8q" path="res://stations/dirt_station.tscn" id="8_b5gb1"] [ext_resource type="PackedScene" uid="uid://sc6i0i1f0o48" path="res://stations/potato_dispenser.tscn" id="9_ok0xr"] +[ext_resource type="PackedScene" uid="uid://b3m2ag8g5rj4r" path="res://items/hamburger.tscn" id="10_ay2lk"] +[ext_resource type="PackedScene" uid="uid://duwrbvwcqcuk3" path="res://items/chips.tscn" id="11_m7v4h"] +[ext_resource type="PackedScene" uid="uid://bp3v1jl8pctro" path="res://Containers/plate.tscn" id="12_h4a0y"] [node name="SmallLine" type="Node3D" unique_id=1844128818] [node name="Counter" parent="." unique_id=1487893288 instance=ExtResource("2_3qwcq")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.6, 0, 0) +[node name="Counter2" parent="." unique_id=707500902 instance=ExtResource("2_3qwcq")] +transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -1.8000001, 0, 1.8000001) + [node name="Sink" parent="." unique_id=2055277359 instance=ExtResource("3_3qwcq")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.2, 0, 0) @@ -30,7 +36,7 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.8000001, 0, 0) [node name="Table" parent="." unique_id=987495548 instance=ExtResource("7_0342k")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.4, 0, 1.2) -[node name="Hob" parent="." unique_id=707500902 instance=ExtResource("1_1wuvq")] +[node name="Hob" parent="." unique_id=484464865 instance=ExtResource("1_1wuvq")] [node name="DirtStation" parent="." unique_id=784427347 instance=ExtResource("8_b5gb1")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.8000001, 0, 0) @@ -38,4 +44,13 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.8000001, 0, 0) [node name="PotatoDispenser" parent="." unique_id=522124817 instance=ExtResource("9_ok0xr")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.8000001, 0, 0.6) +[node name="Hamburger" parent="." unique_id=1675596942 instance=ExtResource("10_ay2lk")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.2, 0) + +[node name="Chips" parent="." unique_id=740493636 instance=ExtResource("11_m7v4h")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.8000001, 1.2, 1.8000001) + +[node name="Plate" parent="." unique_id=190487773 instance=ExtResource("12_h4a0y")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.6, 1.2, 0) + [editable path="Table"] diff --git a/global/helper.gd b/global/helper.gd index 080f570..4bd05e4 100644 --- a/global/helper.gd +++ b/global/helper.gd @@ -13,6 +13,20 @@ static func find_food_item(node: Node) -> FoodItem: return null +# Returns "script.gd::caller_function" of whoever originally called into Helper, skipping recursive self-calls. +static func _get_caller_label() -> String: + var stack := get_stack() + # stack[0] = _get_caller_label, stack[1] = the Helper function that called this + var self_function: String = stack[1].function if stack.size() > 1 else "" + var caller_index := 2 + while caller_index < stack.size() and stack[caller_index].function == self_function: + caller_index += 1 + if caller_index >= stack.size(): + return "unknown" + var caller_script: String = (stack[caller_index].source as String).get_file() + return "{0}::{1}".format([caller_script, stack[caller_index].function]) + + static func find_first_child_of_type(node: Node, type: Variant) -> Node: for child in node.get_children(): if is_instance_of(child, type): @@ -20,22 +34,24 @@ static func find_first_child_of_type(node: Node, type: Variant) -> Node: var nested := find_first_child_of_type(child, type) if nested: return nested + #SweetLogger.warning("{0}: Could not find child of type {1} from node at: {2}", [_get_caller_label(), type.get_global_name(), node.get_path()]) return null - + + # Returns true if decendant is a decendant node of root, false otherwise. Returns false if either node is null. static func is_node_decendant_of(decendant: Node, root: Node) -> bool: if not decendant or not root: - SweetLogger.debug("Decendant or root is null") + SweetLogger.debug("{0}: Decendant or root is null", [_get_caller_label()]) return false var current: Node = decendant while current: if current == root: - SweetLogger.debug("Decendant {0} is a descendant of root {1}", [decendant.name, root.name]) + SweetLogger.debug("{0}: Decendant {1} is a descendant of root {2}", [_get_caller_label(), decendant.name, root.name]) return true current = current.get_parent() - SweetLogger.debug("Decendant {0} is NOT a descendant of root {1}", [decendant.name, root.name]) + SweetLogger.debug("{0}: Decendant {1} is NOT a descendant of root {2}", [_get_caller_label(), decendant.name, root.name]) return false - + # Returns a transform with position snapped to the 0.6 grid and rotation snapped to 90 degrees. static func get_snapped_transform(node: Node3D) -> Transform3D: @@ -50,7 +66,7 @@ static func get_snapped_transform(node: Node3D) -> Transform3D: static func get_node_from_path(from: Node, node_path: NodePath) -> Node3D: var item := from.get_node_or_null(node_path) as Node3D if not item: - SweetLogger.warning("Could not resolve node from node_path: {0}, are the trees in sync?", [node_path]) + SweetLogger.warning("{0}: Could not resolve node from node_path: {1}, are the trees in sync?", [_get_caller_label(), node_path]) return null return item @@ -58,12 +74,12 @@ static func get_node_from_path(from: Node, node_path: NodePath) -> Node3D: static func play_sound(player: AudioStreamPlayer3D, stream: AudioStream, pitch_scale: float = 1): if not player: - SweetLogger.warning("AudioStreamPlayer3D is null, can not play sound") + SweetLogger.warning("{0}: AudioStreamPlayer3D is null, can not play sound", [_get_caller_label()]) return if not stream: - SweetLogger.warning("AudioStream is null, can not play sound") + SweetLogger.warning("{0}: AudioStream is null, can not play sound", [_get_caller_label()]) return player.pitch_scale = pitch_scale player.stream = stream player.play() - SweetLogger.debug("Play sound: {0} with pitch_scale: {1} on {2}", [stream.resource_path, pitch_scale, player.get_parent().name]) + SweetLogger.debug("{0}: Play sound: {1} with pitch_scale: {2} on {3}", [_get_caller_label(), stream.resource_path, pitch_scale, player.get_parent().name]) diff --git a/recipes.yaml.uid b/recipes.yaml.uid new file mode 100644 index 0000000..26c7014 --- /dev/null +++ b/recipes.yaml.uid @@ -0,0 +1 @@ +uid://be8o8m6xpp3kc diff --git a/test/multiPlayerTest.tscn b/test/multiPlayerTest.tscn index 5bc5556..2eaf099 100644 --- a/test/multiPlayerTest.tscn +++ b/test/multiPlayerTest.tscn @@ -4,7 +4,7 @@ [ext_resource type="PackedScene" uid="uid://j5s5wus7tuhb" path="res://prefabs/XROrigin.tscn" id="2_j7vd1"] [ext_resource type="Material" uid="uid://cmia50cfqxxo4" path="res://textures/dev_material_3d.tres" id="3_ssbaf"] [ext_resource type="Sky" uid="uid://c1abtpwhdm2d5" path="res://textures/sky/NightSkyHDRI009_16K.tres" id="4_081u3"] -[ext_resource type="PackedScene" uid="uid://j7caslh27nor" path="res://stations/Hob_old.tscn" id="5_t1fa7"] +[ext_resource type="PackedScene" uid="uid://b052o1fgwq5bw" path="res://stations/Hob.tscn" id="5_t1fa7"] [ext_resource type="PackedScene" uid="uid://dvrk268s7gkxh" path="res://stations/sink.tscn" id="6_6jhmh"] [ext_resource type="PackedScene" uid="uid://bp3v1jl8pctro" path="res://Containers/plate.tscn" id="7_bowes"] [ext_resource type="PackedScene" uid="uid://cnjwtnhwh0i8q" path="res://stations/dirt_station.tscn" id="8_pvl84"]